MySQL
网络写超时分析
PART 01
问题现象
某客户使用TDSQL MySQL8.0版本,在跑批场景下出现连接中断现象。业务反馈的错误信息如下:
ERROR c.a.d.p.DruidPooledPreparedStatement - getMaxFieldSize error
java.sql.SQLException: No operations allowed after statement closed.
...
客户跑批场景是每次从数据库流式获取 2000 条数据,完成相应业务逻辑后再继续获取下一批数据,每批数据之间的间隔约 30s。
PART 02
问题分析
首先查看MySQL相关日志,error log中没有对应时间连接断连的信息;然后查看slow log中记录相关SQL,发现对应时间存在Errno状态为1161的SQL:
# Query_time: 602.623354 Lock_time: 0.000556 Usecs_wait_tp_wq: 0 Rows_sent: 17176 Rows_examined: 268505 Thread_id: 3180834
Errno: 1161 Killed: 0 Bytes_received: 0 Bytes_sent: 3236280 Read_first: 0 Read_last: 0 Read_key: 33
Read_next: 0 Read_prev: 0 Read_rnd: 0 Read_rnd_next: 167 Sort_merge_passes: 0 Sort_range_count: 0
Sort_rows: 0 Sort_scan_count: 0 Created_tmp_disk_tables: 0 Created_tmp_tables: 0
Start: 2023-05-10T10:29:09.170404+08:00 End: 2023-05-10T10:39:11.793758+08:00
通过MySQL错误码1161,可以判定是由于 MySQL写超时(ER_NET_WRITE_INTERRUPTED)导致语句执行失败。
# perror 1161
MySQL error code MY-001161 (ER_NET_WRITE_INTERRUPTED): Got timeout writing communication packets
此时DBA会想到MySQL中多个超时相关的参数,其中参数net_write_timeout是控制server端返回给client端超时时间,具体解释可翻阅MySQL官方文档。
查看数据库当前配置是600s,那么问题基本理清楚。解决方案可以调整业务逻辑,也可以调整MySQL的net_write_timeout参数,或者调整JDBC连接串中的netTimeoutForStreamingResults参数。
那么MySQL如何产生 1161 的逻辑?查看MySQL 8.0源代码寻找答案,当且仅当操作系统返回 ETIMEDOUT 时可能会产生 1161 错误。
MySQL执行写入的具体调用链路为:vio_write -> mysql_socket_send, vio_socket_io_wait -> vio_io_wait。
即 MySQL 在发送消息到 socket 的过程中,除了调用 send() 外,在多次重试超过 net_retry_times 发生 EAGAIN 的情况后,也会调用 poll。
而 poll() 明确指出在超时情况下(tcp 底层内存不足,在申请内存过程中超时)会返回 0。MySQL 会将这个 poll 的 timeout 也等同于 ETIMEDOUT 进行处理。
当packet准备好发送后,会调用net_write_raw_loop函数开始进行发送数据,如果发送数据过程异常,则返回 ER_NET_WRITE_INTERRUPTED 或 ER_NET_ERROR_ON_WRITE。
最终是通过vio_was_timeout函数确认发送中断是否为ETIMEDOUT引起。
其中ER_NET_WRITE_INTERRUPTED和ER_NET_ERROR_ON_WRITE均是宏定义,分别对应错误码1161和1160。
net_write_timeout参数是从第一次遇到阻塞开始计时,如果中间又有写入成功的情况,会重新开始计时。客户在调大net_write_timeout参数为10800后,1161引发业务异常的情况得到缓解。
不过在slow log中依旧存在少量30s左右的1161异常的SQL,耗时远远低于net_write_timeout参数值,说明还有其他场景会触发ETIMEDOUT场景。
# Query_time: 28.221121 Lock_time: 0.000006 Usecs_wait_tp_wq: 0 Rows_sent: 5809 Rows_examined: 268505 Thread_id: 13364592
Errno: 1161 Killed: 0 Bytes_received: 0 Bytes_sent: 1060392 Read_first: 1 Read_last: 0 Read_key: 1
Read_next: 0 Read_prev: 0 Read_rnd: 0 Read_rnd_next: 167 Sort_merge_passes: 0 Sort_range_count: 0
Sort_rows: 0 Sort_scan_count: 0 Created_tmp_disk_tables: 0 Created_tmp_tables: 0
Start: 2023-05-13T15:13:50.683868+08:00 End: 2023-05-13T15:14:18.904989+08:00
通过tcpdump抓包来看,对应的时间点MySQL还在传数据,然后发个fin包到客户端导致的连接中断。而且netstat -s 发现 proxy 机器上 TCPRcvQDrop 较大,存在 tcp 层存在主动丢包情况。
PART 03
调试分析
短时间内没有找出触发ETIMEDOUT的场景,我们通过在MySQL中增加日志的方式继续分析。
1. 在vio_write函数增加 mysql_socket_send函数(最终是 send() ) 返回值输出,用于确认返回值有哪些。
2. 在vio_io_wait函数增加 poll() 返回值输出,用于确认是否有0值。
3. 通过模拟客户读取数据方式进行复现,在本地TDSQL 8.0的2分片环境,复现后查看日志输出信息
# grep -i 'poll ret' /data1/tdengine/log/4008/dblogs/mysqld.err | sort | uniq
TXSQL: The vio_io_wait poll ret is 1.
# grep -i -E 'The error is' /data1/tdengine/log/4008/dblogs/mysqld.err | sort | uniq
TXSQL: The error is 104.
TXSQL: The error is 11.
TXSQL: The error is 110.
服务器 send() 数据,因为客户端已经崩溃,服务器收不到ACK自然会不停的重传。源自Berkeley的重传机制,重传次数受/proc/sys/net/ipv4/tcp_retries2控制,相对第一次传的15分钟后仍没收到ACK,则返回ETIMEDOUT或EHOSTUNREAC错误。如果服务器不理会这个错误,再次调用 send() ,则立马返回Broken Pipe错误。
当前操作系统的配置为6,通常配置为6或者15,6次的超时时间大约在26-27s左右。通过调小tcp_mem=‘1 2 3’参数,可以在本地环境直连DB复现(net_write_time=300s)。
Slow log信息如下,耗时在30s左右:
# Query_time: 26.631368 Lock_time: 0.000629 Usecs_wait_tp_wq: 0 Rows_sent: 167 Rows_examined: 167 Thread_id: 32
Errno: 1161 Killed: 0 Bytes_received: 0 Bytes_sent: 24576 Read_first: 1 Read_last: 0 Read_key: 1
Read_next: 0 Read_prev: 0 Read_rnd: 0 Read_rnd_next: 167 Sort_merge_passes: 0 Sort_range_count: 0
Sort_rows: 0 Sort_scan_count: 0 Created_tmp_disk_tables: 0 Created_tmp_tables: 0
Start: 2023-05-22T20:54:06.175770+08:00 End: 2023-05-22T20:54:32.807138+08:00
总结
MySQL触发1161的timeout writing的情况有两种:
可以使用如下方式规避:
-END-