
接手过线上MySQL的人多半都经历过那种“数据库突然连不上了”的瞬间。应用日志里刷着一排Too many connectionsDBA群里瞬间炸锅第一反应就是敲show variables like max_connections看上限再敲show status like Threads_connected看当前占用。这套动作我做过无数次但老实说早期我对连接数的理解也就停留在“满了就调大”这个层面直到被几次故障教育之后才真正把连接数的查询、配置、监控和治理串成一条线。这篇就把MySQL连接数从查看到配置、从参数到实战的完整脉络梳理一遍全是踩过坑之后沉淀下来的东西。1. 先搞懂连接数到底是怎么回事1.1 连接的本质和开销MySQL的“连接数”本质上是客户端与服务端之间建立的会话数量。每一次连接服务端都要为它分配线程、内存、临时资源连接建立时还要经历TCP握手、认证、权限校验这一整套流程。你可以把每个连接想象成餐厅里的一把椅子——椅子数量有限每个客人坐下来都要占地方服务员线程要招呼他后厨存储引擎要为他备菜。客人走了椅子才能空出来但椅子本身不会自己变多。这个类比解释了为什么连接数不是可以无限涨的。每个连接占用的内存通常在几百KB到几MB之间取决于会话级变量、排序缓冲区、临时表内存等配置。如果有几百个连接同时存在光会话内存就是一笔不小的开销。更重要的是MySQL的线程调度能力有限连接数堆积到一定程度CPU会大量消耗在线程切换上导致整体吞吐下降——这时候数据库不是“满”了而是“瘫”了。1.2 连接被谁占用了排查连接数问题的第一步不是看总数而是看构成。一个典型的MySQL实例上连接主要来自几个方向应用连接池绝大多数连接来自业务侧。Java应用里常见的连接池如Druid、HikariCP每个实例会维护一组连接微服务规模一大连接数自然水涨船高。监控脚本和定时任务很多人会忽略这些。监控采集、报表任务、批量脚本如果写在循环里每次new一个连接又没释放连接数会被慢慢“吃”掉。管理端和手工查询Navicat、Workbench这类GUI工具每个打开的查询窗口对应一个连接。开发环境人手一把看起来不多但叠加起来也可能占掉几十个连接。闲置连接这是最阴险的占用。应用获取连接后忘了归还或者连接池最小空闲数配置过高大量连接处于Sleep状态既不干活也不释放。分类清楚了才能对症下药。这也是为什么我们查连接数时永远要追问一句“这些连接都是谁”。1.3 连接上限从哪来MySQL对连接数的限制最直接的是max_connections参数。它控制的是整个实例允许同时存在的客户端连接总数包括所有库、所有用户。除此之外还有一个max_user_connections单独限制单个账号的最大连接数默认是0也就是不限制。值得说明的是max_connections是实例级别的硬上限但“生效”的方式不是写死而是动态判断当已有连接数达到上限后再来的新连接请求会直接被拒绝错误码是ER_CON_COUNT_ERROR也就是我们熟悉的Too many connections。生产环境最常见的误区是把max_connections当成一个需要“够用就行”的配置结果上线时够用业务一涨直接被打穿。正确做法是按最坏情况估算同时留出缓冲余量。2. 查询连接数从命令行到SQL的全套姿势2.1 第一件事先看全局状态最基础也是最高频的查询命令是这两条SHOW VARIABLES LIKE max_connections; SHOW STATUS LIKE Threads_connected;SHOW VARIABLES查的是配置值SHOW STATUS查的是实时状态。Threads_connected表示当前已建立的连接数这两个值一比就能看出连接水位。比如配置上限是500当前已经480那就得警惕了。除了Threads_connected还有几个状态值值得一起看SHOW STATUS LIKE Threads_running; SHOW STATUS LIKE Threads_created; SHOW STATUS LIKE Aborted_connects; SHOW STATUS LIKE Connection_errors_max_connections;Threads_running当前正在执行的连接数。这个值最能反映数据库真实压力。连接数高不一定有问题但Threads_running持续走高说明SQL真的在跑要关注慢查询和锁等待。Threads_created为处理连接而创建的线程数。如果这个值在持续快速增加说明系统在频繁创建线程通常意味着连接在频繁建立和断开——连接池没用好。Aborted_connects连接建立失败的次数可以用来判断是否有连接被拒。Connection_errors_max_connections因为达到max_connections上限而被拒绝的连接次数。这个值只要大于0就说明被拒过。2.2 查当前活跃连接information_schema 三板斧全局状态只能看到总数想知道“谁占着连接”得上information_schema.processlistSELECT id, user, host, db, command, time, state, info FROM information_schema.processlist ORDER BY time DESC;这个查询会列出所有当前连接重点看几列commandSleep表示空闲连接Query表示正在执行查询Connect表示正在建立连接。time当前状态持续的时间。某个Query的 time 特别大基本就是慢查询或者锁等待。state执行状态比如Sending data、Waiting for table metadata lock、Locked不同状态对应不同问题。info正在执行的SQL语句不过只有CommandQuery时才有内容。排查连接数异常我通常先跑这个SQL把结果按command分组统计再按host分组看来源SELECT command, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY command; SELECT host, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY host ORDER BY cnt DESC;这两刀下去大头在哪基本就清楚了全是Sleep那就是连接池没回收集中在某个host那就是那个应用实例有问题全是Query且 time 很大那就不用管连接数了先查SQL吧。2.3 performance_schema 更细的视角information_schema.processlist在连接数非常多比如上千或者大量被占用时查询本身会有些开销而且只能看到当前瞬间的状态。如果要做历史趋势分析或者更细维度的统计用performance_schema更合适。SELECT EVENT_NAME, COUNT_STAR, SUM_TIMER_WAIT FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;这个用来定位消耗最多的SQL模式。连接数的场景下更常用的是看performance_schema.threadsSELECT TYPE, PROCESSLIST_COMMAND, COUNT(*) AS cnt FROM performance_schema.threads GROUP BY TYPE, PROCESSLIST_COMMAND;performance_schema.threads会包含后台线程和前台连接线程能更精确地区分哪些是系统内部线程哪些是真实客户端连接。不过说实话日常排查我还是优先用information_schema.processlist它直观、覆盖面够用。performance_schema更适合做监控采集和深度分析因为它的历史累计数据是现成的不用自己额外记录。2.4 把连接状态做成监控手工查只能解决“当时发生了什么”生产环境更需要的是“连接数趋势”。我比较推荐的做法是把核心指标接入监控系统至少采集这几个值max_connections配置上限变更时要同步更新监控里的阈值。Threads_connected当前连接数按百分比设置告警阈值。比如达到上限的80%告警达到90%紧急告警。Threads_running当前执行连接数这个值超过几十就要警惕SQL性能问题了。Aborted_connects增量异常时说明连接被拒绝过。采集方式可以直接用SHOW GLOBAL STATUS轮询也可以用performance_schema的表。之前我见过一个坑监控脚本自身也使用了数据库连接去采集状态结果数据库连接打满时监控反而连不上采集任务失败告警没发出来。后来我们把监控采集改成直连本地socket并单独留一条管理通道才彻底避免了这个尴尬。3. 配置连接数参数详解与调优思路3.1 max_connections 到底该设多大关于max_connections网上流传着“设个1000、2000就完事”的说法这是典型的头疼医头。连接数上限不是拍脑袋定的要结合三个因素算因素一是内存。每个连接至少会占用thread_stack加net_buffer_length等固定开销但真正的大头是会话级的排序缓冲、临时表、join缓冲等。粗略估算时可以按每个连接1MB到2MB来算。假设实例内存32GB预留一半给InnoDB缓冲池和操作系统剩下16GB给连接那理论上可以支持8000到16000个连接。但这只是内存约束不是推荐值。因素二是CPU。每个连接的活跃查询都会消耗CPU。一个4核8线程的实例同时能高效运行的查询也就十几个。就算连接数堆到几千绝大多数连接也只能排队等CPU数据库的TPS不会因此提高反而线程切换开销会把性能拖垮。因素三是业务形态。max_connections 预估活跃连接峰值 缓冲余量比如一个应用实例连接池配置20个连接总共50个实例那理论上限是1000再加上监控、管理、备用流量建议设到1200。注意这里说的是“连接池配置的总额”不是当前实际活跃连接数——因为连接池一般会维持比活跃请求更多的空闲连接。我见过一份配置调优记录某电商系统在促销前把max_connections从500调到2000结果促销还没开始数据库先卡死了。原因就是连接数上来了但每连接的内存和CPU开销也上来了系统资源根本扛不住。后来把连接池最大值压下去配合排队机制系统反而稳定了。这就是典型的“连接数配置不是开得越大越好”。3.2 连接生命周期wait_timeout 与 interactive_timeout连接数为什么会越积越多很多时候不是应用故意不释放而是连接变成了“僵尸连接”——既没关闭也不干活就那么一直躺着。控制这个现象的关键参数是wait_timeout和interactive_timeout。SHOW VARIABLES LIKE wait_timeout; SHOW VARIABLES LIKE interactive_timeout;wait_timeout非交互式连接的空闲超时时间默认28800秒8小时。interactive_timeout交互式连接的空闲超时时间同样默认28800秒。这个默认值在云上环境经常成为“连接数泄漏”的帮凶。应用获取连接后不归还连接池又不主动踢掉连接就会一直占着。8小时后才能被服务端强制断开——这个节奏太慢了。建议线上把wait_timeout调小比如600秒10分钟甚至300秒5分钟。但对交互式连接不要一刀切因为DBA自己用命令行连上去可能挂着看个长查询这时候interactive_timeout需要保持足够长。这里有个容易踩的坑修改wait_timeout只对之后新建的连接生效不是的这个参数对已存在的连接立即生效MySQL会周期性检测空闲连接并强制断开。但也正因如此如果连接池里的连接在空闲状态被服务端切掉了应用连接池如果没有做好连接有效性检测第一次使用这个连接时会抛异常走一次重连流程业务上表现为偶发报错。所以调wait_timeout时一定要确认应用连接池的testOnBorrow或validationQuery是开着的。3.3 连接回收与复用thread_cache_size每次新建连接MySQL都要创建一个线程连接断开时线程也要销毁。频繁创建销毁线程是有成本的。thread_cache_size就是用来缓存空闲线程的作用类似连接复用加速器。SHOW VARIABLES LIKE thread_cache_size;这个参数的值表示MySQL可以缓存多少个空闲线程用于复用。设置多大合适可以看状态值SHOW STATUS LIKE Threads_created; SHOW STATUS LIKE Connections;Threads_created除以Connections可以得到线程创建率。如果这个比例很低说明大部分连接都在复用已有线程thread_cache_size够用如果比例偏高说明线程频繁创建销毁可以适当调大thread_cache_size。不过要强调一点thread_cache_size治标不治本。如果连接在频繁创建和销毁真正的问题通常是应用连接池参数不合理——比如连接池最大生命周期太短、空闲回收太激进。这个参数只能缓解症状不该把它当成主力优化手段。3.4 连接数与连接池的联动配置前面说的都是MySQL服务端的参数但线上连接数问题的根源往往不在服务端而在应用侧的连接池配置。这里把两边的指标对应起来看连接池参数作用与MySQL的对应关系maximumPoolSize连接池最大连接数对应单实例对MySQL的最大占用minimumIdle连接池最小空闲数决定常驻连接数maxLifetime连接最大存活时间应小于MySQL的wait_timeoutconnectionTimeout获取连接的超时时间超过后应用侧直接报错不等MySQL拒绝validationTimeout连接有效性验证超时用于检测被服务端断开的连接这里有个黄金法则连接池的maxLifetime必须小于 MySQL 的wait_timeout。这样连接在MySQL侧被强制断开之前应用连接池会主动把老连接淘汰并新建避免拿到失效连接。比如MySQLwait_timeout设为600秒连接池maxLifetime设为480秒留出120秒的安全余量。另一个容易忽略的是连接池大小的设计。很多团队直接套用“max50、min10”这类默认值完全不评估实际并发。连接池太大堆出来的都是空闲连接白占MySQL的连接数上限连接池太小高峰期请求都在排队等连接。一个靠谱的起步值是连接池大小 业务并发峰值 × (单个请求占用连接的平均时间 / 请求平均响应时间)这个公式本质上就是Little‘s Law按需求出稳态下需要的连接数。实际项目中单实例10到20个连接对绝大多数业务都够用宁可让少量请求排队也不要把连接池飙到几十上百。3.5 防呆设计max_user_connections 与账号拆分有时候不是所有业务都值得给满额连接配额。某个慢业务、某个批量任务、某个定时脚本它们如果占用了过多连接可能挤掉核心业务的连接名额。这种情况可以用max_user_connections做账号级别的连接上限控制。比如有一个报表账号专门用来跑数据分析GRANT SELECT ON mydb.* TO report_user% IDENTIFIED BY password; ALTER USER report_user% WITH MAX_USER_CONNECTIONS 20;这样就算报表任务写得很烂、连接池配得很大它最多也就占用20个连接不会影响主业务的连接供给。还有一种更精细的做法核心业务用独立的MySQL账号不同微服务使用不同的账号前缀这样既隔离了连接配额又能在processlist里一眼看出连接归属。3.6 连接数满之后的应急逃生通道连接数打满最尴尬的处境是你想连上去查状态、杀连接结果连不上了因为你也是“连接”之一。这里有几个务实的做法第一预留管理连接。MySQL始终为具备CONNECTION_ADMIN权限或MySQL 5.7及之前版本中的SUPER权限的账号保留一条连接通道。所谓“保留”不是完全不受max_connections限制而是在连接数达到上限后管理员账号仍然可以建立额外的连接。上限是max_connections 1这条额外的通道就是应急逃生的关键。所以生产环境的DBA账号一定要配置为超级权限账号并且千万别把这个账号给应用用。平时用普通账号连接出了问题用管理员账号连接。第二紧急调大连接数。如果是max_connections真的不够管理员能连上之后立即执行SET GLOBAL max_connections 2000;这是动态参数不用重启立刻生效。但如上所述这只能缓解不能根除。紧急调整后必须排查连接的去向。第三清理僵尸连接。找到真正的空闲连接后可以直接killKILL 12345;这里有个细节要注意KILL只能杀Admin连接不用想KILL权限足够就可以。但注意如果有正在执行的查询直接KILL会中断这个查询业务侧可能报错。对于Sleep状态的连接杀掉是安全的。批量清理时可以先把要杀的连接拼出来确认无误后执行SELECT CONCAT(KILL , id, ;) FROM information_schema.processlist WHERE command Sleep AND time 600;把结果复制出来执行也可以配合存储过程循环处理但保险起见手工执行更稳妥省得误杀活跃连接。4. 线上实战连接数问题的排查与规避4.1 一个典型的连接数打满案例某天下午一个内部系统的线上数据库突然告警Threads_connected曲线直线拉升半小时内从200涨到1500直接触顶。我连上去后第一件事就是查processlistSELECT command, state, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY command, state ORDER BY cnt DESC;结果触目惊心大部分连接是Sleep状态占比超过80%而且host全部集中在同一个应用服务网段。再查这个应用对应的连接池配置发现两个问题第一连接池的minimumIdle设置得很大每个实例维持35个空闲连接20个实例就是700个常驻连接——这本身就已经占了很大额度。第二wait_timeout是默认的8小时即便程序偶尔释放连接失败MySQL也不会主动回收。当时做了三件事SET GLOBAL wait_timeout 300; SET GLOBAL max_connections 2000;然后写了一条SQL把所有Sleep超过300秒的连接全部杀掉SELECT CONCAT(KILL , id, ;) FROM information_schema.processlist WHERE command Sleep AND time 300;执行完连接数立刻降下来了。但既然是慢性的就要处理根因。后面协调应用团队改了两处连接池minimumIdle降到5maxLifetime调到480秒并开启连接有效性检测。修改后观察一周连接数稳定在200到300之间水位健康。这个案例可以提炼出一条通用排查路径先看状态总量再看构成command分组再定位来源host分组最后对症处理参数调整连接清理。4.2 慢查询和锁等待怎么伪装成连接数问题有一种场景极具迷惑性表面看是连接数暴涨实际是慢查询和锁等待把连接拖住了。具体来说一个慢查询执行几十秒连接池里等待这个操作释放的请求就会排队每个排队请求实际上占用了连接池的一个连接。连接池耗尽后新的请求只能等待connectionTimeout超时表现和MySQL连接数打满几乎一样——应用日志里都是一堆连接超时报错。但此时MySQL侧的Threads_connected可能并没有超限问题出在应用侧的连接池被“饿死”了。排查这种问题要先看MySQL侧的Threads_running。如果Threads_running很高比如持续超过几十说明确实有查询在执行。这时候要去看慢查询日志定位具体是哪条SQL在拖后腿同时查锁等待SELECT * FROM sys.innodb_lock_waits;sys.innodb_lock_waits是MySQL 5.7之后提供的一个视图直接能看到谁在等锁、谁持有锁、等待多久。大部分情况下找到持有锁的事务并评估是否要kill比盲目处理连接数有效得多。见过不少团队在这种场景下误判成“连接数不够”反复调大max_connections结果只是把问题推迟了慢查询还在新连接来了继续排队整个系统卡到崩溃。记住一句话连接数是结果不是原因。排查时永远要问一句这些连接为什么存在4.3 连接数监控告警的最佳实践最后系统性地聊一下连接数的监控和告警建设。别小看这部分一套好用的告警体系能在故障发生前就把问题按住。建议至少设置三级告警告警级别触发条件响应动作关注连接数利用率 60%观察趋势确认是否有新业务上线警告连接数利用率 80%排查连接构成清理异常连接严重连接数利用率 90% 或出现拒绝连接立即介入准备扩容或kill连接除了比例告警还要监控Aborted_connects的增量。这个值平时基本是0一旦往上走说明连接已经开始被拒了这时候再处理就不如提前处理。采集方式上建议用独立的监控通道采集不占用业务连接配额。可以使用本地socket方式连接MySQL也可以在连接串里显式指定一个专门的监控账号这个账号只有PROCESS、SHOW DATABASES等轻量权限不会对业务造成影响。另外提醒一句Threads_running的监控比Threads_connected更早暴露性能问题。连接数缓慢上升不可怕可怕的是Threads_running突然升高——那往往意味着线上有慢查询或锁风暴正在发酵。4.4 常见问题速查表现象可能原因排查方法解决方案连接数缓慢持续上升连接池未回收连接 / 连接泄漏查看Sleep状态连接数和来源host修复连接池调小wait_timeout连接数瞬间打满连接风暴 / 应用重启 / 流量尖峰查看processlist出现时间点紧急调大max_connectionskill多余连接检查应用侧连接池配置应用报连接超时但MySQL连接数未满应用连接池排队 / 慢SQL占用连接查看应用日志、MySQLThreads_running优化慢SQL调整连接池参数连接频繁建立和断开连接池配置不当 / 超时过短对比Threads_created与Connections调整连接池最小/最大连接数调大thread_cache_size某些账号占用过多连接账号没有配额限制按user统计连接数设置max_user_connections连接被杀后应用报错应用使用失效连接查看应用连接池日志开启连接池的testOnBorrow或启用validationQuery调整wait_timeout后应用偶发异常连接池未感知服务端断开应用连接池配置设置连接池maxLifetime小于wait_timeout4.5 我最后想分享的一点经验连接数这件事说起来是MySQL的一个小参数但背后牵扯到应用架构、连接池配置、运维监控、SQL质量等多个层面。我踩过最深的坑就是“只管MySQL不管应用”参数调了一轮又一轮问题却反复出现。后来我才意识到MySQL侧能做的其实很有限——设好上限、设好超时、做好监控、留好逃生通道剩下的功夫全在应用侧。尤其想提醒刚接手数据库运维的同学连接数查询不是背几条SQL而是要理解每个状态值背后的含义以及它跟应用行为之间的关系。等你遇到一次连接数打满把整个过程从头跟到尾捋一遍你对数据库连接机制的理解会有一个质的飞跃。