ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL连接数排查:从状态指标到max_connections容量

MySQL连接数排查:从状态指标到max_connections容量

凌晨两点被电话叫起来,业务方说一批接口在超时,登录数据库第一件事往往就是看 MySQL 查询连接数——到底连了多少、跑没跑满、谁在占着不放。可真正在故障现场待过的人都知道,SHOW STATUS敲下去容易,看懂那几个数字不容易:Threads_connected和SHOW PROCESSLIST的行数对不上、Threads_running很低但业务报超时、Sleep状态堆了一大片却不知道删不删、max_connections调大了反而更慢。这一篇把 MySQL 查看连接数和连接状态这件事从头拆一遍:几个连接指标到底各自代表什么、四种查询入口怎么选、State 字段怎么翻译成人话、max_connections怎么算出来而不是拍出来、连打满的时候从哪一步开始查、KILL之前要想清楚什么。刚接触数据库运维的同学可以照着抄命令,已经带过线上库的同学可以直接跳到第 4、5 节看容量计算和现场排查路径。

1. 先把“连接数”拆开:你查的到底是哪个数字

1.1 五个最容易混为一谈的连接指标

很多人以为“连接数”就是一个数字,实际上 MySQL 至少提供了五六种口径,每个口径回答的问题完全不同。我在排查时习惯把这几个指标一次全捞出来,摆在一起看才有意义:

SHOW GLOBAL STATUS WHERE Variable_name IN ( 'Threads_connected', -- 当前打开的连接数 'Threads_running', -- 当前非 Sleep 状态的线程数 'Threads_created', -- 累计创建过的线程数 'Threads_cached', -- 线程缓存里闲置的线程数 'Max_used_connections', -- 历史峰值连接数 'Connections', -- 累计连接尝试次数 'Aborted_connects', -- 累计连接失败次数 'Aborted_clients', -- 累计客户端异常断开次数 'Connection_errors_max_connections' -- 因超过上限被拒绝的次数 );

这几个值的关系得像看仪表盘一样连着看:

指标含义判断价值
Threads_connected当前打开的连接总数,包含 Sleep判断连接池水位
Threads_running正在执行语句的线程数,不含 Sleep判断真实压力,通常远小于连接数
Max_used_connections自启动以来的连接峰值配合max_connections算余量
Connections累计连接尝试次数,含失败的结合 Uptime 看建连频率
Threads_created累计创建线程数和Connections对比看线程复用率
Aborted_connects建连阶段就失败的次数认证失败、超时、DNS 反解慢
Aborted_clients连上之后非正常断开的次数客户端崩溃、连接被中间层掐断、超时

Max_used_connections / max_connections这个比值是我最常用的水位指标。低于 50% 说明配置比较宽松,60%~80% 属于正常区间但该建监控了,长期挂在 90% 以上基本等于在刀尖上走,随便一次流量波动或者一批慢 SQL 就能把连接池顶死。这个比值不用自己算,一条 SQL 就能出来:

SELECT (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Max_used_connections') AS peak, (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME='max_connections') AS max_conn, ROUND( (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Max_used_connections') / (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME='max_connections') * 100, 2 ) AS used_pct;

注意:Max_used_connections是自 mysqld 启动以来的累计峰值,不会自动衰减。一次凌晨批量任务把峰值推到 900,这个数字会一直挂在 900,看水位时要结合业务周期判断,别被历史峰值误导。

1.2 为什么 Threads_connected 和 PROCESSLIST 行数经常对不上

这是个高频困惑。明明Threads_connected显示 120,SHOW PROCESSLIST数出来只有 118,差的两个去哪了?常见原因有这么几种:

第一种是内部线程不计入Threads_connected。information_schema.PROCESSLIST里能看到一些user为system user、event_scheduler、system的行,这些是复制线程、事件调度器线程、后台线程,它们属于服务内部的执行体,统计口径和客户端连接不是一回事。MySQL 8.0 之后用performance_schema.threads看得更清楚,TYPE字段会明确区分FOREGROUND(用户连接)和BACKGROUND(后台线程)。

