连接数、线程与连接池

Too many connections 是生产上最常见也最容易被误治的故障:第一反应通常是调大 max_connections,但如果根因是连接泄漏或慢查询占住连接,调大只会让故障来得更晚、更猛。本章把连接数问题拆成“上限、占用、来源”三层来看,并给出连接池侧的对账方法。

现象与背景

  • 应用报 Communications link failureToo 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 接近过小会导致流量突增时集中建连
connectionTimeout3 秒以内应小于接口超时,避免线程堆积
maxLifetime比 MySQL wait_timeout 小 30 秒以上否则拿到已被服务端关闭的连接
leakDetectionThreshold30 秒(仅用于排障)打日志定位未关闭的连接,不要长期开启
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

处置动作

  1. 先摘流量或限流,把新建连接速率压下来,避免继续恶化。
  2. 清理无用连接:确认后 kill 长时间 Sleep 的连接,或直接 KILL 阻塞会话。
-- 批量生成 kill 语句,务必先人工核对结果
SELECT CONCAT('KILL ', id, ';') AS stmt
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 1800 AND user <> 'system user';
  1. 临时抬高 max_connections 只能争取时间,抬高后要同步评估内存:每个连接约占几百 KB 到数 MB,上万连接会吃掉可观内存。
  2. 慢 SQL 导致的连接占满,走 17 章与 18 章的路径处理,不要靠加连接数掩盖。
-- 动态调整上限(重启失效,需写入配置文件)
SET GLOBAL max_connections = 1000;

预防与巡检项

  • Threads_connectedmax_connections 的比值纳入监控,超过 70% 告警。
  • 每次扩容应用实例前,重新计算连接预算总和,扩容评审里带上这项。
  • 连接池开启泄漏检测(仅排障期),平时用压测验证连接归还是否正常。
  • 统一 wait_timeout 与连接池 maxLifetime 的关系,写进配置模板而不是各服务自己定。

小结:连接数问题先判断是“活跃连接被慢 SQL 占住”还是“空闲连接堆积”,前者走慢 SQL 与锁的路径,后者查连接池配置与泄漏;用 Threads_connectedThreads_runningMax_used_connectionsAborted_clients 四个指标定性,用连接预算公式把各实例池上限之和控制在 max_connections 的 70% 以内,并让应用 maxLifetime 始终小于 MySQL wait_timeout

笔记加载中…