连接池与线程池调优

池化是后端最基础的性能手段:把创建代价高的资源(数据库连接、线程)预先准备好并反复复用。但池不是越大越好,参数配错会直接导致“服务没挂,请求却全在等”的故障。本章讲清连接池参数含义与容量估算、线程池的核心参数、队列与拒绝策略、池隔离,以及等待超时的排查顺序。

为什么需要连接池

建立一次数据库连接要经过 TCP 握手、认证、会话初始化,耗时通常在毫秒级;如果每个请求都新建连接,高 QPS 下光握手就把 CPU 消耗光了。连接池解决三件事:

  • 复用连接,省掉建连与销毁的开销。
  • 限制并发连接数,保护数据库不被打爆。
  • 统一管理超时、心跳、泄漏检测等生命周期问题。

常用参数

参数含义取值思路
最大连接数池中同时存在的连接上限按数据库真实承载能力算,不是越大越好
空闲连接数保留的空闲连接数略小于最大连接数,避免频繁创建销毁
获取超时从池里拿连接的最长等待时间100ms ~ 1s,宁可快速失败
连接最大存活时间连接存活多久后强制重建几十分钟,配合数据库侧超时
空闲超时空闲多久后关闭连接几分钟
泄漏检测借出超时未归还时告警打开,并在日志中打印借用堆栈

Go 的 database/sql 常用设置:

// 以下片段需放入 main 函数中运行
db, err := sql.Open("mysql", dsn)
if err != nil {
    log.Fatal(err)
}
db.SetMaxOpenConns(50)                  // 最大连接数
db.SetMaxIdleConns(25)                  // 空闲连接数
db.SetConnMaxLifetime(30 * time.Minute) // 连接最长存活时间
db.SetConnMaxIdleTime(5 * time.Minute)  // 空闲多久后关闭

Java 侧(以 HikariCP 的配置项名为例)思路相同:maximumPoolSizeminimumIdleconnectionTimeoutmaxLifetimeidleTimeout 一一对应。

池过大反而更慢

连接数超过数据库处理能力之后,多出来的连接只会让数据库在锁、上下文切换、内存上排队,单请求耗时反而上升,出现“加大池子 TPS 却下降”的现象。可以先用经验公式定起点:

连接数上限 ≈ 数据库可用 CPU 核数 × 2 + 磁盘并行数
例如 8 核数据库:8 × 2 + 2 = 18,取 20 左右作为压测起点

公式只给起点,真实上限必须压测标定:固定梯度增加并发,观察 QPS 与 P99 的拐点,取拐点前 70% 作为生产配置。

线程池核心参数

参数含义关注点
核心线程数常驻线程数决定常态处理能力
最大线程数线程数上限仅在队列满之后才生效
任务队列排队容器容量既决定缓冲能力,也决定内存风险
空闲回收时间超出核心数的线程存活时间流量波动大时用于回收资源
线程工厂线程命名必须有业务语义名,便于排查
拒绝策略队列满且线程也满时的行为见下表

Java 线程池示例:

// 以下片段需放进 main 方法中运行
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    8,                          // 核心线程数
    32,                         // 最大线程数
    60, TimeUnit.SECONDS,       // 空闲回收时间
    new ArrayBlockingQueue<>(200),                // 有界队列,容量即缓冲上限
    new NamedThreadFactory("order-async"),        // 线程命名,便于定位
    new ThreadPoolExecutor.CallerRunsPolicy());   // 拒绝策略:调用方自己执行,形成背压

任务的处理顺序是:核心线程 → 队列 → 最大线程 → 拒绝策略。因此使用无界队列(如不指定容量的 LinkedBlockingQueue)时,最大线程数永远不会生效,任务会一直堆积直到内存溢出,生产上应使用有界队列。

拒绝策略对比

策略行为适用场景
AbortPolicy抛出异常默认策略,调用方能感知并降级
CallerRunsPolicy调用线程自己执行需要背压、不希望丢任务
DiscardPolicy静默丢弃可丢的埋点、日志类任务
DiscardOldestPolicy丢掉最老任务再入队只关心最新状态的场景

池隔离

不同业务共用一个池是故障扩散的常见原因:慢业务把池占满,快业务跟着一起排队。按业务拆分池,并给每个池独立的指标与告警:

网关线程池 / 订单下单池 / 支付回调池 / 报表导出池
(导出类任务必须独立成池并限制并发,它是最容易拖垮全局的消费者)

隔离同样适用于连接池:核心库与报表库分开配置,避免慢查询占满核心链路的连接。

常见故障:池等待超时

症状是接口大面积超时,日志里出现“获取连接超时”或“队列已满”。排查顺序:

  1. 看池指标:活跃连接数、等待队列长度、等待耗时 P99。活跃数打满说明下游慢或存在泄漏。
  2. 看下游耗时:是否某个查询从 10ms 变成 1s,占住了连接不放。
  3. 查连接泄漏:检查借出后未归还的调用栈(开启泄漏检测会打印),常见于异常分支漏了 finallydefer
  4. 查上游突发:流量是否超过设计容量,此时应限流而不是继续加大池子。

小结:连接池按数据库真实承载能力定上限并用压测标定,线程池必须使用有界队列加明确的拒绝策略,池隔离用来避免故障扩散;遇到“等待超时”类故障,永远先查下游耗时与连接泄漏,而不是先加大池子。

笔记加载中…