第二种是采样时间差。SHOW STATUS和SHOW PROCESSLIST是两次独立的查询,中间隔着几毫秒到几十毫秒,连接在不停地建和断,两次结果天然对不齐。拿着两个相差 1~5 的数字较真没意义,差几十个才值得追。

第三种是权限过滤。普通账号执行SHOW PROCESSLIST只能看到自己的连接,看不到别人的;有PROCESS权限的账号才能看到全部。用应用账号去查连接数,看到的永远是自己那一份,很容易误判“连接很空闲”。

想看准确的全量数据,用performance_schema口径:

SELECT TYPE, COUNT(*) AS cnt FROM performance_schema.threads GROUP BY TYPE;

FOREGROUND的数量基本就等于客户端连接数,而且能进一步按PROCESSLIST_USER、PROCESSLIST_HOST、PROCESSLIST_DB聚合,比PROCESSLIST更好做统计。

2. 手上没有图形化工具时的命令行查法

2.1 SHOW STATUS 系列:三十秒摸清整体水位

线上出问题时,我一般先跑一组固定命令,不依赖任何客户端工具,纯命令行也能完成第一轮判断:

-- 1. 连接水位 SHOW GLOBAL STATUS LIKE 'Threads%'; SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW GLOBAL VARIABLES LIKE 'max_connections'; -- 2. 异常计数 SHOW GLOBAL STATUS LIKE 'Aborted%'; SHOW GLOBAL STATUS LIKE 'Connection_errors%'; -- 3. 超时与等待配置 SHOW GLOBAL VARIABLES WHERE Variable_name IN ( 'wait_timeout','interactive_timeout','connect_timeout','net_read_timeout','net_write_timeout' ); -- 4. 服务器跑了多久 SHOW GLOBAL STATUS LIKE 'Uptime';

Uptime这一条特别关键。Connections是个累计值,不看运行时长根本没法定性。假设Connections是 200 万,跑了 30 天,平均每秒不到 1 次建连,非常健康;但如果跑了 2 小时就 200 万,那说明连接在疯狂地建了又断,八成是连接池配置错误或者应用层没复用连接。心算一下:Connections / Uptime就是平均每秒建连次数,这个值超过每秒几百就要警惕了,正常业务连接池应该是长连接复用,建连频率很低。

同理,Aborted_connects要除以Connections看比例。比例在 0.1% 以下基本可以忽略;超过 1% 就得查原因,常见的是密码错误反复重试、connect_timeout太短导致握手超时、以及 DNS 反解拖慢建连。

2.2 SHOW PROCESSLIST 与 information_schema 的取舍

SHOW PROCESSLIST是最原始也最快的入口,但它有两个短板:一是输出格式不好做二次处理,二是没法直接排序过滤。我一般用information_schema.PROCESSLIST代替,本质是同一份数据,但能写 SQL:

-- 按客户端 IP 聚合,看看谁连得多 SELECT user, SUBSTRING_INDEX(host, ':', 1) AS client_ip, db, COUNT(*) AS conn_cnt, SUM(command = 'Sleep') AS sleep_cnt FROM information_schema.PROCESSLIST GROUP BY user, client_ip, db ORDER BY conn_cnt DESC LIMIT 20;

这条 SQL 是排查连接泄漏的第一把手术刀。输出的结果里,如果某个 IP 上挂着几百个连接且几乎全是Sleep,那基本可以确定是对应服务的连接池开太大、或者连接用完没归还。如果sleep_cnt占比接近 100%,直接去找那个服务的连接池配置,别犹豫。

再看执行时间最长的:

SELECT id, user, host, db, command, time, state, LEFT(info, 120) AS sql_head FROM information_schema.PROCESSLIST WHERE command <> 'Sleep' ORDER BY time DESC LIMIT 20;

time字段的单位是秒,含义随command变化:Sleep状态下是从上次执行完到现在的空闲时长,Query状态下是当前语句已经执行了多少秒。所以看到time = 3600且command = 'Sleep',那是空闲一小时,不用慌;但看到command = 'Query'、time = 3600,那是一条跑了一小时的 SQL,得立刻处理。

2.3 mysqladmin 与操作系统层面交叉验证

