ARTICLE DETAIL

资讯详情

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

MySQL连接数优化:从Too many connections到合理配置max_connections

MySQL连接数优化:从Too many connections到合理配置max_connections 如果你在高峰期遇到过应用连着报ERROR 1040: Too many connections肯定知道这种无力感数据库明明没挂请求却全部被挡在门外。很多人第一反应是把max_connections调大从默认的 151 调到 1000、10000甚至更高。但 MySQL 到底最多能有多少连接这个问题没有固定答案——它取决于操作系统资源、内存预算和你的业务模型。这篇不是教你无脑调参而是从默认值讲起把连接数相关的排查链路、资源边界、连接池规划设计一遍顺便给出可以直接照做的修改和验证步骤适合 DBA、后端开发和运维同学。1. 官方默认值为什么是 151先弄懂连接数到底在数什么1.1 从 SHOW VARIABLES 开始先看几个最基础的命令SHOW VARIABLES LIKE max_connections; SHOW STATUS LIKE Max_used_connections; SHOW STATUS LIKE Threads_connected;第一行返回的通常是151这就是 MySQL 5.7 和 8.0 的默认上限。Max_used_connections记录的是从实例启动到现在连接数的历史最高值Threads_connected是当前正在占用的连接数。如果你从来没过问过这几个数字建议现在就去生产库上跑一下。很多服务从部署到跑了一两年Max_used_connections可能早就接近过上限只是当时没触发报错应用侧连接池把错误吞了而已。这种“闷声接近红线”的状态最危险。MySQL 的max_connections不是根据你的机器配置估算出来的而是编译进二进制的默认值。也就是说哪怕这台机器有 64G 内存、64 核 CPU它默认也只让你同时连 151 个。听起来很保守对吗其实这是故意的。1.2 连接、会话、线程三者不是一回事很多新手把连接数和并发查询数混在一起这是需要先掰扯清楚的概念。MySQL 的经典模型是thread per connection每建立一个客户端连接mysqld 就为它分配一个线程。这个连接的整个生命周期里线程归属不变。你发一个查询在线程上执行你的事务也在对应的会话里提交或回滚。所以“连接数上限”统计的是同时连到 mysqld 的客户端数量包括应用、监控、管理工具、备份工具甚至从库拉到主库的复制连接——后者也占主库一个连接。连接、会话、事务是三个不同层级的东西概念说明连接客户端和 MySQL server 之间的网络通道TCP 层建立会话连接建立后对应的服务端状态包括变量、临时表等事务会话内开始、提交或回滚的一组操作可以跨多个语句一个连接可以只有一个会话一个会话里可以连续执行多个事务。连接建立后即使什么都不干也会占一个线程和一堆内存结构。所以 151 意味着最多 151 个线程在栈空间里待命别小看这一点后面调到几千时线程调度成本会非常明显。1.3 151 这个数字从哪来为什么是 151 而不是 150 或 200官方没有特别详细的解释但从历史看这个数值是从 MySQL 多年的默认行为沿用下来的属于经验阈值。MySQL 早期版本默认是 100后来调整过。设计者的思路大概是默认安装的 MySQL 要能在普通服务器上跑起来不能因为一堆连接直接把资源耗尽。151 这个数字既能让常规业务配合连接池跑得流畅又不会让资源耗尽太容易发生。另外官方留了一个后门即使max_connections已经到达上限拥有SUPER或CONNECTION_ADMIN权限的账号仍然可以再连进来目的就是让管理员能够上去查问题、Kill 会话。很多人在报错后连不上其实是自己用的不是管理账号或者权限不够。这个细节非常关键——生产环境一定要准备一个具有管理权限的账号并把它放在专用的管理通道上别和业务账号混用。2. Too many connections的完整排查链路先找到谁占满了连接2.1 报错前有哪些信号不要等到应用报错你完全可以在指标里提前看到信号。MySQL 有一组状态计数器专门记录连接相关的数据至少要盯四个Threads_connected、Max_used_connections、Connection_errors_max_connections、Aborted_connects。SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Max_used_connections; SHOW GLOBAL STATUS LIKE Connection_errors_max_connections; SHOW GLOBAL STATUS LIKE Aborted_connects;Connection_errors_max_connections只要开始增长说明已经发生了拒绝连接Max_used_connections除以max_connections的比例如果超过 0.8就要准备扩容或治理了。Aborted_connects也值得关注它表示连接建立了但没成功完成握手可能是网络、认证或者连接数限制导致。2.2 达到上限后服务器到底做了什么当客户端发起新连接时mysqld 会检查当前连接数。如果已经达到max_connections握手阶段就会直接返回错误码ER_CON_COUNT_ERROR也就是应用侧常见的ERROR 1040: Too many connections。注意这是在握手阶段拒绝的。你的连接池会不断发起重试每一次重试都会消耗一点网络和 CPU。数据库本身可能还有能力处理查询但新请求全部进不来这种状态比“数据库彻底挂了”更难处理因为局部服务的重试风暴会把网络和 CPU 抬高。已有连接不会被动断开但查询可能因为资源争抢变慢。这里常有一个坑很多连接池的maximumPoolSize设置过大导致每个应用实例都在抢占连接一旦某个节点出现网络分区或应用卡顿连接池里的连接不会立即释放而是等待 timeout。于是数据库侧的连接数被瞬间打满新的请求根本进不来。2.3 一条 SQL 找出连接占用者遇到连接数偏高时第一件事不是改参数而是看这些连接是谁、在干什么。SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO FROM information_schema.PROCESSLIST ORDER BY TIME DESC LIMIT 50;重点关注TIME大、STATE不为空、INFO有查询语句的会话。TIME表示当前命令已经执行了多少秒。很多连接其实只是COMMAND为Sleep的空闲连接大概率是没释放的池化连接。如果COMMAND为Query且TIME很大说明有慢查询或锁等待。找到具体连接后可以用KILL ID清理但动手前要确认它不是正在执行的重要事务。MySQL 8.0 里还可以查performance_schema.threads字段更全甚至能看线程内部状态不过日常排查PROCESSLIST已经足够。2.4 一个典型的连接耗尽案例我处理过一个典型场景晚上十点定时任务启动一个 Java 服务配置了连接池maximumPoolSize200同一套业务有十几个服务实例共用一个实例再加上主从复制的连接、监控连接、备份连接所有连接一次性涌到数据库。当时的max_connections只有 300结果还没到任务高峰就报Too many connections。排查过程是先看PROCESSLIST发现大量COMMANDSleep的闲置连接占了一半来自同一个应用的某个历史版本代码里的Connection对象没有在finally里关闭。应用侧把连接池释放后Threads_connected从 298 掉到 150。接下来才改配置把max_connections调到 800同时给应用侧设置连接池上限。如果你一上来就调大第二个晚上照样打满只是数值更高。连接数爆掉背后几乎总是应用侧有问题。3. 修改 max_connections 之前先算清楚操作系统愿不愿意3.1 文件描述符第一个隐藏门槛一个 TCP 连接在 Linux 上就是一个 socket对应一个文件描述符。mysqld 需要打开文件描述符来接受新连接。如果你的ulimit -n是 1024哪怕你把max_connections设置为 5000连接数到 1024 附近就会开始出问题。查看方法ulimit -n cat /proc/mysql_pid/limits | grep open files修改方法分两层。操作系统层在/etc/security/limits.conf里加mysql soft nofile 65535 mysql hard nofile 65535如果用 systemd 管理 MySQL还要在mysqld.service里加LimitNOFILE65535否则 limits.conf 可能不生效。系统全局还有fs.file-max通过sysctl fs.file-max查看。默认值通常很大但容器环境容易撞到限制。如果你在 Docker 里跑 MySQL别忽略容器 runtime 的 ulimit。3.2 每个连接的内存成本比想象中高连接本身不是一个 TCP 连接那么简单。MySQL 会为每个连接准备执行查询时要用到的 buffer常见的连接级参数包括sort_buffer_size排序缓冲join_buffer_size连表查询缓冲read_buffer_size顺序读缓冲read_rnd_buffer_size随机读缓冲net_buffer_length网络发送缓冲这些 buffer 不是一建立连接就全部分配而是执行到对应操作时按需分配。但最坏情况下每个连接都可能同时占满。举个例子一台服务器max_connections5000同时有 1000 个活跃连接如果每个连接平均被分配到 3MB 的连接级内存这部分就是 3000MB。如果某个 session 正在做大排序sort_buffer_size又设置成 64MB一个连接就能吃掉更多。所以我评估连接数时永远按最坏情况估算而不是按空闲状态算。3.3 线程与 CPU连接越多上下文切换越贵thread per connection模型决定了连接数越多线程越多操作系统调度器需要不停切换。如果线程数明显超过 CPU 核心数切换成本会显著上升。这就是为什么有些场景下调大max_connections后吞吐量不升反降。8 核或者 16 核的机器几百个活跃线程已经是很有压力的状态。如果连接池里存在大量空闲连接这些线程虽然不在跑查询但依然占用内存栈和文件描述符。所以不要把max_connections和性能画等号。3.4 估算合理上限的参考公式一个粗略但实用的公式是合理 max_connections ≈可用内存 - 全局缓冲/ 单连接平均缓冲上限其中全局缓冲主要指innodb_buffer_pool_size、key_buffer_size、binlog 缓存、临时表内存等单连接平均缓冲上限可以按sort_buffer_size join_buffer_size read_buffer_size net_buffer_length等参数加总保守一点就按所有连接级参数的和算。比如一台 64GB 机器innodb_buffer_pool_size设了 32GB其他全局开销约 3GB可用内存约 29GB。单连接按 4MB 算理论上能撑 7000 多个连接。但还要考虑其他进程、突发分配、内存碎片所以实际设 3000 到 5000 更安全。这只是经验值不是精确值。4. 你的业务到底需要多少连接连接池模型下的容量规划4.1 OLTP 场景连接数 应用实例数 × 连接池上限 管理通道很多人看到一个库峰值有几十万 QPS就想把max_connections调到几万这是误区。应用不会为每个请求都新建连接而是通过连接池复用。真实公式是连接数 ≈ 应用实例数 × 每个实例的连接池最大大小 监控、备份、主从、人工维护通道假设你有 20 个 Java 服务实例每个实例的 HikariCPmaximumPoolSize是 50理论最高就是 1000再留 20% 余量设置 1200 就够。HikariCP 的maximumPoolSize不是越大越好。单机数据库的活跃执行能力有限连接池太大只会把请求积压到数据库并不会提高吞吐。常见估算方式是按高峰 TPS 和平均事务耗时来算连接池大小 ≈ TPS × 平均事务时间(秒)同时参考 CPU 核数核数不高时不要超过几十。几十万 QPS 的架构靠的是分库分表和缓存不是单库几万连接。4.2 报表、批处理与慢查询场景排队比扩连接更有效报表、数仓工单、定时任务这类场景查询本身跑得慢一个连接可能被占用几十秒。这时候把max_connections从 500 调到 2000表面上是容纳更多人实际是让更多慢查询并发执行把 CPU 和 IO 拖垮平均响应时间反而恶化。正确做法是在应用侧做任务队列和并发限流让多余请求排队而不是并发拥进去。数据库侧设置合理的锁等待和语句超时避免个别大查询长期占着连接。4.3 max_user_connections控制单一应用的影响面全局max_connections管的是整个实例。除了它MySQL 还提供max_user_connections可以针对单个账号限制并发连接数。如果你有多个应用共享一个实例这个参数非常有用。比如 A 应用连接池配置错误一次性建立 1000 个连接如果没有账号级限制整个实例都会遭殃。如果给 A 账号设置max_user_connections200最多只能占 200 个其他应用还能存活。注意max_user_connections对SUPER权限账号不生效设置时要考虑运维通道。4.4 把连接数拉到几千后性能真的更好吗我见过有人把max_connections调到 20000理由是报表任务多。压测之后发现 QPS 从 8000 掉到 5000因为线程争抢和锁等待把瓶颈提前了。max_connections只是上限不是目标值。你要做的是监控让现实中的Max_used_connections离上限有充足余量同时把活跃线程Threads_running控制在 CPU 可承受范围内。如果Threads_connected很高Threads_running却很低通常是连接池没有释放空闲连接如果Threads_running也接近Threads_connected说明并发请求真的很多这时候调大上限只是缓解症状查询优化、加索引、减少锁冲突才是出路。5. 动手修改 max_connections动态调整、持久化与重启验证5.1 检查当前值与历史峰值改之前先记录当前状态方便改完后对比SHOW VARIABLES LIKE max_connections; SHOW STATUS LIKE Threads_connected; SHOW STATUS LIKE Max_used_connections; SHOW STATUS LIKE Connection_errors_max_connections;如果你已经走到这一步大概率是Max_used_connections接近了上限或者错误计数在涨。先记下数据改完再复查。5.2 动态修改SET GLOBAL 与 SET PERSISTMySQL 支持运行时修改不用重启。最直接的是SET GLOBAL max_connections 1000;这个操作立即生效对所有新连接生效但重启后会被配置文件覆盖。MySQL 8.0 推荐用SET PERSIST它会写入数据目录下的mysqld-auto.cnf重启后自动恢复SET PERSIST max_connections 1000;如果只想保存到文件、暂不改变当前值可以用SET PERSIST_ONLY max_connections 1000;这个值会在下次重启时生效。MySQL 5.7 没有PERSIST语法只能手写配置文件注意版本差异。5.3 写配置文件my.cnf 与常见坑如果你想用传统方式持久化就在/etc/my.cnf或当前配置文件 include 的文件里找[mysqld]段加一行[mysqld] max_connections1000保存后重启 mysqld。注意是[mysqld]不是[client]也不是[mysql]写错段位不会生效。常见坑有三个MySQL 8.0 用过SET PERSIST之后又手改 my.cnf两边值不一样mysqld-auto.cnf优先级更高容易让排查困惑。RPM 安装和源码安装的默认配置文件路径不同可能不在/etc/my.cnf用mysqld --verbose --help | grep -A 1 Default options查看实际读取顺序。Docker 部署时改的是容器内配置容器重建后配置就丢了必须挂载配置文件或用环境变量方式管理。5.4 压测验证别只改不测修改之后要验证不能只看当前Threads_connected没涨就觉得够了。简单做法是用 sysbench 或 mysqlslap 模拟压力同时监控Threads_running和Threads_connected。比如用mysqlslap做并发连接测试mysqlslap --host127.0.0.1 --userroot --passwordxxx \ --concurrency200 --iterations10 \ --create-schematest --querySELECT SLEEP(0.1) --number-of-queries10000执行期间另开一个终端每秒记录一次Threads_connected、Threads_running和 CPU 使用率。如果Threads_running一直接近 CPU 核数的 4 到 6 倍说明活跃并发已经超出机器承受能力这时候应该回头调整业务并发而不是继续加max_connections。6. 连接数优化的三板斧堵漏水、缩占用、早发现6.1 入口治理连接池管理和泄漏检测绝大多数连接数问题的源头不在数据库而在应用。Java 的 HikariCP 要设置maximumPoolSize、minimumIdle、connectionTimeout并且开启泄漏检测leakDetectionThreshold。Python 的 SQLAlchemy 要设置pool_size和pool_recycle。Node 的 mysql2 连接池同理。每类客户端连接都要设置超时避免长时间Sleep占用。如果你在PROCESSLIST里发现大量COMMANDSleep的连接先查应用是不是忘了close。这是最经典的连接泄漏。6.2 用超时参数缩短连接占用MySQL 侧有几个参数值得关注。wait_timeout和interactive_timeout控制空闲连接多久被断开默认是 8 小时。对于连接池之外的空闲连接来说太长。如果你有大量不通过连接池的脚本连接可以调短一些比如 300 秒。innodb_lock_wait_timeout控制事务等待行锁的秒数默认 50 秒。如果锁冲突严重适当调小可以减少连接长时间卡在锁等待的状态。MySQL 8.0 还支持单条 SELECT 的MAX_EXECUTION_TIMEHint限制单个查询的最长执行时间避免慢查询一直占着连接不释放。6.3 慢查询和锁等待是连接数的放大器一个查询慢 5 秒连接就多占用 5 秒100 个连接同时卡在锁等待后续 900 个请求只能在连接池里排队。所以治理连接数本质是治理查询和锁。建议开启慢查询日志分析执行频率高、耗时长、锁等待多的语句SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;如果慢查询日志里频繁出现Waiting for table metadata lock或Waiting for lock wait timeout要重点处理 DDL、索引和事务而不是扩连接。6.4 监控与告警让数据替你说话最后是监控。Prometheus 搭配 mysqld_exporter 可以直接采集max_connections、threads_connected、threads_running这些指标。告警规则可以这样设计Threads_connected / max_connections 0.8持续 5 分钟触发 Warning。Connection_errors_max_connections 0触发 Error。Threads_running持续超过 CPU 核数的 4 倍时提示查询过慢。我在实际处理连接数问题时雷打不动的顺序是先看Max_used_connections和线程数再看PROCESSLIST里Sleep和Query的分布最后才决定是改参数还是改应用。坦白说真正需要把max_connections调大两倍以上的场景很少大多数是连接泄漏、慢查询、锁等待和连接池配置不合理。调大上限只是给系统更多余量但如果不解决源头余量迟早会被填满。这也是我一直强调的连接数不是越大越好而是够用且留有余量并且所有余量都要有监控盯着。
返回列表