打开MySQL的error log,发现几百行一模一样的Got an error reading communication packets,第一反应多半是“数据库是不是要挂了”。但先别慌,这个报错在绝大多数情况下不是数据库崩溃,也不是数据损坏,而是连接层面出了问题。我最早接手线上库时也被它吓过,后来排查多了才摸清楚:它就是一个连接异常终止的信号,真正要命的是背后那一堆没正常断开的会话,以及被拖垮的线程资源。
这篇文章我把这个报错的完整原理、高频诱因、排查流程和对应的参数调优方案一次讲透。不管你是开发、运维还是DBA,只要MySQL错误日志里反复出现这条信息,按下面的思路走一遍基本都能定位到根因,而且大部分场景下不用重启实例就能压下去。
1. 先搞懂这个报错到底在说什么
1.1 报错产生的完整链路
MySQL在通信层有一套自己的协议,客户端连上来之后,不管是查询、写入还是断开,都要走TCP连接上的一套报文交互流程。服务端在某个时刻尝试从客户端连接的socket里读取数据,却发现连接已经不可用了,这时候就会往错误日志里写一条Got an error reading communication packets。
这句话直译就是“读取通信数据包时报错”,但它背后表达的信息量比字面意思大得多。MySQL服务端把每个连接的异常终止都会记录下来,所以这条日志本质上是“非正常断开连接”的一种残留痕迹。正常断开时客户端会发送COM_QUIT,服务端收到后优雅关闭,不会记录这条日志;只有连接直接没了、半路断了、或者客户端根本没来得及说再见,服务端才会在尝试读数据时发现异常,然后把这条信息写进错误日志。
这里有个细节需要理解:MySQL报这个错的时候,它不是主动去“查”连接有没有问题,而是被动地在一次读操作过程中发现了问题。也就是说,如果没有任何查询或网络交互发生,服务端一般不会频繁触发这个检测。所以当你看到大量同类报错扎堆出现时,基本可以反推:当时有大量的连接正在尝试通信,但都半路夭折了。
1.2 为什么这个报错会成批出现
成批出现是这类问题的典型特征,因为单条连接异常不太可能引发几十上百条报错。最常见的场景是应用连接池。连接池为了保证请求响应速度,会在初始化时建立一批连接放在池子里,空闲的会话超过一定时间后,如果MySQL设置了wait_timeout,服务端就会主动把空闲连接断掉。而池子里的客户端此时并不知道服务端已经关闭了连接,下一次取出连接发起查询时,服务端读取请求包才发现连接没了,自然就记一条“读包失败”。
还有一种典型情况是监控系统或健康检查脚本。很多监控组件会定期连MySQL执行SELECT 1,如果监控程序本身异常退出,或者网络被防火墙重置,同样会让服务端记录大量类似的错误日志。再往上层看,负载均衡器、网关设备的空闲连接回收机制,也会触发同样的现象。
所以解决这个问题的第一步不是急着调参数,而是先判断这些报错是从哪一类客户端来的。判断方法可以借助MySQL的通用日志或者performance_schema,把报错发生时间段的连接来源IP和账号拉出来对照,通常一眼就能锁定对象。
2. 五个高频根因,对照排查你的环境
2.1 连接池空闲连接被服务端回收
这是出现频率最高的根因。MySQL默认的wait_timeout是8小时,很多线上环境可能调到了10分钟甚至5分钟。连接池里的空闲连接一旦超过这个时间,就会被服务端强制断开。而连接池默认不会立刻感知到这个变化,除非配置了空闲连接检测或者取连接时做testOnBorrow类型的校验,否则还是会把已经失效的连接发给业务线程使用。
表现特征很明确:错误日志里的报错时间和业务低谷期高度吻合,比如凌晨的定时任务跑完后,第二天早上集中出现一批;或者应用刚发布完,旧连接被回收后出现一小撮报错。
判断方法也简单,看Aborted_clients和Aborted_connects这两个状态值的变化。如果Aborted_clients增长明显,说明客户端连接在使用过程中被异常终止;如果两个值一起涨,那还要考虑网络层面的问题。
2.2 应用端异常退出与网络抖动
应用进程被kill -9、容器被重启、OOM导致进程直接消失,这些情况都不会给MySQL发送优雅的关闭指令。服务端那边的TCP连接就成了半开连接,直到下一次读操作或者TCP层探活才发现连接失效。云环境里如果负载均衡的安全策略比较激进,把超过一定时长没有数据传输的连接直接丢弃,同样会产生大量Got an error reading communication packets。
这类问题处理起来比较复杂,因为它往往意味着需要同时协调应用团队、网络团队和DBA。但反过来想,它也是个信号:你的应用可能在频繁重启,或者网络拓扑层存在不合理的空闲连接回收策略。
2.3 max_allowed_packet 太小
这个根因容易误判,因为它的报错表现同样是Got an error reading communication packets,但实际上问题出在报文大小上。当客户端发送的数据包超过max_allowed_packet限定时,服务端会认为通信异常,断开连接并记录错误。典型场景是:应用往MySQL里写入大字段、批量插入大量数据,或者导入SQL文件时包含超大的INSERT语句。
如果错误日志里同时出现Got a packet bigger than 'max_allowed_packet' bytes这类信息,基本上就是这个问题。只看前一条报错不看上下文,很容易排查半天找不到方向。
2.4 反向DNS解析拖慢连接
MySQL在客户端建立连接时有一步默认行为:根据客户端的IP地址做反向DNS解析,也就是把IP解析成主机名。如果DNS服务器响应慢或者根本解析不了,这一步会消耗大量时间。连接建立的慢,加上客户端侧等待超时机制,就可能让连接异常中断,最终在服务端留下读包失败的错误记录。
这个问题的另外一个典型特征是:连接建立的耗时忽高忽低,错误日志里的报错集中在某个IP网段。MySQL为此提供了skip_name_resolve参数,开启后跳过反向解析,但这要求所有授权表里的账号必须使用IP而不是主机名,改动前要提前检查和整理账号授权。
2.5 中间层设备杀掉空闲连接
云数据库和自建库都存在这个场景。云厂商的安全组、防火墙、四层负载均衡设备,经常会配置连接空闲超时策略。比如云SLB默认的空闲连接超时可能是60秒或几分钟,一旦连接空闲超过阈值,中间设备会发送RST断开连接。客户端在长时间没有SQL执行的情况下,感知不到这个变化,下一次发起查询时,TCP层可能还在使用一个已经被中间设备断掉的socket。
这个问题和2.1很像,区别在于根因不在MySQL层面,而在网络设备侧。如果你已经调大了MySQL的wait_timeout,也把连接池的空闲检测时间缩短了,但报错依然频繁出现,就要重点怀疑网络设备。具体判断方法可以在应用机器上长ping数据库地址,观察网络中断是否和报错时间重叠,或者抓包看TCP层有没有RST包。
3. 排查流程与参数调优实操
3.1 用状态变量快速定位问题方向
不管通过什么渠道了解到这个问题,我都建议先执行下面这组SQL,把全局状态值捞出来看看。
SHOW GLOBAL STATUS LIKE 'Aborted%'; SHOW GLOBAL STATUS LIKE 'Connections'; SHOW GLOBAL STATUS LIKE 'Threads%'; SHOW GLOBAL STATUS LIKE 'Max_used_connections';重点关注:
Aborted_clients:客户端连接建立成功后,异常终止的累计次数Aborted_connects:连接建立阶段就失败的累计次数Connections:总的连接尝试次数Max_used_connections:历史最大并发连接数
如果Aborted_clients很大而Aborted_connects很小,说明服务端本身工作正常,问题集中在客户端连接建立之后的通信过程。反之如果Aborted_connects也涨得很猛,就要排查连接数上限、认证失败、网络连通性等问题。
再配合performance_schema看具体来源,可以更精确地锁定是哪个账号、哪个IP在制造报错。
SELECT USER, HOST, COUNT(*) FROM performance_schema.events_statements_summary_by_account_by_event_name GROUP BY USER, HOST ORDER BY COUNT(*) DESC LIMIT 20;也可以临时开启通用日志,但通用日志在高并发下对IO压力比较大,不建议在业务高峰期开启,如果非开不可,最好只开一小段时间,或者用log_output = TABLE把日志写到表里,方便过滤和分析。
3.2 关键参数调整方案与计算过程
针对不同根因,参数调整的侧重点也不同。这里把我实际环境中验证过的几组参数整理出来,附带调整理由和参考值。
wait_timeout 与 interactive_timeout
如果确认是连接池空闲连接被回收,可以适当调大wait_timeout,但不要盲目调到8小时,因为每个空闲连接即使不干活也占用着MySQL的线程和内存资源。参考值为:wait_timeout = 28800已经足够覆盖绝大多数业务的夜间空闲期;如果你的连接池设置了较短的空闲回收时间,也可以把wait_timeout设得比连接池的最大空闲时间长一些,比如连接池空闲回收为60秒,就设为wait_timeout = 120,留给客户端足够的主动回收时间窗口。
interactive_timeout针对的是通过mysql命令行交互式连接,如果应用端的交互式连接也需要持久化,记得同步调整,否则会出现一种奇怪的现象:用命令行连上去明明没动,过一会儿就被断了。
net_read_timeout 和 net_write_timeout
这两个参数控制的是服务端在读写数据过程中的超时时间。如果业务中有大批量导入导出、复杂的全表扫描,或者存在慢查询导致单次查询耗时长,可以考虑适当调大。参考值:net_read_timeout = 60、net_write_timeout = 60。注意这两个参数只影响连接建立后的读取和写入阶段,不会影响整体的查询执行时间,所以调大它们不会掩盖慢查询问题。
max_allowed_packet
默认值是64MB或4MB,取决于MySQL版本。如果确认是大报文写入导致的报错,需要同时调整服务端和客户端的这个参数。服务端通过max_allowed_packet控制,客户端在连接时也可以指定max_allowed_packet,而且客户端的值不能大于服务端的值。比如应用要批量写入单条10MB的数据,建议服务端设置为max_allowed_packet = 64M,客户端连接参数也设置为64M,预留2倍以上的余量比较稳妥。
修改配置时用在线方式即可,这几个参数都是动态生效的:
SET GLOBAL wait_timeout = 120; SET GLOBAL interactive_timeout = 120; SET GLOBAL max_allowed_packet = 67108864; SET GLOBAL net_read_timeout = 60; SET GLOBAL net_write_timeout = 60;但要注意,SET GLOBAL只对之后的连接生效,已有连接不受影响。如果想让配置永久化,务必同步改到my.cnf对应节点的[mysqld]段下,否则下次重启实例就还原了。
3.3 应用侧连接池的配套修改
只调服务端参数不调应用侧,治标不治本。连接池需要在取连接和归还连接时做健康检查,同时主动淘汰空闲过久的连接。以阿里巴巴的Druid连接池为例,推荐配置:
spring: datasource: druid: testWhileIdle: true testOnBorrow: true testOnReturn: false validationQuery: SELECT 1 timeBetweenEvictionRunsMillis: 30000 minEvictableIdleTimeMillis: 60000testOnBorrow开启后,每次取连接都会执行一次SELECT 1验证连接是否可用,虽然会有一点性能开销,但换来的是绝对不会把死连接交给业务线程。对于并发量极大的场景,可以把testOnBorrow关掉,仅保留testWhileIdle和定期清理机制,性能更好,但死连接会多存活一段时间。
HikariCP的类似配置是这样:
spring: datasource: hikari: connection-test-query: SELECT 1 validation-timeout: 3000 idle-timeout: 60000 max-lifetime: 1800000特别强调一下max-lifetime这个参数。HikariCP的max-lifetime官方建议要小于数据库的wait_timeout,否则连接池里的连接可能还没来得及被池子淘汰,就先被数据库服务端断掉了。这个不匹配是生产环境里很常见的坑,排查半天发现就是max-lifetime和时间差导致的。
4. 网络层与架构层面的根治手段
4.1 启用TCP keepalive与调整系统层参数
MySQL服务端有skip_networking和本地socket的选项,但TCP连接层面的保活还是要靠操作系统。在应用服务器上调整TCP keepalive参数,可以让客户端更早地发现网络异常,避免长时间使用已经断开的连接。
Linux下通过sysctl调整:
net.ipv4.tcp_keepalive_time = 600 net.ipv4.tcp_keepalive_intvl = 75 net.ipv4.tcp_keepalive_probes = 9tcp_keepalive_time表示连接空闲多久开始发送探测包,默认值是7200秒,太长了。调成600秒意味着连接空闲10分钟后,内核开始探测对端是否可达。如果对端没响应,会按tcp_keepalive_intvl的间隔连续探测9次,总共大约11分钟判定连接失效,然后应用程序会在下一次读写时收到错误,从而触发连接池的重建逻辑。
MySQL自身也支持init_connect设置,可以在连接建立时自动执行一些初始化语句,但TCP keepalive的配置只能在系统层面做,MySQL没有直接暴露开关。
4.2 跳过反向DNS解析,缩短建连耗时
如果你的服务器没有配置可靠的DNS反向解析,强烈建议开启skip_name_resolve。开启前先确认授权表结构:
SELECT user, host FROM mysql.user WHERE host NOT IN ('localhost', '127.0.0.1') AND host NOT LIKE '192.168.%';如果发现授权记录里有主机名的形式,比如'webapp'@'app-server-01.example.com',直接开启skip_name_resolve会导致这些账号连接失败,需要先把host改成IP或者网段。转换完成后,在配置文件中加入:
[mysqld] skip_name_resolve然后重启实例或者用动态方式设置(注意动态设置只对之后的新连接生效)。这个改动不仅能消除一部分报文读取错误,还能显著降低建连延迟,尤其是DNS本身响应慢的场景效果立竿见影。
4.3 错误日志的合理配置与轮转清理
既然解决方案已经确定了,错误日志本身的管理也别忽视。log_error_verbosity参数可以控制日志的详细程度,默认值是2或3。如果MySQL版本支持,并且你不需要记录所有通信层的notice级别的信息,可以考虑设置:
SET GLOBAL log_error_verbosity = 2;但注意这个参数只是降低日志的“聒噪”感,并不能真正解决问题,核心还是前面说的参数和连接池调整。另外要关注日志文件的大小,MySQL自带log_error文件并不会自动按大小切割,建议通过系统层的logrotate做轮转,避免一个日志文件长到几个G之后,排查问题变成大海捞针。
5. 常见问题与避坑实录
5.1 问题速查表
| 现象特征 | 首选排查方向 | 有效解决手段 |
|---|---|---|
| 报错集中在业务空闲期 | 连接池空闲连接被回收 | 调大wait_timeout,配置连接池空闲检测 |
| 报错和大批量写入同时出现 | max_allowed_packet过小 | 同步调整服务端与客户端参数 |
| 报错伴随连接耗时波动 | 反向DNS解析慢 | 开启skip_name_resolve |
| 应用频繁重启后大量报错 | 客户端异常退出 | 连接池配testOnBorrow,优化应用退出逻辑 |
| 所有连接都被中间设备断开 | 网络设备空闲超时 | 调整TCP keepalive,协调网络侧策略 |
| 报错同时出现慢查询 | net_read_timeout太小 | 适当调大net_read_timeout |
| 报错出现在主从切换后 | 复制断开重连 | 检查半同步复制和重连参数 |
5.2 我踩过的几个坑
第一次排查这个问题时,我盯着wait_timeout改了又改,报错还是没减少,后来才发现问题出在应用用了HikariCP,而max-lifetime设成了60秒,比MySQL的wait_timeout大不少。连接池里的连接还没到自己定义的淘汰时间,就被数据库端断掉了,双方时间窗错位,导致源源不断的读包失败。这里建议一个基本原则:连接池的max-lifetime永远比数据库的wait_timeout小30到60秒以上,给连接池留出主动淘汰的时间余量。
第二个坑是调整了max_allowed_packet之后忘了重启客户端连接池。服务端参数虽然是动态生效的,但应用侧的连接池里已经建立的旧连接还沿用着旧值,需要重启应用或者主动让连接池重建,调整才会完全生效。有些连接池有定期重建连接的机制,不重启也能在几十秒内自动build新的连接,但如果你用的连接池没有这个机制,就可能出现“明明改了配置还是继续报错”的假象。
第三个坑比较隐蔽:skip_name_resolve开启后,一条报警电话打过来,说所有应用都无法连接。原因就是授权表里的host用了域名,而不是IP。所以这个参数不是随手就能开的,改动前一定要先检查mysql.user表,把host字段全部改成IP,并且要检查所有连接串里的host写的是不是IP。如果你无法确认所有应用侧配置,宁可不开这个参数,也不要冒中断服务的风险。
5.3 最后的补充技巧
排查这类问题,日志时间对齐非常关键。MySQL错误日志默认记录的是系统本地时间,而应用日志可能用的是UTC或者带时区的时间。建议把两边的日志格式统一起来,或者在做时间对比时主动做时区换算,否则会出现一种很尴尬的情况:错误日志显示21:00有报错,应用日志在21:05有一批重连,看起来时间对不上,白白浪费很久的排查时间。
另外,报错数量本身也是监控指标。如果平时每天几十条,突然某天涨到几千条,即使原因和你之前处理过的一样,也值得重新审视一下线上环境是不是有什么新的变更,比如发布了新版本、改了网络策略、扩了容。这类报错大多数是“症状”而不是“疾病”,追着日志本身调参,不如顺着连接的生命周期把所有环节都捋一遍。