凌晨两点手机上弹出一条数据库告警第一反应往往是登上机器敲SHOW PROCESSLIST然后被几百行 Sleep 状态刷屏越看越糊涂——连接数到底是 200 还是 2000是数据库扛不住了还是应用端在白白占着不放这类场景里MySQL 查看连接数和查看连接状态看起来是最基础的两条命令实际上口径不清、字段不熟、权限不对都会让你在关键时刻拿到一份误导性的数据。我见过不止一次事故复盘结论从数据库连接数打满一路查到应用连接池配置写错一位数中间绕了两三个小时根子就在于没人把几个数字之间的关系理清楚。这篇内容面向的是需要真正盯 MySQL 连接状况的人写业务代码的后端、负责数据库的运维、做容量评估的架构岗也包括刚接手一套老系统、面对一堆慢查询和连接泄漏不知道从哪下手的新同学。下面我不会只给你一堆命令清单而是把每个数字背后的含义、命令之间的差异、排查的完整链路以及参数调整时那些文档里不会写的坑一层层拆开讲。1. 先把连接数这个词拆开三个口径不能混着看很多人上来就问现在有多少连接这个问题本身就是模糊的。在 MySQL 里至少存在三套计数方式它们各自回答不同的问题混用就会得出矛盾结论进而把排查方向带偏。1.1 Threads_connected、Threads_running 与 TCP 连接数的错位Threads_connected是服务端当前认为已经建立并持有的连接线程数量包含正在干活的和在睡觉的。Threads_running是其中不处于 Sleep 状态、真正在执行语句的线程数。这两个值差距巨大是常态比如Threads_connected300、Threads_running5说明两百九十多个连接在空转数据库本身并不忙问题出在连接管理而不是算力。SHOW GLOBAL STATUS WHERE Variable_name IN (Threads_connected,Threads_running,Threads_cached,Threads_created, Max_used_connections,Aborted_connects,Connection_errors_max_connections);而操作系统层面数出来的 TCP 连接又和Threads_connected不完全相等。用ss数建立态连接会包含三次握手刚完成、服务端还没来得及分配线程的短暂时窗也会因为客户端和服务端在同一台机器上、存在本地回环连接而多算或少算。ss -tn state established ( sport :3306 ) | tail -n 2 | wc -l我的经验是这三套数字不要试图让它们完全对上正负 5% 的偏差都是正常的。真正有意义的是看趋势——Threads_connected随时间缓慢爬升且不回落才是需要警觉的信号某一瞬间三个数字差距大反而说明不了什么问题。注意SHOW STATUS默认行为在部分版本和客户端下会返回会话级变量为避免歧义涉及连接数的查询统一写成SHOW GLOBAL STATUS。1.2 为什么 PROCESSLIST 行数和 netstat 计数经常对不上最常见的困惑是SHOW PROCESSLIST显示 120 行netstat数出 180 个 ESTABLISHED差的 60 个去哪了可能的原因有这么几类按发生频率排下来很好用。差异来源典型表现判断方法权限不足导致行被过滤只能看到自己的连接用高权限账号查一次对比短连接快速建连断连netstat 瞬时值偏高间隔重复采样看是否稳定客户端侧连接池已建连但未认证服务端尚未分配线程看Aborted_connects是否增长中间层转发代理、负载均衡服务端连接数远小于客户端查代理侧连接统计CLOSE_WAIT 堆积netstat 中非 ESTABLISHED 状态多ss -tan state close-wait权限这一条是新手最容易踩的。用普通业务账号执行SHOW PROCESSLIST只能看到自己会话发起的线程其余全部不可见而Threads_connected是全局统计不受权限影响。于是就会出现我查了只有 3 个连接为什么还报 Too many connections这种自相矛盾的局面。排查连接问题第一件事就是换一个有PROCESS权限的账号或者干脆用管理账号。2. 五种查法的取舍从一条命令到一张视图真正常用的查询方式大概五种各有各的适用场景。选错了不是查不出来而是查出来的信息量不够支撑判断。2.1 SHOW STATUS 与 mysqladmin秒级体检只想快速知道现在几个连接、历史峰值多少SHOW GLOBAL STATUS是最快的。但更省事的是mysqladmin它不需要进交互式客户端适合写进脚本和监控采集。mysqladmin -h127.0.0.1 -uroot -p status mysqladmin -h127.0.0.1 -uroot -p extended-status | grep -E Threads_|Max_used|Abortedmysqladmin status输出的一行里包含Threads:字段就是当前连接数。Max_used_connections反映自启动以来的历史峰值这个指标在做容量规划时比当前值有用得多——它告诉你系统最糟糕的时候顶到过多少max_connections是不是留足了余量看这个比看现在准。MySQL 8.0.14 之后还多了Max_used_connections_time能直接告诉你峰值出现在什么时候配合当时的业务日志能快速对上某个批处理或者大促活动。Aborted_connects和Connection_errors_max_connections这两个计数器值得单独盯。前者是连接尝试失败次数认证失败、握手超时等后者是明确因为达到max_connections上限而被拒绝的次数。只要后者在涨就说明系统已经真真切切地拒绝过业务请求了不是可能有问题是已经出过问题。2.2 SHOW PROCESSLIST 与 information_schema.PROCESSLIST 的差异与权限坑要看明细SHOW PROCESSLIST是入口但它有两个容易被忽略的特性。第一个是Info列默认只显示前 100 个字符稍长一点的 SQL 就被截断成半句话排查慢查询时看着像天书必须用SHOW FULL PROCESSLIST。第二个是它输出的是快照不是实时流在连接数剧烈波动的时候你看到的和半秒后看到的可能完全是两回事。information_schema.PROCESSLIST的价值在于能当表来查可以聚合、可以排序、可以塞进子查询。这在排查时是质变——从肉眼扫一百行变成一条 SQL 出结论。-- 按用户、来源、库、命令类型聚合 SELECT user, db, command, COUNT(*) AS cnt FROM information_schema.PROCESSLIST GROUP BY user, db, command ORDER BY cnt DESC; -- 按来源 IP 聚合Host 列形如 10.0.0.5:54321 SELECT SUBSTRING_INDEX(host, :, 1) AS client_ip, COUNT(*) AS cnt FROM information_schema.PROCESSLIST GROUP BY client_ip ORDER BY cnt DESC LIMIT 20;第二条 SQL 特别值得记住。连接数异常时先按来源 IP 聚合一次往往一眼就能看出是不是某个应用实例的连接池没配好或者某台机器上的定时任务在疯狂建连。比起逐行看 Host 列这个办法能省掉十几分钟的机械劳动。提示MySQL 8.0 中information_schema.PROCESSLIST的数据来源已经改为performance_schema.processlist在performance_schema被关闭的极端配置下这张表可能不可用此时退回SHOW FULL PROCESSLIST即可。2.3 performance_schema 和 sys.session能看等待事件的版本前面两种方式只能告诉你这个连接在跑什么 SQL、跑了多久看不到它在等什么。真遇到连接卡住但 CPU 不高的情况就必须往下挖一层performance_schema和建立在它之上的sys库视图是唯一答案。-- 正在执行的非 Sleep 会话带当前语句和等待事件 SELECT thd_id, conn_id, user, db, command, time, state, LEFT(current_statement, 100) AS stmt FROM sys.session WHERE command Sleep ORDER BY time DESC;sys.session是官方提供的封装视图把performance_schema.threads、events_statements_current、events_waits_current关联成一张人类可读的表省掉了自己写多表 JOIN 的麻烦。它的state列会给出比SHOW PROCESSLIST更细的等待信息比如具体在等哪把锁、等哪个 IO 事件。如果这个视图查不出东西先确认performance_schema相关 consumer 是否开启。默认配置下语句和等待事件的采集是打开的但有些为了性能做过裁剪的实例会关掉此时只能看到线程列表看不到语句明细。这是接手一套陌生实例时必须先验证的一项别等到故障现场才发现监控是瞎的。3. 把 PROCESSLIST 的 Command 和 State 读明白拿到明细之后绝大多数人的问题变成了这一屏到底哪个不正常。Command和State这两列就是答案但它们的取值有几十种全背下来没意义抓住几个高频的就行。3.1 CommandSleep 才是连接数的大头Sleep意味着服务端分配给这个连接的线程正闲着等待客户端发下一条语句。它占用了一个连接位、一份线程栈和若干缓冲区内存但不消耗 CPU。生产环境里 80% 以上的连接通常都是 Sleep 状态这是连接池机制的必然结果——连接建立后不急着关下次复用。问题就出在这个不急着关上。如果应用拿连接的方式有泄漏比如异常分支里没归还或者连接池的maxLifetime配得远大于数据库的wait_timeoutSleep 连接会越积越多直到顶到max_connections。这时候来一条新请求就是Too many connections而数据库的 CPU、IO 可能都还很闲。-- 找出睡得最久的连接 SELECT id, user, SUBSTRING_INDEX(host, :, 1) AS client_ip, db, time, LEFT(info, 60) AS last_sql FROM information_schema.PROCESSLIST WHERE command Sleep AND time 600 ORDER BY time DESC LIMIT 30;time列在 Sleep 状态下表示空闲了多少秒。超过wait_timeout本该被服务端主动断开如果还能查到说明wait_timeout被调得很大或者连接一直在临界点前被复用。看到成片的 3000 秒以上的 Sleep 连接基本可以判定连接池空闲回收策略没配或者没生效。除了 SleepQuery是最正常的执行态Killed表示线程收到了终止指令正在退出Binlog Dump是主从复制线程。如果在一台从库上看到大量Binlog Dump别慌那是正常的复制连接。3.2 State 里的几个高危信号State描述的是线程当前正在做的事情它的取值会随版本变化但有几个信号跨版本通用看到就该警觉。State 取值含义处理方向Waiting for table metadata lock等元数据锁找阻塞的 DDL 或未提交事务Waiting for table flush等表刷新完成通常由长查询或 FLUSH 操作引起Locked等行锁查innodb_trx找持锁事务Copying to tmp table建临时表检查排序字段索引、结果集大小Creating sort index排序中同上关注sort_buffer_sizeSending data读数据并发给客户端大结果集或网络慢不等同于慢查询statistics优化器统计信息阶段高频出现说明查询解析开销大Waiting for table metadata lock是线上最让人头疼的一个因为它会连锁——一个未提交的长事务挡住了 DDLDDL 挡住后续所有访问该表的查询连接数在几分钟内就能堆到上限。这个场景下的连接数暴涨是结果不是原因如果只盯着连接数去调max_connections方向完全错了。SELECT * FROM information_schema.innodb_trx ORDER BY trx_started ASC LIMIT 10;这张表里trx_started最早的那条往往就是罪魁祸首。配合trx_mysql_thread_id可以拿到连接 id再对照 processlist 就能定位到具体是哪个应用发的。4. 连接数飙升时的现场排查链路真到了告警时刻需要一套固定的动作顺序而不是凭感觉乱点。我按自己用过最多次的路径整理一遍每一步都给出判断依据遇到不符合的情况能及时换方向。4.1 从聚合统计锁定来源第一步永远是分清连接多但闲还是连接多且忙。查Threads_connected和Threads_running的比值如果后者只占前者的百分之几问题在连接管理如果两者接近说明数据库真的在被压。SELECT (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAMEThreads_connected) AS connected, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAMEThreads_running) AS running, (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAMEmax_connections) AS max_conn;确认是多而闲之后进入第三步按来源和用户聚合找出是谁在堆连接。这一步用 2.2 节里那条按 IP 聚合的 SQL 就能完成。实战中常见的结果是某一台应用服务器的连接数占了总量的七成其他几台都很正常。这种情况几乎可以断定是那台机器的实例配置异常或者上面的定时任务在跑批量逻辑去查它比查数据库有效得多。如果聚合结果比较分散、每台都差不多那要考虑整体容量问题连接池总容量是不是超过了max_connections。算一笔账——8 个应用实例每个实例连接池最大 50 个连接理论上限就是 400如果数据库max_connections只有 300那么在流量高峰必然打满。这种配置错误在上线评审时极难被发现因为两边单独看都合理。-- 查看被拒绝的累计次数确认是否真的打满过 SHOW GLOBAL STATUS LIKE Connection_errors_max_connections;这个值非零就是铁证。我建议把它加进监控告警阈值设成1 分钟内增长大于 0因为被拒绝意味着真实用户请求失败了没有任何容忍空间。4.2 长事务和元数据锁造成的假连接堆积有些连接数异常跟连接池一点关系都没有纯粹是被阻塞出来的。典型链条业务发起一个批量更新没有走索引扫了全表并且迟迟不提交另一个会话执行了一条 DDL后续所有访问这张表的请求全部进入Waiting for table metadata lock状态连接就这么一个个堆上去。这时候能看到的表象是连接数曲线陡直上升五分钟内从 100 冲到 500。如果单纯提高max_connections只是把爆炸时间往后推阻塞依然存在最终还是打满而且更高的上限意味着更多线程在等锁内存压力更大可能诱发更严重的连锁故障。-- 找执行时间最长的非空闲会话 SELECT id, user, SUBSTRING_INDEX(host, :, 1) AS ip, db, command, time, state, LEFT(info, 80) AS sql_head FROM information_schema.PROCESSLIST WHERE command Sleep ORDER BY time DESC LIMIT 20;拿到可疑的 id 之后KILL是最后手段但必须先确认三件事这条语句是不是真的可以中断、它是否已经改了一半数据、中断后应用能否正确处理异常。对一个大事务直接 KILL 会触发回滚回滚耗时可能比执行本身还长期间锁依然持有局面不会立刻好转。这个细节很多文档不会强调但在故障现场非常关键。如果确认要批量清理阻塞连接可以先生成语句再人工核对不要直接执行拼接结果SELECT CONCAT(KILL , id, ;) AS stmt FROM information_schema.PROCESSLIST WHERE command Sleep AND time 1800 AND user app_user;把输出复制出来逐条看一遍确认没有误伤再执行。我吃过一次亏条件里少写了一个user限制差点把复制账号的连接也带进去。5. 参数侧的取舍max_connections 不是越大越好调参这件事最大的误区是把max_connections当成一个可以随便往大调的旋钮。它确实能立刻缓解症状但背后有两道看不见的墙撞上去的时候报错信息完全不同排查起来很容易走弯路。5.1 文件描述符与内存的两道隐形上限每个连接在操作系统层面都要占用一个文件描述符。Linux 默认的单进程 fd 上限常见是 1024如果max_connections调到 2000 而open_files_limit和系统ulimit -n没跟上等到连接数真正涨到那个量级时报的错会是Cant create a new thread或者Too many open files而不是连接数超限。这类错误信息指向性很差容易让人误以为是线程栈或者内存问题。# 查看数据库进程当前实际可用的 fd 上限 cat /proc/$(pidof mysqld)/limits | grep open filesopen_files_limit在配置文件里设置不总是生效MySQL 会尝试用setrlimit去提升但受限于 systemd 服务单元里LimitNOFILE的配置。也就是说改了my.cnf还得改服务单元改完还得重启服务三件缺一不可。这个坑在容器化部署里更隐蔽——容器内的ulimit和宿主机的不是一回事。内存这一侧更微妙。每个连接都会分配连接级缓冲区read_buffer_size、sort_buffer_size、join_buffer_size、read_rnd_buffer_size这些参数是按需分配但用的连接一多累加起来非常可观。粗算一下假设四个缓冲区各 256KB一个活跃连接峰值能吃到 1MB200 个并发活跃连接就是 200MB 额外的内存开销还不算performance_schema本身占用的部分。所以把max_connections从 500 提到 2000如果没有相应的内存余量代价可能是频繁的 swap 甚至实例被系统 OOM 杀掉后果比连接拒绝严重得多。我的做法是max_connections的取值优先服从应用侧连接池总容量 预留 30%而不是拍一个整数。先把每个应用实例的连接池上限列出来求和再加上运维工具、备份、复制、监控采集的固定占用最后留出三成余量。这个数值往往比拍脑袋的结果小反而更稳。5.2 wait_timeout 与连接池 maxLifetime 的配合wait_timeout默认 28800 秒也就是 8 小时决定服务端多久主动断开一个空闲连接。interactive_timeout管的是交互式客户端命令行登录属于这一类。这两个值调大能减少连接被服务端悄悄断掉导致的应用报错但代价是空闲连接长期占用资源。真正的问题出在两侧超时不匹配上。如果连接池的maxLifetime配成 30 分钟而wait_timeout是 8 小时没问题反过来maxLifetime是 8 小时而wait_timeout只设了 600 秒就会出现连接池以为连接还活着取出来发语句服务端早就断开了应用侧报出Communications link failure或者那句经典的The last packet successfully received from the server was xxx milliseconds ago。解决办法是把连接池的maxLifetime设得明显小于wait_timeout留出足够的安全余量。经验值是一半左右比如wait_timeout600 秒、maxLifetime240 到 300 秒。另外别忘了让连接池开启保活检测绝大多数成熟连接池都有keepaliveTime或者借出前校验的开关把这层保险加上比反复调整服务端参数有效。参数侧别建议关系常见错误wait_timeout数据库略大于业务空闲间隔设为 8 小时却抱怨连接堆积maxLifetime连接池约为wait_timeout的一半设得比服务端超时还长idleTimeout连接池小于maxLifetime从不回收Sleep 长期堆积maxPoolSize连接池总和小于max_connections的七成各实例相加超过数据库上限提示interactive_timeout和wait_timeout都会影响连接存活只改其中一个有时看不到效果建议成对调整并同步观察Threads_connected曲线是否真的有回落。6. 指标落地把一次排查变成常态监控排查做完不代表事情结束。同一类问题第二次出现时如果没有指标支撑你还得再走一遍完整链路。把关键量固化下来是让运维成本指数下降的一步。6.1 值得盯的六个状态量状态变量有几百个全都采集既浪费存储也淹没重点。围绕连接问题下面六个足够覆盖绝大多数场景采集频率 10 到 15 秒一次比较合适。指标含义关注点Threads_connected当前连接数趋势是否单调上升Threads_running活跃线程数与上一项的比值Max_used_connections历史峰值是否逼近上限Threads_created累计创建线程数增长速率反映长连接是否失效Aborted_connects连接失败次数认证或网络问题Connection_errors_max_connections因超限被拒次数一旦非零即为故障Threads_created的增速特别值得单独说。它是累计值正常使用连接池的系统里应该增长得非常缓慢因为线程被复用。如果发现它每秒都在涨说明连接在被频繁地创建和销毁连接池形同虚设要么是配置写错要么是代码里每次操作都新建连接。thread_cache_size能缓解一部分开销但治标不治本还是得回到应用侧去查。配合Threads_cached看命中情况能进一步确认线程缓存的效率。6.2 告警阈值与基线怎么定阈值不能照抄别人的。我习惯的做法是先跑两周只观察不告警拿到这条业务连接数的日内波动曲线找出它自己的规律再定阈值。比如有的系统白天 80、夜里 20有的系统因为定时任务每天凌晨冲一次 300波动本来就大用固定阈值一定会误报。实用的组合是两条规则的叠加。第一条看绝对占比Threads_connected超过max_connections的 80% 持续 1 分钟就告警这条对突发流量有效。第二条看相对变化相比过去 7 天同时段基线上涨 50% 就提醒这条对缓慢爬升型的连接泄漏更敏感——那种每天涨一点、一周后打满的问题绝对阈值要等到最后才触发而相对基线能在第三天就给出信号。Threads_running的告警阈值要低得多我一般设在连接的 20% 或者绝对值 30 这两者取小。因为这个数一旦上去意味着真实并发压力比起连接数打满它更直接地反映用户体验恶化。另外别忘了给Connection_errors_max_connections单设一条零容忍规则它一旦增长就是已经在拒绝请求了属于最高优先级。对于sys.session这类带等待事件的视图适合做成周期采样落库用来回答某次故障时数据库到底在等什么这种回溯性问题。采样频率不用高一分钟一次只保留command Sleep的行存储压力很小但在复盘时的价值极大——比事后凭记忆回忆当时好像是锁等待可靠得多。最后说个我在实际项目里踩出来的体会连接数问题十次有八次根子不在数据库而在应用怎么用连接。所以每次排查完别只在那台数据库上改参数一定要顺着 Host 列找到对应的应用实例把连接池配置、连接归还逻辑、超时设置一起拉出来看一遍。数据库侧的参数只是兜底真正的病根通常藏在业务代码里一段没有finally的获取连接逻辑或者一个连异常都没处理的批量任务中。