机器上出现大量time_wait怎么办

背景

Linux系统下,TCP/IP连接断开后,会以TIME_WAIT状态保留一定的时间,然后才会释放端口。当并发请求过多的时候,就会产生大量的 TIME_WAIT状态的连接,无法及时断开的话,会占用大量的端口资源和服务器资源。这个时候我们可以考虑优化TCP/IP 的内核参数,来及时将TIME_WAIT状态的端口清理掉。

1、TIME_WAIT是什么?它怎么产生的?

答: 它是在tcp连接关闭的过程中出现的

先复习下连接关闭的过程:

  • 主动关闭连接的一方,调用close();协议层发送FIN包

  • 被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close操作

  • 被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态

  • 主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态

  • 等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态

通过上面的一次socket关闭操作,你可以得出以下几点:

  1. 主动关闭连接的一方 - 也就是主动调用socket的close操作的一方,最终会进入TIME_WAIT状态

  2. 被动关闭连接的一方,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接

  3. TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态;

  4. 在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的!

这里又出现两个问题:

  1. 上文提到的连接重用,那连接到底是个什么概念?

  2. 协议层为什么要设计一个TIME_WAIT状态?这个状态为什么要等待2MSL时间才会进入CLOSED

  1. TIME_WAIT的产生条件:主动关闭方在发送四次挥手的最后一个ACK会变为TIME_WAIT状态,保留此状态的时间为两个MSL(linux里一个MSL为30s,是不可配置的,除非修改内核)
  2. 为什么要有TIME_WAIT?:

    1、  网络情况不好时,如果主动方无TIME_WAIT等待,关闭前个连接后,主动方与被动方又建立起新的TCP连接,这时被动方重传或延时过来的FIN包过来后会直接影响新的TCP连接;

    2、  同样网络情况不好并且无TIME_WAIT等待,关闭连接后无新连接,当接收到被动方重传或延迟的FIN包后,会给被动方回一个RST包,可能会影响被动方其它的服务连接。

  3. TIME_WAIT为什么保留的时间是两个MSL:可靠安全的关闭TCP连接。比如网络拥塞,主动方最后一个ACK被动方没收到,这时被动方会对FIN开启TCP重传,发送多个FIN包,在这时尚未关闭的TIME_WAIT就会把这些尾巴问题处理掉,不至于对新连接及其它服务产生影响。

  4. 在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的!

2、怎么查有多少个TIME_WAIT?

$ netstat -a |grep TIME_WAIT|wc -l
2050

3、数量多了会对机器的性能有什么影响?它会占用机器哪些资源?

1、内存

你可以看到的任何数据,一般来说,内核里都需要有相关的数据结构来保存这个数据。

内核里有一个保存所有连接的hash table,这个hash table里面既包含TIME_WAIT状态的连接,也包含其他状态的连接。主要用于有新的数据到来的时候,从这个hash table里快速找到这条连接。不同的内核对这个hash table的大小设置不同,你可以通过dmesg命令去找到你的内核设置的大小

还有一个hash table用来保存所有的bound ports,主要用于可以快速的找到一个可用的端口或者随机端口:

不过占用内存很少很少。 一个tcp socket占用不到4k。1万条TIME_WAIT的连接,也就多消耗1M左右的内存

2、CPU

每次找到一个随机端口,还是需要遍历一遍bound ports的吧,这必然需要一些CPU时间。

3、源端口数量 (net.ipv4.ip_local_port_range)

处于TIME_WAIT状态说明连接还未关闭,没关闭的连接自然会占用一个端口,一台机器的端口是有限的,最多只有65536个。(可用的是65535,因为,端口号 0 是一个预留的端口号,代表的意思就是它在TCP或者UDP网络传输中应该不会被用到。但是在网络编程中,尤其是在unix socket编程当中,它有一些特殊的含义。在unix socket编程当中,端口号 0 是一种由系统指定动态生成的端口。当建立新的TCP和UDP socket连接时,需要给它们指定端口号。 为了避免这种写死端口号的做法或者说为了从本地系统中找到可用端口。网络编程员们可以以端口号0来作为连接参数。这样的话操作系统就会从动态端口号范围内搜索接下来可以使用的端口号。windows系统和其他操作系统在处理端口号0时有一些细微的差别。)

4、TIME_WAIT bucket 数量 (net.ipv4.tcp_max_tw_buckets)

TCP: time wait bucket table overflow产生原因及影响:原因是超过了linux系统tw数量的阀值。危害是超过阀值后﹐系统会把多余的time-wait socket 删除掉,并且显示警告信息,如果是NAT网络环境又存在大量访问,会产生各种连接不稳定断开的情况。

5、文件描述符数量 (max open file)

一个socket占用一个文件描述符

4、数量有多少算多呢,达到什么数量级我们就需要处理了?

