MySQL 大量 Sleep 连接:先查 wait_timeout,别急着调大连接池
SHOW PROCESSLIST 里出现大量 Command 为 Sleep 的会话,Threads_connected 接近 max_connections,但 CPU、QPS 都不高——直觉反应常是“连接池配小了”,于是调大上限。这个方向经常是错的,先看 wait_timeout 更省事。
先理解 Sleep 是什么
Sleep 表示连接已建立、当前没有语句在执行,服务端在等客户端发下一条命令。它不占 CPU,问题是空闲连接不被回收,占住 max_connections 名额。
回收靠两个变量:
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
wait_timeout 作用于非交互式连接(大多数驱动和连接池),interactive_timeout 作用于交互式连接(mysql 命令行默认是交互式)。默认值通常都是 28800 秒,即空闲连接要躺满 8 小时才被断开。
判断方向:Sleep 连接的 Time 普遍只有几十秒到几分钟,说明服务端在正常回收,问题在客户端不断新建连接;能累积到几千、几万秒,说明回收周期太长,连接池又默认保持长连接,两边都不主动断。
具体怎么做
确认当前值,以 GLOBAL 为准,SESSION 可能被客户端连接时覆盖:
SHOW GLOBAL VARIABLES LIKE '%timeout%';
SHOW SESSION VARIABLES LIKE 'wait_timeout';
看 Sleep 连接的时长分布:
SELECT COMMAND, COUNT(*) AS cnt, MIN(TIME) AS min_s, MAX(TIME) AS max_s
FROM information_schema.PROCESSLIST
WHERE COMMAND = 'Sleep'
GROUP BY COMMAND;
max_s 远大于 wait_timeout,说明有连接绕过了超时;明显小于,说明连接是被客户端关掉的,服务端超时不是瓶颈。
按业务容忍度调低全局值:
SET GLOBAL wait_timeout = 600;
SET GLOBAL interactive_timeout = 600;
只影响之后新建的会话,已有连接需重连才生效。持久化写进 my.cnf 的 [mysqld] 段:
[mysqld]
wait_timeout = 600
interactive_timeout = 600
再对齐连接池。以 HikariCP 为例,maxLifetime(默认 1800000 毫秒)应小于服务端 wait_timeout,否则池子会把已被服务端断开的连接当可用连接借出,触发 Communications link failure 或 The last packet successfully received from the server was ... milliseconds ago。
为什么先看它有效
连接池负责复用连接、控制并发,不负责判断服务端何时踢掉连接。服务端超时和客户端保活是两套独立机制,任何一边假设错了,另一边都表现为连接数异常。把 wait_timeout 调到与业务空闲周期匹配(比如 10 分钟),再让 maxLifetime 略小于它,空闲连接会被按时回收,池子按需重建。反过来,先盲目调大连接池,只是把更多连接塞进 8 小时的等待队列。
怎么确认生效
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Aborted_clients';
Threads_connected 应稳定在并发量附近而非持续爬升。Aborted_clients 明显上升,说明客户端在服务端断开后仍在用旧连接,回头检查 maxLifetime 和 testOnBorrow 类配置;应用日志里的 Communications link failure 是同一类信号。
边界与副作用
wait_timeout 不能设得过小。低于业务最长空闲间隔时,连接会在两次请求之间被断开,客户端下次使用报错,表现为间歇性失败,比连接数高更难排查。一般不低于 60 秒,常见取值 300 到 1800 秒。
它只解决“空闲连接不被回收”,不解决连接泄漏。Sleep 连接 Time 都很短但总数持续增长,说明应用反复建连而不复用,调超时没用。长事务或 LOCK 等待导致的连接堆积,Command 不是 Sleep,要看 State 字段区分。
云数据库的 wait_timeout 可能由控制台参数模板管理,直接 SET GLOBAL 会被覆盖或拒绝。