数据库内部的数字有时候会骗人——比如中间层做了连接复用,MySQL 看到的连接数远小于真实的客户端连接数。这时候要去操作系统层面数 TCP 连接:

# 统计到 3306 的 ESTABLISHED 连接数 ss -tn state established '( sport = :3306 )' | tail -n +2 | wc -l # 按来源 IP 聚合,看谁连得最多 ss -tn state established '( sport = :3306 )' \ | tail -n +2 \ | awk '{print $5}' \ | cut -d: -f1 \ | sort | uniq -c | sort -rn | head -20

ss出来的数字通常比Threads_connected略大,因为包含了正在三次握手、还没完成认证的连接。如果两者差距很大,说明有大量连接卡在建连阶段,回去看Aborted_connects和connect_timeout。

mysqladmin则是另一个轻量入口,适合写进脚本做定时采集:

# 一行概览:运行时长、线程数、查询数、QPS mysqladmin -h127.0.0.1 -uroot -p status # 完整进程列表 mysqladmin -h127.0.0.1 -uroot -p processlist # 批量拉状态值 mysqladmin -h127.0.0.1 -uroot -p extended-status | grep -iE 'threads|aborted|max_used'

mysqladmin status的输出长这样:Uptime: 864000 Threads: 42 Questions: 128730291 Slow queries: 1204 Opens: 3021 Flush tables: 3 Open tables: 512 Queries per second avg: 148.992。这一行里Threads就是Threads_connected,Queries per second avg是自启动以来的平均 QPS。别小看这一行,在没有监控的临时环境里,它能三十秒给出整体画像。

提示:生产环境别把密码写在命令行里,-p后面不加值会交互式提示输入,这是最基本的习惯。写脚本采集时用~/.my.cnf或者环境变量传参。

3. 连接状态(State)逐条翻译:Sleep 不代表没事干

3.1 常见 State 值对照表

PROCESSLIST的state字段是信息量最大的列,也是最容易看错的一列。它描述的是线程“此刻正在做什么”,很多值看着像错误,其实只是正常执行过程中的一个阶段:

State 值实际含义需要关注吗
Sleep连接空闲,等客户端下一条命令数量多且来源单一 → 查连接池
Query正在解析或执行语句结合time看是否超长
Sending data读数据并往客户端发(名不副实,包含读表)大结果集或全表扫描时常见
Copying to tmp table正在往临时表写数据频繁出现 → 查GROUP BY/DISTINCT
Creating sort index排序中大排序、缺索引
Waiting for table metadata lock等元数据锁高危,往往被长事务堵住
Waiting for table flush等表刷新完成常与备份、ALTER冲突
Locked被表级锁阻塞(多为 MyISAM)换存储引擎或错峰
login正在做认证握手大量堆积说明建连慢
cleaning up语句执行完做收尾正常
Waiting for handler commit等引擎提交提交频繁或刷盘慢
System lock等内部或表锁结合锁等待一起看
binlog dump复制线程在推 binlog正常,别 kill

看这张表有个体会:真正需要马上动手的其实只有三类——Waiting for table metadata lock、Locked、以及Query状态长时间不动的。其余大部分状态值是执行过程中的一个瞬时快照,你连着刷两次PROCESSLIST,同一个连接的状态可能就变了。

举个真实的场景:某次业务反馈写入卡住,SHOW PROCESSLIST里看到十几条Waiting for table metadata lock。这种情况不可能是那些等待的语句本身有问题,它们是受害者,真正的凶手是某个持着元数据锁不放的长事务。查法:

-- MySQL 8.0:直接看元数据锁等待关系 SELECT * FROM performance_schema.metadata_locks WHERE LOCK_STATUS = 'PENDING';

杀元数据锁的持有者之前,先用sys.session找到它:

SELECT conn_id, user, host, db, command, time, state, trx_state, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_alive_sec, LEFT(current_statement, 100) AS sql_head FROM sys.session WHERE conn_id IS NOT NULL ORDER BY trx_alive_sec DESC LIMIT 20;

