ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL 1040报错“Too many connections”分析与连接数管理方案

MySQL 1040报错“Too many connections”分析与连接数管理方案

凌晨三点,电话把我从梦里拽起来。某服务监控告警,数据库连接数直接撞到天花板,应用日志里密密麻麻刷着一行报错:

pymysql.err.OperationalError: (1040, 'Too many connections')

用过MySQL加Python组合的,这行报错迟早会碰上。它翻译成人话就是:数据库的连接数被占满了,你对MySQL说“我要连一下”,MySQL回答“没位置了,你走吧”。最麻烦的是,这种报错从来不会只影响一个接口——所有依赖数据库的请求会同时失败,整个应用跟着雪崩。今天就把这个问题的定位思路、MySQL参数调优方法、代码侧修复方案一次性讲透,文末还有我自己的踩坑记录,希望能帮你少走弯路。

1. 先搞清楚1040是怎么发生的

1.1 从连接握手到拒绝

MySQL处理一个新连接,大致要经过这几步:TCP握手、认证、建立会话,然后把当前连接数加一。MySQL内部维护了一个计数器,记录当前活跃的线程数,当这个数值超过了一个叫max_connections的配置项时,新的连接请求会被直接拒绝,返回的错误码就是1040。

你可以把max_connections想象成一家餐厅的座位数。餐厅按桌数接待客人,座位满了新来的只能排队或者被请走。MySQL不会让新连接排队,它会直接在门口挂个牌子:“人满了,改天再来。”这个“改天再来”在代码层面就是Too many connections。

很多人以为这个报错只有DBA才会遇到,其实开发侧更容易踩中。因为max_connections默认值并不大——MySQL 5.7及以上版本默认是151,而你的应用可能同时起了十几个实例,每个实例搞了个连接池,池子大小设成30,一算就是几百个连接,早就超过了默认上限。所以这个问题的高发场景,往往不是数据库负载真的有多高,而是连接的使用姿势出了问题。

1.2 连接数被吃掉的五类常见原因

根据我这些年处理过的线上问题,连接数爆满的原因基本逃不出下面这几类:

  • 连接用完不释放。代码里执行完SQL之后,没有调用conn.close(),也没有用连接池归还连接。最经典的写法是循环里每次新建连接,但异常分支里漏了关闭,跑一晚上连接数就上去了。
  • Sleep线程大量堆积。客户端连上MySQL但长时间不干活,连接处于空闲的Sleep状态。很多ORM框架默认开启了连接池,但空闲连接不回收,池子里全是僵尸连接。
  • 慢查询占着茅坑不拉屎。某条SQL走了全表扫描,一跑就是几十秒,并发一高连接全部被占住,新请求自然连不上。
  • 短连接风暴。应用频繁创建和销毁连接,但MySQL释放连接是异步的,大量TIME_WAIT状态的连接还没来得及清理,新连接又涌了进来。
  • 连接池参数不收敛。应用侧连接池的maxsize设置得过大,多个服务实例叠加之后,总连接数远超MySQL上限。这是最容易被忽视的,因为单看每个实例都合理,合到一起就爆了。

这时候解决问题的思路很简单:先确认连接数是不是真的到了上限,再看是谁占着连接不放,最后决定是调MySQL参数还是修代码。

2. 三分钟定位连接数是否真的到了上限

2.1 三个必查命令的解读

登录MySQL之后,第一步先跑这三个命令:

SHOW VARIABLES LIKE 'max_connections'; SHOW STATUS LIKE 'Threads_connected'; SHOW STATUS LIKE 'Max_used_connections';
  • max_connections:MySQL允许的最大连接数,这是“天花板”。
  • Threads_connected:当前实际存在的连接数,这是现在的“占用量”。
  • Max_used_connections:自MySQL启动以来,历史上同时连接数的峰值。

如果Threads_connected已经等值于或非常接近max_connections,基本就可以断定是连接数满了。如果Threads_connected不高,但Max_used_connections很高,说明之前出现过尖峰连接数,有可能是某个时刻的并发请求打爆了上限。

还有一种情况需要注意:Threads_connected不高,但依然报1040。这种通常是短时间内有大量连接请求涌入,MySQL在瞬间收到了超过上限的握手请求,即使当前活跃连接数还没到峰值,也会触发拒绝。这种场景在应用刚重启、缓存刚失效的时候特别常见。

提示:这三个状态值都只能看到当前会话所在的MySQL实例。如果有主从架构,一定要在主库上排查,因为报1040的通常是写入压力更大的主库。

2.2 揪出占着连接不走的元凶

确认连接数满了之后,下一步就是看这些连接都是谁。执行这条SQL:

SELECT user, host, db, command, time, state, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY user, host, db, command ORDER BY cnt DESC;

