ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MySQL连接数排查与配置实战:从Too many connections到性能优化

MySQL连接数排查与配置实战:从Too many connections到性能优化 接手一个数据库性能问题十次有八次是从“连接数”开始的。你可能会看到监控面板上Connections曲线突然拉满应用日志里抛出一排Too many connections紧接着客服电话就被打爆了。很多人第一反应是重启应用、加大连接数上限但折腾一圈下来问题隔天又回来了。说白了连接数不是单纯调大就完事它既是MySQL资源的“入口闸门”也是整个系统健康状况的“晴雨表”。这篇文章我想把连接数的查询和配置从头到尾捋一遍把那些命令行背后的原理、参数之间的关联、以及日常排障时容易踩的坑都讲清楚给正在为数据库连接问题头疼的朋友一个可以直接照着做的参考。我默认你看这篇文章是想真正解决问题而不是只想要一条SHOW STATUS的复制粘贴命令。所以我会把查询手段、配置逻辑、以及常见的排障思路串在一起讲配合实际场景来解释为什么要这么做。这中间有些结论是我在多个生产环境里实测验证过的写出来供你参考也希望你能少走点弯路。1. 连接数问题本质先弄明白瓶颈在哪里1.1 连接数失控的几个典型症状先看几个最常见的现场。第一种是应用日志里出现SQLException: Data source rejected establishment of connection, message from server: Too many connections这种一般发生在数据库连接数达到max_connections上限的时候。第二种情况没有那么直接业务偶尔卡顿数据库CPU和内存都正常但你翻慢查询日志又找不到几个慢SQL这时候多半是连接数分配失衡某个应用或某个账号把连接全部占满其他业务模块抢不到连接。第三种是数据库层没报错但应用侧的连接池一直在超时这往往和数据库的超时参数、或者连接池与数据库之间的配置不匹配有关。坦白讲人在现场第一眼看到的常常只是表象数据是整个链条的最后一环。我见过不少团队在连接数告警之后直接把max_connections调成原来的两倍最后数据库负载反而上去了因为真正的瓶颈往往不是连接数本身而是连接背后的线程、内存和锁资源。在动参数之前先搞清楚连接数和系统资源之间的关系方向才不会跑偏。1.2 每一个连接背后消耗的“隐性成本”要知道为什么连接数不能无限调大就得先弄清楚一个连接在MySQL内部到底消耗了什么。MySQL每接收一个客户端连接服务端就会分配一个线程来处理这个连接上的查询请求。在Linux系统上这个线程会占用一定的内存默认情况下线程栈大小是1MB左右可通过thread_stack参数调整。再加上连接本身的网络缓冲区、排序缓冲区、join缓冲区等一个空闲连接占用的内存少则几百KB多则数MB。这还只是静态开销真正可怕的是当大量连接同时提交查询时MySQL需要同时调度大量线程争抢CPU和InnoDB的行锁、表锁资源性能会急剧下降。很多人会问我的机器是64GB内存开5000个连接总没问题吧单从内存角度算可能确实够但你还要考虑文件描述符的限制也就是open_files_limit。在Linux下一个连接至少要占用一个socket文件描述符如果连接数开得很大而ulimit没调MySQL还是会报Cant create a new thread这类错误。所以连接数的上限不是单独由MySQL一个参数决定的而是由操作系统限制、MySQL内部线程资源、内存总量三者共同约束的。1.3 连接数和连接池是一对“互相配合的搭档”生产环境里应用侧几乎都会用连接池比如HikariCP、Druid、C3P0。连接池的作用是复用数据库连接减少频繁建连和断连的开销。这里有一个很常见的配置误区应用连接池的最大连接数明明设置的20但数据库连接数监控却显示100多然后DBA把数据库的max_connections调大到300。其实问题在于——很多微服务会同时部署多个实例假设服务有8个Pod每个Pod连接池是20那同一时间打到数据库的连接就是8×20160。如果你只盯着单实例的连接池配置而不汇总计算所有实例的总连接需求数据库连接数就会被低估等到流量一来连接就撑爆了。反过来连接池太小而业务并发很高的场景也会造成应用侧“等连接”超时。所以连接数的配置必须“两头看”一头数清楚应用实例总共需要多少个连接另一头算清楚数据库还能承载多少个连接然后在这个区间里找到平衡点。2. 查询连接数的核心手段从状态变量到实时线程2.1 最基础也最常用的连接数查询语句连接数是动态变化的时刻在涨跌所以在排查问题时最关键的是拿到“当前这一刻”的快照。最经典的一条查询就是SHOW STATUS LIKE Threads_connected;Threads_connected表示当前有多少个客户端连接正处于连接状态。与之配套的几个状态变量也非常重要状态变量含义我的使用建议Threads_connected当前打开的连接数判断当前负载最直接的指标Threads_running当前正在执行查询的线程数这个值大于CPU核数很多时基本可以确定有SQL在堆积Connections累计尝试连接的总次数含失败观察趋势用看单位时间新增多少连接Max_used_connections历史达到过的最大连接数通过它反推连接数配置是否合理Connection_errors_max_connections因为超过上限而失败的连接次数如果这个数一直在涨说明触顶情况在反复发生我通常在排查时一次性把所有连接状态相关变量都拉出来看而不是一个个单独执行。分享一个相对省事的查询方式SHOW GLOBAL STATUS WHERE Variable_name IN ( Threads_connected, Threads_running, Connections, Max_used_connections, Connection_errors_max_connections, Aborted_connects );这样一次就能看到全局状态。注意SHOW STATUS如果不加GLOBAL默认展示的是当前会话级别在排查连接数时意义不大所以务必养成写GLOBAL的习惯。如果是在命令行客户端里也可以用mysqladmin -u root -p extended-status | grep -E Threads_connected|Threads_running|Max_used_connections|Connectionsmysqladmin的优势在于不需要进入交互式的MySQL客户端适合写脚本做定时采样。比如你想观察一天之内连接数的波动曲线crontab里每5分钟执行一次把结果追加到日志文件里后面分析增长趋势时数据基础就有了。2.2 从processlist里看清连接的“来龙去脉”SHOW STATUS能告诉你连接有多少但回答不了“谁在连”“连上来在干什么”这个问题。要回答这个问题就得用SHOW PROCESSLIST或者直接查information_schema.processlist表SELECT id, user, host, db, command, time, state, left(info, 100) AS info FROM information_schema.processlist ORDER BY time DESC;把结果按执行时间倒序排列基本一眼就能看出哪些连接是真正在干活的哪些是空占着连接资源。需要注意几个关键点Command列如果是Sleep说明这个连接处于空闲状态啥也没干很可能就是连接池里待回收的闲置连接。如果大量连接都是Sleep且数量居高不下一般不是应用并发真的高而是连接池最大连接数设得太宽松或者连接的空闲回收时间太长了。某个连接的Time值特别大而State卡在Sending data、Waiting for table metadata lock这类状态上通常意味着这个连接背后有一个慢SQL或者被阻塞的事务在拖着不放行。User和Host两列能帮你判断是哪个业务账号、从哪台应用服务器发起的连接尤其在多个业务共用一个MySQL实例时这一眼就能定位到“罪魁祸首”。我记得有一次线上出现连接数告警查Threads_connected显示800多但SHOW PROCESSLIST一拉出来发现超过600个连接都是Sleep而这600多个连接里有400多个是同一个应用账号发起的。当时直觉就是连接池参数要么没生效要么把maximum-pool-size调到了不可思议的值。后来一查配置果然是某个服务的连接池最大连接数没有按规范设置改完之后连接数立刻降下来了。processlist的价值就在这里它帮你把“连接数很高”这个模糊的信号拆解成“哪些连接是正常占用、哪些是浪费资源”。2.3 深挖连接来源按账号、IP多维统计如果processlist里的记录已经多到让你看不过来可以用SQL做一下聚合统计把连接按不同的维度归归类。-- 按账号统计连接数 SELECT user, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY user ORDER BY cnt DESC; -- 按来源IP统计连接数 SELECT SUBSTRING_INDEX(host, :, 1) AS host_ip, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY host_ip ORDER BY cnt DESC;在实际排障场景里这两个查询能帮你直接定位到“是哪个应用账号、从哪个IP段打过来的连接最多”。如果聚合结果里某个来源IP的连接数远超其他那就去对应的服务端查一下连接池配置。如果是从外部IP集中过来的连接还要再多想一步是不是数据库被外网扫描或者暴力破解了曾经有朋友半夜收到连接数告警一查全是password登录失败的连接不断重试最后确认是公网端口被扫了。所以连接数的异常背后既可能是内部的配置问题也可能是外部的安全风险多一个维度分析就多一分排查准确性。3. 连接数配置实战参数背后的逻辑和换算方法3.1max_connections该怎么定一串简单的算术题MySQL默认的max_connections是1515.7及以后版本这对一个只有十来台应用服务器的中小型系统其实够用了但很多人从来没有主动验证过这个值到底够不够。有一个简单的评估办法把所有应用服务的最大连接数加总再加上一定余量就是数据库侧max_connections的下限参考值。举个例子。假设你的订单服务有4个实例每个实例连接池最大连接数50用户服务有2个实例每个连接池30后台任务服务有2个实例每个连接池20。简单算一下4×502×302×20280。也就是说同一时刻数据库至少要能容纳280个连接。再考虑到监控、备份、管理端等少量额外连接我会建议至少设置到350左右。更稳妥的做法是在这个基础上再留20%~30%的缓冲。[mysqld] max_connections 350在5.7之后的版本max_connections是支持在线修改的不用重启数据库就能生效SET GLOBAL max_connections 350;但要注意用SET GLOBAL修改只对当前运行实例生效重启之后会被配置文件里的值覆盖。所以线上验证没问题之后一定要同步修改配置文件否则下一次数据库重启连接数又被打回原形。这个细节在很多事故复盘里都出现过改完参数忘记写配置文件下一次运维重启直接把业务给重启崩了。3.2 防止连接泄漏的隐形防线wait_timeout和interactive_timeout把上限调大只是“治标”更关键的是要让那些闲置连接能及时退出把资源让出来。这就是wait_timeout和interactive_timeout两个参数发挥作用的地方。wait_timeout控制的是非交互连接比如应用通过JDBC建立的连接在空闲多少秒后被MySQL主动断开默认是28800秒也就是8小时。interactive_timeout对应的是交互式连接比如命令行客户端默认同样是28800秒。在生产环境里8小时的空闲回收时间太长了尤其是有些业务有夜间低峰期大量连接夜里一直挂着资源白白被占用不说一旦白天流量突然上来很容易直接撞上连接数天花板。我个人的实践是把wait_timeout调成300秒左右也就是5分钟。这样空闲超过5分钟连接就会被数据库断开应用连接池会自动感知并把该连接从连接池里剔除下次请求进来再建立新连接。这个时间既能容忍业务上一时的空闲又能避免连接长期不释放。在5.7及以上版本里这两个参数都可以在线调整SET GLOBAL wait_timeout 300; SET GLOBAL interactive_timeout 300;同理这两个参数最终也要写进配置文件。另外再提醒一个坑有些云数据库控制台是允许你直接调整这些参数的但如果你的应用侧连接池里有connectionTestQuery或者testWhileIdle这类探活机制并且探活间隔大于数据库的wait_timeout就可能出现“连接还没被探活数据库就先把它断了”的情况。配置的时候最好让连接池的心跳间隔小于数据库的空闲回收时间避免连接被中途切断。3.3 连接数上限之外的“同伙”参数max_connect_errors和skip-name-resolve连接数本身是一个直观的门槛但有一些“帮凶”参数也会在特定情况下限制连接。先说max_connect_errors它记录的是从某台主机发起的连接连续失败的次数如果超过这个值MySQL会直接拒绝来自该主机的后续连接请求并报Host is blocked because of many connection errors; unblock with mysqladmin flush-hosts。这经常出现在某个应用服务器的数据库密码配置错误的时候凌晨配置变更密码填错应用不断重试很快这个应用服务器IP就被MySQL“拉黑”了。解决办法是调大max_connect_errors默认只有100实际生产环境可以调成1000甚至更大同时用FLUSH HOSTS手动解除被阻塞的主机。再有一个容易被忽略的是DNS解析。MySQL在收到连接请求时默认会对客户端IP做反向DNS解析。如果DNS解析超时或者配置了错误的DNS服务器连接建立就会变得异常缓慢甚至直接失败。在IP直连的架构下强烈建议在配置文件中加上[mysqld] skip-name-resolve开启这个参数后MySQL不会再对客户端IP做反向解析连接握手会快很多。账号授权时用IP地址来指定主机权限而不是主机名这个参数在实际运维中帮助很大。踩过的坑是开启skip-name-resolve之后如果授权表里还写着userlocalhost并且应用通过TCP/IP远程连接是没法命中的需要改成user127.0.0.1或者具体的内网IP。这个切换过程容易引发一阵连接报错所以改造前必须把账号权限梳理一遍。3.4 与连接数强关联的线程资源参数前面提到每个连接MySQL都会分配一个线程去处理。如果要让连接数真正“跑得起、扛得住”除了关注数量上限还得看线程资源够不够。跟这个强相关的参数有这么几个thread_cache_size决定了MySQL缓存多少个空闲线程以便复用。如果连接频繁建立和断开而缓存又太小MySQL就会频繁创建和销毁线程这种情况在性能监控上看到的现象是CPU使用率不低、但没有明显的慢SQL。经验值建议设置为16~64之间视连接波动幅度而定观察Threads_created这个变量的增长趋势如果它的值涨得很快说明线程被反复重建此时调大thread_cache_size能显著减少线程创建的开销。performance_schema里也可以查到一些有用的信息不过它就比较依赖具体版本了。5.7及以上版本默认是开启的其中events_statements_summary_by_thread这种表能看到每个线程执行SQL的统计情况。不过这类表在连接数很多的情况下也会占用额外内存生产环境需要评估后再决定是否完全开启。3.5 配置文件里的完整示例把上面这些参数汇总成一份可供参考的配置大概长这样[mysqld] # 连接数上限按应用连接池总量加30%余量评估 max_connections 500 # 空闲连接回收时间秒防止连接长期挂起 wait_timeout 300 interactive_timeout 300 # 单次最大允许连续失败次数防止应用配置错误导致IP被拉黑 max_connect_errors 1000 # 跳过反向DNS解析加快握手避免DNS故障引发连接问题 skip-name-resolve # 缓存线程数减少线程反复创建开销 thread_cache_size 32 # 打开文件数上限至少要大于max_connections两倍以上 open_files_limit 2048 # 连接失败计数与日志便于排查 log_error_verbosity 3open_files_limit这里单独解释一下。MySQL打开的文件句柄不只是网络连接还包括表文件、日志文件、临时文件等。经验上它需要设置为max_connections的2倍以上或者更高。有些系统在启动时即使你在配置文件里写了open_files_limitMySQL可能也会被系统级的ulimit -n限制住启动后实际生效值低于配置值。遇到这种情况需要在系统层面把服务进程的文件描述符上限也调大再重启数据库进程才能彻底生效。一条命令就能查实际生效值SHOW VARIABLES LIKE open_files_limit;如果查出来远低于配置值别犹豫去查系统服务脚本里的LimitNOFILE设置如果是systemd管理的话。4. 实操过程记录一次连接数排障的完整复盘4.1 第一现场现象和初步判断一次典型的线上告警发生在下午三点左右业务高峰期。当时监控显示Threads_connected在五分钟内从200多涨到700多而max_connections当时是1000虽然还没到上限但告警规则设的是超过80%就报警。我登录到服务器上做的第一件事不是调参数而是快速收集现场数据mysql -u root -p -e SHOW GLOBAL STATUS LIKE Threads_connected; mysql -u root -p -e SHOW GLOBAL STATUS LIKE Threads_running; mysql -u root -p -e SHOW PROCESSLIST;Threads_running这个时候显示到了40多这在一个8核的数据库实例里已经算比较高的并发水位了正常业务应该稳定在10以下。processlist里大量的连接状态集中在Sending data少数在Waiting for table metadata lock。这是一个很典型的信号真正让连接数堆积的往往不是连接本身而是查询执行得太慢每个查询占着一个连接迟迟不释放新的请求就只能不断建立新连接于是连接数开始滚雪球式上涨。4.2 顺着线索追根因慢SQL和元数据锁顺着Sending data状态追下去我用performance_schema和慢查询日志交叉确认最终定位到了一条新上线报表业务的SQL。那是一条关联了6张表的聚合查询最内层子查询没有走索引对整个大表做了全表扫描。因为这条SQL的执行时间从几秒一路劣化到几十秒查询期间一直持有相关表的共享锁后续对这些表的任何写操作都会阻塞在Waiting for table metadata lock状态上。一个慢SQL就像一个堵在路口的车后面的车只能在原地等着连接数自然就涨上来了。解决路径就很清晰了一方面立刻跟业务确认了这条SQL是否可以下掉在高峰期之前先在应用侧限制了该报表功能的开放另一方面在数据库侧用KILL把已阻塞住的查询线程清掉恢复连接资源。这里贴一个在紧急情况下常用的杀会话操作-- 查看执行时间超过120秒的非Sleep连接 SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE command ! Sleep AND time 120 ORDER BY time DESC; -- 如果有明确的要终止的会话id执行KILL KILL 12345;KILL操作有时会让人犹豫担心杀掉业务SQL会造成数据问题。其实对于长时间卡住的查询留着它只会让更多连接堆积越早处理影响越小。注意如果是通过主从复制架构运行杀查询不会影响已经提交的事务数据一致性没有问题最多就是该条SQL返回失败应用层重试一下而已。4.3 事后配置调整堵住源头而非只是救火现场把连接掐断之后并不是结束。如果不调整配置同样的告警明天还会来。我做了三件事把慢SQL对应的表补上了合适的索引。新增了联合索引之后这条SQL的执行计划从全表扫描变成索引关联查询时间从30多秒降到了1秒以内。重新评估了连接池和实例数。那段高峰期出现连接堆积跟应用侧连接池偏大也有关系原来的连接池最大连接数设置偏高调整到合理值后即便下次再出现慢SQL连接堆积的速度也会慢很多。调整了数据库侧的超时参数把wait_timeout从28800降到600防止夜间低峰期连接大量挂起白白消耗资源。那次之后连接数监控曲线平稳了很多Threads_connected长期稳定在100上下高峰也不会超过200。回顾整个过程连接数告警只是入口真正的杠杆点是在SQL执行效率和连接生命周期管理上单纯调大max_connections属于治标不治本。5. 常见问题与排查技巧实录一份能直接抄作业的速查表我把这几年遇到过的连接数相关问题和排查技巧整理成了一张速查表按场景分类每一条都是踩过坑之后沉淀下来的希望能让你在遇到类似问题时少翻点文档。现象可能原因快速排查方法解决方向应用报Too many connectionsmax_connections设置偏低或连接未释放查Threads_connected是否长期接近上限查Connection_errors_max_connections是否在涨调大max_connections同时排查连接池和空闲回收大量连接处于Sleep状态应用连接池最大连接数偏大、wait_timeout过长按user/host统计连接数分布调小连接池最大连接数把wait_timeout调到300~600Host is blocked报错max_connect_errors超过阈值IP被临时拉黑查错误日志中是否有 blocked 字样的记录mysqladmin flush-hosts解除并调大max_connect_errors连接建立非常慢但最终能连上反向DNS解析超时看skip_name_resolve是否为OFF开启skip-name-resolve授权表改用IP连接数飙高的同时CPU也高慢SQL并发执行占满CPU查Threads_running、慢查询日志和processlist优化慢SQL、加索引、必要时限流客户端报Connection reset by peer连接被MySQL主动断开wait_timeout小于应用空闲时间查应用日志与数据库错误日志时间线适当调大wait_timeout或让连接池定期探活改了max_connections重启后又变回原值只用了SET GLOBAL在线调整未改配置文件查SHOW VARIABLES LIKE max_connections与配置文件是否一致同步修改my.cnf并持久化文件打开数达到上限导致无法新建连接open_files_limit或系统ulimit偏低查open_files_limit和系统ulimit -n同步调大系统级文件和MySQL参数除了这张表另外还有几个技巧想单独提一下。一个是采样趋势数据。建议在监控系统里对Threads_connected、Threads_running、Max_used_connections以及Connection_errors_max_connections这4个指标做持续的采集和告警而不是只配一个连接数总量。因为只看连接数总量会掩盖很多问题比如连接数不高但Threads_running一直很高说明SQL效率有问题连接数只是一个滞后指标而已。另一个技巧是善用performance_schema来查历史连接的统计信息。比如想了解某段时间里连接都是被谁创建出来的DTO的accounts表和host_cache表里都有按账号、按主机名汇总的连接统计能够给你提供精确的数据支撑比翻应用配置文件猜省力得多。再有就是变更要小步走。无论是调max_connections还是wait_timeout我建议一次只改一个参数并且在低峰期观察一段时间然后再决定下一步。很多线上事故就是一次性把一堆参数全部改了出了问题都不知道是哪一项改坏的。数据库调优和做饭有点像调味料是一样一样加的一股脑全部倒进去菜咸了都说不清是酱油放多了还是盐放多了。6. 连接数之外一个好习惯带来的长期回报连接数的配置和查询本身并不复杂真正考验人的是面对“连接数异常”时能不能快速定位到根因。我自己经历过的几次“事故”都证明连接数只是冰山浮在水面上的那一角底下藏着慢SQL、连接池配置不合理、参数之间互相牵制、甚至应用层连接泄漏等问题。所以每当你准备调大max_connections的时候先停下来问自己一句连接数上涨是因为业务真的变好了还是因为有连接在“赖着不走”方向对了调整才有意义。另一个想特别提醒的是连接数的日常治理是一个持续过程不是一次调参就一劳永逸。业务在迭代、数据量在增长、应用架构在调整连接数的水位也会跟着变。建议每季度回顾一下Max_used_connections的历史曲线如果它离配置的上限越来越近就提前做扩容或优化如果它离上限很远但连接数告警频繁那问题大概率出在连接生命周期上优先去看连接池配置和超时参数。最后分享一条我用了很久的实操习惯每次处理完连接数问题我都会把当时的截图、执行的SQL、参数变更记录和后续的监控曲线存成一个文档放在团队的数据库知识库里。等到下一次遇到类似问题直接翻出旧记录比对能快很多。你踩过的坑、调过的参数都是值钱的资产别让它们白白流失。
返回列表