
哪天真被那句 Too many connections 拍在脸上的时候才会意识到 MySQL 的连接数不是个小问题。我见过不少团队应用一上线就疯狂报数据库连接失败翻日志全是 Too many connections第一反应是升级配置其实很多时候是连接数上限设得太保守或者应用侧的连接池参数没调好。这篇文章围绕 MySQL 数据库连接数的查询与配置把从查看当前连接、分析连接来源到调整 max_connections、wait_timeout 等参数的完整思路捋一遍适合刚接触数据库维护的开发、运维也适合那些正在排查线上连接数异常的人参考。内容会覆盖三类核心问题怎么查当前连接数、怎么定位每个连接是谁创建的、怎么合理地设置连接数上限。前两件事解决看得见最后一件解决管得住。中间穿插一些我实际踩过的坑比如连接池打满后数据库假死、wait_timeout 设置太久导致连接堆积这些坑如果你不提前知道排查起来真的会怀疑人生。1. 连接数到底是怎么消耗资源的1.1 一个连接对应一条会话线程很多对 MySQL 还不太熟的同事会把连接数理解成一个简单的数字实际上每一个连接背后都对应着一整套资源占用。当你通过客户端、应用框架或者命令行登录 MySQL 时服务端会先完成 TCP 握手和身份认证然后为这个会话分配一个线程。这个线程在会话存活期间是常驻的不管你有没有在跑 SQL它都在那里占着内存、占着 CPU 调度资源。如果把 MySQL 比作一家餐厅连接数就是同时进店的客人数量。餐厅的座位是有限的如果同时涌进来几百个客人后厨执行 SQL 的线程就会忙不过来更严重的是连排队等候区都会被挤爆。MySQL 的连接管理机制也是类似的逻辑max_connections 限制的就是最多同时允许多少个客人进店超过的只能得到一句抱歉客满了。这个类比虽然简单但能解释很多现象。比如为什么连接数打满时整个数据库好像死了一样因为不只是新连接进不来已有的连接也可能因为资源争抢而变慢。MySQL 在接近连接数上限时还会用一部分资源去处理被拒绝的连接这个动作本身这会让情况雪上加霜。所以连接数管理从来不是改个数字那么简单它牵涉到线程调度、内存分配、应用行为等多个层面。1.2 每个连接大概吃多少内存连接占用的内存主要是两部分一部分是线程基础栈空间另一部分是各类会话级 buffer。MySQL 里和连接相关且经常需要关注的参数包括 sort_buffer_size、join_buffer_size、read_buffer_size、read_rnd_buffer_size 等。这些参数虽然是按需分配的但一旦触发排序、关联查询或顺序读就会立刻从内存里划走对应的空间而不是等你手动去释放。简单算一笔账假设一台服务器上 sort_buffer_size 是 2MBjoin_buffer_size 是 2MBread_buffer_size 是 1MB再加上线程栈和元数据缓存的固定开销单个连接在极端情况下吃到 5~6MB 内存并不稀奇。如果有 1000 个并发连接光会话级 buffer 就可能吃掉 5~6GB 内存这还没有算 InnoDB 缓冲池和其他全局缓存。所以说连接数不是想调多高就调多高它和服务器内存是硬绑定关系配置之前必须先算账。还有一个容易忽略的点是连接的创建和销毁本身也很贵。每次新建连接都要经历 TCP 三次握手、MySQL 协议握手、权限校验、初始化会话状态这个过程在并发量高的时候会显著拖慢应用响应。这也是为什么应用层几乎都会配连接池而不是请求来了现连一个。既然连接池会在应用侧保持一批常驻连接数据库侧的连接数规划就必须把连接池的大小也考虑进去两边必须联动否则要么池子太小不够用要么池子太大直接把数据库打满。1.3 连接泄漏是怎么把资源吃光的有一种情况特别坑叫连接泄漏。代码里获取了连接正常应该在 finally 块里释放但总会有各种原因导致连接没有归还比如异常被吞掉、事务迟迟不提交、ORM 框架使用不当等。泄漏的连接不会立刻消失它会在 MySQL 侧以 Sleep 状态挂在那里一直占用线程和内存。我处理过一个案例应用代码里有个循环每个分支都会 new 一个数据库连接但只有正常路径调用了 close()异常路径直接抛出去了。结果线上跑了三天连接数从 50 慢慢涨到 2000最后把整个库打挂。这种问题很难靠数据库侧配置解决因为只要泄漏速度大于 wait_timeout 的回收速度连接数就会持续上涨。所以排查连接数异常时一定要记住连接数不只是数据库的配置问题也是应用的资源管理问题。2. 怎么查当前连接数与连接明细2.1 一条命令看懂当前连接状态查询当前连接数最核心的命令是SHOW STATUS LIKE Threads_connected;这条命令返回的数值就是当前时刻 MySQL 上活跃的客户端连接数量是现在有多少连接最直接的答案。与之配套的还有另外几条值得一起看的状态项。SHOW STATUS LIKE Threads_running; SHOW STATUS LIKE Max_used_connections; SHOW GLOBAL STATUS LIKE Connection_errors_max_connections;Threads_running 表示正在执行 SQL 的线程数通常远小于连接数因为大多数连接是在等待应用发起下一次请求。Max_used_connections 是自 MySQL 启动以来曾经达到过的最大连接数这个值非常关键它可以告诉你历史峰值到底有没有逼近过上限。Connection_errors_max_connections 统计的是因为超过连接上限而被拒绝的次数只要这个值大于 0就说明此前确实发生过连接被拒的情况。还有一种更快速的命令行方式直接用 mysqladminmysqladmin -uroot -p status返回的结果里有一项 Threads就是当前的连接数。这条命令适合在 shell 脚本里用于快速巡检比进入 mysql 客户端敲 SQL 方便得多。我实际维护的服务器上就挂着这样一个 crontab每 5 分钟跑一次 mysqladmin status把 Threads 数据和系统负载写进日志出问题时能调出历史曲线。2.2 从 information_schema.processlist 看每个连接的来源只看总数还不够当连接数异常升高时你需要知道这些连接到底是谁创建的、从哪里来的、正在干什么。这时候要查 information_schema.processlist 表SELECT id, user, host, db, command, time, state, LEFT(info, 80) AS sql_preview FROM information_schema.processlist ORDER BY time DESC;这张表包含的信息非常全id 是连接线程的 IDhost 显示连接的 IP 和端口db 是当前选中的数据库command 表示连接当前的状态Sleep、Query、Locked 等time 是当前状态的持续时间info 是最新执行的 SQL 前 80 个字符。实际排查中我一般会先按 time 降序看有没有长时间 Sleep 的连接再按 user 和 host 分组看是不是某个应用服务器占了大量连接。如果想知道每个来源 IP 各占了多少连接可以用这条聚合查询SELECT SUBSTRING_INDEX(host, :, 1) AS client_ip, COUNT(*) AS connection_count FROM information_schema.processlist GROUP BY client_ip ORDER BY connection_count DESC;这条语句会在排查中反复用到。有一次线上连接数告警我执行完这条聚合查询一眼就看出某一个应用服务器的 IP 占了 80% 的连接赶紧去查那台机器上的服务状态发现是它的连接池配置被某次发布改回了默认值瞬间就从 20 个连接涨到了 500 个。2.3 再往里挖一层线程状态和引擎状态processlist 里的 state 字段很多人不太看它其实能帮你判断连接正在经历什么阶段。常见的值包括Sending data 表示正在读取数据并发送给客户端Sorting result 表示正在排序Copying to tmp table 表示正在把中间结果写入临时表Waiting for table metadata lock 表示被元数据锁卡住了Statistics 表示正在计算统计信息。遇到某些疑难杂症时我还会用 SHOW ENGINE INNODB STATUS 来确认事务和锁的情况SHOW ENGINE INNODB STATUS\G这个命令输出的内容很长但关键看 TRANSACTIONS 段它会列出当前活动事务等待锁的信息。有一次我发现连接数正常但 Threads_running 一直很高一查发现是一条 update 语句持有了某张表的行锁后面几十个查询全在等待锁释放。这种问题只看连接数根本看不出来必须把线程状态和引擎状态结合起来看才能定位。2.4 连接明细表打不开时的替代方案有时候 information_schema.processlist 也会出问题尤其是连接数已经接近上限时执行这条查询本身也可能需要分配额外资源。而且 SELECT * 会把 info 字段的内容全量拿出来如果连接里有很多超长 SQL反而更容易卡住。所以我习惯先只查 id、host、command 这些轻量字段或者直接查 performance_schema 里的 threads 表SELECT THREAD_ID, PROCESSLIST_ID, PROCESSLIST_USER, PROCESSLIST_HOST, PROCESSLIST_STATE FROM performance_schema.threads WHERE PROCESSLIST_ID IS NOT NULL;performance_schema 的 threads 表能看到更底层的线程信息包括后台线程和前台会话线程而且它是通过内存表实现的查询开销比 processlist 更稳定。当然在绝大多数场景下 processlist 就够用了performance_schema 更多是当你需要深入分析线程状态时才用得上。在连接数打满、半只脚踩进事故现场时记住这件事能拿到连接明细比什么都重要有时候一条轻量查询比一堆复杂分析更能救命。3. 连接数上限的配置思路与实践3.1 max_connections 到底该设为多大查询当前上限用这条SHOW VARIABLES LIKE max_connections;默认情况下 MySQL 8.0 的 max_connections 是 151这个值对很多小业务来说其实够用但一旦应用实例较多、连接池每个实例又开得比较大151 很快就会被占满。常见的做法是结合前文提到的内存预估来定一个合理的值。一个简化的预估过程是这样的先看服务器物理内存总量预留操作系统和其他进程的内存通常留 20%~30%从剩余内存里划出 InnoDB buffer pool 的部分这个一般占 50%~60% 总内存剩下的空间再除以单连接预估内存默认按 2MB~4MB 保守估计得出一个参考连接数同时参考 Max_used_connections 的历史峰值最终值要在峰值基础上留 20% 左右的余量。举一个具体例子服务器 16GB 内存InnoDB buffer pool 设为 8GB操作系统和应用预留 3GB剩下还有 5GB。单连接按 3MB 估算适合的连接数上限就是 5GB 除以 3MB约 1700。如果历史峰值到过 800日常均值 300~400那 max_connections 设成 1500 或 2000 就挺合理。当然如果应用侧查询里有大量排序和 join单连接内存就不能按 3MB 算要适当上调。3.2 配套参数千万别漏掉只调 max_connections 不够和连接数相关的配置还有几个它们的联动关系很容易被忽略。wait_timeout 是服务端关闭一个非交互连接前的等待时间默认 8 小时。如果一个应用拿到连接后不释放这个连接就会在 MySQL 上挂很久。实际生产环境我一般会调到 60~120 秒这样即使应用连接池有泄漏也能在 MySQL 侧及时回收。interactive_timeout 针对的是通过命令行交互式连接的空闲超时它的默认值也是 8 小时如果管理工具连接过多也得相应调小。thread_cache_size 控制线程缓存数量。连接断开后MySQL 不会马上销毁线程而是放进缓存供新连接重用。这个值设置得合适可以减少频繁建连、断连带来的线程创建开销。一般说来thread_cache_size 设为 16~64 就够了不必追求太大。back_log 表示 MySQL 在停止接受新请求前可以堆积的连接请求数量。如果在高并发瞬间连接数打满back_log 决定有多少请求可以排队等待。它的值通常受操作系统限制Linux 下默认值是 80适当调大是可行的但不能无限调设置完最好压测验证一下。这几个参数看起来各管各的实际上是一套组合拳。wait_timeout 控制连接的回收速度thread_cache_size 控制线程的重用效率back_log 控制突发高峰的排队能力。单独调大 max_connections 却不管配套参数等于只扩大了门口没考虑排队区和座位安排迟早还会出问题。3.3 修改配置的两种正确姿势第一种是临时修改不需要重启数据库适合处理线上突发情况SET GLOBAL max_connections 300; SET GLOBAL wait_timeout 120; SET GLOBAL thread_cache_size 32;注意SET GLOBAL 只对之后新建的连接生效已有的旧连接不会立刻被新 wait_timeout 约束。而且这种修改重启后会失效只适合临时处理问题。第二种是永久修改编辑 MySQL 配置文件 my.cnf 或 my.ini在 [mysqld] 段落下添加配置[mysqld] max_connections 300 max_user_connections 150 wait_timeout 120 interactive_timeout 120 thread_cache_size 32 back_log 128修改完成后需要重启 MySQL 服务或者通过 SET GLOBAL 先让它当场生效等下次重启时配置文件中已经写好的值也会一并生效。我个人的习惯是配置文件和 SET GLOBAL 双管齐下先在线调整保证业务不中断再顺手把配置文件改好避免哪天数据库重启又退回原来的值造成二次事故。这里还有一个细节值得注意MySQL 8.0 之后引入了持久化系统变量你可以用 SET PERSIST 让变量在重启后仍然保留SET PERSIST max_connections 300;这条命令会把配置写入 mysqld-auto.cnf 文件相当于把在线调整和永久生效一步到位。不过它和手工编辑 my.cnf 两种方式不要混用容易搞不清配置到底生效在哪里我建议团队里明确一个统一入口要么全部走 SET PERSIST要么全部走配置文件。3.4 按用户限制连接数还有一种场景值得单独说就是多个业务共用一个 MySQL 实例时某个业务连接数暴涨会拖垮其他业务。这时可以给每个业务账号加连接数上限GRANT USAGE ON *.* TO app_user% WITH MAX_USER_CONNECTIONS 100;或者通过参数 max_user_connections 在全局层面限制每个账号最多同时建立多少个连接。这个方法特别适合数据库账号分配给了多个应用团队、大家各自也不知道对方连接池配置的场合。我接手过一个现场三个业务系统共用一个 MySQL其中一个应用的连接池配置出现了问题瞬间把 max_connections 打满另外两个业务全线报错。后来给每个账号单独设了 MAX_USER_CONNECTIONS互相隔离之后才彻底安心。这种做法的本质是故障隔离。数据库层面把每个账号的资源使用圈定在可控范围内即使某个应用写崩了影响面也不会扩散到整个实例。在预算有限、无法给每个业务单独拆库的团队里这可能是性价比最高的保护手段。4. 应用侧连接池与 MySQL 连接数的联动4.1 一个连接池配置错误引发的连坐事故我在前面提过连接池和 MySQL 连接数必须联动这里展开说一个实际案例。有个项目用的是 HikariCP某次发布把 maximumPoolSize 从 20 调成了 200理由是高峰期查询太慢想多开点连接提高并发。结果应用有 10 个实例每个实例 200 个连接瞬间就是 2000 个连接同时打到 MySQL 上max_connections 只有 500整个库直接拒绝服务。这个案例的问题不是某个参数调错了而是缺少全链路视角。连接池里的每个连接对应 MySQL 侧的一个真实会话应用实例数乘以连接池大小才是数据库实际承受的并发连接数。调整连接池参数时一定要算一下所有应用实例加起来的总连接数再对比 MySQL 的 max_connections两边要留出足够余量。4.2 连接池核心参数与 MySQL 参数的对照关系下面这张对照表可以帮你快速理解连接池参数和 MySQL 参数之间的对应关系调整时有据可依。连接池参数对应的 MySQL 侧风险联动调整建议maximumPoolSize 过大总连接数超过 max_connections总连接数控制在 max_connections 的 70% 以内minimumIdle 过大大量空闲连接长期占用内存不要让空闲连接数超过业务低谷期实际用量connectionTimeout 过短连接耗尽时应用快速报错配合 MySQL max_connections 合理设计超时时间idleTimeout 过长空闲连接在 MySQL 侧堆积保持 idleTimeout 小于 wait_timeoutmaxLifetime 过长连接异常后长时间不被替换maxLifetime 要比数据库侧的 wait_timeout 短这条对应关系是我在实际维护中慢慢摸出来的。比如 idleTimeout 和 wait_timeout如果连接池的空闲超时比 MySQL 的 wait_timeout 更长那么连接在 MySQL 侧被断开时应用侧还不知道使用时才拿到一个失效连接触发额外的重连逻辑。反过来如果 idleTimeout 设置得太短连接池会频繁销毁重建连接建连开销又被放大。4.3 多个应用实例时的连接数估算一个很现实的场景是应用做水平扩展实例数从 2 个扩到 10 个。很多人只关注应用本身的负载均衡忽略了每个实例都有一份连接池连接总数会线性增长。假设单个实例连接池 maximumPoolSize 是 50实例数从 2 扩到 10连接总数就从 100 涨到 500。如果 MySQL 的 max_connections 还停留在原来的 300扩容后的第一波流量就会触发连接拒绝。所以每次扩容前都要做一次乘法应用实例数乘以单实例连接池配置得出数据库侧预期的最大连接数再和 max_connections 比对。还有一个容易踩的坑是微服务架构下一个业务调用链上可能涉及多个服务每个服务都有自己的数据库连接池。虽然每个服务的连接数看着都不高但叠加起来可能很惊人。我建议每个团队在接入新服务时把数据库连接数的预算作为一个整体指标来追踪而不是各管各的池子。5. 常见问题与排查技巧实录5.1 真的遇到 Too many connections 了怎么办当应用开始报 Too many connections 错误时最重要的一点是别慌也别立刻重启 MySQL。重启确实能清空连接但紧接着所有应用会同时重新建连瞬间又可能把连接数打满反而造成二次故障。正确的应急流程应该是先想办法登录 MySQL哪怕只保留一个管理通道。可以通过 mysql -uroot -p 本地登录必要时先用 SET GLOBAL 临时加大 max_connections。如果 root 也登不进去可能需要通过 skip-networking 模式临时启动数据库这种方式只允许本地 socket 登录先断开异常连接源再恢复正常模式。登录后立刻查询 processlist找到占用连接数的元凶。绝大多数情况是某个应用 IP 或者某个账号的连接特别多直接和对应的开发负责人确认是不是连接池参数配错了。如果是连接堆积可以手动 kill 掉长时间 Sleep 的连接KILL 12345;或者批量生成 kill 语句SELECT CONCAT(KILL , id, ;) FROM information_schema.processlist WHERE command Sleep AND time 300;这条命令会生成一串 KILL 语句复制出来执行就可以清理掉空闲超过 5 分钟的连接。注意动手前先确认那些连接确实没有正在跑任务杀错正在执行的连接会造成业务请求异常。我在生产环境有过一次手滑把还在跑报表查询的连接给 kill 了结果当天运营那边就来找我问数据导出怎么中断了。从那以后我再执行批量 kill 之前一定会先看一眼 state 和 info 字段。5.2 连接数稳定但不低的排查思路有一种比较隐蔽的问题连接数不算特别高但始终压在一个较高的水平稍微有一点流量波动就会顶到上限。这时候要关注的不是 max_connections 够不够而是连接的消耗速度。我的排查思路是按照下面的顺序来。第一看应用侧连接池配置。连接池的 minimumIdle 和 maximumPoolSize 设置不合理是常见原因。minimumIdle 设得过大应用一启动就会建立一堆空闲连接挂在 MySQL 上maximumPoolSize 设得过大在高并发时会把数据库连接数拉得很高。连接池里每个连接在 MySQL 侧都是一个真实会话应用越多叠加效应越明显。第二看连接是否被频繁创建销毁。如果应用代码里每次请求都新建连接、用完再 close那即使并发量不高MySQL 侧也会不断经历建连、断连的过程Threads_created 状态值会持续增长。遇到这种情况优先推动应用侧接入连接池不要把建连开销留给每一次请求。第三看有没有慢查询拉着连接不放。一个查询跑 30 秒期间它的连接肯定一直占着而且还会占用一个执行线程。如果慢查询很多即使 QPS 不高连接池也会被长期占满。这时候要配合慢查询日志定位具体 SQL先把慢查询解决掉连接数往往会自然下降。5.3 连接被 sleep 占满的经典场景关于连接占而不释放的问题我想多说一个我反复遇到的场景。有些团队使用 ORM 框架但事务管理没做好比如在方法上加了 Transactional 注解内部却进行了长时间的远程调用或者等待。事务不结束连接就一直不归还时间一长连接池就耗尽了。这种问题的典型特征是 MySQL 侧能看到大量 Sleep 状态的连接而且 time 值不小。遇到这种情况kill 连接只能救急真正要修改的是事务边界。让代码尽快提交或回滚事务避免在事务里做耗时操作才能从根本上解决连接被占住的问题。必要时还可以通过 SHOW ENGINE INNODB STATUS 查看是否有长事务在持有锁进一步判断连接长时间不释放是否和锁竞争有关。还有一个容易被忽略的场景是备份任务和定时任务。比如某个凌晨跑批的存储过程如果设计得不好会对某些表加上长事务锁第二天早上业务高峰期连接数就会异常。这类任务的排查逻辑是先把连接数和时间点做关联分析看连接数暴涨是不是固定发生在某个时刻如果是就去查那个时间点的定时任务和批处理脚本。5.4 连接数监控与预警建议不管配置调得多合理连接数的监控都是必须做的。我的建议是至少监控以下几个指标Threads_connected 当前连接数、Max_used_connections 历史峰值连接数、Connection_errors_max_connections 被拒绝连接数、Threads_running 正在执行 SQL 的线程数、Threads_created 累计创建线程数。预警规则可以参考这样的思路如果 Threads_connected 连续 5 分钟超过了 max_connections 的 80%就触发告警如果 Connection_errors_max_connections 持续增长直接告 P0因为这已经说明有连接被拒了。Threads_created 通常只需要关注趋势如果持续高速增长说明应用侧建连频繁连接池没有生效。监控工具层面Zabbix 和 Prometheus 都有 MySQL 相关的采集插件。如果还没有接入监控系统也可以用一条简单的 shell 脚本定时采集#!/bin/bash MYSQL_CMDmysql -uroot -pYourPassword -N CURRENT$(echo SHOW STATUS LIKE Threads_connected | $MYSQL_CMD | awk {print $2}) MAXCONN$(echo SHOW VARIABLES LIKE max_connections | $MYSQL_CMD | awk {print $2}) echo $(date %F %T) current$CURRENT max$MAXCONN /var/log/mysql_conn.log这个脚本每 5 分钟跑一次把连接数和上限写进日志文件后期也能用来观察连接数的变化趋势。虽然简陋但在没有成熟监控系统的团队里它确实能帮你提前发现连接数异常。我见过不止一次连接数日志里能清晰看到每天早上 9 点之后开始爬坡到 11 点冲顶下午回落这种规律性信息对后续容量规划特别有价值。5.5 一条 SQL 手工检查连接数的速查表最后整理一张速查表方便大家在维护数据库时快速找到对应的命令。我把日常最常用的几条查询都列出来建议直接收藏。想做什么执行的命令查看当前连接数SHOW STATUS LIKE Threads_connected;查看连接数上限SHOW VARIABLES LIKE max_connections;查看历史最大连接数SHOW STATUS LIKE Max_used_connections;查看被拒连接数SHOW GLOBAL STATUS LIKE Connection_errors_max_connections;查看每个连接的明细SELECT * FROM information_schema.processlist;按来源 IP 统计连接SELECT SUBSTRING_INDEX(host, :, 1), COUNT(*) FROM information_schema.processlist GROUP BY host;杀掉一个连接KILL id;临时调整连接数上限SET GLOBAL max_connections 500;我个人在实际排查中最大的体会是连接数的管理并不是调一个参数就完事的它更像是数据库侧和应用侧的一场协同战。数据库侧要看 max_connections、wait_timeout、thread_cache_size 这些参数是否匹配应用侧要看连接池的大小、事务边界是否合理。只要有一方失控另一方再努力也会很被动。另一个值得提醒的点是任何参数的调整都要有数据支撑。不要拍脑袋把 max_connections 调到 50000而是在每次连接数告警时把当时的 Threads_connected、Max_used_connections、慢查询记录都留下来积累几周之后再做判断。有了这些历史数据你才能知道当前业务的连接数趋势到底是怎么走的调整参数时才有底气。最后再分享一个小技巧。在排查连接数问题时顺手把 processlist 里出现的所有 IP 整理成一个白名单清单这个习惯能省很多事。下次再看到陌生 IP 出现在连接列表里你就能第一时间判断是有新服务接入了还是有异常来源在尝试连接。这个清单不需要很正式一个文档就行但它能帮你建立对线上连接来源的完整认知排查问题时会快很多。