外观
连接堆积避坑实录:sleep 连接与 wait_timeout 联调
问题背景:Sleep 连接为什么会拖垮 MySQL
一次典型故障通常这样开始:应用响应变慢,监控显示 MySQL Threads_connected 持续逼近 max_connections,但 SHOW PROCESSLIST 中大量会话的 Command 是 Sleep,Time 从几百秒到数小时不等。此时新请求无法获取连接,业务侧表现为接口超时,而数据库 CPU、IO 可能并不高。
Sleep 连接表示客户端已经建立连接,但当前没有正在执行的 SQL。它本身不是故障,连接池为了复用连接,本来就会保留一定数量的空闲连接;真正的问题是:空闲连接数量长期失控、应用忘记归还连接、网络断链未被发现,或 MySQL、连接池、防火墙三者的超时策略不一致。
治理这类问题不能只改一个参数。正确思路是先识别连接来源和空闲时长,再让数据库超时、连接池空闲回收、应用获取连接校验形成闭环。
排查流程:用系统表定位连接堆积来源
先查看当前会话状态,重点关注 db、user、host、command、time 和 state:
sql
-- 查看当前所有连接,Sleep 连接重点看 Time 与 Host
SHOW FULL PROCESSLIST;
-- MySQL 8.0 推荐使用性能模式会话表,信息更结构化
SELECT
id,
user,
host,
db,
command,
time,
state,
LEFT(info, 120) AS sql_text
FROM information_schema.processlist
ORDER BY time DESC;按用户、来源和数据库聚合,可以快速判断是单应用泄漏,还是所有服务都在堆积:
sql
-- 按来源统计 Sleep 连接数量与最长空闲时间
SELECT
user,
SUBSTRING_INDEX(host, ':', 1) AS client_ip,
db,
COUNT(*) AS sleep_count,
MAX(time) AS max_idle_seconds,
AVG(time) AS avg_idle_seconds
FROM information_schema.processlist
WHERE command = 'Sleep'
GROUP BY user, client_ip, db
ORDER BY sleep_count DESC, max_idle_seconds DESC;再看实例级容量和历史峰值:
sql
-- 查看连接容量、当前连接数、历史峰值
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';
SHOW STATUS LIKE 'Aborted_clients';
SHOW STATUS LIKE 'Aborted_connects';判断标准可以简单记为:
Threads_connected长期接近max_connections,风险高;Max_used_connections / max_connections长期低于 20%,连接池可能配置过大;Aborted_clients持续增长,常见于连接被服务端关闭后客户端仍在使用;- Sleep 时间集中在某个固定值附近,通常能在
wait_timeout、防火墙 NAT 超时或连接池空闲时间中找到对应关系。
方案对比:杀连接、调大上限,还是治理连接池
| 方案 | 见效速度 | 优点 | 风险 | 适用阶段 |
|---|---|---|---|---|
| 临时 Kill Sleep 会话 | 最快 | 立即释放连接 | 可能中断尚未正确归还的事务,根因仍在 | 故障止血 |
| 调大 max_connections | 快 | 容纳更多连接 | 增加内存与调度压力,可能掩盖泄漏 | 容量确实不足时 |
| 调小 wait_timeout | 中 | 服务端强制回收空闲连接 | 连接池拿到死连接,引发首次请求报错 | 配合连接池校验 |
| 优化连接池配置 | 慢但根治 | 控制总量、空闲时间和连接有效性 | 参数组合复杂,需要压测 | 长期治理 |
| 修复应用连接泄漏 | 根治 | 避免连接被长期占用 | 需要代码审计与发布 | 必做事项 |
下面这张图展示了连接从应用到 MySQL 的完整超时链路:
实操步骤一:确认并调整 wait_timeout
wait_timeout 控制交互式以外连接在无活动后被服务端关闭的秒数。注意还有一个 interactive_timeout,对普通连接池通常主要看 wait_timeout,但很多客户端工具会走交互式超时。
sql
-- 查看全局和会话级超时
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
-- 在线调整为 1800 秒,注意重启后会失效
SET GLOBAL wait_timeout = 1800;
SET GLOBAL interactive_timeout = 1800;需要持久化时,在 MySQL 8.0 可使用:
sql
-- 持久化到 mysqld-auto.cnf,重启后仍生效
SET PERSIST wait_timeout = 1800;
SET PERSIST interactive_timeout = 1800;不建议一开始就把它调到 8 小时甚至 24 小时。过长的 wait_timeout 会让泄漏连接长期占位;过短又可能让连接池保留的空闲连接被数据库提前关闭。若云数据库或防火墙在 600 秒左右回收 TCP 连接,而 MySQL 设置为 28800 秒,就会出现客户端以为连接可用、实际链路已失效的问题。
实操步骤二:建立连接堆积监控视图
治理不能只靠故障时手动执行命令。可以用如下 SQL 定期采集,建议每 30 秒或 1 分钟一次:
sql
-- 创建一个监控用数据库,生产中可按规范放入运维库
CREATE DATABASE IF NOT EXISTS ops_monitor;
USE ops_monitor;
-- 保存 Sleep 连接快照
CREATE TABLE IF NOT EXISTS mysql_sleep_connection_snapshot (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
snapshot_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_name VARCHAR(128) NOT NULL,
client_ip VARCHAR(128) NOT NULL,
db_name VARCHAR(128) DEFAULT NULL,
sleep_count INT NOT NULL,
max_idle_seconds INT NOT NULL,
avg_idle_seconds DECIMAL(12,2) NOT NULL,
PRIMARY KEY (id),
KEY idx_snapshot_time (snapshot_time),
KEY idx_user_ip (user_name, client_ip)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 每次采集时写入聚合结果
INSERT INTO mysql_sleep_connection_snapshot
(user_name, client_ip, db_name, sleep_count, max_idle_seconds, avg_idle_seconds)
SELECT
user,
SUBSTRING_INDEX(host, ':', 1) AS client_ip,
db,
COUNT(*) AS sleep_count,
MAX(time) AS max_idle_seconds,
AVG(time) AS avg_idle_seconds
FROM information_schema.processlist
WHERE command = 'Sleep'
GROUP BY user, client_ip, db;再配合状态指标做告警:
sql
-- 查看最近 1 小时内最严重的 Sleep 来源
SELECT
user_name,
client_ip,
db_name,
MAX(sleep_count) AS peak_sleep_count,
MAX(max_idle_seconds) AS peak_idle_seconds
FROM mysql_sleep_connection_snapshot
WHERE snapshot_time >= NOW() - INTERVAL 1 HOUR
GROUP BY user_name, client_ip, db_name
HAVING peak_sleep_count >= 20 OR peak_idle_seconds >= 600
ORDER BY peak_sleep_count DESC;告警阈值不要只设连接总数,建议同时包含三类:连接数使用率超过 80%;单来源 Sleep 数异常增长;最长 Sleep 时间超过连接池预期空闲周期。
实操步骤三:连接池配置与 MySQL 超时联动
常见 HikariCP 配置可以参考下面的组合。假设 MySQL wait_timeout=1800 秒,连接池应在数据库关闭前主动回收:
yaml
spring:
datasource:
hikari:
# 最大连接数按实例容量和应用副本数反推,不是越大越好
maximum-pool-size: 20
minimum-idle: 5
# 10 分钟空闲后回收,小于 MySQL 的 1800 秒
idle-timeout: 600000
# 连接最大生命周期 30 分钟,定期换新
max-lifetime: 1800000
# 借出连接时校验,适合网络设备会静默断链的环境
connection-test-query: SELECT 1
keepalive-time: 300000
connection-timeout: 3000
validation-timeout: 3000核心关系是:idle-timeout 和 max-lifetime 应小于 wait_timeout;如果中间存在 600 秒断链的防火墙,保活间隔也要小于 600 秒。连接池总上限还要乘以应用实例数。例如 MySQL 设置 500 连接,不能让 10 个应用实例各配 100,因为理论峰值会达到 1000。
Druid 则重点关注这些参数:
properties
# 初始与活动连接数
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
# 空闲连接检测与回收,单位毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.keep-alive=true
# 借出时校验,避免失效连接直接到达业务 SQL
spring.datasource.druid.test-on-borrow=true
spring.datasource.druid.validation-query=SELECT 1避坑总结:三个高频错误及修正方法
1. 只调大 max_connections,结果内存和线程压力上升
常见现象是连接数告警消失不久后复发,数据库内存上涨,慢查询反而增多。连接数不是吞吐能力,事务竞争、锁等待和上下文切换会随并发增加而恶化。修正方法是先按来源统计 Sleep 数,再根据 Max_used_connections、应用副本数和压测结果计算连接池上限,而不是把 max_connections 当作唯一阀门。
2. wait_timeout 调小后没有配置连接池校验
典型报错是 The last packet successfully received from the server was ... milliseconds ago,或业务偶发第一次请求失败、重试后恢复。原因是 MySQL 已关闭空闲连接,但连接池仍把旧连接借给应用。修正方法是让连接池空闲回收时间小于 wait_timeout,并开启 keepalive、借出校验或驱动级连接有效性检测。
3. 把连接池 maxActive 配得比数据库总容量还大
多个微服务各自设置 max-active=100,低峰看似正常,发布或重试时同时建满连接,直接触发 Too many connections。修正方法是建立全局连接预算:预留管理连接后,将业务连接额度按服务重要性和实例数分配,并在监控中展示每个服务的连接占比。
4. Kill Sleep 会话却忽略未提交事务
有些 Sleep 会话并不是无害空闲,可能处于事务中并持有行锁或元数据锁。盲目批量 Kill 会造成业务回滚,虽然比长时间锁等待更安全,但仍应先确认。可使用如下 SQL:
sql
-- 查看事务仍在运行的 Sleep 会话及其锁等待
SELECT
p.id AS processlist_id,
p.user,
p.host,
p.time AS sleep_seconds,
trx.trx_started,
trx.trx_state,
trx.trx_rows_locked,
trx.trx_query
FROM information_schema.innodb_trx trx
JOIN information_schema.processlist p
ON trx.trx_mysql_thread_id = p.id
WHERE p.command = 'Sleep'
ORDER BY trx.trx_started;连接堆积治理的关键不是追求某个“最佳 wait_timeout”,而是让数据库、网络和连接池对空闲连接的认知一致。先用 SQL 找到来源,再用监控验证趋势,最后通过连接池预算、主动回收和有效性校验闭环,才能既避免 Sleep 连接占满实例,也避免超时调整引入新的偶发故障。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。