sys.session是官方 sys schema 里最实用的视图之一,它把PROCESSLIST和INNODB_TRX拼在一起,一条 SQL 就能同时拿到连接信息、事务状态、事务起始时间、当前语句。trx_alive_sec动辄几百秒的那些行,就是你要盯的对象。

3.2 从 Sleep 连接里揪出连接泄漏

Sleep是最容易被误解的状态。很多人一看几百个Sleep就觉得“连接在浪费”,想批量 kill 掉,这个想法很危险。Sleep只代表这条连接当前没有语句在执行,它可能是连接池里正常闲置的连接,也可能是被应用遗忘的泄漏连接,两者从单条记录上分不出来,得看分布。

我的判断方法是看三个维度的组合:

SELECT SUBSTRING_INDEX(host, ':', 1) AS client_ip, COUNT(*) AS total_cnt, SUM(command = 'Sleep') AS sleep_cnt, MAX(CASE WHEN command = 'Sleep' THEN time ELSE 0 END) AS max_sleep_sec, ROUND(AVG(CASE WHEN command = 'Sleep' THEN time ELSE NULL END), 0) AS avg_sleep_sec FROM information_schema.PROCESSLIST GROUP BY SUBSTRING_INDEX(host, ':', 1) ORDER BY total_cnt DESC;

判断逻辑大致是这样:

  • 单个 IP 上连接数远超其实例数 × 池大小的预期,且avg_sleep_sec很大 —— 典型泄漏,去查这个服务的连接归还逻辑。
  • 连接数符合预期,avg_sleep_sec在几十秒到几分钟—— 正常的长连接池,不用动。
  • max_sleep_sec接近或超过wait_timeout—— 这些连接随时会被服务端主动断开,客户端如果不知道自己被断了,下次取用时会报Communications link failure,得让连接池配validationQuery或心跳。

这里有个配置上的坑我一直提醒别人:连接池的空闲回收时间必须小于wait_timeout。MySQL 默认wait_timeout = 28800(8 小时),很多连接池默认idleTimeout是 10 分钟到 30 分钟,看起来没问题,但有些框架的默认值是 0(永不回收)或者干脆没配,结果连接挂着挂着被服务端单方面切断,客户端毫不知情,下次用就报错。稳妥的做法是:wait_timeout设成 3600 甚至 1800,连接池的空闲回收设成比它小 5~10 分钟,同时开连接有效性检测。

注意:改wait_timeout只影响新建的连接,已有的长连接还按老值算。在线修改用SET GLOBAL wait_timeout = 3600;,但要让存量连接生效,得等它们自然重连或者主动踢掉。

4. max_connections 到底该设多少:算一遍比拍脑袋靠谱

4.1 用利特尔法则倒推业务侧连接需求

max_connections是 MySQL 里被改得最随意的参数之一,出问题就调大,从 500 调到 5000,然后发现更慢了。原因很简单:连接是操作系统线程,每个连接都有独立的栈空间和会话内存,连接数上去之后上下文切换、内存占用、锁竞争全都跟着涨,吞吐不一定提升,延迟反而恶化。

