作者:鲍凤其

爱可生 dble 团队开发成员,主要负责 dble 需求开发,故障排查和社区问题解答。少说废话,放码过来。

本文来源:原创投稿

*爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。


背景

在稳定性环境中,当 dble 初始化后端连接池后,后端连接池会出现连接计数器(totalConnections)和实际连接(allConnections)数量不符合的情况,理论情况下两个变量会保持最终一致性。

后续通过查阅网上相关文档,找到了相关文档:https://mp.weixin.qq.com/s/FEybf-jRL8gMUYPBDH3aQg ,详细分析过程可参考此文章。

简单来说,在 dble 初始化后端连接池的过程中,瞬时创建的连接数量可能过大,导致部分 TCP 连接握手时触发了 TCP 的 syn_cookie 机制并且第三次 TCP 握手的 ACK 报文丢失了,从而导致了上述的情况。

后续,在稳定性环境中将 TCP 的 syn_cookie 关闭之后暂时解决了此种情况。

但假设正常 TCP 三次握手出现如下三种异常情况:

  • TCP 第一次握手包 SYN 丢包了
  • TCP 第二次握手包 SYN、ACK 丢包了
  • TCP 第三次握手包 ACK 包丢了

客户端和服务端是如何处理的,如果重传,重传多少次?每次间隔时长多少?

实验环境

在一台服务器上启动 MySQL 服务,端口是3306,IP地址:10.186.60.69

在一台服务器上使用 MySQL client 连接 MySQL 服务,IP地址:10.186.60.60

第一种场景

TCP 第一次握手包 SYN 报文丢包了,会发生什么?

在 MySQL 服务器上执行,通过 iptables 阻断客户端的发送过来的所有TCP报文:

$ iptables -i eth0 -A INPUT -p tcp --dport 3306 -j DROP

在 MySQL 服务器上开始抓包:

$ tcpdump -i eth0 tcp and port 3306 -w tcp_syn_timeout.cap

通过 MySQL client 连接 MySQL 服务端,过了一分多钟后,客户端返回报错,此时停止抓包,如下图:

$ mysql -h10.186.60.69 -uroot -p123456 -P3306
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 2003 (HY000): Can't connect to MySQL server on '10.186.60.69' (110)

wireshark 分析抓包文件:

客户端在收不到 TCP SYN 报文的 ACK 报文后,会不断进行重试,示例中会进行六次重试并且每次 RTO 是不同的:

  • 第一次是1秒后重试
  • 第二次是3秒后重试,和第一次相差 2s 左右
  • 第三次是7秒后重试,和第二次相差 4s 左右
  • 第四次是15秒后重试,和第三次相差 8s 左右
  • 第五次是31秒后重试,和第四次相差 16s 左右
  • 第六次是65秒后重试,和第五次相差 32s 左右

每次超时时间 RTO 是指数上涨的。另外,这里的重试次数可以配置,由客户端机器的如下内核参数指定:

$ cat /proc/sys/net/ipv4/tcp_syn_retries
6 
 
# 不同的发行版本,参数可能不同
$ uname -a
Linux ubuntu 4.15.0-36-generic #39-Ubuntu SMP Mon Sep 24 16:19:09 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

可以尝试修改此参数看看效果:

$ echo 2 > /proc/sys/net/ipv4/tcp_syn_retries

因此:

TCP 第一次握手包 SYN 报文丢包了,会发生什么?

客户端会重传 SYN 报文,直到收到 ACK 或者达到最大次数,每次重试的时间是翻倍上涨的。

第二种场景

TCP 第二次握手的 SYN + ACK 报文丢包了,会发生什么?

为了模拟 SYN + ACK 的丢包情形,在客户端设置防火墙,将MySQL服务端的报文全部拦截:

$ iptables -A INPUT -p tcp -s 10.186.60.69 -j DROP

在 MySQL 服务器端抓包:

$ tcpdump -i eth0 tcp and port 3306 -w tcp_syn_ack_timeout.cap

在 wireshark Statistics下面 flow graph 功能分析 tcp 流:

从上图来看,可以分为两个视角:

客户端视角:客户端发出 SYN 报文之后,由于设置了防火墙,没有收到 SYN + ACK 报文,因此,客户端会不断进行重试,直到收到 SYN + ACK 或者达到最大重试次数

服务器视角:服务器端在收到 SYN 报文之后,发送SYN + ACK 报文,但是收不到最后一次握手的 ACK 报文,因此服务器端会不断进行重试发送SYN + ACK 报文。

客户端超时重传的 SYN 包抵达了服务端后,服务端然后回了 SYN、ACK 包,但是 SYN、ACK 包的重传定时器并没有被重置,仍然持续在重传。

