连接池与线程池调优
池化是后端最基础的性能手段:把创建代价高的资源(数据库连接、线程)预先准备好并反复复用。但池不是越大越好,参数配错会直接导致“服务没挂,请求却全在等”的故障。本章讲清连接池参数含义与容量估算、线程池的核心参数、队列与拒绝策略、池隔离,以及等待超时的排查顺序。
为什么需要连接池
建立一次数据库连接要经过 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 的配置项名为例)思路相同:maximumPoolSize、minimumIdle、connectionTimeout、maxLifetime、idleTimeout 一一对应。
池过大反而更慢
连接数超过数据库处理能力之后,多出来的连接只会让数据库在锁、上下文切换、内存上排队,单请求耗时反而上升,出现“加大池子 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 | 丢掉最老任务再入队 | 只关心最新状态的场景 |
池隔离
不同业务共用一个池是故障扩散的常见原因:慢业务把池占满,快业务跟着一起排队。按业务拆分池,并给每个池独立的指标与告警:
网关线程池 / 订单下单池 / 支付回调池 / 报表导出池
(导出类任务必须独立成池并限制并发,它是最容易拖垮全局的消费者)
隔离同样适用于连接池:核心库与报表库分开配置,避免慢查询占满核心链路的连接。
常见故障:池等待超时
症状是接口大面积超时,日志里出现“获取连接超时”或“队列已满”。排查顺序:
- 看池指标:活跃连接数、等待队列长度、等待耗时 P99。活跃数打满说明下游慢或存在泄漏。
- 看下游耗时:是否某个查询从 10ms 变成 1s,占住了连接不放。
- 查连接泄漏:检查借出后未归还的调用栈(开启泄漏检测会打印),常见于异常分支漏了
finally或defer。 - 查上游突发:流量是否超过设计容量,此时应限流而不是继续加大池子。
小结:连接池按数据库真实承载能力定上限并用压测标定,线程池必须使用有界队列加明确的拒绝策略,池隔离用来避免故障扩散;遇到“等待超时”类故障,永远先查下游耗时与连接泄漏,而不是先加大池子。