https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux#fn-netfilter
一句话总结| 不要开启 net.ipv4.tcp_tw_recycle
在Linux内核文档上,关于 net.ipv4.tcp_tw_recycle
和 net.ipv4.tcp_tw_reuse
都语焉不详。
针对recycle:能够快速回收TIME-WAIT状态的sockets,默认不开启。没有专家建议不应改动
针对reuse:能够复用处于TIME-WAIT状态的sockets,默认不开启。没有专家建议不应改动
官方文档的缺失导致了市面上很多调优指南上都建议开启这两个参数,以减少TIME-WAIT状态的链接。但是在 tcp(7)
manual page上却提到,这两个参数的开启造成了一个十分难以发现的隐患:在NAT设备后的多个服务器可能会无法处理正常请求。
接下来我要给那些瞎逼逼的人上一课🐶
另外虽然从参数的的名称上有ipv4的字样,但是该参数同样适用于ipv6。所以谨记我们是在处理Linux tcp协议族的事情,如果从抓包的角度分析那就可能需要调整一下了。
TIME-WAIT 状态
让我们简单复习一下TIME-WAIT状态。它到底是什么?我们看一下TCP状态流转图:
只有主动关闭链接的一方会进入到TIME-WAIT的状态,被动关闭的一方通常都会快速关闭。
目的
TIME-WAIT状态存在有两个目的:
- 最被大家熟知的是出于可能存在数据包发送后有延迟,然后被后来创建的一个连接处理了。当然序列号( sequence number)也是在特定范围的才能接收,这让问题发生的概率降低了,但是问题仍然存在。RFC 1337解释了当TIME-WAIT不存在的时候的情况。以下是一个图解
但是这样的情况TIME-WAIT只保持一个MSL就可以了
- 另外一个目的是确保远端连接已经关闭。如果最后一个ACK丢失,此时被动关闭端处于LAST-ACK状态,如果不存在time-wait 状态的话,一个创建新链接的请求可能会发送到LAST-ACK状态的连接上,此时端上会返回RST,新链接的创建就失败了。
RFC 793 要求time-wait状态必要持续2 MSL,Linux上是不可调的,内核写死60s。
https://tools.ietf.org/html/rfc1185
The proper time to delay the final close step is not really
related to the MSL; it depends instead upon the RTO for the
FIN segments and therefore upon the RTT of the path.*
Although there is no formal upper-bound on RTT, common
network engineering practice makes an RTT greater than 1
minute very unlikely. Thus, the 4 minute delay in TIME-WAIT
state works satisfactorily to provide a reliable full-duplex
TCP close. Note again that this is independent of MSL
enforcement and network speed.
有人建议把这个参数设置成可调节的,但是基于保持time-wait状态是有益的这一前提下,该提议被否决了。
问题
为什么这个状态在处理海量连接的服务器上会变的十分恼人呢。以下是其中的三个问题:
无法创建新链接
socket 占用内存
额外的CPU使用
连接表卡槽
一个处于time-wait状态的连接大概会在连接表中存在一分钟,在这个时间内具有相同源地址源端口目标地址目标端口的连接是不能创建的。
对于一个web服务器来说,外网地址和端口往往都是固定的。如果是一个7层负载均衡后的web服务。源地址同样是固定的。在linux上客户端的端口范围大概是30000个(可以通过 net.ipv4.ip_local_port_range 参数更改),也就是说负载服务器和web服务器每分钟只能建立30000个,QPS大概是500
客户端的time-wait现象很容易定位,connect()的方法调用会抛出EADDRNOTAVAIL的异常,并且也会有对应的日志记录。但是在服务端就变得十分复杂了,只能通过命令行的方式看当前那些ip 端口被占用了。
如果是客户端端口不够用,通过 net.ipv4.ip_local_port_range 调大端口的可用范围
如果是服务端的端口不够用,添加额外的端口
如果是客户端的IP不够用, 通过round-robin 进行客户端的负载均衡
如果是服务端的ip不够用,配置多个服务端ip
内存
如果需要处理超多的连接,这些socket额外处于open状态一分钟会耗点内存,举个例子来说,如果每秒钟要处理10000个请求的话,time-wait状态的请求就会有60*10000个。那需要消耗的内存大概是多少呢?实际上也没有那么多
首先,从应用的角度考虑,一个处于time-wait状态的socket不会消耗任何的内存。因为socket已经被关闭了。从内核的角度考虑,socket主要以三种不同的形式存在内存中。
hash table of connections set of lists of connections hash table of bound ports
参考linux的代码,40000个入站连接大概需要10m的内存,40000个出站的连接大概需要2.5M。
CUP
从CPU的角度看,找到一个可用的端口确实是略昂贵的。 inet_csk_get_port()
function 方法需要加锁迭代的方式找到可用的端口。 如果在TIME-WAIT状态下有很多出站连接(如与memcached服务器的短暂连接),则此哈希表中的大量条目通常不会成为问题:连接通常共享相同的配置文件,该功能将很快 找到一个可用端口,因为它按顺序迭代它们。
其他的解决办法
如果上面的回答仍然没有解决你的问题,还有三个方法可以尝试。
disable socket lingering,
net.ipv4.tcp_tw_reuse
, andnet.ipv4.tcp_tw_recycle
.
Socket lingering
当close() 被调用的时候,内核缓冲区的数据会被在后台继续发送然后socket最终会转为time-wait状态。应用程序可以立即继续工作,并假设最终将安全地传递所有数据。
应用程序可以选择禁用这个功能,称之为Socket lingering。它有以下两种特性:
1.余下的数据会被直接抛弃,而且不进行四次挥手的操作,连接直接发送RST并且直接关闭。不存在TINE-WAIt状态的情况。
2.第二个场景下,如果发送缓存中仍然有未发送的数据,进程在调用close()后会休眠直到数据完全发完并且已经通知到了对端,或者配置的延迟时间过期了。还有另外一个场景,线程并不阻塞并且持续的发送数据包,如果成功发完就正经关闭,然后连接变为time-wait状态。如果没发完过期了,就直接发RST然后抛弃剩余的包。
开启改参数并不能适用于所有的场景,在HAProxy or Nginx 的服务器上可以在确认合适的情况下开启。
net.ipv4.tcp_tw_reuse
time-wait状态的存在避免了延迟包被无关的连接处理的情况,但是在特定场景下,我们可以认为新的连接不会处理老连接的包。
RFC 1323 提供了一系列在高并发的情况下的提升性能的扩展。这其中就包含两个四字节的timestamp fields字段,前两个用于记录发送包的时间戳,后两个记录从远端收到的最新的时间戳。
开启了 net.ipv4.tcp_tw_reuse 参数后,如果linux收到一个明显比上个连接最后一个包时间晚的请求,那linux就会复用这个处于time-wait状态的连接。大概一秒钟后time-wait的连接就能继续使用
这样能确保合法么?time-wait状态是为了避免收到一个其他连接请求的包,但是因为timestamp fields字段的存在,类似的包会因为超时而被丢弃掉。
另外一个原则是为了确保对端不会因为最后一个ACK包的丢失而导致连接一直处于LAST-ACK状态。对端会一直发送FIN包知道以下几种场景:
1:主动放弃(并且关闭连接)
2:收到了期望的ACK
3:收到RST包
如果FIN包被及时接收了,本端连接仍然保持在time-wait状态,然后ACK包也会照常发送。
一旦新的连接代替了time-wait的坑位,新链接的SYN包会被返回FIN,然后FIN包会被应答RST。然后对端就脱离了LAST-ACK状态了。连接就会马上进行重建
net.ipv4.tcp_tw_recycle
此机制还依赖于timestamp选项,但会影响传入和传出连接。这在服务器通常首先关闭连接时更实用。
TIME-WAIT状态计划更快到期:它将在RTO(从RTT及其方差计算的重传超时)之后被删除。 你可以通过ss命令找到活动连接的相应值。
参数开启后,linux会记录请求包最新的时间戳,然后老的都删除。但是远程主机是一个NAT设备,NAT后的不同主机的时间戳可能是乱序的,所以就会导致丢包的问题。
Linux 4.10 后,linux随机生成时间戳的偏移量,所以这两个参数就废了。在4.12后这个参数被移除了。
总结一哈
通用的解决方案毫无疑问是添加足够的可用连接数量,这样就不会因为time-wait焦头烂额。
在服务端,不要开启 net.ipv4.tcp_tw_recycle
参数,开启net.ipv4.tcp_tw_reuse 参数对入栈连接也没什么用。
在客户端,开启net.ipv4.tcp_tw_reuse 也算一个相对合理的方法。开启net.ipv4.tcp_tw_recycle也没什么大用。
制订协议的时候,不要让客户端先关闭连接。这样客户端就不用处理time-wait状态的连接了。服务端处理这些问题更合理的。
最后引用一句 W. Richard Stevens 在Unix Network Programming中的一句话:
time-wait状态是我们的好朋友是来帮助我们的(比如让一些老的重复的网络包过期掉)。相比极力避免折这种状态,我们更应该充分理解它。