command列是关键。正常执行SQL中的连接,command是Query;空闲连接,command是Sleep。如果你看到大量Sleep状态的连接,而且time字段几十秒甚至几分钟,那基本就是连接泄漏或者连接池的空闲回收策略没生效。

再精细一点的排查,可以按时间排序,把所有耗时长的Query找出来:

SELECT id, user, host, db, command, time, state, LEFT(info, 200) AS sql_text FROM information_schema.processlist WHERE command = 'Query' AND time > 10 ORDER BY time DESC;

这条查询能帮你快速定位慢查询。time列单位是秒,如果发现有SQL跑了超过10秒还在执行,它的连接就是“被占住”的那一批。这时候DBA常用的处理方式是KILL掉这些慢连接,给新请求腾位置,但这只是救火,根因还是要回到SQL优化和代码修复上。

2.3 从历史峰值反推正常水位

Max_used_connections很多同学会忽略,但在容量规划时它特别有用。假如你的max_connections是1000,Max_used_connections是850,说明系统历史上已经跑到了85%的利用率,这个水位其实已经很危险了,随时可能因为一次流量高峰直接1040。

我习惯的评估标准是:Max_used_connections占max_connections的比例长期超过70%,就要开始做扩容或者优化连接管理了。如果已经到90%以上,那这次1040就不是偶然,而是必然。结合这个历史峰值,你才能判断单纯调大max_connections够不够,还是必须得在代码层面压连接数。

3. 参数调整的完整方案:既要调得动也要算得准

3.1 临时调优和永久修改的正确方法

确认连接数满了,最直接的应急手段是把max_connections调大。

临时调的话,执行完立即生效,MySQL重启后会丢失:

SET GLOBAL max_connections = 1000;

永久调的话,需要改配置文件,然后重启MySQL:

[mysqld] max_connections = 1000

这里要注意权限问题:SET GLOBAL需要SUPER权限,普通账号执行会直接报权限不足。如果你用的是云数据库,可能连SUPER权限都没有,那就得去控制台上调整参数组。

注意:不要一上来就调到1万2万。连接数不是越多越好,每个连接线程都要吃内存,调完之后MySQL不发生1040了,但内存被吃爆、触发OOM,那是另一种事故。

3.2 连接线程的内存账本:max_connections不能拍脑袋调

为什么我一直强调不能乱调max_connections?因为每个连接线程不是免费入驻的。MySQL给每个连接分配的内存结构,包括thread_stack、net_buffer_length、sort_buffer_size、join_buffer_size、read_buffer_size、read_rnd_buffer_size等。这些参数里有些是按会话分配的,有些是按SQL执行时分配的,但每个连接至少都会占用几MB到十几MB的内存。

简单估算公式可以这么写:

每个连接内存 ≈ thread_stack + net_buffer + sort_buffer + join_buffer + read_buffer

默认配置下,thread_stack是256KB,sort_buffer_size是256KB,join_buffer_size是256KB,read_buffer_size是128KB,几个加起来大概每连接1MB左右。听着不多?如果你的max_connections调到5000,光连接线程就要吃掉5GB内存。这还不算实际执行大查询时额外分配的临时表空间和排序空间。

所以合理的做法是先看机器内存,再反推连接上限。比如机器是16GB内存,MySQL专用,系统和其他进程预留约4GB,剩下12GB能分给连接线程。按每连接约2MB的保守估算(考虑高负载时的缓冲开销),max_connections设置在6000以内才比较安全。但实际通常建议留足余量,压到3000~4000这个区间,配合连接池使用完全够撑起正常的业务并发。

3.3 两个关键超时参数的联动调整

光调大连接上限还不够,还得让空闲连接尽快释放,这时候两个参数就很重要:

[mysqld] wait_timeout = 60 interactive_timeout = 60

wait_timeout针对普通连接,interactive_timeout针对交互式命令行连接。单位是秒,默认是28800秒也就是8小时。默认值意味着一个连接空闲8小时才会被MySQL主动收走,这期间它一直占着一个连接名额。生产环境如果不改,8小时内连接池的空闲连接全堆在MySQL这边,连接数当然高。

把两个超时都调到60秒,MySQL会在一分钟之后主动断开空闲连接。但这里有个坑要提醒:如果应用连接池的ping重连机制没做好,MySQL把空闲连接断开后,连接池还在复用这些陈旧连接,就会报Lost connection to MySQL server during query这类错误。调试时我一般建议先从wait_timeout=600开始,让连接至少能扛过业务上正常事务的时长,确认稳定后再逐步调小。

4. 代码侧修复:这才是治本的事

4.1 典型错误写法与正确释放姿势

