ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MySQL连接数爆炸的故障排查指南:从Too many connections到根治方案

MySQL连接数爆炸的故障排查指南:从Too many connections到根治方案 1. 故障第一现场Too many connections 不是一件小事下午三点监控群里突然炸了。先是 zabbix 面板里 MySQL 的 Threads_connected 曲线直接拉满紧跟着业务方发来一连串报错截图核心都是同一句话java.sql.SQLException: Data source rejected establishment of connection, message from server: Too many connections。这套库平时连接数稳定在三百左右我给它配了max_connections500按道理留了将近一半的余量怎么会在一个平平无奇的下午突然被打满先解释一下为什么Too many connections比慢查询更让人头疼。慢查询只是单个请求拖沓影响的是这一条 SQL 背后关联的几个连接而连接数一旦顶到上限所有新连接都会在握手阶段被 MySQL 直接拒绝。业务方看到的不是某个接口变慢而是服务直接瘫痪——连接池里拿不到连接新请求进不来已经进来的请求又可能因为等不到数据库响应而超时整个链路就像停车场入口被一辆堵死的车封住了后面的车排到马路上越堵越多。很多人第一反应是冲上去把max_connections调大比如改成 2000让连接数不再触顶。但你先别急调大这个数字之前得先搞清楚一件事这 500 个连接是被谁占走的是业务并发真的上来了还是有人写了代码只借不还是慢查询把连接占住不撒手还是锁等待导致一堆事务堵在路上病因不同处理方式完全不同。如果连问题根源都没看到就盲目扩容那你只是把炸药的引信又加长了一点下次炸得更凶。所以我登录服务器后的第一件事不是改参数而是先看一眼当前连接状态SHOW STATUS LIKE Threads_connected; SHOW STATUS LIKE Threads_running; SHOW VARIABLES LIKE max_connections;当时看到的结果是Threads_connected498Threads_running23max_connections500。这个组合非常有信息量——连接数已经几乎封顶但真正正在跑 SQL 的线程只有 23 个。换句话说绝大多数连接都闲置着占着茅坑不拉屎。这时候要是直接去调max_connections2000只会让更多空闲连接堆积起来数据库的线程栈、文件描述符、内存开销白白被消耗问题依旧存在。这里也顺带说一个很多新手容易忽略的点MySQL 在连接数打满之后即使你用 root 从本机 socket 登录也可能被拒绝因为max_connections限制的是总数包括本机管理连接。社区版没有专门预留的管理通道所以这时候你只能靠监控脚本提前发现或者寄希望于系统里还残留着某个已经建立的会话可以执行 SQL。这也正是为什么后面我会反复强调“监控一定要在打满之前发现”等到彻底打满再介入可操作空间就非常小了。还有一点必须看清楚Threads_connected指的是“当前有多少条客户端连接”Threads_running才是“当前真正在干活、执行 SQL 的线程数”。二者差距悬殊时脑子里的第一反应不该是“并发太高”而是“连接生命周期管理出了问题”。带着这个判断我开始了第二步排查。1.1 为什么这个报错比慢查询更致命慢查询再慢通常也只是某个接口超时其他请求还能继续走。但连接数爆满不一样它是全局性的数据库不给你建立新连接的机会所有依赖这个库的服务全部受影响。而且是越到业务高峰期越容易触发因为峰值时各服务的连接池都在拼命创建新连接一旦达到上限新连接失败、已有连接被请求占着不放整个系统会进入一个“拿到连接的就阻塞、拿不到连接的就报错”的恶性循环。举个例子一个订单服务在高峰期有 200 个 Tomcat 线程每个线程处理请求时从连接池拿一条连接理论上 200 条连接就够了。但如果某条 SQL 特别慢执行 30 秒还没结束这 200 个线程就会一直占着 200 条连接后续请求又不断进来连接池只能继续创建新连接直到把数据库连接数吃光。慢查询是引子连接数爆满是结果。所以你只盯着连接数看永远抓不到根必须往下去找“谁让连接迟迟不归还”。还遇到过一种情况应用层做了重试机制发现拿不到连接就重试重试本身又会新建连接导致更加雪上加霜。这种时候调大max_connections其实是在给系统喂退烧药烧退了一会儿药劲过了又复发。1.2 我第一件事不是改参数而是先看这两条记录SHOW STATUS和SHOW VARIABLES这两条命令看起来基础但在故障现场比什么都管用。我见过有人一上来就SHOW FULL PROCESSLIST看到一堆Sleep就慌了开始乱 kill结果把正常的连接池预热连接也杀了业务重启后瞬间又爆发一波连接风暴。正确顺序应该是先看连接总数是不是真的接近上限再看正在运行的线程数到底多不多。这两个数字能快速帮你把问题分成两类Threads_connected高、Threads_running低连接堆积大概率是空闲连接没释放问题出在应用连接池或wait_timeout配置上。Threads_connected高、Threads_running也高真的有大并发或慢 SQL需要立刻抓processlist看哪些 SQL 在执行。我当时看到Threads_running23基本就锁定了第一类接下来可以直接去看processlist里的Sleep连接到底有多老、来自哪里。这一步会让你少走至少半小时弯路。2. 用 processlist 把“连接堆满”拆成四种脸谱光知道“连接数高”还不够得知道高在哪个状态。MySQL 的processlist里每一行代表一条客户端连接其中最关键的三个字段是Command、State和Time。Command表示这条连接正在干什么Sleep是空闲Query是在执行 SQLState是这个命令当前到了哪一步Time是已经在这个状态停留的秒数。第一步跑一条分组 SQL快速把当前连接按类型归归类SELECT command, state, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY command, state ORDER BY cnt DESC;这条语句能让你一眼看清连接是大量堆积在Sleep还是都在Query里排队又或者是卡在某个锁状态。之后再针对数量最多的类别深挖效率会高很多。下面是我这次排查时看到的典型分布也是我后来在多次故障里总结出的四种“脸谱”。2.1 用一条 SQL 给连接分类先判断大头在哪在输出里我当时看到的是Sleep有 460 多条Query只有 20 多条剩下的零散分布在Locked和Waiting for table metadata lock。看到这个分布心里其实松了口气没有出现大规模慢查询说明核心数据库本身没被压垮主要是连接长期空闲不释放。再细看一下这些Sleep连接的存活时间SELECT id, user, host, db, command, time, state FROM information_schema.processlist WHERE command Sleep ORDER BY time DESC LIMIT 30;结果非常夸张有几十条连接的空闲时间已经超过 3000 秒也就是接近一个小时。正常业务连接池里的空闲连接通常几十秒到几分钟就会被回收或复用这些上百上千秒的Sleep显然不正常。它们占着连接数、占着线程、占着内存却什么活也不干。如果只是偶尔几条可能是有人用 Navicat 连接后忘了关但一下出现几百条几乎可以肯定是应用层问题。2.2 Sleep 堆积连接池只会借不会还Sleep连接大量堆积最常见的三个原因我按出现频次排列第一应用代码里手动获取了连接但没释放。比如一个定时任务用DriverManager.getConnection()每次新建连接try-catch里只写了查询finally里忘了close()。跑一次任务漏几条跑一个多月连接数就到了天花板。这种问题最阴的地方在于平时你根本察觉不到只有等连接数缓慢爬到上限才开始炸。第二连接池的空闲连接回收参数配得太大。Druid 的minIdle如果设成 50意味着连接池会尽量保持至少 50 条空闲连接minEvictableIdleTimeMillis默认是 30 分钟也就是空闲超过 30 分钟才会考虑回收。如果应用实例很多每个实例都保持不小的minIdle数据库这边看到的Sleep连接就会居高不下。第三wait_timeout太久。MySQL 服务端默认wait_timeout28800秒也就是 8 小时。客户端断开后如果服务端迟迟等不到 TCP 断开信号这条连接会一直留在Sleep状态直到超过wait_timeout才被清理。虽然 MySQL 理论上能感知客户端异常断开但在某些网络环境下半开连接需要很长探测时间导致大量 ghost 连接堆积。我这次遇到的情况就是典型的“只借不会还”某个内部项目组上线了一个报表模块代码里用的是最原始的JdbcTemplate但是工具类里没有统一的连接管理每个查询方法都自己getConnection()只有少部分写了finally close()。结果一个上午就多出两百多条连接加上原有的正常连接直接把 500 打满。2.3 Query 排队慢 SQL 用连接当门票另外一次故障里我看到的Sleep并不多反而是Query状态下面挂着两百多条连接State大多为Sending dataTime普遍在二三十秒左右。这种情况下问题就非常明显这些查询全在等待执行。一查慢查询日志果然有一条 SQL 在跑全表扫描目标表有上千万行没有命中索引单次执行耗时 35 秒。应用是同步调用一个请求占一条连接等这条 SQL 跑完请求线程才能继续连接才会释放。并发一高数据库连接数瞬间就被这类慢 SQL 消耗光了。这里特别说一个容易被误解的点Sending data并不是字面意义上的“正在把数据发给客户端”它其实包括服务端读取数据、生成结果集等一系列操作。看到大量Sending data且Time很长优先怀疑 SQL 性能问题而不是网络问题。快速查看方法SELECT id, user, db, command, time, state, left(info, 80) FROM information_schema.processlist WHERE command Sleep ORDER BY time DESC;info字段能看到正在执行的 SQL 前 80 个字符基本够你判断是不是有人在跑大查询了。如果Time超过 30 秒的都在同一张表上做UPDATE或SELECT那基本就是索引失效或者全表扫描。2.4 Lock 堵塞一个不提交的事务卡住一条街还有一种被很多人忽略的情况连接数爆满不是因为有大量查询而是因为有事务长期不提交把一堆要操作相同行的请求全堵住了。processlist里这类连接的状态通常叫Locked或Waiting for table metadata lock数量可能不大但每一条都堵着一群后续请求越积越多。典型场景是这样的应用在事务里先UPDATE了一行数据然后去调用一个第三方接口第三方接口超时了 10 秒这 10 秒里事务一直没提交锁就一直不释放。其他请求想要更新同一行只能排队等待。如果这种操作同时发生在多行数据上等待的连接就会像滚雪球一样滚起来最终把连接数打满。定位锁等待我一般直接查系统表SELECT * FROM sys.innodb_lock_waits\G这个视图会直接告诉你哪条连接在等锁哪条连接持有锁已经等了多久。找到持有锁的那个连接后再去information_schema.innodb_trx查它的事务状态SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id FROM information_schema.innodb_trx ORDER BY trx_started;如果看到某个事务trx_started已经是几分钟前而trx_state还是RUNNING那基本可以断定它就是堵路的源头。此时要么等它自己提交要么由 DBA 评估后 kill 掉这条连接让它回滚释放锁。3. 定位链条从“连接爆满”追到“谁制造的连接”看到大量Sleep之后我的排查还没有结束。知道连接是“闲着的”还得知道它们是“谁建立”的、从“哪台机器”来的。这一步能帮你确定是某一个服务出了问题还是所有服务集体踩了坑。我常用的一条 SQL 是从processlist里按来源 IP 分组SELECT SUBSTRING_INDEX(host, :, 1) AS client_ip, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY client_ip ORDER BY cnt DESC;当时的输出让我立刻锁定了目标10.0.0.13 一台机器贡献了 300 多条连接而其他几台应用服务器每台只有几十条。这就非常反常了。业务流量是负载均衡分发的正常情况下各台机器连接数应该差不多如果某一台特别高要么是它的连接池配置被人改过要么是它上面的应用出现了连接泄漏。这里顺便提醒一句host字段可能是10.0.0.13:52134这种带端口号的格式所以要用SUBSTRING_INDEX(host, :, 1)把端口去掉。如果你在 Windows 上用 MySQL连接来源显示可能没有端口那就直接用host分组也一样。3.1 按来源 IP 排查连接是均匀分散还是单点喷发定位到具体 IP 之后我登录那台应用服务器去查进程确认上面跑了哪个服务再看它的连接池配置。结果发现那个服务最近上线了一个新版本配置中心里把连接池的maximumPoolSize从 80 改成了 300。改动原因是开发觉得最近接口变慢想通过增加连接数来提高并发结果一个实例就占了 300数据库总共才 500 的额度其他服务瞬间没饭吃了。这类问题在多人协作的团队里特别常见数据库连接数是公共资源但连接池参数是每个应用自己配的。如果没有人做全局预算每个应用都觉得自己多配几条没关系加起来必然爆满。我后来专门做了一个表格把所有应用的实例数、每个实例的连接池上限、部署环境统计清楚算出每个环境的总连接上限必须小于 MySQLmax_connections的 80%发布前检查这个表。3.2 连接池参数联调最大连接数是加法而不是乘法很多人在计算连接池上限时容易犯一个错误只看单个服务的连接池最大连接数忘了乘以实例数。比如一个服务有 6 个实例每个实例连接池最大 100简单算6×100600如果数据库max_connections500那这个服务自己就能把数据库打穿。正确的算法应该这样所有应用实例的连接池最大连接数之和 DBA 预留的管理连接 监控工具连接必须小于数据库 max_connections再留 20% 左右的余量。例如数据库设 500那所有业务连接池加起来尽量不要超过 350400剩下的留给排查工具、备份任务和突发流量。具体到连接池本身还要注意minimumIdle和maximumPoolSize的配合。minimumIdle是为了减少创建连接的开销但如果设得太大每个实例都会保持一堆空闲连接几十个实例加起来就很可观。我一般建议minimumIdle设成maximumPoolSize的一半以下空闲回收时间控制在 5 分钟左右既能保证热连接又不至于长期占用数据库资源。3.3 踩过的坑连接池最大连接数设得比数据库还大再说一个我印象很深的坑。有一次一个同事接手了一个老项目项目是多服务架构所有服务共享同一个 MySQL 用户、连同一个库。他为了提升性能把 HikariCP 的maximumPoolSize设成了 200理由是“单看 200 也没超过数据库的 1000”。但他没算上另外三个服务每个服务也都配了 100300 不等的连接数四个加起来超过 800再加上日常监控脚本和管理员手动排查用的连接高峰期轻松破 1000照样报Too many connections。这个案例最讽刺的地方在于单看任何一个服务连接池上限都不离谱甚至可以说是克制但因为没有人做全局汇总最终集体超卖。后来我养成了习惯每次新服务上线或修改连接池参数都要看一眼线上所有服务的连接池总预算而不是只盯着单个服务问“你的连接池够不够用”。连接池不是越大越好够用且留缓冲才是最好的。4. 止血动作与根治方案当天怎么恢复之后怎么避免定位只是第一步接下来是最考验手速和判断力的环节怎么把当前这 500 个连接降下来让业务先恢复这一步动作太快会误伤太慢会持续雪崩下面这套顺序是我验证过比较稳妥的。4.1 第一刀kill 哪些会话是安全的我的建议是优先处理耗时超长的 Sleep 连接不要去动正在执行事务的 Query 连接。因为KILL一个处于事务中的连接会触发回滚如果用它的事务正好占着重要锁回滚可能需要很长时间反而把系统拖得更慢。对于Sleep连接我一般先看time字段超过 300 秒且来源 IP 明确是某台异常机器的优先 kill。生成批量 kill 语句很直接SELECT CONCAT(KILL , id, ;) AS sql_cmd FROM information_schema.processlist WHERE command Sleep AND time 300 AND user NOT IN (root, monitor);把这组 SQL 复制出来执行即可。但我强烈建议不要一次性全杀先杀 50 条看看业务有没有异常再继续。因为有些连接可能是连接池里的热连接虽然空闲了 300 多秒但随时可能被复用如果杀了太多应用侧连接池会瞬间感知连接断开然后集中重建连接短时间产生大量新建连接的握手请求可能把数据库打得更崩。另外注意权限问题kill 别的用户的连接需要 SUPER 或 CONNECTION_ADMIN 权限。如果业务账号权限不足记得用有管理员权限的账号执行。4.2 临时扩容调 max_connections 前先看三个硬指标如果你判断业务高峰确实比平时高且连接数还有继续暴涨的可能那么临时调大max_connections是可以接受的补救手段。但调之前必须确认三个指标第一是open_files_limit。MySQL 每一条连接都要占用文件描述符此外还有表文件、日志文件等如果系统文件句柄上限不够单纯调大连接数可能让 MySQL 直接报Cant open file或者进程崩溃。查看方式SHOW VARIABLES LIKE open_files_limit;一般要保证open_files_limit至少是max_connections的三到五倍。第二是可用内存。每条连接都有线程栈和缓存开销虽然大部分内存是按需分配但在高并发下如果连接数翻倍内存压力会明显上升。16G 内存跑 1000 个连接问题不大但如果只有 4G建议先评估性价比。第三是 CPU 上下文切换。连接数一旦超过某个量级CPU 大量时间会花在切换线程而不是执行 SQL 上。所以调整max_connections时我同时会看SHOW GLOBAL STATUS LIKE Threads_created;如果线程创建速率异常高说明连接在疯狂地建了又关、关了又建调大连接数治标不治本。临时生效用SET GLOBAL max_connections 1000;但如果重启 MySQL 就会失效所以要持久化到my.cnf的[mysqld]段里。4.3 根治配置连接池、超时、复用三层优化当天止血之后真正的重头戏是防止它再次发生。我从三个层面做了优化第一个层面是应用连接池。把那个异常服务的maximumPoolSize改回合理值同时给所有应用建立了连接池总预算表。如果某个服务确实需要更大的连接数那就先压缩其他服务的额度或者申请单独拆库避免大家挤在一起互相伤害。第二个层面是 MySQL 服务端的连接生命周期。把wait_timeout从默认的 28800 秒调到了 300 秒。这个值的含义是“一条空闲连接最多挂 5 分钟”超过就被服务端回收。对绝大多数业务来说 5 分钟完全够用反而能定期清掉那些“死了没人知道”的连接。同时把interactive_timeout也调成 300 秒这个参数管的是交互式客户端比如命令行连接避免 DBA 或开发手滑留下一堆永不关闭的窗口。第三个层面是防止连接风暴的兜底策略。如果连接数再次达到告警阈值监控脚本会自动记录当时processlist的完整快照方便事后复盘。同时业务方的连接池要开启连接健康检查像 HikariCP 的connectionTestQuery或者 Druid 的testWhileIdle确保被杀掉的连接能很快被替换成新连接而不是反复复用死连接。这里还想补充一个容易被忽略的点不要滥用 PHP 的 mysql_pconnect 这类持久连接。它虽然减少了重复建连的开销但如果业务代码处理不当很容易让连接数长期高居不下清理起来比普通连接麻烦得多。生产环境我宁愿选择连接池方案也不要盲目上持久连接。5. 事后复盘这些监控指标比“连接数”本身更值得盯故障处理完并不算结束。真正让这次事故变成经验的是后来的一次复盘。我把监控体系重做了一遍发现之前只盯着“连接数”一个指标太片面了很多真正有用的信号都被忽略掉了。5.1 zabbix 监控 TCP 连接数和 MySQL 内部连接数的区别团队里有人用 zabbix 去监控 Windows 服务器的 TCP 连接数看到数值高就报警说 MySQL 连接爆满。这里有个容易被混淆的地方操作系统层的 TCP 连接数和 MySQL 内部的 Threads_connected 并不是一回事。TCP 连接数是网络层的只要建立了 TCP 四元组就算一条连接哪怕它还没通过 MySQL 认证或者已经断开了还在 TIME_WAIT 状态中残留。而Threads_connected是 MySQL 内部已经认证成功、分配了线程的连接数。监控探活、负载均衡健康检查、Navicat 的未关闭窗口这些都不一定算业务连接却会计入 TCP 连接数里。所以只盯操作系统 TCP 连接数很容易出现“连接数告警了但 MySQL 其实没事”的误报。真正要盯的应该是 MySQL 实例暴露出来的状态值比如用 zabbix 的 MySQL 模板采集Threads_connected、Threads_running和Slow_queries。如果数据库跑在 Windows 上还要注意 zabbix 采集器要用独立的监控账号连接不要占用业务连接池的额度否则监控本身反而成了压垮数据库的一根稻草。5.2 我的连接数告警阈值设置经验关于阈值我的习惯是分两级max_connections的 70% 出 warning85% 出 critical。比如连接数上限 500那么 350 时就要收到告警425 时必须紧急处理。为什么留这么大的缓冲因为从收到告警到登录服务器执行排查至少需要几分钟如果连接数已经涨到 480 你才开始看 processlist很可能还没找出原因就已经打满了。对于高优先级系统我还会额外配一个“连接数增长速度”的告警。如果五分钟内Threads_connected涨了 100 多即便绝对值还没到阈值也要触发通知。这种陡增往往意味着连接池泄漏或 SQL 突变越早介入越好治。5.3 从一次误判学到的Threads_running 与 Threads_connected 要分开看最后想分享一个我踩过的实际坑。在我刚接触 MySQL 运维时有一次监控显示连接数并不高只有 100 多但系统负载很高。我盯着连接数看了半天觉得一切正常直到同事提醒我看了Threads_running才发现这个数字已经飙到 80 多。正常情况下100 条连接里只有几条正在执行 SQL而当时 80 条都在跑说明有大量慢 SQL 在并发执行CPU 全被它们耗光了。连接数不高数据库照样可以被拖死。反过来也有一次连接数爆满但Threads_running只有个位数怎么查慢查询都查不出来后来才发现是连接池泄漏。所以我现在无论看监控还是处理故障都是把Threads_connected和Threads_running放在一起看前者反映“占了多少资源”后者反映“真正在消耗什么资源”。只有两者结合起来才能准确判断是该查 SQL、该查锁还是该查连接池配置。处理完这次故障后我在运维笔记里新增了一句话连接数爆满不是病根是症状。它只是无数可能性汇聚到同一个出口的结果。真正的排查价值在于透过这个症状往下挖一层看到那些占着连接却不干活的线程、那些慢到离谱的查询、那个忘了提交的事务然后针对病灶做手术而不是一味给 MySQL 加号。希望我这趟完整的定位过程能让你下一次面对连接数爆满时少一点慌乱多一条清晰的排查路径。
返回列表