十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL连接数上限之谜:从max_connections到连接池治理的完整排查指南

MySQL连接数上限之谜:从max_connections到连接池治理的完整排查指南 前几天一个朋友在生产环境收到告警数据库连接数满了。他第一反应是跑到监控里把 max_connections 从默认值调到 2000结果重启后不到两小时连接又满了而且这次整个 MySQL 实例都开始响应缓慢。这个问题很有代表性——“MySQL 最多能有多少连接”表面上看是问一个参数值实际上牵扯到内存预算、线程模型、操作系统文件描述符、应用侧连接池设计还有一堆日常文档里很少写清楚的系统级限制。这篇文章我不会只给一个数字而是把“最多”这件事拆开上限由谁决定、连接是怎么被占满的、出问题时怎么定位、不同业务怎么给一个合理值以及真正进阶的线程池和连接治理思路。适合正在被“Too many connections”折磨的开发也适合想搞懂 MySQL 连接机制、应付面试的初学者。1. 先回答“最多”max_connections 参数、默认值与三层限制1.1 max_connections 是明面上那道闸门MySQL 对连接数的控制最直接的参数就是 max_connections。它决定了 MySQL 实例能同时接受的客户端连接总数。注意是“总数”不是“每个库”也不是“每个用户”——一个实例只有一个总闸门所有应用、工具、监控、主从复制连接都算在里面。查看和修改的命令很简单SHOW VARIABLES LIKE max_connections;SET GLOBAL max_connections 1000;动态修改不需要重启但要注意两点第一SET GLOBAL 只对后续新连接生效已经建立的连接不受影响所以把 max_connections 调低不会踢掉现有连接第二这个修改只在当前实例运行期间有效重启后会回到配置文件里的值。如果用的是 MySQL 8.0可以用 SET PERSIST 把它持久化到 mysqld-auto.cnfSET PERSIST max_connections 1000;1.2 默认值是多少版本之间有什么差异默认值这个坑好多人记混。MySQL 5.5 的默认值是 100从 5.6 开始调整为 1515.7、8.0 一直沿用 151。也就是说你装一个全新的 MySQL 8.0初始最多只能同时接待 151 个客户端连接。一台稍微有点流量的应用服务器一个连接池加上监控采集分分钟就能把这 151 个全部占满。还有一个容易忽视的兄弟参数叫 max_user_connections默认值是 0表示不限制单个用户。一旦设置了具体的数字同一个 MySQL 账号最多只能建立指定数量的连接。这个参数在“多业务共用一个实例、要隔离资源”的场景里很有用但日常开发环境基本碰不到面试里偶尔会问你可以记一下。1.3 光调大参数没用操作系统和内存才是天花板很多人以为把 max_connections 调到 100000 就高枕无忧了其实不是。每个连接对 MySQL 来说至少是一个 socket 文件描述符、一个独立的线程以及一组会话级内存缓冲区。文件描述符受操作系统的 ulimit -n 限制线程数受 max user processes 限制内存就更直接——连接越多内存占用越大。MySQL 为每个会话线程至少要分配 thread_stack默认 256KB真正执行 SQL 时还会按需分配 sort_buffer、join_buffer、read_buffer 等。虽然这些缓冲区不全是一上来就占满但按经验一个比较活跃的连接在稳定运行后大约要吃掉 2-5MB 内存。1000 个连接就是 2-5GB这个量级对一台 4G 内存的机器来说已经是灾难了。所以内存越小的实例越不能把连接数调得太高。2. 连接是怎么被占满的生命周期与积压成因2.1 一条连接从建立到释放的完整链路要搞清楚连接为什么满先得知道一条连接的生命周期。客户端发起 connect 之后MySQL 服务端完成 TCP 握手进入认证阶段校验用户名、密码和客户端 IP 是否在白名单里。认证通过后建立一个会话分配一个线程来负责后续命令客户端开始发 SQL服务端执行完返回结果。最后客户端关闭连接或者连接空闲超时线程被回收或缓存。这个流程用医院的窗口来类比特别贴切窗口就是 MySQL 的连接每个病人就是一条客户端请求医生看病的速度就是 SQL 执行速度。如果某个病人病情复杂慢查询窗口就被长时间占着后面排队的人越来越多最终整个大厅挤爆。理解了这种资源占用模型再看连接数打满的问题思路就清晰了——要么是窗口太少max_connections 偏小要么是病人久久不走连接不释放要么是看病的动作太慢SQL 太慢。2.2 连接积压的五个常见来源第一应用连接池配得太大。这是一个非常典型的反模式为了预留高峰容量把连接池最大连接数设成 300应用又部署了 10 个实例加起来就是 3000把数据库的 max_connections 吃得干干净净。第二连接泄漏。Java 里典型的例子是使用某个数据库框架时异常分支没有归还连接或者在一个线程里反复创建数据库连接却不 close。看起来应用只开了几十个线程但数据库侧 Threads_connected 却在持续上涨直到爆掉。第三长事务和慢查询。一条 UPDATE ... WHERE id IN (SELECT ...) 如果子查询执行计划很糟糕一次全表扫描加上行锁等待可能跑好几分钟。这个连接在这几分钟里是“活着”但不能被复用的。业务 QPS 一高连接数立刻见顶。第四空闲连接被长时间保留。默认 wait_timeout 等于 28800 秒也就是 8 小时。很多应用长连接建立后应用服务器和数据库之间没有心跳保活机制或者连接池的空闲超时设置比 wait_timeout 还大导致一堆睡着的连接一直占着名额。第五突发流量没有限流。秒杀、爬虫、异常重试风暴任意一个都能在秒级把连接数打上去。这种情况考验的不是 max_connections 有多大而是你有没有提前把连接池和入口限流做好。3. “Too many connections”事故排查从告警到根因的完整链路3.1 先确认错误到底是谁抛的连接打满时客户端和服务端各有各的表现。在 MySQL 端最常见的是ERROR 1040 (HY000): Too many connections在应用侧日志里通常不是这个原始错误而是一堆和连接池相关的报错比如 HikariCP 报“Connection is not available, request timed out after 30000ms”Druid 报“GetConnectionTimeoutException”。如果你在监控上看到大量这种异常别急着怀疑连接池代码先确认 MySQL 是不是已经没有连接额度了。有一个状态变量可以帮你直接确认是不是真的因为达到上限而拒绝连接SHOW GLOBAL STATUS LIKE Connection_errors_max_connections;只要这个计数在持续增长基本就可以判定打满的原因就是抵达了 max_connections 上限。3.2 六步定位法查到是谁占满了连接我排障时习惯按照下面这六步走每一步都能筛掉一批候选原因。第一步看当前到底有多少连接SHOW GLOBAL STATUS LIKE Threads_connected;如果这个值和 max_connections 非常接近说明确实满了。第二步看连接构成。跑一遍 processlist或者直接查询 information_schema.processlistSELECT id, user, host, db, command, time, state, info FROM information_schema.processlist;第三步按来源和状态分组统计这一步最关键SELECT user, host, command, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY user, host, command ORDER BY cnt DESC;结果里如果有一大堆来自同一个应用服务器的 Sleep 连接这就是“连接泄漏”或者“连接池过大”的铁证如果 Time 列出现大量几百秒甚至几千秒的 Query 或 Locked那就是慢查询、长事务在作怪。第四步把 Sleep 空闲时间最长的连接挑出来看看它们是不是早就该被回收了SELECT id, user, host, db, time FROM information_schema.processlist WHERE command Sleep ORDER BY time DESC LIMIT 20;第五步结合慢查询日志看同一时刻有没有集中出现慢 SQL。慢查询日志默认关闭生产环境可以临时打开一段时间重点看 long_query_time 设成 1 秒甚至 2 秒以后有没有大范围的慢语句堆积。需要注意做这个动作本身也会带来额外开销别在业务高峰一开就是一天。第六步如果判断是某个应用导致的让运维去查对应机器的 TCP 连接数以及应用连接池的 active 和 idle 曲线。到了这一步根因基本已经水落石出。提示开 general log 可以记录每一条连接建立和断开的历史对排查“我是被谁连满的”很有帮助但 general log 在高峰期的写入量非常大一般只在低峰期短时间开启别当成常规手段。3.3 紧急处理先救火再治本如果线上已经打满业务正在报错首要目标是恢复可用性。第一步紧急提升连接上限。前提是实例的内存和 CPU 还有余量否则只会加速宕机SET GLOBAL max_connections 2000;第二步批量清理掉长时间空闲的连接给真正的业务让路SELECT CONCAT(KILL , id, ;) FROM information_schema.processlist WHERE command Sleep AND time 60;把生成的语句复制出来执行即可。这里要提醒一句不要一把梭把所有 Sleep 连接都 kill 掉应用连接池里的空闲连接被杀后应用会重连反而可能把连接数二次打高。先杀超过 60 秒不活动的相对安全。第三步把数据库配置持久化。如果是 8.0用 SET PERSIST如果是 5.7写进 my.cnf 后等重启窗口生效。第四步也是最关键的一步回到 3.2 的定位结果去修应用侧的问题。紧急扩容连接数只是给车加水不修发动机水温还是会再上来。4. 合理设置连接数不同业务场景的参考配方4.1 先按内存算上限别拍脑袋填数字我的建议是配置 max_connections 之前先做一个简单的内存预算估算实例可用的总内存扣除 innodb_buffer_pool_size 预留的大头剩下的内存除以单连接平均内存开销经验值按 3MB 计生产环境可以按 5MB 预留。比如一台 8G 内存的 MySQLinnodb_buffer_pool_size 设为 4G操作系统和其他开销留 2G能给连接用的就是 2G。按 5MB 一个连接算连接上限大约 400留点余量可以设 300-350。如果你硬要设 2000内存分配一上去swap 或者 OOM 马上会找上门。4.2 应用连接池该给多大一个简单到可手算的公式连接池大小有一个特别实用的估算公式并发需要的连接数约等于 QPS 乘以单请求平均数据库耗时。假设业务目标 QPS 是 1000单次数据库操作平均耗时 20ms那么同一时刻大约有 1000 * 0.02 20 个请求正在数据库里执行连接池配 20-30 就已经够用配到 200 纯属浪费。这条公式背后的直觉是连接池不需要承载全部 QPS它只需要承载“同一瞬间正在数据库里执行的那些请求”。只要数据库操作足够快小连接池完全能扛住高 QPS。HikariCP 官方文档也一直在强调“小连接池、快连接”而不是无脑给大数值。实际业务当然还要考虑慢查询毛刺、批处理任务所以我会在理论值基础上再乘 1.5-2 的余量系数。4.3 分场景给一个可直接抄的配置下面是我自己在不同负载类型下常用的参考值注意这只是出发点不是银弹场景max_connections 参考应用连接池参考个人开发环境50-10010-20中小业务 OLTP4C8G / 8C16G300-50030-50高并发 OLTP16C32G1000-200050-100数仓 / OLAP大查询为主100-20010-20靠任务队列限流多从库复制场景在基础值上按每从库 1 预算同上这里有两个细节别漏掉第一主从复制中每个从库的 I/O 线程在主库侧也是一个普通连接从库特别多时这部分也会占用 max_connections第二如果设置了 max_user_connections单用户能建连的数量会被单独框死可能出现总连接数没满但某个应用连不上的情况。5. 容易被忽略的周边限制文件描述符、端口与容器环境5.1 ulimit连接数的真实物理瓶颈MySQL 每建立一个客户端连接服务端就要分配一个 socket 文件描述符和一个线程。不少服务器默认的 ulimit -n 是 1024也就是说即使你把 max_connections 调到 3000文件描述符这一层就先把你卡死在 1024 附近。我处理过一个案例MySQL 的 max_connections 设置的是 2000但连接数到 800 左右就无法继续增长实际就是操作系统的打开文件数限制在作祟。排查时可以看 mysqld 进程的实际限制cat /proc/$(pgrep mysqld | head -1)/limits重点看 Max open files 和 Max processes。如果不够需要修改 /etc/security/limits.conf或者在 systemd 的 mysql 服务单元里加[Service] LimitNOFILE65535 LimitNPROC65535改完记得重启 mysqld然后重新查看 limits 确认生效。这一步不做max_connections 永远只是配置文件里的理想值。5.2 短连接风暴与端口耗尽应用如果习惯不用连接池而是每次请求都新建连接、用完全部断开在高 QPS 下客户端会碰上源端口耗尽的问题。本地可用的临时端口有限默认范围通常在 32768-60999而频繁断开的连接还会进入 TIME_WAIT端口不能立刻复用最终表现就是应用报“Cannot assign requested address”数据库日志倒是干干净净。优化方向有两个一个是推进应用使用连接池从根上减少短连接另一个是调大客户端的临时端口范围并开启端口复用sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1默认端口 3306 本身没有特殊魔法连接数打满时从网络层看就是 3306 的接入被拒抓包时看到 RST 或者连接超时要能联想到数据库层的连接数状态。5.3 Docker 部署 MySQL 时连接数还有一个容器级别的坑很多团队现在用 Docker 跑 MySQL连接数相关的坑在容器环境里会放大。Docker 容器默认继承宿主机的 ulimit如果宿主机没有调优容器里的 mysqld 一样受 1024 文件描述符限制。启动容器时建议显式指定docker run -d \ --ulimit nofile65535:65535 \ --ulimit nproc65535:65535 \ -v /myconf:/etc/mysql/conf.d \ mysql:8.0另外容器常用的内存限制参数 --memory 也要和 MySQL 的缓冲区、连接数匹配。比如限制容器内存为 4G却设置 innodb_buffer_pool_size4G、max_connections2000那 OOM 几乎是必然的。很多容器环境下 MySQL 频繁被 kill排查到最后问题往往就出在这里。6. 进阶线程池、监控指标与连接治理6.1 线程池把连接和线程拆开官方 MySQL 社区版的线程模型是“一个连接对应一个线程”简单直接但连接数上来以后线程上下文切换的成本会非常可观。这里提一下线程池Percona Server、MariaDB 以及 MySQL 企业版提供的线程池会用一个比较小的固定工作线程集合去处理大量连接上的命令让大多数连接在“待命”状态下几乎不消耗 CPU。连接池解决的是客户端复用连接的问题线程池解决的是服务端复用工作线程的问题两者不是一回事。如果你的连接数长期很高、Threads_running 却很低说明大量连接其实在睡觉线程池能明显降低 CPU 开销如果瓶颈真的是并发执行的 SQL 太多线程池救不了这时候得处理慢查询和锁等待。6.2 监控这几个指标连接数就不会突然爆掉与其等“Too many connections”报警不如把连接数的前兆指标纳入监控指标含义建议关注点Threads_connected当前连接数达到 max_connections 80% 时预警Threads_running当前正在执行 SQL 的连接数长时间大于 CPU 核数 4 倍需警惕Connections累计尝试连接次数观察增量趋势Aborted_connects建立失败或中途失败的连接数突增说明网络或认证异常Connection_errors_max_connections因连接数达上限被拒的次数0 就说明曾经打满查询命令可以直接用SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Threads_running; SHOW GLOBAL STATUS LIKE Aborted_connects; SHOW GLOBAL STATUS LIKE Connection_errors_max_connections;6.3 让连接“短平快”SQL 治理才是治本连接数最终是被 SQL 的执行时间决定的。把 SQL 写慢、把事务拉长连接就会出现“虚高”的占用。我会把下面这些动作作为连接数治理的常态打开慢查询日志定期用 EXPLAIN 分析执行计划重点看 type 是不是 ALL全表扫描、rows 是否远超预期给高频查询的 WHERE 条件创建合适的索引热搜里常被搜的“MySQL 创建索引”其实也是这个问题的解法更新语句尤其要小心比如 UPDATE 加子查询时确认被更新的行和子查询扫描的范围避免行锁范围无限扩大长事务拖着连接不释放避免长事务超过几十秒的事务要单独审查它不只锁数据还会让 MVCC 的 undo 不断堆积同时把一个宝贵的连接占死。我自己在线下和线上环境里都踩过连接数打满的坑最后总结出来的经验就三句话先按内存和文件描述符确认物理上限再按业务并发模型给应用连接池定一个“够用就好”的值最后用 Threads_connected 和 Threads_running 盯住前兆指标。连接数是数据库最表面的资源但它往往是 CPU、内存、磁盘 IO 异常的第一块多米诺骨牌把它管好了很多深层问题根本不会有机会浮出水面。
返回列表