线上不少1040其实是代码挖的坑。我见过很多同学这样写:

import pymysql def save_data(items): for item in items: conn = pymysql.connect( host="127.0.0.1", user="app", password="******", database="app_db", ) cur = conn.cursor() cur.execute("INSERT INTO t_order (sku_id, qty) VALUES (%s, %s)", (item["sku_id"], item["qty"])) cur.close() conn.close()

表面上每条数据都调用了conn.close(),好像没问题。但真实线上代码在查询和写入之间可能夹杂着大量业务逻辑,任何一个return、continue、异常抛出都会跳过close()。跑一个通宵任务,连接数就一点一点涨上去了。

正确的做法是用上下文管理器,确保任何分支都会关闭连接:

import pymysql def save_data(items): for item in items: conn = pymysql.connect( host="127.0.0.1", user="app", password="******", database="app_db", ) try: with conn.cursor() as cur: cur.execute( "INSERT INTO t_order (sku_id, qty) VALUES (%s, %s)", (item["sku_id"], item["qty"]), ) conn.commit() except Exception: conn.rollback() raise finally: conn.close()

关键点在finally块。不管业务逻辑执行到哪一步,连接最终都能被关闭。但我必须说句实话:这只是最基本的“不泄漏”,还远远不够。因为频繁地创建和销毁连接,握手认证的开销很大,高并发下性能会受影响,而且之前提到的短连接风暴也会让MySQL端压力陡增。

4.2 给pymysql加一个轻量级连接池

要同时解决“连接复用”和“防止泄漏”两个问题,正经做法是引入连接池。连接池的思路很简单:提前创建一批连接,业务去池子里借,用完还回来,只有空闲连接超过阈值时才真正关闭。

网上很多方案直接推DBUtils,但我更建议先搞懂一个最小可用的连接池是怎么实现的,这样调起参数来心里有底。我写过一个轻量版,核心代码长这样:

import queue import threading import pymysql from pymysql.err import OperationalError class MysqlPool: def __init__(self, host, port, user, password, database, maxsize=10, timeout=5): self._config = { "host": host, "port": port, "user": user, "password": password, "database": database, "charset": "utf8mb4", "cursorclass": pymysql.cursors.DictCursor, "autocommit": True, } self._timeout = timeout self._pool = queue.Queue(maxsize=maxsize) self._created = 0 self._maxsize = maxsize self._lock = threading.Lock() def _create_conn(self): return pymysql.connect(**self._config) def acquire(self): try: conn = self._pool.get_nowait() # 取出连接先探活,避免拿到一条被MySQL断掉的死连接 try: conn.ping(reconnect=True) return conn except Exception: conn.close() return self._create_conn() except queue.Empty: with self._lock: if self._created < self._maxsize: self._created += 1 return self._create_conn() raise OperationalError("连接池已满,请稍后重试") def release(self, conn): try: conn.ping(reconnect=True) self._pool.put_nowait(conn) except Exception: conn.close() with self._lock: self._created -= 1

关键逻辑有三点:

  • acquire时优先复用池里的旧连接,取出来先ping一下,确保连接还是活的,然后再交给业务使用。
  • 池空了但没到maxsize才真正新建连接,这样连接数的峰值会被maxsize死死压住。
  • release时如果发现连接已经不可用就关闭,同时把_created减一,下次需要时再创建新的。

业务侧的使用姿势也简单:

conn = pool.acquire() try: with conn.cursor() as cur: cur.execute("SELECT * FROM t_order WHERE order_no = %s", ("SO2024001",)) row = cur.fetchone() finally: pool.release(conn)

注意这里release替代了原来的close,连接不会真的被关掉,而是回到池子里继续待命。这样应用侧几十个请求并发,用的其实就是池子里的那最多10条连接,MySQL的连接数压力瞬间就下来了。

提示:生产环境我不会推荐自己造连接池轮子,直接用DBUtils.PooledDB会更稳,它内部处理了线程隔离、连接失效验证等细节。上面的代码只是为了帮你理解连接池的工作原理。

4.3 多实例部署下的连接数预算表

接前面说的,1040的常见场景是多个应用实例叠加之后连接数超标。我一直建议,部署新服务之前先把连接预算算一遍:

实例类型实例数每实例连接池上限对MySQL的连接总量
订单服务1020200
用户服务61590
报表服务430120
合计——410

如果MySQL的max_connections配的是800,那加上监控账号、管理后台、数据同步任务这些额外的连接,安全水位是够的。如果合计已经占到max_connections的60%以上,就不要再往上加服务实例了,要么扩容数据库,要么把每个实例的连接池上限调小。

