连接数、线程与连接池
Too many connections 是生产上最常见也最容易被误治的故障:第一反应通常是调大 max_connections,但如果根因是连接泄漏或慢查询占住连接,调大只会让故障来得更晚、更猛。本章把连接数问题拆成“上限、占用、来源”三层来看,并给出连接池侧的对账方法。
现象与背景
- 应用报
Communications link failure或Too many connections,新连接一律被拒。 - 已经建好的连接还能用,但响应时间变长,因为活跃线程在抢 CPU。
- 监控上
Threads_connected直线上升,Threads_running同步升高——这是慢 SQL 占住连接的典型形态。 - 如果只有
Threads_connected高而Threads_running很低,那是连接空闲堆积,问题在连接池或超时参数。
两种形态的处置方向完全相反,先分清再动手。
第一步:量化三个数字
-- 5.7 / 8.0 通用
SHOW GLOBAL VARIABLES LIKE 'max_connections';
SHOW GLOBAL VARIABLES LIKE 'wait_timeout';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW GLOBAL STATUS LIKE 'Threads_created';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Aborted_clients';
SHOW GLOBAL STATUS LIKE 'Aborted_connects';
| 指标 | 含义 | 判读标准 |
|---|---|---|
| Threads_connected | 当前连接总数(含 Sleep) | 长期高于上限 70% 就要做连接预算 |
| Threads_running | 正在执行的线程 | 高于 CPU 核数 2~3 倍说明 SQL 在排队 |
| Max_used_connections | 历史峰值连接数 | 与 max_connections 接近说明上限设置已不合理 |
| Threads_created | 累计创建线程数 | 快速增长说明连接频繁断开重建 |
| Connections | 累计接受连接数 | 除以运行秒数得到建连速率,异常高常是短连接滥用 |
| Aborted_clients | 客户端未正常关闭即断开 | 增长说明连接被强制超时或应用未归还连接 |
| Aborted_connects | 握手阶段失败 | 增长说明认证失败、网络抖动或连接池探活异常 |
第二步:看谁在占连接
-- 找 Sleep 很久的连接(典型泄漏特征)
SELECT id, user, host, db, command, time, state
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 600
ORDER BY time DESC
LIMIT 20;
-- 找非 Sleep 且耗时长的连接(典型慢 SQL 特征)
SELECT id, user, host, db, command, time, state, LEFT(info, 120) AS sql_text
FROM information_schema.processlist
WHERE command <> 'Sleep'
ORDER BY time DESC
LIMIT 20;
| 看到的现象 | 判断 | 优先动作 |
|---|---|---|
| 大量 Sleep 且时间集中在同一应用 | 连接池空闲连接过多,或池配置远大于实际需要 | 下调 maxPoolSize,关闭多余实例 |
| 大量 Sleep 且时间超过 wait_timeout 仍未断 | 应用侧 keepalive 掩盖了超时,或代理层保活 | 检查连接池与中间件保活配置 |
| 多条非 Sleep 语句卡在同一张表 | 慢 SQL 占住连接,通常伴随锁等待 | 先清慢 SQL 与阻塞事务 |
| 连接来自不在册的 IP 或旧版本实例 | 僵尸实例或灰度残留 | 摘流量后下线 |
| 连接数随时间锯齿状波动 | 连接池回收后重建,属于正常现象 | 关注峰值是否触顶 |
第三步:三类根因
| 根因 | 特征 | 验证方式 |
|---|---|---|
| 连接池配置过大 | 所有实例的池上限之和超过数据库 max_connections,扩容一个实例就触顶 | 把各服务 maxPoolSize 相加与上限对比 |
| 连接泄漏 | 活跃连接持续上涨不回落,Sleep 连接逐渐累积,重启应用后立刻恢复 | 观察应用重启前后的连接数差 |
| 慢查询占住连接 | Threads_running 高、Threads_connected 同步高,锁等待计数上涨 | 看 processlist 中长耗时语句 |
连接预算的粗略公式:
数据库 max_connections ≥ Σ(每个应用实例的 maxPoolSize) × 安全系数 1.3
+ 运维/监控/备份等专用连接(建议预留 20~50)
例:订单服务 8 实例 × 20 + 用户服务 6 实例 × 15 + 预留 30
= 160 + 90 + 30 = 280 → max_connections 至少 400
第四步:应用侧连接池对账
| 参数 | 典型值 | 与数据库的关系 |
|---|---|---|
| HikariCP maximumPoolSize | 单实例 10~20 | 乘实例数后必须小于数据库上限 |
| minimumIdle | 与 maximumPoolSize 接近 | 过小会导致流量突增时集中建连 |
| connectionTimeout | 3 秒以内 | 应小于接口超时,避免线程堆积 |
| maxLifetime | 比 MySQL wait_timeout 小 30 秒以上 | 否则拿到已被服务端关闭的连接 |
| leakDetectionThreshold | 30 秒(仅用于排障) | 打日志定位未关闭的连接,不要长期开启 |
| keepaliveTime | 小于 wait_timeout | 维持长连接存活,减少重建 |
# HikariCP 关键配置,注意 maxLifetime 与 MySQL wait_timeout 的关系
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
max-lifetime: 600000 # 10 分钟,小于 wait_timeout(默认 8 小时但常被调小)
leak-detection-threshold: 30000
MySQL 侧对应参数:
-- 服务端空闲连接超过 wait_timeout 会被关闭,默认 8 小时
-- 若调小到 600 秒,应用 maxLifetime 必须更小,否则出现连接已断但池里还在用
SET GLOBAL wait_timeout = 600;
Aborted_clients 持续增长是最容易被忽略的信号:它通常意味着服务端主动断开了客户端连接,而应用并不知情,下一次使用该连接就报 Communications link failure。根因多半是应用 maxLifetime 大于 MySQL wait_timeout。
处置动作
- 先摘流量或限流,把新建连接速率压下来,避免继续恶化。
- 清理无用连接:确认后 kill 长时间 Sleep 的连接,或直接
KILL阻塞会话。
-- 批量生成 kill 语句,务必先人工核对结果
SELECT CONCAT('KILL ', id, ';') AS stmt
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 1800 AND user <> 'system user';
- 临时抬高
max_connections只能争取时间,抬高后要同步评估内存:每个连接约占几百 KB 到数 MB,上万连接会吃掉可观内存。 - 慢 SQL 导致的连接占满,走 17 章与 18 章的路径处理,不要靠加连接数掩盖。
-- 动态调整上限(重启失效,需写入配置文件)
SET GLOBAL max_connections = 1000;
预防与巡检项
- 把
Threads_connected与max_connections的比值纳入监控,超过 70% 告警。 - 每次扩容应用实例前,重新计算连接预算总和,扩容评审里带上这项。
- 连接池开启泄漏检测(仅排障期),平时用压测验证连接归还是否正常。
- 统一
wait_timeout与连接池maxLifetime的关系,写进配置模板而不是各服务自己定。
小结:连接数问题先判断是“活跃连接被慢 SQL 占住”还是“空闲连接堆积”,前者走慢 SQL 与锁的路径,后者查连接池配置与泄漏;用 Threads_connected、Threads_running、Max_used_connections、Aborted_clients 四个指标定性,用连接预算公式把各实例池上限之和控制在 max_connections 的 70% 以内,并让应用 maxLifetime 始终小于 MySQL wait_timeout。