ARTICLE DETAIL

资讯详情

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

MySQL“Too many connections”故障排查:从连接池到系统参数调优实战

MySQL“Too many connections”故障排查:从连接池到系统参数调优实战 1. 报错现场先复现再动手改参数遇到Too many connections这事大部分人的第一反应都是“把 max_connections 调大不就行了”。但我劝你先别急着动配置——我在 CentOS 上处理过太多次这个报错每次都发现根因各有不同直接调大参数往往治标不治本甚至会把问题掩盖成更严重的故障。先来看这个报错的典型现场。客户端连 MySQL 时直接被拒绝报错信息通常长这样ERROR 1040 (HY000): Too many connections如果你的 Java、PHP、Python 应用在跑报错就会以各种形式出现在日志里SQLException: Too many connections或者Caused by: com.mysql.cj.exceptions.CJException: Data source rejected establishment of connection, message from server: Too many connections这时候如果通过命令行访问 MySQL会发现一个很现实的问题普通账号已经连不进去了。只有拥有SUPER或CONNECTION_ADMIN权限的管理员账号才能连上去做排查比如 root。这也是很多新手卡住的第一道坎——还没走到排查那一步连数据库都进不去。我的建议是遇到这个报错先按下面这套动作走一遍把现场信息收齐再动手# 用管理员账号连入 MySQL mysql -uroot -p # 查看当前连接数使用情况 SHOW STATUS LIKE Threads_connected; SHOW STATUS LIKE Max_used_connections; # 查看 max_connections 当前配置 SHOW VARIABLES LIKE max_connections;Threads_connected是当前有多少个连接Max_used_connections是历史最高连接数max_connections是上限。如果你的Max_used_connections已经撞到了max_connections的上限那基本可以确认是连接数打满。但如果Threads_connected显示的当前连接数不高却还是报错那就要换个方向排查——这种情况很可能是文件描述符限制导致 MySQL 根本没法创建新的连接这个问题在 CentOS 上非常典型后面我会专门讲。1.1 Too many connections不一定是连接数超了刚入行那会儿我犯过一个错误看到Too many connections就以为只是max_connections打满了改完参数结果第二天又报警。后来仔细排查才发现问题的真正根源是系统层面的 open files 限制。MySQL 每接收一个连接就会占用一个文件描述符。Linux 默认对进程的文件描述符数量有限制如果这个限制小于max_connections的配置值MySQL 会在文件描述符用尽之后拒绝新的连接报错信息同样是Too many connections。这种情况在 CentOS 上用systemctl启动 MySQL 时特别容易踩坑因为 systemd 对进程的限制和普通的ulimit不是一回事。判断是不是这个原因可以用一条命令# 查看 MySQL 进程实际的文件描述符上限 cat /proc/$(pgrep -x mysqld | head -1)/limits这里会输出Max open files一行如果它的值很小比如 1024 或 4096而你的max_connections配置是 1000那连接数其实远没到配置上限就已经被系统卡住了。还有一种情况是权限层面的MySQL 默认只给SUPER权限的账号保留几个逃生通道用于管理员在连接满时进场处理。如果你是用普通账号在报错期间尝试登录收到Too many connections是完全正常的但只要你用 root 或者有SUPER权限的账号登录通常就能进去。所以看到报错别慌先换管理员账号。1.2 现场数据怎么收集动手改配置之前我强烈建议先把以下几类信息收集完整不然很可能改完参数仍然复现或者问题解决了但不知道当初为什么挂# 1. 当前所有连接快照 SHOW FULL PROCESSLIST; # 2. 连接状态统计 SHOW GLOBAL STATUS LIKE Threads_%; SHOW GLOBAL STATUS LIKE Aborted_%; # 3. 最大连接数历史峰值 SHOW GLOBAL STATUS LIKE Max_used_connections; SHOW GLOBAL STATUS LIKE Max_used_connections_time; # 4. 慢查询状态 SHOW GLOBAL STATUS LIKE Slow_queries; SHOW VARIABLES LIKE slow_query_log; SHOW VARIABLES LIKE long_query_time;SHOW FULL PROCESSLIST是重点因为这一条命令能直接看到当前所有连接的来源 IP、登录用户、执行的 SQL、以及当前的执行状态。如果能看到大量Sleep状态的连接说明连接处于空闲挂起状态如果看到一堆Copying to tmp table或者Sending data的长时间运行查询说明是慢查询和长事务把连接占住了。Max_used_connections_time这个变量很多人不看但它很有用——它能告诉你连接峰值到底出现在哪个时间段方便结合应用日志和定时任务去定位是谁在那个时间点发起大量连接。2. 连接数为什么会打满理解连接池和睡眠连接的工作方式在 CentOS 上排查 MySQL 连接数问题绕不开连接池的概念。很多没做过运维的后端同学会把连接数理解成同时执行的 SQL 数量但实际完全不是一回事。一个连接从建立到关闭经历的是TCP 握手 → MySQL 鉴权 → 会话建立 → 执行 SQL → 空闲等待 → 关闭。连接建立后不会因为一条 SQL 执行完就立刻释放它会保持一会儿甚至在应用侧连接池的管理下保持很久。默认的wait_timeout是 8 小时也就是说一个空闲连接在没有任何操作的情况下可以趴在 MySQL 上 8 小时不关。如果你的应用使用了连接池绝大多数 Java、Python、PHP 应用都会用连接池里的连接在池子里的存活时间通常也很长。这就导致一个结果连接数是累积的而不是并发的。MySQL 默认的max_connections只有 151。对一个小型应用来说 151 似乎够用但如果你的应用用了多个服务实例、每个实例配置了连接池、连接池的 minimumIdle 又设得比较大几个实例一加起来连接数轻松突破 200。这还没算上后台的定时任务、数据同步脚本、运维手工连上去执行 SQL 的会话。用一个容易理解的类比MySQL 的连接数就像餐厅的餐桌数量。餐桌要接待的不仅是正在吃饭的客人还有那些已经吃完但还在聊天不走的客人以及预订了但还没到场的客人。如果你只盯着正在吃饭的人数来判断餐厅够不够坐那肯定会低估实际的餐桌需求。2.1 谁在占用连接三种典型场景根据我处理过的线上问题连接数打满基本逃不出下面三种场景。场景 A应用连接池配置和实例数不匹配假设你有 10 个后端实例每个实例连接池配了maximumPoolSize30理论上高峰可能用掉 300 个连接但 MySQL 的max_connections还停留在默认的 151。这种属于配置层面的硬冲突几乎必然导致连接被打满。识别方法很简单看Max_used_connections是否稳定超过配置上限再看SHOW PROCESSLIST里的连接是不是都来自同一批应用服务器 IP。场景 Bwait_timeout 设置过长导致 Sleep 连接堆积这一类是最常见的。连接池为了保证复用会维护一个最小空闲连接数如果wait_timeout是默认的 28800 秒8 小时这些空闲连接会长期占据连接名额。当应用流量突发增长时连接池会尝试创建新连接却发现 MySQL 的连接名额已经被占满。识别方法也简单SHOW PROCESSLIST里大量Sleep状态的连接且Command列显示为Sleep、Time列很大几百秒甚至几千秒。场景 C慢查询和长事务持锁占满连接一条跑了 50 秒还没结束的SELECT或者一个一直没提交的UPDATE事务都会把连接牢牢占住。如果这样的慢查询同时出现十几条连接数很快就见底。识别方法SHOW PROCESSLIST里能看到Info列有具体 SQLTime列很大State列是Sending data、Updating或者Waiting for table metadata lock之类的状态。2.2 从 processlist 和错误日志反向定位源头收集完SHOW FULL PROCESSLIST的输出之后我习惯按HOST列分组统计一下看看哪个来源 IP 占用的连接最多SELECT SUBSTRING_INDEX(HOST, :, 1) AS client_ip, COUNT(*) AS conn_cnt FROM information_schema.PROCESSLIST GROUP BY client_ip ORDER BY conn_cnt DESC;如果某个应用服务器的 IP 占据了绝大多数连接那问题大概率出在那个应用侧的连接池配置上。如果来源 IP 很分散那要结合时间点去看是不是有人在跑批量任务、数据导入或者某个接口被恶意刷请求。错误日志也要看。CentOS 上 MySQL 的错误日志默认位置在/var/log/mysqld.log如果改了配置则去my.cnf里找log_error参数。连接数相关的问题在错误日志里经常有蛛丝马迹比如[Warning] Aborted connection ... to db: test user: root host: ... (Got timeout reading communication packets)这种提示说明有连接超时中断侧面印证连接已经紧张到影响了正常通信。3. CentOS 上的实际操作临时调优、永久配置、验证流程确认了连接数确实被打满、并且排除了文件描述符问题之后才轮到真正的解决方案环节。在 CentOS 上调整 MySQL 连接数限制有三个层面我按操作顺序一个一个说清楚。3.1 第一步临时调大重启失效不用重启 MySQL直接通过 SQL 设置全局参数这对线上环境最友好SET GLOBAL max_connections 1000;需要注意两点。第一这个修改不会自动写入配置文件MySQL 重启之后会恢复到配置文件的旧值。第二SET GLOBAL只对之后的新连接生效已经存在的连接不受影响。第三如果你的 MySQL 版本是 8.0这个语句需要账号有SYSTEM_VARIABLES_ADMIN权限root 用户可以直接执行。执行完用下面语句确认生效SHOW VARIABLES LIKE max_connections;SHOW VARIABLES输出的值如果从 151 变成了 1000说明修改成功。作为对比SHOW GLOBAL STATUS LIKE Max_used_connections显示的是历史峰值它不会因为调参而重置如果你想知道调参后的峰值变化需要手动FLUSH STATUS或者过一段时间看差异。3.2 第二步永久写入配置文件临时改只能用来救火要根治必须改配置文件。CentOS 上 MySQL 的配置文件路径根据安装方式有所不同安装方式配置文件路径yum 安装的 MySQL 5.7/etc/my.cnfyum 安装的 MySQL 8.0/etc/my.cnf同样适用编译安装通常在/usr/local/mysql/etc/my.cnf或安装时指定的路径Docker 容器容器内/etc/my.cnf但建议通过挂载/etc/mysql/conf.d/配置用mysql --help | grep my.cnf可以查看当前实例读取了哪些配置文件mysql --help | grep -A1 Default options修改方式是在[mysqld]段落里加上[mysqld] max_connections 1000然后重启 MySQL 服务。CentOS 7/8 上是sudo systemctl restart mysqldCentOS 6 以及更老的版本用sudo service mysqld restart重启后务必再次验证mysql -uroot -p -e SHOW VARIABLES LIKE max_connections;确认输出是 1000说明永久配置生效。3.3 第三步把系统层的文件描述符限制同步调大这一步在 CentOS 上特别容易被忽略但恰恰是很多改完 max_connections 还是一样报错的元凶。前面提到过MySQL 每个连接都要占用一个文件描述符如果系统的限制跟不上改再大的max_connections也是白搭。先确认当前 MySQL 进程的文件描述符上限cat /proc/$(pgrep -x mysqld | head -1)/limits关注Max open files那一行如果显示的是 1024 或者 4096 之类的值就需要处理。CentOS 7/8 下有两个层面要查层面一操作系统用户的 ulimit编辑/etc/security/limits.conf加上针对 mysql 用户或者你运行 MySQL 的用户的限制mysql soft nofile 65535 mysql hard nofile 65535层面二systemd 服务限制CentOS 7 开始 MySQL 通过 systemd 管理systemd 会无视/etc/security/limits.conf的部分设置必须在 service 文件里显式指定。执行systemctl edit mysqld这会打开一个 override 配置在里面写入[Service] LimitNOFILE65535保存后重载服务配置并重启 MySQLsystemctl daemon-reload systemctl restart mysqld重启后再确认一次cat /proc/$(pgrep -x mysqld | head -1)/limitsMax open files应该变成 65535。这一步做完才算真正把连接数的天花板从系统层面抬高了。我见过太多同事只改了my.cnf里的max_connections结果发现Max open files只有 1024线上服务在连接数到 1024 之前就因为文件描述符耗尽而报错——那真是改了个寂寞。4. 调大之后还要动哪些参数配套调优与风险评估socket层面能容纳的连接数提上去了但 MySQL 能不能扛住这么多连接是另一回事。盲目把max_connections从 151 调到 5000如果服务器只有 4G 内存那是在给自己埋雷。这里要理解一个基本事实每增加一个连接MySQL 都要分配对应的线程栈内存。默认情况下每个线程的thread_stack按 256KB 计算同时每个线程还要占用sort_buffer_size、join_buffer_size、read_buffer_size等会话级内存——虽然这些是按需分配并非全部预分配但并发线程一旦多了内存消耗是实打实的。一个粗略的估算方式如果max_connections设为 2000每个连接按最保守的 1MB 内存估算就是 2GB 内存被预留给连接使用。这只是连接本身的开销还没算 InnoDB 缓冲池、查询缓存、临时表等。所以我个人的习惯是调大连接数的同时先看看服务器物理内存有多少free -h如果内存紧张宁可删掉一些无用的 sleep 连接和优化慢查询也别单纯靠调大参数硬扛。4.1 配套参数和它们的取舍max_connections调大后下面几个参数会跟着产生联动影响thread_cache_size连接关闭后MySQL 会缓存一部分线程以便快速复用避免频繁创建线程的开销。如果你的应用有大量短连接比如脚本任务、监控采集这个参数值得调大一般设为 16-64 之间。它不占太多内存但对连接频繁建立关闭的场景提升明显。wait_timeout和interactive_timeout这是解决连接堆积问题的核心参数。前者是非交互连接的超时时间后者是交互式连接命令行客户端的超时时间。如果你的场景是应用连接池为主建议把wait_timeout从默认 28800 秒调小到 60-300 秒之间让空闲连接尽快释放。[mysqld] max_connections 1000 thread_cache_size 64 wait_timeout 120 interactive_timeout 120这里有个坑必须提醒你把wait_timeout调得太小比如 10 秒如果应用连接的创建频率很高MySQL 频繁关闭连接会反过来增加数据库的压力。调这个参数前要评估应用的连接池是否符合长连接、复用的特征。连接池维护的存活连接如果空闲超过wait_timeout被 MySQL 杀掉后连接池会自动新建补位这本身没问题但如果你的应用代码里存在每次请求都新建连接而不复用的情况把wait_timeout调小只会让连接风暴更严重。max_used_connections的观察调大参数后不能不管不顾我会加一个例行巡检用SHOW GLOBAL STATUS看Max_used_connections的增长曲线。如果它很快又逼近新上限说明真的有并发需求不是简单的参数不够。4.2 连接数打满时的一段急救 SQL如果连接数已经打满但因为配置了SUPER权限你作为 DBA 还能连进去这时候可以手工清理空闲连接来腾位置而不是干等着重启-- 查看当前 sleep 状态的连接 SELECT id, user, host, db, command, time FROM information_schema.PROCESSLIST WHERE command Sleep AND time 100; -- 杀掉空闲超过 100 秒的连接 -- 注意先确认这些连接确实来自连接池且可以重建不要贸然 kill 掉正在跑事务的连接 KILL thread_id;这个操作要非常谨慎。杀掉连接池里空闲的连接通常没问题连接池会自动重建但如果一个连接虽然显示Sleep其实在一个未提交的事务里那 kill 掉它会导致事务回滚可能引发业务报错。所以动手之前先看一眼time列和db列确认是真正的空闲连接再 kill。4.3 配套的运维监控手段在 CentOS 上我用得最顺手的还是脚本加 crontab 的轻量方案。一个简单的巡检脚本#!/bin/bash MAX_CONN$(mysql -uroot -p密码 -N -e SHOW VARIABLES LIKE max_connections | awk {print $2}) CUR_CONN$(mysql -uroot -p密码 -N -e SHOW GLOBAL STATUS LIKE Threads_connected | awk {print $2}) THRESHOLD$((MAX_CONN * 80 / 100)) if [ $CUR_CONN -ge $THRESHOLD ]; then echo $(date %Y-%m-%d %H:%M:%S) 连接数告警: $CUR_CONN / $MAX_CONN /var/log/mysql_conn_alert.log # 这里可以接上短信/钉钉/微信机器人通知 fi放在 crontab 里每 5 分钟跑一次*/5 * * * * /usr/local/bin/mysql_conn_check.sh生产环境如果有条件还是建议上 Prometheus 加 mysqld_exporter 或者 Zabbix 做完整的监控告警连接数只是新能源表盘上的一个指标慢查询、锁等待、主从延迟这些才是真正需要盯的。5. Docker 环境下的连接数问题与排查差异热词里C:\Users... docker安装mysql这类检索需求不少说明很多人在 CentOS 上用 Docker 跑 MySQL。容器化部署和裸机部署在连接数问题上有一些差别如果你用的是 Docker 方式安装 MySQL下面的内容应该能帮你少走弯路。Docker 容器里的 MySQL连接数打满的表现和裸机完全一样都是ERROR 1040 (HY000): Too many connections。但排查路径不太一样——因为容器内看不到 systemd 服务systemctl是失效的配置文件挂在镜像里需要先确认你的 MySQL 镜像版本和挂载方式。先看容器内 MySQL 的当前连接数docker exec -it mysql_container mysql -uroot -p -e SHOW STATUS LIKE Threads_connected; SHOW VARIABLES LIKE max_connections;如果确认是max_connections太小不要在容器里改完就完事——容器一旦重建镜像内的配置会被还原。正确做法是把 MySQL 配置通过挂载目录传进容器。在启动容器时加上配置目录挂载docker run -d \ --name mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD你的密码 \ -v /etc/mysql/conf.d:/etc/mysql/conf.d:ro \ mysql:5.7然后在宿主机的/etc/mysql/conf.d/下新建一个配置文件写入[mysqld] max_connections 1000这样容器重建后配置仍然存在。Docker 场景还有一个隐蔽的限制容器默认的文件描述符上限。在宿主机上执行docker inspect mysql_container | grep -i ulimits如果Ulimit为空则容器会继承 Docker daemon 的默认值通常比较高但如果加了限制也需要调大。在docker run时可以加--ulimit nofile65535:65535参数来显式放行。另外容器内如果遇到连接数打满执行docker exec进入容器后因为容器里默认没有安装系统工具pgrep、cat /proc/...这类命令可能不可用。我习惯是在宿主机用docker top mysql_container查看容器内进程的 PID再配合/proc观察文件描述符情况。6. 我在 CentOS 上处理这个问题的完整复盘说一个我印象很深的案例。某次线上系统在下午两点半准时开始告警应用日志里开始出现Too many connections报错。我登录服务器先看了Max_used_connections发现峰值 160而max_connections是默认的 151。看起来就是连接数打满正常思路是改大配置。但我在SHOW PROCESSLIST里看到了异常有二十多个连接来自同一个 IP全部处于Sending data状态Info列里全是同一张报表的查询语句。追了一下才发现是运营后台有人定时导出数据的功能被重写后出现了死循环调用每个请求都会触发一次全表扫面的 SQL。连接数只是表象真正的问题在业务代码。当时我先用KILL清理了那批异常连接然后在应用层把死循环的调用停掉连接数立刻就降下来了。max_connections我后来确实也调大了从 151 调到 500但这是在确认正常业务并发确实需要这么多连接之后才做的调整而不是因为在故障期间拍脑袋改的。这个案例说明一个道理连接数打满不是病它只是症状。如果不去看是谁、因为什么占满了连接只把max_connections拉大症状可能暂时消失但下一次会在更高的阈值上再次爆发。真正的做法是先把根因找到把该修的修好然后基于合理的峰值数据去调整参数。最后再分享一个我长期维护服务器的习惯每次处理完连接数问题我都会在/etc/my.cnf里留一个注释写清楚修改时间、修改原因、目标值是多少、是谁改的。等到下次再出问题的时候翻一翻注释就能快速回忆起当初为什么这么调能省下不少重复排查的时间。
返回列表