背景
在稳定性环境中,当 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报文:
代码语言:sql复制$ iptables -i eth0 -A INPUT -p tcp --dport 3306 -j DROP
在 MySQL 服务器上开始抓包:
代码语言:sql复制$ tcpdump -i eth0 tcp and port 3306 -w tcp_syn_timeout.cap
通过 MySQL client 连接 MySQL 服务端,过了一分多钟后,客户端返回报错,此时停止抓包,如下图:
代码语言:sql复制$ 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 是指数上涨的。另外,这里的重试次数可以配置,由客户端机器的如下内核参数指定:
代码语言:sql复制$ 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
可以尝试修改此参数看看效果:
代码语言:sql复制$ echo 2 > /proc/sys/net/ipv4/tcp_syn_retries
因此:
TCP 第一次握手包 SYN 报文丢包了,会发生什么?
客户端会重传 SYN 报文,直到收到 ACK 或者达到最大次数,每次重试的时间是翻倍上涨的。
第二种场景
TCP 第二次握手的 SYN ACK 报文丢包了,会发生什么?
为了模拟 SYN ACK 的丢包情形,在客户端设置防火墙,将MySQL服务端的报文全部拦截:
代码语言:sql复制$ iptables -A INPUT -p tcp -s 10.186.60.69 -j DROP
在 MySQL 服务器端抓包:
代码语言:sql复制$ 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 报文之后,后面重试了四次,将之前重试的一次也算在内。
这个重试次数也由内核参数控制:
代码语言:sql复制$ cat /proc/sys/net/ipv4/tcp_synack_retries
5
将客户端内核参数 tcp_synack_retries 设置成 1 之后,TCP 交互图:
这样重试次数就更加明显了。
第三种场景
TCP 第三次握手的 ACK 丢包了
在 MySQL 服务器端设置防火墙,拦截 TCP 第三次握手的 ACK 报文:
代码语言:sql复制$ iptables -A INPUT -p tcp --tcp-flag ack ack --dport 3306 -j DROP
在 MySQL 服务器端抓包:
代码语言:sql复制$ tcpdump -i eth0 tcp and port 3306 -w tcp_3th_ack_timeout.cap
分析抓包文件:
从上图来看,可以分为两个视角:
客户端视角:对于客户端来说,其实连接已经建立好了
通过 netstat 命令查看连接的状态:
代码语言:sql复制$ 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 状态:
代码语言:sql复制$ 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 服务器端通过防火墙隔离客户端的报文:
代码语言:sql复制$ iptables -A INPUT -p tcp -s 10.186.60.60 -j DROP
在 MySQL 服务端进行抓包:
代码语言:sql复制$ tcpdump -i eth0 tcp and port 3306 -w tcp_data.cap
之后通过刚才建立的连接,下发 use test ; 语句。
下面分析一下抓包文件:
分析:
我们发现客户端一共重传了十一次。
TCP 建立连接后的数据包传输,最大超时重传次数是由 tcp_retries2 指定,默认值是 15 次,这里为了便于观测,将数值调整成了 10 次,如下:
代码语言:sql复制$ cat /proc/sys/net/ipv4/tcp_retries2
10
可是这里的抓包文件中传输了十一次报文,这里参考文章:https://perthcharles.github.io/2015/09/07/wiki-tcp-retries/
无任何操作
在 MySQL 的协议中,TCP 建立完成之后,MySQL 服务端会发送握手包,由于 MySQL 服务端连接已经不在,因此不会下发握手包,客户端会一直 hang 住。
代码语言:sql复制$ mysql -h10.186.60.69 -uroot -p123456 -P3306
mysql: [Warning] Using a password on the command line interface can be insecure.
此时客户端连接的存活由 TCP 的保活机制确保。
keep-alive 机制:
- 首先,有个前提:在特定的时间段内,连接如果没有任何动作,TCP 保活机制会开始作用。
- 保活机制会每过一个固定时间发送一个「探测报文」,如果连续几个探测报文都没有得到响应,则认为该 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
代码语言:sql复制$ 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 小时内如果没有任何连接相关的活 动,则会启动保活机制
我们可以修改参数看下效果:
代码语言:sql复制$ 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 过了大约一分钟后,就报错了:
代码语言:sql复制$ 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