我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

MySQL连接数过高排查与配置:从Threads_connected到max_connections

MySQL连接数过高排查与配置:从Threads_connected到max_connections 做后端的同学大概率都遇到过这种场景半夜收到告警业务接口开始报Too many connections打开监控一看连接数曲线像火箭一样往上蹿。我第一次被这个问题折腾是在给一个内部系统做压测的晚上当时对 MySQL 数据库连接数的了解还停留在“查询一下、不行就调大”的层面结果越调越糟。这篇文章想跟你聊的就是围绕 MySQL 连接数的查询、配置展开的一整套思路从看懂状态指标到算清楚参数再到连接飙高时怎么排查尽量把我会踩的坑都提前告诉你。连接数这件事放在整个数据库运维里不算最复杂的技术但它特别容易成为压垮系统的“最后一根稻草”。你去看大部分线上故障很多都不是 SQL 本身多复杂而是某个凌晨的连接数突然被打满新的请求全部被拒在数据库门外然后应用层开始疯狂超时、堆积、雪崩。所以连接数的管理本质上是一个“预判 容量规划 应急”的综合活需要在平时就把数据和经验攒下来。这篇内容适合正在接触 MySQL 的应用开发、后端研发、初级 DBA以及那些被连接数告警折磨过的同学。我会从查询命令讲起逐步深入到参数配置、容量估算和排障方案保证你看完能直接用起来而不是只知道几个状态变量名字。1. 连接数是什么为什么总有人栽在这里1.1 一条 MySQL 连接背后到底占了多少资源很多人觉得连接数就是个数字大不了调大max_connections就行。但你要真去理解 MySQL 的连接机制就会发现它牵扯的资源比你想象的多得多。从客户端发起连接开始MySQL 服务器端要为这个连接做几件事完成 TCP 握手、鉴权、分配一个线程thread然后这个线程带着自己的内存上下文开始工作。在 MySQL 8.0 中每个连接至少对应一个线程线程栈默认是 1MB常见版本可以通过thread_stack调整。注意这个 1MB 不是全部内存它只是固定分配的线程栈空间。除此之外每个会话还有一堆 buffer 的上限值例如sort_buffer_size、join_buffer_size、read_buffer_size、net_buffer_length等。也就是说一个很简单的连接在最坏情况下可能吃掉几 MB 甚至几十 MB 的内存。你可能会问既然这么多 buffer为什么线上 500 个连接还没挂因为这些 buffer 大多是“按需分配”的连接空闲时只占线程栈和最小化的网络缓冲真正执行复杂查询、排序、join 的时候才把 buffer 拉到上限。所以评估连接数对内存的影响不能简单用“连接数 × 所有buffer上限”来算而是要结合业务的实际查询特征。不过有一点是确定的每条连接都有固定开销连接是无状态资源中的“重资产”。连接数从 0 涨到 2000数据库端的线程调度、上下文切换、内存占用都会明显上升。尤其是在高并发场景下大量线程互相切换带来的 CPU 开销有时候比 SQL 执行本身还大。1.2 连接数和并发别把这两个概念搞混这是我在排查中经常要纠正的一个误区Threads_connected高不代表并发高反过来并发高的时候连接数不一定高。拿一个典型场景来说某系统连接池配置了 200 个最大连接业务高峰期Threads_connected稳定在 200但Threads_running可能只有 20。为什么因为大部分连接在等待应用端分配任务或者坐在池子里睡大觉真正在执行 SQL 的线程只是其中一小部分。Threads_running才是执行语句的线程数它更能反映数据库的“忙碌程度”。所以连接数和并发是两个维度的指标。连接数反映的是客户端会话规模并发执行线程数反映的是数据库的繁忙度。排查问题时如果只看连接数容易把问题带偏。正确的做法是先看Threads_running和Threads_connected的比例如果 running 很高说明数据库真的在被大量查询轰炸如果 running 很低但 connected 很高那大概率是连接堆积、持有不释放应用端连接池配置或者 SQL 慢导致的问题。2. 查询连接数几条命令把家底摸清楚2.1 状态变量怎么看先把指标含义吃透连接数的“查询”看起来是一句命令的事但里面几个状态变量的含义一定要掰扯清楚。我最常用的查询命令是这一组SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Threads_running; SHOW GLOBAL STATUS LIKE Threads_created; SHOW GLOBAL STATUS LIKE Connections; SHOW GLOBAL STATUS LIKE Max_used_connections; SHOW GLOBAL STATUS LIKE Aborted_connects;逐个拆解一下Threads_connected是当前时刻所有打开的连接数包含空闲连接这是你看连接池水位最直接的指标。Threads_created是自 MySQL 启动以来创建过的线程总数。这个值除以运行时间能得到平均每秒新建线程数。如果这个数值持续大幅增长说明应用在频繁地建立和销毁连接而不是复用连接。这种场景下就算Threads_connected不高数据库也会因为频繁创建线程而产生不小的开销。Connections是历史累计连接次数包括成功的和失败的。它适合用来观察某个时间窗口内的新建连接速率。Max_used_connections是自启动以来出现过的最大的连接数峰值这个指标简直是容量规划神器。每次改配置之前先看一眼它你就能知道系统到底有没有触顶过。这套指标组合起来基本能回答三个问题现在有多少连接、历史上最多有多少连接、连接是否在频繁创建。2.2 processlist 才是排查的主战场状态变量告诉你“连接多不多”但不会告诉你“连接在干什么”。想知道连接在干什么必须看processlist或performance_schema里的会话信息。最朴素也最常用的命令是SHOW FULL PROCESSLIST;它会列出当前所有连接字段包括Id、User、Host、db、Command、Time、State、Info。我排查连接数告警时第一眼永远是看Command和State两列。Command常见值有Sleep连接空闲等客户端发下一条命令。大量 Sleep 说明应用持有连接但没用常见于连接池偏大且低峰期闲置。Query正在执行语句。Binlog Dump主从复制的接收线程正常现象别误杀。Connect正在建立连接的 “认证握手”阶段如果这个很多说明应用正在大量创建新连接。State里的学问更多。比如大量连接停留在Sending data、Waiting for table metadata lock、Updating、Statistics都说明问题可能出在 SQL、锁或慢查询上而不是单纯连接数不够。在排查现场我一般会写一个按来源主机聚合的统计查询快速定位“哪个应用节点在我这里占了一堆连接”SELECT SUBSTRING_INDEX(HOST, :, 1) AS client_host, COUNT(*) AS connection_count FROM information_schema.processlist GROUP BY client_host ORDER BY connection_count DESC;这里用information_schema.processlist而不是SHOW PROCESSLIST是因为它可以直接用 SQL 做聚合。这个查询在线上真的救过我有一次一个服务端节点因为连接池回收异常光它一个 IP 就占了 800 多个连接这个 SQL 一跑出来问题马上定位到具体应用实例然后重启那个实例数据库就恢复呼吸了。2.3 顺手统计一下连接都来自哪里除了按主机统计有时候还需要按用户、按库来统计。比如一个实例给多个业务共用时某个业务占用的连接数得多直接影响其他业务的可用性。下面这两个查询建议收藏-- 按用户名统计连接数占比 SELECT user, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 2) AS pct FROM information_schema.processlist GROUP BY user ORDER BY cnt DESC; -- 按数据库统计连接数 SELECT db, COUNT(*) AS cnt FROM information_schema.processlist WHERE db IS NOT NULL GROUP BY db ORDER BY cnt DESC;注意 8.0 中information_schema.processlist的访问可能受权限影响普通账号看不到其他用户的连接。DBA 账号没问题但如果你用的是应用账号可能只能看到自己的。这点在排查时容易误判先确认自己的账号权限再决定要不要找 DBA 要权限。3. 配置连接数参数背后的计算与取舍3.1 max_connections 不是越大越好那到底多大才合适这是配置连接数时最核心的参数max_connections。它决定了 MySQL 能同时打开的连接数上限。默认值在不同版本里略有差异5.7 和 8.0 一般是 151MariaDB 默认是 100。很多人一看到连接数打满第一反应就是把max_connections从 151 加到 2000、5000。这里面有个很危险的认知连接数上限大不等于数据库能扛住。每个连接都要吃内存和线程资源虽然 buffer 是按需分配但线程栈、会话上下文是固定的连接一多CPU 上下文切换成本也会急剧上升。我一般用一个经验公式来粗算合理的max_connectionsmax_connections ≈ (业务峰值 QPS × 单条 SQL 平均耗时(秒)) × 安全系数其中安全系数通常取 2~3。举个例子某业务峰值 QPS 是 2000单条 SQL 平均耗时 50ms0.05 秒那么并发执行中的 SQL 大约是 2000 × 0.05 100加上连接池本身会预留空闲连接乘以 2~3 倍安全系数大概 200~300。这种情况下max_connections配成 500 就非常宽裕了直接拉满到 2000 反而没用。再看内存侧估算。如果你真想压榨性能可以按“最坏情况”粗算max_connections × 单连接预估内存上限。单连接的内存上限不好精确说但经验上按 1~3MB 估算是常见做法。按 3MB 算2000 个连接就是 6GB再加上 InnoDB 缓冲池、排序临时表、日志缓冲等一台 16GB 的实例很容易被吃紧。这个数字不是吓唬人我见过很多次因为max_connections盲目调高数据库内存严重吃紧、触发 swap最终整体性能下降的案例。所以结论是max_connections的值应该基于业务并发量和资源余量来算不是越高越稳。3.2 wait_timeout、interactive_timeout 与 max_connect_errors 的影响连接数打满往往不只是一个参数的问题。围绕连接生命周期还有几个参数经常要一起调。wait_timeout和interactive_timeout决定了一个空闲连接在被服务器关闭之前能存活多久默认都是 28800 秒8 小时。interactive_timeout是针对 mysql 命令行这类交互式客户端的连接wait_timeout是针对所有非交互连接的。如果你用的是应用连接池通常走的是非交互连接主要受wait_timeout影响。很多线上 MySQL 连接数高居不下的原因就是应用连接池把连接占着但wait_timeout又是默认的 8 小时。照理说 8 小时也不短为什么还会打满因为连接池的最小空闲连接数和最大连接数设置不合理低峰期大量空闲连接占着位置高峰期应用又需要创建新连接结果一下子把连接数推高。遇到这种空闲连接堆积的情况我比较常用的做法是把wait_timeout调到 60~120 秒。但要特别注意如果应用连接池的空闲连接存活时间比wait_timeout长就会出现连接被 MySQL 断开后连接池还在用它的情况报Communications link failure或者Connection is not available, request timed out。所以调wait_timeout要和应用连接池的idleTimeout、maxLifetime配合起来才不容易踩坑。max_connect_errors是另一个容易被忽视的参数。它默认是 100代表单个客户端 IP 连续连接失败超过这个次数后MySQL 会直接锁住这个 IP 的后续连接报Host xxx is blocked because of many connection errors。这种情况在应用端临时调错账号密码、网络抖动反复重连时很容易触发。排查时可以看Aborted_connects状态值如果它在持续上涨说明确实有大量失败连接在发生。很多人在连接数不低不高但连不上的时候查错方向根本不是这里结果白白折腾很久。3.3 一个容易忽略的连接生命周期我自己做连接数治理之后养成了一个习惯把一条连接到 MySQL 的整个生命周期在脑子里过一遍。它大概经历这几个阶段TCP 建连客户端发起 TCP 三次握手然后进入 MySQL 握手。鉴权MySQL 校验账号、主机白名单、密码。会话初始化分配线程、初始化变量。执行 SQL应用通过连接执行各种语句。空闲等待SQL 执行完连接挂起等待下一条命令。断开回收客户端主动断开、wait_timeout超时服务端断开、网络异常断开。连接数配置和排查本质上就是要保证阶段 4 的 SQL 都能快速拿到连接同时阶段 5 的空闲连接数量不至于把数据库拖垮。我见过一个印象很深的案例某团队的连接池配了 100 个连接但每次数据库一抖动应用侧就把所有 SQL 超时当作连接失效然后疯狂重连。MySQL 侧Threads_created暴涨Aborted_connects也涨连接数瞬间触顶。根因反而是应用框架的异常重试机制没有做退避策略。这让我意识到连接数问题常常不是 MySQL 单侧的问题而是应用、网络、连接池、MySQL 四方共同作用的结果。4. 连接数配置实操从估算到落地的完整过程4.1 连接数估算用 QPS 和 RT 算出一组靠谱的初始值前面讲了公式这里展开一次完整的实操过程。假设你接手一个业务要在新环境里配置 MySQL 的max_connections没有任何历史监控数据你怎么下手第一步确认业务峰值 QPS 和单 SQL 平均耗时RT。这两个值一般可以在压测报告、应用层监控、APM 系统里找到。假设业务方给出的峰值 QPS 是 3000平均 SQL 耗时 80ms。第二步算并发执行中的 SQL 数3000 × 0.08 240。这是数据库里同时处于执行状态的查询数量级。第三步加连接池预留。连接池为了减少建连开销通常会维持一批空闲连接。以常见连接池配置为例假设应用层准备配置 maximumPoolSize100那么所有应用实例的空闲连接总量可能有几十到上百。把这部分算进去240 100 左右再乘 2~3 倍安全缓冲就能得到 680~1020 的量级。保守一点建议初始max_connections配置在 800 左右后续观察Max_used_connections的曲线再微调。这里必须说清楚安全系数的 2~3 倍不是拍脑袋。它缓冲了两种情况一是 QPS 和 RT 估算本身的误差二是业务突发的尖刺流量。但别把安全系数搞到 10 倍那无异于把连接数上限放在一个无害但无用的高位反而容易掩盖连接池配置不合理的问题。4.2 配置修改的正确姿势动态调整和持久化要分开MySQL 参数修改有两种途径动态修改运行期生效和配置文件修改重启生效。连接数相关的大部分参数都是动态可调的但要注意生效范围。比如临时处理故障时可以执行SET GLOBAL max_connections 800; SET GLOBAL wait_timeout 60;SET GLOBAL修改的是全局变量影响后续新建的连接不会影响已存在的连接。注意在 MySQL 8.0 中SET GLOBAL设置的值在重启后会丢。如果你希望永久生效有两个选择一是修改my.cnf配置文件里的对应项二是用 MySQL 8.0 的SET PERSIST它会将配置写入mysqld-auto.cnf重启后自动加载。SET PERSIST max_connections 800; SET PERSIST wait_timeout 60;SET PERSIST是个好功能但在生产环境使用时我会特别谨慎。因为它会把配置持久化到数据目录下的自动配置文件中如果哪天你想“临时改一下、重启后还原”结果发现重启后依然生效可能会措手不及。用SET PERSIST_ONLY可以先只持久化而不改当前值适合准备下次重启生效的场景。还有一个细节SET GLOBAL max_connections是立即生效但已经存在的连接不会主动断开。所以当连接数已经打满、应用还在报Too many connections时动态调大max_connections只能让新的连接有机会进来但真正要解决的还是旧连接为什么一直占着不动。4.3 应用侧连接池才是真正的“连接制造商”MySQL 侧的max_connections只是屋顶真正决定连接数量的是应用侧的连接池。只调 MySQL 不调应用或者只调应用不调 MySQL都属于治标不治本。以最常见的 HikariCP 为例几个核心参数是这样的maximumPoolSize连接池中最大的连接数量决定了单个应用实例最多能持有多少连接。minimumIdle连接池最小的空闲连接数如果设得太高低峰期也会有大量空闲连接占着 MySQL。connectionTimeout应用向连接池请求连接的超时时间默认 30 秒如果超过这个时间拿不到连接应用就报连接超时。idleTimeout空闲连接被回收前可以存活的时间受minimumIdle影响如果minimumIdle等于maximumPoolSize则以最大连接数为准空闲连接不会被回收。maxLifetime连接的最大存活时间默认 30 分钟。它必须小于 MySQL 侧的wait_timeout否则连接会被 MySQL 先切断应用还在用“僵尸连接”。我见过一个很常见的错配某个应用实例maximumPoolSize50总共部署了 20 个实例MySQL 的max_connections500。理论上 20×501000 已经超出 500应用侧每天高峰就绪前连接池做预热瞬间把连接数全部打满。所以算连接池总大小时一定要把所有实例加起来和 MySQL 的max_connections整体对账。调应用连接池的原则也很简单数据库 CPU 没打满之前优先优化 SQL 和连接池参数而不是傻傻地加 MySQL 的连接数上限。连接池的maximumPoolSize在大部分业务场景下单个实例 50~200 之间就非常够用了单实例再多收益也很小因为 CPU 核数限制了真正能并行执行的语句数量。5. 连接数飙高时怎么办常见问题与排查实录5.1 现场实录Too many connections 的应急处理这里复盘一个我之前遇到的真实现场。某天下午业务侧突然告警接口大面积报Too many connections部分应用实例完全无法请求数据库。我到现场后没有立刻重启数据库那是大忌。先做了三件事第一步确认连接状态。执行SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Threads_running; SHOW GLOBAL STATUS LIKE Max_used_connections; SHOW GLOBAL STATUS LIKE Aborted_connects;看到Threads_connected持续在 500 上下恰好触顶max_connections而Threads_running只有十几。这说明大多数连接都处于空闲状态。第二步看 processlist确认连接从哪里来、在干什么。执行SHOW FULL PROCESSLIST;结果一大片Command SleepTime从几秒到几百秒不等。再跑一个按来源聚合的查询发现某两个应用实例各占了 200 多个连接。基本能判断是应用侧连接池异常扩大而不是 SQL 变慢导致的。第三步应急处理。我先在 MySQL 侧把max_connections临时调大两级给了新连接一个喘息空间SET GLOBAL max_connections 800;同时把异常应用实例的连接池参数重置待它们的连接数回落后再把max_connections调回正常值。这里强调一下调大max_connections只是止血不是根治。如果业务是正常的连接数不可能瞬间冲这么高如果是应用侧连接池泄漏你把数值调到 1000 也救不了它反而会让数据库内存越吃越多。5.2 现场实录连接数周期性告警的排查思路还有一种情况更磨人连接数不触发打满但每个固定时间段都会从 300 跳到 700触发告警过一阵又回落。这种周期性波动通常跟定时任务、晚间批量程序、应用启动预热有关联。排查这类问题我的做法是拉长时间轴来看把Threads_connected和Threads_created的曲线调出来找到波峰发生的时间点和对应的业务任务。然后到general_log或慢查询日志里看看这个时间段有没有大量短连接在反复建立。如果Threads_created曲线跟着一起涨基本就是连接没有被复用应用在反复建连销毁。这种问题的解法往往比调整 MySQL 参数更靠应用侧配合定时任务复用同一个连接池、避免每次执行都新建连接、合理配置批处理线程数。MySQL 侧能做的是适当调大thread_cache_size让线程在连接断开后能保留复用减少重复创建线程的开销。但这个参数只是缓解不能根治。5.3 现场实录连接数不高却被拒绝连接这是个特别容易被误判的场景。某次线上排查Threads_connected只有 150max_connections是 500但部分客户端就是连不上报Host is blocked because of many connection errors。我当时第一反应是去看Aborted_connects状态值发现它一直在涨。这说明确实有大量连接在握手或鉴权阶段失败触发了 MySQL 的max_connect_errors机制。再往下查问题的根源是某应用版本升级后连接串里的密码没有同步更新应用不断重试失败连接把该应用所在主机 IP 的失败次数刷到了阈值以上。阈值一到MySQL 直接对该 IP 启用了连接黑名单。处理办法是FLUSH HOSTS;这条命令会清空host_cache里的主机锁状态。同时我查看并修正了应用配置里的密码最后再把max_connect_errors从一个偏小的值调大一些避免下次再有瞬时网络抖动就把主机锁了。这个案例想说明的是MySQL 的连接数排查不能只看连接数这一个指标。连接失败、认证失败、主机被锁这些看上去和连接数无关的现象恰恰会在关键时刻以“连接不上”的形式爆发出来。你手边的状态变量越熟练定位就越快。5.4 监控连接数不要等满了才去看连接数管理如果要落到日常运维监控一定要前置。我不建议把告警阈值设在max_connections的 100%那时候应用已经开始报错了。比较健康的做法是设置两级阈值预警阈值Threads_connected达到max_connections的 70% 或 80% 时触发预警开始排查增长原因。告警阈值达到 90% 或 95% 时立即触发紧急响应。另外历史数据远比瞬时值重要。如果不想自己写脚本可以用 Prometheus 的mysql_exporter采集 MySQL 状态变量。它默认就能把Threads_connected、Threads_running、Max_used_connections、Aborted_connects这些指标抓出来你只需要配置好采集频率和告警规则然后像滚雪球一样累积历史数据。一个月之后你再看曲线就能知道业务低谷、高峰的时间规律配置参数也就不再靠猜了。监控不只是看数字还要能关联分析。连接数飙高时最好同时看 QPS、CPU、慢查询数、锁等待数、应用 JVM 线程数。我把这种多指标联动排查叫做“连接数全景图”因为没有全景图你很难判断连接数飙高是原因还是结果。6. 最后聊聊个人经验跟连接数打了这么多年交道我最大的体会是连接数配置最怕的不是不会查而是“分不清状态”。很多人盯着Threads_connected看却忽略了Threads_created在飞速上涨看到连接数打满就调max_connections却不去看一眼这些连接到底在干嘛。把状态变量的含义吃透把连接池和 MySQL 当作一个整体去调优比记住任何一条命令都管用。还有一个经验想分享给你调连接数参数之前务必先攒历史数据。至少观察 3~4 周的Max_used_connections、Threads_connected、Threads_created曲线再根据峰值乘上合理的安全系数去改配置。很多时候你觉得某个参数不合理其实是因为你只看了一天两天的监控根本没有掌握业务高峰和低谷的节奏。数据先到位调整才有依据。最后说一个很实用的小技巧每当你准备在 MySQL 侧改连接数相关的参数时顺手把应用侧的连接池总大小一起对个账。很多连接数问题在 MySQL 这边看起来像容量不够实际上只是应用侧没有把资源配额控制好。一条公式记住它所有应用实例连接池最大连接数之和必须小于等于MySQL max_connections × 0.8。留出那 20% 的余量给 DBA 手工排查、命令行连接、监控采集这些“隐形连接”用。这个小习惯能帮你少踩很多打满连接数的坑。
返回列表