正确的做法是先算业务到底需要多少并发连接。这里可以借用利特尔法则(Little's Law):平均并发数 = 到达速率 × 平均停留时间。

放到数据库场景:

期望并发执行语句数 ≈ QPS × 单条语句平均耗时(秒)

举个具体例子。某库峰值 QPS 3000,平均 SQL 耗时 8ms:

并发执行语句数 = 3000 × 0.008 = 24

也就是说,理论上只要有 24 个连接同时在跑,就能扛住这个压力(前提是 SQL 都很快)。当然现实中要考虑突发、慢查询、锁等待,一般乘 3~5 倍安全系数,得到 72~120 个“活跃连接”就绰绰有余了。

那剩下的几百个连接是什么?是持有但闲置的。连接总数取决于连接池怎么配,而不是数据库能力。所以真正要做的算术是这一条:

max_connections ≥ Σ(各应用实例数 × 单实例池上限) + 后台任务 + 监控探针 + 运维预留

拿一个中等规模的线上系统举例:

来源实例数单实例连接小计
核心服务连接池3010300
定时任务5525
数据同步任务31030
报表/离线查询22040
监控探针 + 运维客户端——20
合计415

415 是基线,考虑业务高峰时连接池会打满、滚动发布时新旧实例并存(瞬时可能翻倍)、以及故障时人工介入要连得进去,max_connections设到 800~1000 比较稳妥,也就是留出 50%~60% 的冗余。这个冗余不是为了“性能”,是为了故障时的操作空间——连接数贴着上限,你连登都登不进去,只能重启。

4.2 内核与内存侧的隐性上限

max_connections不是孤立参数,它有几条隐性的天花板,设得再大也会被别的东西卡住。

第一是open_files_limit。每个连接打开表、临时文件都要消耗文件描述符,MySQL 会尝试把open_files_limit调整到max_connections × 5左右(如果系统允许)。如果open_files_limit被系统限制住,max_connections实际生效值会被悄悄压下来。查证方法:

SHOW VARIABLES LIKE 'open_files_limit'; SHOW VARIABLES LIKE 'max_connections';
# 看 mysqld 进程的实际限制 cat /proc/$(pidof mysqld)/limits | grep -i 'open files' ulimit -n

如果open_files_limit明显小于max_connections × 5,就要去my.cnf里显式设置open_files_limit,同时在 systemd 单元文件里配LimitNOFILE,两处都要改,只改一处不生效——这个细节踩过一次就记住了。

第二是back_log。它控制的是 TCP 连接队列长度,也就是瞬间涌入大量建连请求时,内核能排队等待 MySQL 处理的数量。默认值通常和max_connections挂钩,但如果业务有“瞬时批量建连”的场景(比如大批容器同时启动),队列太短会导致连接被内核拒绝,客户端看到的是Connection refused而不是Too many connections。这类问题排查起来特别费劲,因为数据库日志里什么都没有。

第三是内存。每个连接的基础内存开销在几百 KB 量级(会话缓冲、网络缓冲、线程栈等),跑复杂查询时还会按需申请sort_buffer_size、join_buffer_size、read_buffer_size这些会话级缓冲。所以:

连接内存粗略估算 ≈ 连接数 × (基础开销 + 平均会话缓冲)

1000 个连接按每连接 1MB 保守估,就是 1GB。这个数字要跟 buffer pool 一起算进总内存预算,不能只盯着innodb_buffer_pool_size。真实开销最好的测法是在压测环境里把连接数打上去,看mysqld的 RSS 增长曲线,比任何公式都准。

第四是 MySQL 8.0 的线程模型。连接是“一连接一线程”,不是协程也不是事件驱动。连接数上千之后,线程调度和上下文切换的成本会明显上升。这也是为什么很多团队在连接数超过某个量级后转向中间件做连接复用——把几千个应用连接收敛成几百个数据库连接。

4.3 连接池与数据库上限的乘法陷阱

这是我最想强调的一条:连接池的池大小是“每实例”的,总数要乘实例数。出过的事故里,至少一半是这个乘法没算。

场景还原:某服务单实例连接池配了 20,平时跑 10 个实例,总共 200 连接,max_connections设 500,很宽裕。某次大促把实例扩到 40 个,连接瞬间变成 800,直接把库顶死,报Too many connections。扩容本身没错,错在扩容时没人算连接数。

所以容量规划里要有这条硬规则:

max_connections > (服务实例上限 × 单实例池大小) × 1.5

注意是“实例上限”不是“当前实例数”,因为自动扩缩容随时会把实例数推上去。如果用的云环境开了自动伸缩,更要按最大副本数算。

反过来还有另一个坑:连接池设太大反而降低吞吐。池子大了,同时打到数据库的活跃查询变多,锁竞争、CPU 争抢都加剧,单条 SQL 的耗时被拉长,单位时间完成的总量反而下降。对于普通 OLTP 业务,单实例池 10~20 通常就够,宁可让请求在应用层排队,也不要让它们全部涌进数据库。

5. 连接打满/突增的现场排查路径

5.1 五步定位法

连接数突然飙高时,我的固定动作是这五步,顺序不要乱:

第一步,确认是不是真打满。先看Threads_connected对比max_connections,同时看Connection_errors_max_connections是否在增长。如果这个错误计数在涨,说明已经拒绝过连接了,问题确认。

第二步,看增量来源。按 IP 聚合,找出连接数增长最快的来源:

SELECT SUBSTRING_INDEX(host, ':', 1) AS client_ip, user, COUNT(*) AS cnt FROM information_schema.PROCESSLIST GROUP BY client_ip, user ORDER BY cnt DESC LIMIT 10;

如果集中在少数几个 IP,基本可以锁定是某几个服务实例;如果均匀分布在几十个 IP 上,说明是整体流量上涨或者连接池集体故障。

第三步,看这些连接在干什么。是Sleep堆着,还是都在Query?前者是连接泄漏或池配置问题,后者是真压力。

第四步,如果是真压力,找出最耗资源的 SQL。

SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT/1e12 AS total_sec, AVG_TIMER_WAIT/1e9 AS avg_ms, SUM_ROWS_EXAMINED FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;

MySQL 8.0 的events_statements_summary_by_digest是按 SQL 指纹聚合的,能直接把“哪类 SQL 最耗时”排出来,比一条条看PROCESSLIST高效得多。SUM_ROWS_EXAMINED特别大而返回行数很小的,大概率是缺索引。

第五步,如果是Sleep堆积,找泄漏源。看那些 IP 上连接的空闲时长分布,结合应用侧的发布记录、扩缩容记录、定时任务触发时间去对时间点。连接数突增的时间点和某个任务启动对得上,问题基本就定位了。

5.2 Too many connections 发生时的应急入口

连接打满之后最尴尬的局面是:你想进去处理,但是连不上。这时候有几条路可以走:

路径一:用带CONNECTION_ADMIN权限的账号。MySQL 在连接数达到max_connections时,会为拥有该权限的账号额外保留一个连接名额。所以平时就要准备一个专门的管理账号,授予CONNECTION_ADMIN,别用业务账号兼任。这是最省事的一条路。

路径二:MySQL 8.0.14 之后的管理端口。配置admin_address和admin_port,会开一个独立监听,不受max_connections限制:

[mysqld] admin_address = 127.0.0.1 admin_port = 33062

连接方式是mysql -h127.0.0.1 -P33062 -uroot -p。这个端口只建议监听本地或内网管理网段。

路径三:从应用侧止血。如果能操作应用,先摘流量或者停掉一部分实例,连接会自然释放。这比在数据库上乱 kill 安全得多。

路径四:FLUSH HOSTS和临时调大上限。如果是因为某个 IP 达到max_connect_errors被拉黑,FLUSH HOSTS能解锁。临时调大max_connections也能缓一口气,但要清楚这只是争取时间,不是解决问题:

SET GLOBAL max_connections = 2000;

注意:SET GLOBAL改的值重启就没了,应急之后务必同步写进配置文件,否则下次重启回到老值,问题复现。

5.3 不要随手 KILL:KILL QUERY 与 KILL CONNECTION 的区别

现场最容易犯的错误是看到慢 SQL 就KILL,连KILL QUERY和KILL CONNECTION都不区分。

-- 只终止当前语句,连接保留 KILL QUERY 12345; -- 终止语句并断开连接 KILL CONNECTION 12345;

KILL QUERY相对安全,只把正在跑的语句打断,连接还在,客户端能收到错误码然后继续用。代价是有的事务可能处在中间状态,需要应用自己处理回滚。

KILL CONNECTION更粗暴,连接直接断掉,未提交的事务会被回滚。如果这条连接上挂着一个大事务、已经改了几十万行,回滚过程本身可能比重跑还慢,而且在回滚期间还会继续持锁。所以 kill 之前先看一眼事务状态:

SELECT trx_id, trx_state, trx_mysql_thread_id, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS alive_sec, trx_rows_locked, trx_rows_modified FROM information_schema.INNODB_TRX ORDER BY trx_started;

trx_rows_modified很大的那些,尤其是几万几十万的,尽量不要硬杀。真要处理,优先跟业务确认能否分批提交、或者等它自己跑完。

另外,批量 kill 有个常用写法:

-- 生成 kill 语句,人工确认后再执行 SELECT CONCAT('KILL QUERY ', id, ';') FROM information_schema.PROCESSLIST WHERE command = 'Query' AND time > 300 AND user = 'app_user';

先生成、再人工扫一眼、最后执行,这个流程别省。直接拼GROUP_CONCAT一把梭的脚本,早晚会杀错人。

6. 常见问题速查与踩坑记录

6.1 问题速查表

现象最可能的原因优先排查动作
Too many connections报错连接总数超上限看Threads_connected和按 IP 聚合
Threads_connected持续爬升不回落连接泄漏或池配置错误看 Sleep 连接的 IP 分布和空闲时长
Threads_running很高但连接数正常慢 SQL 堆积查events_statements_summary_by_digest
Aborted_connects持续增长认证失败、DNS 反解慢、超时短查错误日志,检查skip_name_resolve
Aborted_clients持续增长客户端非正常退出查应用是否有未关闭的 ResultSet
大量Waiting for table metadata lock有长事务或长查询阻塞 DDL查metadata_locks和INNODB_TRX
建连特别慢(几秒以上)DNS 反解开skip_name_resolve
应用报Communications link failure连接被服务端超时断开池回收时间 vswait_timeout
扩容后立刻连接打满实例数 × 池大小超上限重算容量,调池大小或上限

关于skip_name_resolve这一条展开说一下。MySQL 在建连时会尝试把客户端 IP 反解成主机名,用于匹配授权表里的 Host 规则。如果 DNS 响应慢或者压根没配 DNS,每次建连都要等几秒,高峰期连接全卡在login状态,雪崩。开这个参数之后只按 IP 匹配,建连快很多,但要注意授权表里的 Host 必须写成 IP 或%,写成主机名会失效。这个改动影响面不小,改之前先把账号的 host 规则捋一遍。

6.2 几个我踩过的坑

坑一:只调max_connections不调open_files_limit。曾经把max_connections从 500 调到 2000,重启之后SHOW VARIABLES LIKE 'max_connections'显示的确实是 2000,但实际连到 1000 出头就报错了。查了半天才发现open_files_limit卡在 5000 左右,一个连接要占几个 fd,到 1000 就顶住了。后来在my.cnf和 systemd 两处同时把文件描述符限制放开,问题才解决。

坑二:SHOW PROCESSLIST用普通账号查,误判“连接很空闲”。应用账号没有PROCESS权限,只能看到自己的几条连接,一查只有 3 条,以为数据库很闲。实际上别的服务挂了几百条。后来统一用带PROCESS权限的监控账号采集,或者直接读performance_schema.threads。

坑三:把Sleep连接当垃圾清掉。早期做过一次批量 killSleep连接的操作,结果应用侧瞬间报了一堆连接异常,因为那些连接是连接池里准备复用的,被踢掉之后池子里的对象全失效了。正确做法是让连接池自己管理生命周期,数据库侧只设wait_timeout兜底,不要主动去杀空闲连接。

坑四:扩容时忘了算连接数。这个前面讲过,一次自动扩缩容把实例从 12 个扩到 50 个,每个池 20,瞬间 1000 连接打满 800 的上限。后来在部署模板里加了一条硬约束:池大小 × 最大副本数必须小于数据库上限的 60%,超了直接构建失败。

坑五:监控只采了Threads_connected。有一段时间监控面板上连接数一直很平稳,但业务偶发超时。后来补采了Threads_running才发现,连接数平稳是因为连接池在排队,真实并发早就顶到 CPU 上限了。现在我的监控面板上固定放四条线:Threads_connected、Threads_running、Max_used_connections、Aborted_connects,四条线一起看才能看出问题。

最后分享一个日常习惯:把连接数的采集周期从 1 分钟缩短到 10 秒。连接数是个瞬时指标,1 分钟采一次很容易错过尖峰,尤其是短时批量任务或者连接池集体重连的场景,1 分钟的粒度基本看不出来。10 秒粒度下,那些“看着很平稳突然就爆了”的曲线才露出真面目。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进