我之前接手过一套系统,订单服务30个实例,每个连接池上限50,光这一个服务就要占1500个连接,而MySQLmax_connections才配1000,不1040才怪。后来把每个实例的池上限从50砍到10,数据库侧完全没动,连接数压力直接降了一半多。

5. 监控、告警与应急兜底

5.1 数据库连接数的日常巡检脚本

连接数这种问题,只靠半夜被报警电话叫醒才处理,实在太被动。我习惯在数据库侧放一个巡检脚本,定时跑一遍,把连接数指标实时记录下来:

import pymysql monitor_config = { "host": "127.0.0.1", "user": "monitor", "password": "******", "database": "mysql", } conn = pymysql.connect(**monitor_config) with conn.cursor() as cur: cur.execute("SHOW VARIABLES LIKE 'max_connections'") max_conn = int(cur.fetchone()[1]) cur.execute("SHOW STATUS LIKE 'Threads_connected'") current_conn = int(cur.fetchone()[1]) cur.execute("SHOW STATUS LIKE 'Max_used_connections'") max_used = int(cur.fetchone()[1]) ratio = current_conn / max_conn * 100 print(f"max_connections={max_conn}, current={current_conn}, " f"max_used={max_used}, used_ratio={ratio:.1f}%") conn.close()

脚本拿到的used_ratio可以作为告警依据。我建议的阈值是:超过70%发预警,超过85%立即通知DBA介入。单看当前连接数其实不够,最好把峰值Max_used_connections也监控起来,它会告诉你系统历史上最紧张的时刻,对后续容量规划很有参考价值。

5.2 应用侧重试与熔断设计

再好的预防也架不住极端流量突发。应用侧需要对1040做兜底,最基础的是退避重试:

import time from pymysql.err import OperationalError def acquire_with_retry(pool, retries=5): for attempt in range(1, retries + 1): try: return pool.acquire() except OperationalError as exc: if exc.args[0] == 1040: wait = min(2 ** attempt, 30) print(f"[warning] Too many connections, retry in {wait}s") time.sleep(wait) else: raise raise OperationalError("获取数据库连接失败,已重试多次")

重试间隔使用指数退避,1秒、2秒、4秒、8秒、30秒封顶,避免所有实例在同一秒疯狂重连造成“惊群”效应。有些同学会在重试时把间隔设成固定2秒,结果数据库恢复之后,所有应用实例同时涌入连接,又造成一波瞬间峰值,这一点务必注意。

如果重试了几次还是失败,说明数据库侧压力非常大,这时候再做重试已经没有意义,应该快速失败,触发下游熔断。比如把连接池的acquire超时控制在2秒以内,连接池排队超过2秒直接抛异常,宁可让接口报错,也不要让请求无限堆积把应用自身的内存也拖垮。

6. 常见问题速查与实战避坑记录

6.1 问题排查速查表

我把实际操作中最常遇到的问题整理成一张速查表,方便你遇到类似情况时直接对着排查:

现象可能原因排查方向解决思路
Threads_connected 长期在高位连接池无回收机制查连接池maxsize与空闲回收配置引入连接池,设置合理的空闲回收时间
大量 Sleep 连接堆积连接泄漏或超时时间过长查 processlist 的 Sleep 占比修复代码关闭逻辑,调小 wait_timeout
Query 状态连接长时间不释放慢查询占住连接查time > 10的 SQL优化SQL、加索引、拆分大事务
连接数没满但偶发1040瞬时并发握手冲击查监控中的连接创建速率应用侧加熔断缓冲,避免重启后集中建连
调整配置后立即失效没有同步修改配置文件检查配置文件和其余实例改配置文件并逐步滚动重启
KILL 连接后马上又满了应用自动重连且流量过大看应用日志重连频率在应用侧限流,数据库侧临时扩容

6.2 几个只有踩过坑才知道的细节

第一个坑:临时SET GLOBAL max_connections之后,很多人以为完事了,结果某天MySQL主从切换一重启,连接上限又掉回原来的值,应用在业务高峰期再次1040。临时修改只能用来救急,必须同步改配置文件,并且确认所有实例都改到位。

第二个坑:把max_connections调到5000之后,MySQL确实不报1040了,但内存被吃掉了大半,磁盘I/O也飙上去。原因很简单,连接数上去了,但缓冲池和线程内存没有同步扩容,数据库整体性能反而下降。所以调连接上限是跟内存预算绑定的,不是单独决策。

第三个坑:连接池里的连接如果长时间不用,会被MySQL端超时回收,但连接池本身不知道。如果不做ping探活,拿出来的就是一条死连接,执行第一条SQL就报Lost connection错误。我自己见过不止一次,连接池用着用着突然报错,查下来全是这个原因。所以连接池一定要做连接有效性检测。

第四个坑:批量任务最容

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进