哈喽哈喽大家好,今天要说的话题是关于TCP的TIME-WAIT状态
TIME-WAIT是服务器优化必然会谈到的一个话题,而我们常见的问题就是TIME-WAIT过多怎么处理?
常见的解决方法就是:
1、快速回收
2、链接复用
而这里有个误区就是到底TIME-WAIT要优化到什么程度,有的童鞋甚至看到TIME-WAIT就觉得需要优化,今天就是想聊一下这个话题
要聊明白,还是要从原理来说起
TIME-WAIT是TCP链接的一种状态,之前有写文章介绍过TCP的11中链接状态,有兴趣可以再去看下
这张图整个展示了所有状态的转换过程,总共分为三个部分,上半部分是建立连接的过程,下面部分分成主动关闭和被动关闭的过程
可以看到TIME_WAIT只在主动关闭的过程中出现,实际上TIME_WAIT是TCP为了解决复杂的网络问题提出的一种解决方案
解决什么问题呢?看下面两个场景
- 四次挥手中,A 发 FIN, B 响应 ACK,B 再发 FIN,A 响应 ACK 实现连接的关闭。而如果 A 响应的 ACK 包丢失,B 会以为 A 没有收到自己的关闭请求,然后会重试向 A 再发 FIN 包。 如果没有 TIME_WAIT 状态,A 不再保存这个连接的信息,收到一个不存在的连接的包,A 会响应 RST 包,导致 B 端异常响应。 此时, TIME_WAIT 是为了保证全双工的 TCP 连接正常终止。
- 我们还知道,TCP 下的 IP 层协议是无法保证包传输的先后顺序的。如果双方挥手之后,一个网络四元组(src/dst ip/port)被回收,而此时网络中还有一个迟到的数据包没有被 B 接收,A 应用程序又立刻使用了同样的四元组再创建了一个新的连接后,这个迟到的数据包才到达 B,那么这个数据包就会让 B 以为是 A 刚发过来的。 此时, TIME_WAIT 的存在是为了保证网络中迷失的数据包正常过期。
第一种场景下,TIME_WAIT是为了确保被动关闭方收到ACK,连接正常关闭,且不因被动关闭方重传FIN影响下一个连接
第二种场景下,TIME_WAIT保留2个MSL,以确保数据不会丢失
注释:MSL(Maximum Segment Lifetime) 最大分段寿命,它表示一个TCP分段可以存在于互联网系统中的最大时间,由TCP实现,超出这个寿命的分段都会被丢弃,RFC 1122建议是2分钟,但在不同的unix实现上,这个值并不确定
由于以上的两种场景,TCP引入了TIME_WAIT状态来解决,由此可见TIME_WAIT并不是完全不存在才是合理的或以消除TIME_WAIT为优化的一个目标
那么TIME_WAIT具体要怎么优化,什么样的状态是一个合理的状态?
我们还是看常用的两种优化方法,先说不建议使用的一种:快速回收
由上面可知,TIME_WAIT的时长是2MSL,按照RFC建议2分钟的话,就是4分钟,对于高并发的服务器来说,本身local_port就有固定的量,如果4分钟才回收TIME_WAIT,那么端口很快就会被用尽
尽管CentOS系统中,MSL可以通过修改参数tcp_fin_timeout来设置MSL的时间,默认是30s,这样的话,一个四元组(local_ip, local_port, remote_ip,remote_port)会被冻结60s的时间,系统默认可分配端口约30000个,可通一下文件查看过
/proc/sys/net/ipv4/ip_local_port_range
那么从同一个客户端发起请求,并发只能到500QPS左右
所以提出了快速回收的方法,即TCP连接状态在TIME_WAIT状态的时候,立即回收连接,不等待2MSL,以快速回收资源用于新的连接
而快速回收的弊端,相信都有了解,就是关于快速回收引发的SYN无法得到ACK的问题,具体如下:
TCP有一种行为,可以缓存每个连接最新的时间戳,后续请求中如果时间戳小于缓存的时间戳,即视为无效,相应的数据包会被丢弃。Linux是否启用这种行为取决于tcp_timestamps和tcp_tw_recycle,因为tcp_timestamps缺省就是开启的,所以当tcp_tw_recycle被开启后,实际上这种行为就被激活了。在nat环境中会出现时间戳错乱的情况,后面的数据包就被丢弃了,具体的 表现通常是是客户端明明发送的SYN,但服务端就是不响应ACK。因为NAT设备将数据包的源IP地址都改成了一个地址(或者少量的IP地址),但是却基本上不修改TCP包的时间戳,则会导致时间戳混乱
建议:如果前端部署了三/四层NAT设备,尽量关闭快速回收,以免发生NAT背后真实机器由于时间戳混乱导致的SYN拒绝问题
而另一种优化方式就是建议的方式:复用
由上面的分析可以确定,并发上不去是因为四元组被冻结2MSL时长导致没有新的资源可用,而四元组在被冻结的时间内是不能被使用的,所以提出了复用的方式,但是复用是有前提的,它需要tcp_timestamps同时开启,看下内核关于这部分的代码
reuse的前提是收到最后一个包后超过1s,所以正常的情况下开启reuse是可以达到快速复用TIME_WAIT状态的socket链接的,而且也不会有像recycle那样的“副作用”
除了以上两种解决TIME_WAIT的方式,还有一个参数是必须要关注的,就是tw_buckets,可以通过
代码语言:javascript复制cat /proc/sys/net/ipv4/tcp_max_tw_buckets
查看该参数值,这个值是TIME_WAIT可以达到的最大数量,当TIME_WAIT存在的数量和tcp_max_tw_buckets相同时,会处于tw_buckets溢出状态,默认是4096
当弃用reuse的时候,在一个高并发的服务器上,TIME_WAIT并不会快速回收,而是复用之前的链接,这样的情况下TIME_WAIT必然会保持一定的数量,那么tw_buckets就很关键,如果按照local_port在3w左右,那么tw_buckets如果是默认4096的情况下,TIME_WAIT也会很快到达瓶颈,无法再增加
所以应该保持tw_buckets至少在local_port之上,虽然长时间大量TIME_WAIT会消耗一定的内存资源,但是对于现在的服务器,TIME_WAIT所占用的内存是可以容忍的
总结:
结合上面的分析,recycle在内核4.1以后就被弃用了,所以它并不是TIME_WAIT优化的最好方案,最好的方式就是通过reuse进行复用,复用的前提是开启tcp_timestamps,并且要关注tw_buckets溢出的情况,适当增加tw_buckets,以应对并发量