凌晨两点被电话叫起来业务方说一批接口在超时登录数据库第一件事往往就是看 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_NAMEMax_used_connections) AS peak, (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAMEmax_connections) AS max_conn, ROUND( (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAMEMax_used_connections) / (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAMEmax_connections) * 100, 2 ) AS used_pct;注意Max_used_connections是自 mysqld 启动以来的累计峰值不会自动衰减。一次凌晨批量任务把峰值推到 900这个数字会一直挂在 900看水位时要结合业务周期判断别被历史峰值误导。1.2 为什么 Threads_connected 和 PROCESSLIST 行数经常对不上这是个高频困惑。明明Threads_connected显示 120SHOW 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 -20ss出来的数字通常比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_usedmysqladmin 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_connectedQueries 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/DISTINCTCreating 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 288008 小时很多连接池默认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然后发现更慢了。原因很简单连接是操作系统线程每个连接都有独立的栈空间和会话内存连接数上去之后上下文切换、内存占用、锁竞争全都跟着涨吞吐不一定提升延迟反而恶化。正确的做法是先算业务到底需要多少并发连接。这里可以借用利特尔法则Littles Law平均并发数 到达速率 × 平均停留时间。放到数据库场景期望并发执行语句数 ≈ QPS × 单条语句平均耗时秒举个具体例子。某库峰值 QPS 3000平均 SQL 耗时 8ms并发执行语句数 3000 × 0.008 24也就是说理论上只要有 24 个连接同时在跑就能扛住这个压力前提是 SQL 都很快。当然现实中要考虑突发、慢查询、锁等待一般乘 3~5 倍安全系数得到 72~120 个“活跃连接”就绰绰有余了。那剩下的几百个连接是什么是持有但闲置的。连接总数取决于连接池怎么配而不是数据库能力。所以真正要做的算术是这一条max_connections ≥ Σ(各应用实例数 × 单实例池上限) 后台任务 监控探针 运维预留拿一个中等规模的线上系统举例来源实例数单实例连接小计核心服务连接池3010300定时任务5525数据同步任务31030报表/离线查询22040监控探针 运维客户端——20合计415415 是基线考虑业务高峰时连接池会打满、滚动发布时新旧实例并存瞬时可能翻倍、以及故障时人工介入要连得进去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 不要随手 KILLKILL 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_digestAborted_connects持续增长认证失败、DNS 反解慢、超时短查错误日志检查skip_name_resolveAborted_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 秒粒度下那些“看着很平稳突然就爆了”的曲线才露出真面目。