网站空闲一段时间后,第一个请求报 MySQL server has gone away;导入 SQL 到一半出现 ERROR 2006;后台导出跑了几十秒,又遇到 ERROR 2013: Lost connection to MySQL server during query。这些现象都指向数据库连接中断,但修复方法取决于连接在哪个阶段失效。
先记录报错时间、完整错误、执行场景和 MySQL 日志,再判断是旧连接被回收、大包被拒绝、传输或客户端超时,还是数据库进程重启。本文以自管 Linux VPS 上的 MySQL 8.0 / 8.4 为主,命令示例按 MySQL 8.4 官方文档核对;MariaDB、托管数据库和不同驱动的参数、权限需另查对应文档。
| 什么时候报错 | 优先检查 | 修复后如何验证 |
|---|---|---|
| 长时间没请求,第一次访问失败,刷新后正常 | 应用复用失效连接;数据库或代理的空闲超时 | 等待超过原故障间隔,再从应用发起首个请求 |
| 导入或批量 INSERT 总在相近位置失败 | 单条 SQL / 单行 BLOB 大小;客户端和服务端包限制 | 在隔离目标中重试同一批次,核对行数和业务结果 |
| 查询、导出中途断开 | 驱动读超时、网络、结果传输、数据库日志 | 从原应用路径完成同类查询,观察耗时与错误 |
| 多个应用几乎同时断连 | MySQL 重启、OOM、维护操作、代理切换 | 核对进程稳定运行时间和错误日志 |
| 新建连接就失败,出现 authorization packet 字样 | 握手阶段、网络链路、connect_timeout | 从原客户端路径建立新连接 |
MySQL 官方将 2006 对应的 CR_SERVER_GONE_ERROR 定义为客户端无法把请求发给服务器,2013 对应的 CR_SERVER_LOST 则表示客户端没有收到完整响应。**错误码能说明连接失败的表现,不能单独确定根因。**参见 MySQL 客户端错误参考。
如果实际报错是 1040 Too many connections,应转到连接池与 max_connections 排查。把连接数上限调大不会修复已断开的旧连接。
下面的 Bash 命令在 VPS 终端执行,SQL 则进入 MySQL 客户端后执行。把 diagnostic_user 换成已有诊断账号;-p 交互输入密码,不把密码写进命令。需要读取全局状态或修改配置时,使用有对应权限的账号。
# 数据库确实监听在本机 TCP 3306 时使用
mysql --protocol=TCP -h 127.0.0.1 -P 3306 -u diagnostic_user -p --skip-reconnect
Docker 数据库可在正确的 Compose 项目目录执行以下命令;db 是示例服务名。不要为了排查临时把数据库端口暴露到公网。
docker compose exec db mysql -u diagnostic_user -p --skip-reconnect
诊断期间关闭客户端自动重连,方便发现真实断连;这个命令行选项不会改变网站所用驱动的设置。选项说明见 mysql Client Options。
先运行只读 SQL,保存输出并记录采集时间:
SELECT NOW() AS sampled_at, VERSION() AS mysql_version,
@@hostname AS server_host, @@port AS server_port,
CONNECTION_ID() AS diagnostic_connection_id;
SELECT @@GLOBAL.wait_timeout AS global_wait_timeout,
@@SESSION.wait_timeout AS session_wait_timeout,
@@GLOBAL.interactive_timeout AS global_interactive_timeout,
@@GLOBAL.max_allowed_packet AS global_max_allowed_packet,
@@SESSION.max_allowed_packet AS session_max_allowed_packet,
@@SESSION.net_read_timeout AS session_net_read_timeout,
@@SESSION.net_write_timeout AS session_net_write_timeout,
@@GLOBAL.connect_timeout AS global_connect_timeout;
SHOW GLOBAL STATUS
WHERE Variable_name IN
('Uptime', 'Aborted_clients', 'Aborted_connects',
'Threads_connected', 'Threads_running');
SHOW GLOBAL VARIABLES
WHERE Variable_name IN ('log_error', 'log_error_verbosity');
这里的 SESSION 值属于当前诊断连接。应用可能在建连时设置了不同值,应在应用自己的连接上读取相关参数;不能把管理员客户端的结果当作连接池的结果。
Aborted_clients 和 Aborted_connects 是累计计数。前者可反映成功连接后异常断开的情况,后者反映连接建立失败;两次采样的增量更有参考价值,单个数字不能证明某次事故的原因。见 Communication Errors and Aborted Connections。
如果同一时刻多个业务都报错,先查数据库进程。应用日志时间、数据库日志时间和内核日志时间要换算到同一时区。
systemd 安装可这样读取近期信息,实际服务名也可能是 mysqld:
sudo systemctl status mysql --no-pager -l
sudo journalctl -u mysql --since '30 minutes ago' --no-pager -n 200
sudo journalctl -k --since '30 minutes ago' --no-pager |
rg -i 'out of memory|oom|killed process'
free -h
df -h
df -i
没有 rg 的服务器可以用 grep -Ei 做同样的文本筛选。如果错误日志写入文件,根据前面查到的 log_error 路径读取对应时段;journal 不一定包含全部数据库错误。
Docker 部署在项目目录执行:
docker compose ps -a
docker compose logs --since 30m --tail 200 db
db_container_id=$(docker compose ps -a -q db)
if [ -n "$db_container_id" ]; then
docker inspect --format 'Name={{.Name}} RestartCount={{.RestartCount}} State={{json .State}}' "$db_container_id"
docker stats --no-stream "$db_container_id"
fi
该示例按一个 db 容器编写,多副本应逐个检查。看退出时间、重启次数、OOMKilled、数据库启动日志是否与断连吻合。当前运行容器的状态可能已经覆盖上一次退出的信息,因此 OOMKilled=false 也不能代替历史内核日志。
如果证据指向 OOM,检查宿主机可用内存、容器限额、连接数和业务峰值,再调整容量或负载。不要在内存不足时同时增大连接池和包限制。故障发生后直接重启可能清掉现场,删数据目录或执行 docker compose down -v 更不属于断连修复步骤。
MySQL 官方建议用实例运行时间和错误日志判断服务器是否崩溃或重启,再调查对应原因。见 MySQL server has gone away。
典型过程是:应用把连接放回池中;数据库或中间代理在空闲超时后关闭它;下一次请求从池中拿出这条连接,发送 SQL 时才发现连接失效。
应同时核对三处:
- 应用连接实际使用的
wait_timeout。 - 数据库代理、隧道或网络设备是否有更短的空闲超时。
- 连接池是否支持取出前检查、失效连接丢弃和生命周期回收。
MySQL 的 wait_timeout 管理非交互连接空闲等待时间;交互客户端会影响会话初始值来源。SET GLOBAL 修改这类参数后,已有连接的会话值通常不会随之更新。因此只在命令行看全局值,很容易误判应用已生效。见 Using System Variables。
优先修复应用连接管理。以已经使用 SQLAlchemy 2.x 和 PyMySQL 的 Python 服务为例:
import os
from sqlalchemy import create_engine
engine = create_engine(
os.environ["DATABASE_URL"],
pool_pre_ping=True,
pool_recycle=900,
)
环境变量应提供既有的 mysql+pymysql 连接 URL,不要在日志中打印它。900 秒只是示例,需低于确认过的连接失效时间,并结合建连成本选取。这个配置应在应用原有的 Engine 初始化位置修改,不能每个请求新建一个 Engine。
pool_pre_ping 在取出连接时检查可用性;pool_recycle 在取出时替换超过指定年龄的连接。它们不会保护已经被业务持有、正在执行事务的连接。多进程服务还应避免父子进程共用已建立的数据库连接。依据 SQLAlchemy 连接池文档。
Node.js、PHP、Java 的驱动和连接池选项不同,按实际库文档配置,不要直接照搬参数名。上线后等待超过原故障间隔,再验证空闲后的首个业务请求;刚重启应用后连续刷新几次,不能证明这一类问题已经解决。
优先寻找 ER_NET_PACKET_TOO_LARGE,并比较失败位置、单条 INSERT 大小、单行 BLOB 或长字符串大小。整个备份文件 2 GB,不代表需要 2 GB 的包限制;关键是协议中的单个包,比如一条 SQL 或一行结果。
MySQL 客户端和服务端各有包大小限制,一侧过小仍会失败。见 Packet Too Large。
处理顺序建议是:先缩小批量 INSERT 的批次、减少一次发送的数据,再按真实需求适度提高限制。以下仅演示已确认需要 128 MiB 的场景,应先记录原值并确认 VPS 有内存余量:
-- 由有权限的管理员执行;这是运行时修改
SELECT @@GLOBAL.max_allowed_packet AS original_packet_limit;
SET GLOBAL max_allowed_packet = 134217728;
服务端会话的 max_allowed_packet 在 MySQL 8.4 中是只读值,不能用 SET SESSION 修改。修改全局值后,应新建连接再核对会话值,应用连接池也需要有序换新。该参数是上限,不等于每个连接立即分配同样大小的缓冲区,但并发大包仍会增加内存压力。见 max_allowed_packet 官方说明。
在已备份、可核对结果的隔离数据库中进行恢复测试。下面命令只演示本机 TCP 导入路径,账号、文件和目标库必须按实际情况调整:
mysql --protocol=TCP -h 127.0.0.1 -P 3306 \
-u restore_user -p --skip-reconnect --max-allowed-packet=128M \
restore_test < /srv/backups/restore-test.sql
--max-allowed-packet 是 mysql 命令行客户端设置,网站驱动不一定读取它。还要检查备份文件是否带有 USE、CREATE DATABASE 或库名限定,防止导入到错误目标;测试账号应只拥有计划恢复目标的权限。
已经失败一半的导入不能直接重复执行:前面的语句可能已提交。先明确已完成范围、唯一键冲突、是否可重复执行,再选择重建隔离恢复目标或从可确认的批次继续。不要用 --force 把错误跳过后当作恢复成功。
| 参数或超时 | 管理什么 | 常见误区 |
|---|---|---|
wait_timeout | 已连接但空闲的等待 | 当成所有 SQL 的执行时限 |
connect_timeout | 服务端建立连接的握手等待 | 用它解决已连接后的长查询 |
net_read_timeout | 服务端从客户端读取后续数据的等待 | 以为所有导出都该增大它 |
net_write_timeout | 服务端向客户端写数据的等待 | 忽略客户端消费结果过慢 |
| 驱动读 / 写超时 | 客户端等待或发送数据 | 以为改 MySQL 配置就会同步改变 |
| HTTP、代理或任务超时 | 应用请求、网关或作业期限 | 将页面超时直接当成数据库断连 |
传输参数定义见 Server System Variables。固定在相近秒数失败时,先比对整个请求链路的期限,而不是同时把所有参数调大。
例如客户端读超时是 30 秒,而正常查询需要 45 秒,应评估查询计划、索引和任务设计,再决定是否提高客户端期限。大结果集则检查分页、流式读取、消费速度和网络。不要为了测试在生产库执行无限制的全表查询。
确认是服务器等待上传数据超时后,可以在同一条专用导入连接中短期调整对应会话参数,任务结束后恢复原值或关闭该连接。调参依据应该是日志和测量结果,不是只看 2013。
如果故障只发生在远程应用,比较同一 SQL 在原客户端路径与数据库本机路径的表现。再检查代理、隧道、丢包或连接重置;本机成功只能缩小范围,不能证明远程链路正常。MySQL 对握手、网络和大包情况的讨论见 Lost connection to MySQL server。
先做运行时验证,确认有效后再持久化。每次只改有证据支持的参数,并记录:原全局值、应用会话值、原配置来源、修改时间、影响范围和验证结果。
如果现有部署使用 MySQL 配置文件管理,可在已经被实例读取的配置中保存确认过的值,例如:
[mysqld]
max_allowed_packet=128M
配置文件中的改动不代表当前运行值已经改变。Docker 部署应确认实际挂载的配置文件、启动参数和数据卷,避免在容器可写层临时改文件后丢失设置。按既有维护流程安排重启;不要为了一个动态参数立即中断数据库。
MySQL 8.x 也支持 SET PERSIST 保存配置:它同时修改全局运行值并写入数据目录的 mysqld-auto.cnf。文件配置、启动参数和持久化值可能共同作用,应明确谁负责管理。容器重建是否保留该设置,取决于数据目录是否保留以及是否启用持久化配置加载。见 Persisted System Variables。
回退时,把改过的运行值恢复为记录的原值,再恢复对应配置来源。如果使用过 SET PERSIST,还要核对此前是否已有同名持久化设置:已有设置应恢复原值;本次新加的覆盖项可以按变量名移除。RESET PERSIST 只移除持久化条目,不会恢复当前运行值,也不要用无变量名的命令清空其他配置。
回退或正式保存之后,重新读取全局值和新连接的会话值,确认应用没有在连接初始化时再次覆盖它们。
读请求可以在确认业务允许的前提下做有限次数重试。写请求要先判断结果,尤其是下单、扣款、库存和数据库迁移:客户端没有收到成功响应,不能据此认定服务器没有执行或没有提交。
建议按下面顺序处理:
- 丢弃失效连接,让连接池创建新连接。
- 用业务唯一 ID、幂等键或可审计记录核对原操作是否完成。
- 若确认需要重试,以可安全重放的完整事务为单位,限制次数并设置退避。
- 对结果不确定、涉及外部副作用的操作进入核对流程,不自动重放单条写 SQL。
自动重连不会把旧连接的事务状态、会话变量和临时对象原样迁移到新连接,具体影响见 MySQL 自动重连说明。连接池健康检查也不能消除事务执行中发生的断连;SQLAlchemy 文档明确要求应用处理这类失败。
- 进程稳定:MySQL 运行时间持续增加,没有同时间段的重启、OOM 或崩溃日志。
- 连接池稳定:等待超过原空闲故障间隔后,首个业务请求仍成功;没有无限重连。
- 大包场景通过:在隔离恢复目标中完成原失败批次,核对记录数量和必要的业务一致性。
- 长查询路径通过:从原应用 / 代理路径完成同类操作,核对耗时和结果完整性。
- 配置生效:运行值、持久化来源、新连接会话值一致,应用初始化配置已核对。
- 容量合理:观察连接数、内存和错误增量,确认调整没有引入 1040 或新的 OOM。
- 重试可控:写入具备幂等或核对机制,没有重复订单、重复迁移或重复导入。
一次 SELECT 1 成功只证明这一次连接可用。验收要覆盖原先失败的条件,并持续观察至少一个原故障周期。
还不能。重启可能让连接池建立新连接,也可能暂时释放资源。若空闲超时、异常大包或 OOM 根因仍在,故障会回来。应保留现场信息,再验证对应场景。
这可能减少数据库主动回收空闲连接,但无法解决代理超时、网络中断或事务中途失效。还可能让闲置连接占用资源更久。优先处理失效连接回收和容量预算。
不是。网络、进程退出、大包和客户端超时都可能产生类似表现。先找报错时点与日志,再确认是否需要优化 SQL 或调整期限。
先让它列出每个参数控制的阶段、当前值、支持修改的日志证据、容量影响和回退办法。更适合交给 Agent 的任务是整理同一时间线、检查连接池初始化位置和生成配置差异;涉及数据库写入和迁移的重试要保留人工核对结果的环节。