正如上面第三个问题所提到的,如果数量级多到占用的cpu和内存很多了,或者快达到相关的内核参数的限制了。那就需要处理了。

5、如果需要处理,那怎么处理?

通过优化相关的内核参数来解决这个问题。相关的参数如下:

注意:

对于tw的reuse、recycle其实是违反TCP协议规定的,服务器资源允许、负载不大的条件下,尽量不要打开

net.ipv4.tcp_tw_reuse = 1

字面意思,reuse TIME_WAIT状态的连接。

时刻记住一条socket连接,就是那个五元组,出现TIME_WAIT状态的连接,一定出现在主动关闭连接的一方。所以,当主动关闭连接的一方,再次向对方发起连接请求的时候(例如,客户端关闭连接,客户端再次连接服务端,此时可以复用了;负载均衡服务器,主动关闭后端的连接,当有新的HTTP请求,负载均衡服务器再次连接后端服务器,此时也可以复用),可以复用TIME_WAIT状态的连接。

通过字面解释,以及例子说明,你看到了,tcp_tw_reuse应用的场景:某一方,需要不断的通过“短连接”连接其他服务器,总是自己先关闭连接(TIME_WAIT在自己这方),关闭后又不断的重新连接对方。

那么,当连接被复用了之后,延迟或者重发的数据包到达,新的连接怎么判断,到达的数据是属于复用后的连接,还是复用前的连接呢?那就需要依赖前面提到的两个时间字段了。复用连接后,这条连接的时间被更新为当前的时间,当延迟的数据达到,延迟数据的时间是小于新连接的时间,所以,内核可以通过时间判断出,延迟的数据可以安全的丢弃掉了。

这个配置,依赖于连接双方,同时对timestamps的支持。同时,这个配置,仅仅影响outbound连接,即做为客户端的角色,连接服务端[connect(dest_ip, dest_port)]时复用TIME_WAIT的socket。

net.ipv4.tcp_timestamps

RFC 1323 在 TCP Reliability一节里,引入了timestamp的TCP option,两个4字节的时间戳字段,其中第一个4字节字段用来保存发送该数据包的时间,第二个4字节字段用来保存最近一次接收对方发送到数据的时间。有了这两个时间字段,也就有了后续优化的余地。

tcp_tw_reuse 和 tcp_tw_recycle就依赖这些时间字段。

这个参数在有nat网关的环境下开启会导致连接异常

net.ipv4.tcp_tw_recycle

字面意思,销毁掉 TIME_WAIT。

当开启了这个配置后,内核会快速的回收处于TIME_WAIT状态的socket连接。多快?不再是2MSL(一般为60s),而是一个RTO(retransmission timeout,数据包重传的timeout时间)的时间,这个时间根据实际网络状况RTT动态计算出来,但是远小于2MSL。大概在700ms数量级左右,但是对于极端糟糕的网络环境,这里可能有个坑,回收时间有可能大于正常回收的60s。

有了这个配置,还是需要保障 丢失重传或者延迟的数据包,不会被新的连接(注意,这里不再是复用了,而是之前处于TIME_WAIT状态的连接已经被destroy掉了,新的连接,刚好是和某一个被destroy掉的连接使用了相同的五元组而已)所错误的接收。在启用该配置,当一个socket连接进入TIME_WAIT状态后,内核里会记录包括该socket连接对应的五元组中的对方IP等在内的一些统计数据,当然也包括从该对方IP所接收到的最近的一次数据包时间。当有新的数据包到达,只要时间晚于内核记录的这个时间,数据包都会被统统的丢掉。

这个配置,依赖于连接双方对timestamps的支持。同时,这个配置,主要影响到了inbound的连接(对outbound的连接也有影响,但是不是复用),即做为服务端角色,客户端连进来,服务端主动关闭了连接,TIME_WAIT状态的socket处于服务端,服务端快速的回收该状态的连接。

由此,如果客户端处于NAT的网络(多个客户端,同一个IP出口的网络环境),如果配置了tw_recycle,就可能在一个RTO的时间内,只能有一个客户端和自己连接成功(不同的客户端发包的时间不一致,造成服务端直接把数据包丢弃掉)。

 tcp_max_tw_buckets = 256000 

TIME_WAIT状态的最大数量。如果超过此数的话﹐TIME_WAIT socket 会被立即断开,并显示警告信息TCP: time wait bucket table overflow

https://zhuanlan.zhihu.com/p/40013724

https://yq.aliyun.com/articles/700002?spm=a2c4e.11155435.0.0.707b6552NVl5pc

一个TCP TIME_WAIT过高引起的连接mysql超时案例

问题背景:

     客户将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内核,难度和风险都较大。

https://cloud.tencent.com/developer/article/1409308

猜你喜欢

转载自blog.csdn.net/fanren224/article/details/89849276