Copywang 2019-04-23
本文作者:鲁越
客户将mysql从IDC迁移至公有云后,时常有出现建立连接超时的情况,业务使用的场景是PHP短连接到mysql,每秒的新建连接数在3000个左右,这个量算是比较大。 客户反馈在IDC内自建时也是这样的使用场景,从未遇到过这个问题。
1、首先肯定是排查mysql以及mysql所在的物理机是否有异常,排查了一圈之后,发现数据库的慢查询基本没有,数据库和物理机的负载都不算高,基本可以排除是数据库本身的问题。
2、超时问题最容易联想到的就是网络上有异常,在物理机上抓包后,抓到在有问题的时间点确实有syn包的重传。
3、仔细排查这个异常的流,发现重传并不是因为没有收到包或者发出去了包没有响应,那么说明问题并不是出现在网络链路上。这个流中,客户端首先发了一个SYN包给服务器,奇怪的是,服务器在收到这个SYN包过后,并没有按照TCP三次握手的方式回复一个SYN+ACK,而是回复了一个普通的ACK,而且这个ACK回复的seq并不是SYN包的seq 0,而是一个比较奇怪的数字706168360。
4、另外,在物理机上netstat发现,有大量的连接处于TIME_WAIT状态。
5、那这里就产生了两个疑点:
1)为什么Server端会有大量的连接进入TIME_WAIT状态?
2)为什么Server端没有正常回复SYN+ACK,而是回复了一个普通的ACK?
6、要解释第一个问题,我们先来回顾一下TCP四次挥手的流程
从流程里面我们看到,进入TIME_WAIT状态是先发送FIN包的一方,也就是主动断开连接的一方。一般来说,客户端连接服务器,如果没有什么异常,连接是会由客户端主动断开的。那这里为什么服务器上面会有大量的连接处于TIME_WAIT状态?难道这个场景下连接是服务器主动断开的?
我们来看看一个程序跟mysql通信的一个常规过程,程序首先跟mysql建连,建连完成之后执行SQL请求进行数据通信,通信完成后,会发送一个quit命令给mysql服务器断开连接。这个流程看似没有什么疑点,但重点就在这个quit命令上面。我们考虑一下mysql服务器在收到这个quit请求后会做一些什么处理。 首先肯定是处理应用层的一些连接相关的信息。处理完成之后,再处理网络层的连接。 网络层的连接怎么处理呢? 等客户端发送FIN包过来吗? 要是客户端一直不发怎么办呢?其实这里我们不难找到答案,也很容易猜想出mysql的处理方式:主动发送FIN包来断开这个TCP连接。 这个也就解释了为什么大量进入TIME_WAIT状态的是mysql服务器而不是客户端。不仅仅是mysql,包括redis、mongodb等会接受到类似quit命令退出的数据库产品,也都是相似的处理方式。
7、第一个问题解释清楚了, 我们再来看看第二个,为什么服务器没有正常的回复SYN+ACK。
首先,我们知道,linux下连接进入TIME_WAIT状态的时间是2个MSL,也就是120秒。在每秒3000个短连接的情况下,120秒内可以产生大约36万个进入TIME_WAIT状态的连接。而客户端可以使用的总端口数是65536,除去一些系统固定分配的,差不多也就60000个左右。假如这3000个每秒的短连接都是由一台客户端连接过来的,那20秒的时间就会复用到之前已经使用过的端口,这个时候该端口对应在服务器端的连接还在TIME_WAIT状态。所以服务器在收到新连接的SYN包后,并不认为这是一个新建连接请求的SYN包过来,而是把它当做一个普通数据包来处理,所以回复了一个普通的ACK和一个较大的seq。其实这个seq就是上一次连接的最后一个seq。
8、排查到这里,这个问题的处理思路就比较明确了, 就是减少服务器上进入TIME_WAIT状态的连接数量,首先想到的当然是开启time_wait的快速回收。但在实际开启后,我们发现快速回收并没有生效, 这里又涉及到另一个问题:
开启time_wait快速回收需要开启net.ipv4.tcp_timestamps,但是这个参数在有nat网关的环境下开启会导致连接异常,云上vpcgw层会在接受到带有timestamps包后把这个字段去掉,导致了即使开启了快速回收也没有实际生效。这里也解释了为什么客户在自建IDC内没有问题而迁移到云上之后开始出问题。
1、客户端改用长连接
需要客户端的改动比较大,但能彻底解决问题,高并发的场景下,长连接的性能也明显好于短连接。
2、增加客户端的个数,避免在2MSL时间内使用到重复的端口
能够降低出问题概率,但需要增加成本,性价比不高。
3、降低net.ipv4.tcp_max_tw_buckets(有风险)
能够降低出问题概率,降低的程度视修改的参数值而定,设置为0可以完全解决。此方法在网络状况不好的情况下有风险,一般内网低延迟的网络风险不大。
4、客户端在断开连接时,不用quit的方式退出,直接发FIN或者RST
能够彻底解决问题,需要修改客户端底层库,有一定风险。
5、修改linux内核减小MSL时间
能够降低出问题的概率,需要修改linux内核,难度和风险都较大。