从图中可以看出,服务端在收到第三次的 SYN 报文并发出的 SYN + ACK 报文之后,后面重试了四次,将之前重试的一次也算在内。

这个重试次数也由内核参数控制:

$ cat /proc/sys/net/ipv4/tcp_synack_retries
5

将客户端内核参数 tcp_synack_retries 设置成 1 之后,TCP 交互图:

这样重试次数就更加明显了。

第三种场景

TCP 第三次握手的 ACK 丢包了

在 MySQL 服务器端设置防火墙,拦截 TCP 第三次握手的 ACK 报文:

$ iptables -A INPUT -p tcp --tcp-flag ack ack --dport 3306 -j DROP

在 MySQL 服务器端抓包:

$ tcpdump -i eth0 tcp and port 3306 -w tcp_3th_ack_timeout.cap

分析抓包文件:

从上图来看,可以分为两个视角:

客户端视角:对于客户端来说,其实连接已经建立好了

通过 netstat 命令查看连接的状态:

$ netstat -napt|grep 3306
tcp 0 0 10.186.60.60:42490 10.186.60.69:3306 ESTABLISHED 14391/mysql

此时连接的状态是 ESTABLISHED 状态。

服务器视角:由于没有收到第三次的 ACK 报文,和第二种场景类似,服务器会一直重新发送 SYN + ACK 报文,直到达到最大次数

在重试期间,服务端连接的状态一直处于 SYN_RECV 状态:

$ netstat -napt|grep 3306
tcp        0      0 10.186.60.69:3306       10.186.60.60:42868      SYN_RECV    -

过了半分钟左右,也就是服务器端达到重试次数之后,服务端刚才处于 SYN_RECV 的状态的 TCP 连接不见了。

可是此时客户端的连接却依然存在。

客户端的连接之后怎么处理?

此时分场景讨论:

一种场景是,客户端在 TCP 连接建立完成之后,直接发送数据。

另一个种场景是,客户端没有任何操作。下面对这两种情况进行讨论。

客户端发送数据

为了模拟这种场景,我们预先通过客户端连接 MySQL 服务器。

连接上之后在 MySQL 服务器端通过防火墙隔离客户端的报文:

$ iptables -A INPUT -p tcp -s 10.186.60.60 -j DROP

在 MySQL 服务端进行抓包:

$ tcpdump -i eth0 tcp and port 3306 -w tcp_data.cap

之后通过刚才建立的连接,下发 use test ; 语句。

下面分析一下抓包文件:

分析:

我们发现客户端一共重传了十一次。

TCP 建立连接后的数据包传输,最大超时重传次数是由 tcp_retries2 指定,默认值是 15 次,这里为了便于观测,将数值调整成了 10 次,如下:

$ cat /proc/sys/net/ipv4/tcp_retries2
10

可是这里的抓包文件中传输了十一次报文,这里参考文章:https://perthcharles.github.io/2015/09/07/wiki-tcp-retries/

无任何操作

在 MySQL 的协议中,TCP 建立完成之后,MySQL 服务端会发送握手包,由于 MySQL 服务端连接已经不在,因此不会下发握手包,客户端会一直 hang 住。

$ mysql -h10.186.60.69 -uroot -p123456 -P3306
mysql: [Warning] Using a password on the command line interface can be insecure.

此时客户端连接的存活由 TCP 的保活机制确保。

keep-alive 机制:

  1. 首先,有个前提:在特定的时间段内,连接如果没有任何动作,TCP 保活机制会开始作用。
  2. 保活机制会每过一个固定时间发送一个「探测报文」,如果连续几个探测报文都没有得到响应,则认为该 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

$ sysctl -a|grep keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
  
# 也可以通过下面的方式来查看:
$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
$ cat /proc/sys/net/ipv4/tcp_keepalive_probes
$ cat /proc/sys/net/ipv4/tcp_keepalive_time

参数解释:

  • tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
  • tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
  • tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活 动,则会启动保活机制

我们可以修改参数看下效果:

$ echo 10 > /proc/sys/net/ipv4/tcp_keepalive_intvl
$ echo 40 > /proc/sys/net/ipv4/tcp_keepalive_time
$ echo 2 > /proc/sys/net/ipv4/tcp_keepalive_probes

通过抓包文件查看:

可以看到在 40s 时候,开始探活包,探测了两次,每次间隔 10s 中,符合参数修改的定义

修改参数之后,client 过了大约一分钟后,就报错了:

$ mysql -h10.186.60.69 -uroot -p123456 -P3306
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 110

avatar
100
  Subscribe  
提醒