一、关于net.ipv4.tcp_tw_recycle
我们发现,网上很多linux参数调整指南都建议把这些参数net.ipv4.tcp_tw_recycle 设置1「启用」,用于快速减少在TIME-WAIT状态TCP连接数。
但是,在TCP(7)手册中,参数net.ipv4.tcp_tw_recycle 非常蛋疼,尤其是在普通用户家中,有多台设备,或者网吧、公司等多台设备,共用同一个NAT设备环境下,TW回收选项是很有问题的面向公共服务器作为它不会把手连接两台不同的计算机上,这问题很难发现,无从下手。
启用TIME-WAIT状态sockets的快速回收,这个选项不推荐启用。在NAT(Network Address Translation)网络下,会导致大量的TCP连接建立错误。
如果没有技术大神的指点的话,千万不要去改动他!!!
二、关于TCP连接的TIME-WAIT状态
关于TCP连接的TIME-WAIT状态,它是为何而生,存在的意义是什么?
让我们回忆一下,什么是TCP TIME-WAIT状态?
当TCP连接关闭之前,首先发起关闭的一方会进入TIME-WAIT状态,另一方可以快速回收连接。
TIME-WAIT状态的作用:
一、人尽皆知的是,防止上一个TCP连接的延迟的数据包(发起关闭,但关闭没完成),被接收后,影响到新的TCP连接。
唯一连接确认方式为四元组:源IP地址、目的IP地址、源端口、目的端口,包的序列号也有一定作用,会减少问题发生的几率,但无法完全避免。尤其是较大接收windows size的快速(回收)连接。RFC1137解释了当TIME-WAIT状态不足时将会发生什么。
如果TIME-WAIT状态连接没有被快速回收,会避免什么问题呢?
请看下面的例子:
缩短TIME-WAIT的时间后,延迟的TCP 包会被新建立的TCP连接接收。
二、另外一个作用是,当最后一个ACK丢失时,远程连接进入LAST-ACK状态,它可以确保远程已经关闭当前TCP连接。
如果没有TIME-WAIT状态,当远程仍认为这个连接是有效的,则会继续与其通讯,导致这个连接会被重新打开。
当远程收到一个SYN 时,会回复一个RST包,因为这SEQ不对,那么新的连接将无法建立成功,报错终止。
如果远程因为最后一个ACK包丢失,导致停留在LAST-ACK状态,将影响新建立具有相同四元组的TCP连接。
RFC 793中强调TIME-WAIT状态必须是两倍的MSL时间(max segment lifetime),在Linux上,这个限制时间无法调整,写死为1分钟了,定义在include/net/tcp.h
曾有人提议将TCP TIME-WAIT时间改为一个可以自定义配置的参数,但被拒绝了,其实,这对TCP规范,对TIME-WAIT来说,是利大于弊的。
三、TIME-WAIT带来的问题
我们来看下,为什么这个状态能影响到一个处理大量连接的服务器,从下面三个方面来说:
1. 新老连接(相同四元组)在TCP连接表中的slot复用避免
2. 内核中,socket结构体的内存占用
3. 额外的CPU开销
而 ss -tan state time-wait|wc -l 的结果,并不能说明这些问题。
Connection table slot连接表槽
处于TIME-WAIT状态的TCP连接,在链接表槽中存活1分钟,意味着另一个相同四元组(源地址,源端口,目标地址,目标端口)的连接不能出现,也就是说新的TCP(相同四元组)连接无法建立。
对于web服务器来说,目标地址、目标端口都是固定值。如果web服务器是在L7层的负载均衡后面,那么源地址更是固定值。在LINUX上,作为客户端时,客户端端口默认可分配的数量是3W个(可以在参数net.ipv4.up_local_port_range上调整)。
这意味着,在web服务器跟负载均衡服务器之间,每分钟只有3W个端口是处于established状态,也就大约500连接每秒。
如果TIME-WAIT状态的socket出现在客户端,那这个问题很容易被发现。调用connect()函数会返回EADDRNOTAVAIL,程序也会记录相关的错误到日志。
如果TIME-WATI状态的socket出现在服务端,问题会非常复杂,因为这里并没有日志记录,也没有计数器参考。不过,可以列出服务器上当前所有四元组连接的数量来确认:
# $ ss -tan 'sport = :80' | awk '{print $(NF)" "$(NF-1)}' | sed 's/:[^ ]*//g' | sort | uniq -c
解决办法是,增加四元组的范围,这有很多方法去实现。(以下建议的顺序,实施难度从小到大排列)
修改net.ipv4.ip_local_port_range参数,增加客户端端口可用范围。
增加服务端端口,多监听一些端口,比如81、82、83这些,web服务器前有负载均衡,对用户友好。
增加客户端IP,尤其是作为负载均衡服务器时,使用更多IP去跟后端的web服务器通讯。
增加服务端IP。
当然了,最后的办法是调整net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle。但不到万不得已,千万别这么做,稍后再讲。
内存
保持大量的连接时,当多为每一连接多保留1分钟,就会多消耗一些服务器的内存。
举个例子,如果服务器每秒处理了1W个新的TCP连接,那么服务器将会存货1W/s*60s = 60W个TIME-WAIT状态的TCP连接,那这将会占用多大的内存么?别担心,少年,没那么多。
从应用的角度来看,一个TIME-WAIT状态的socket不会消耗任何内存:socket已经关了。
所以,当服务器上有4W个连进来的连接进入TIME-WAIT状态时,才用了10MB不到的内存。
如果服务器上有4W个连接到远程的连接进入TIME-WAIT状态时,才用了2.5MB的内存。
TIME-WAIT状态的连接占用内存非常的小。
如果你的服务器上要处理每秒成千上万的新建TCP连接,你可能需要多一点的内存才能 正确无误的跟客户端做数据通信。但TIME-WAIT状态连接的内存占用,简直可以无视。
CPU
在CPU这边,查找一个空闲端口的操作,还是蛮珍贵的。
这由inet_csk_get_port() 函数,加锁,遍历整个空闲端口列表实现。
这个哈希表里条目数量大通常不是问题,如果服务器上存在大量连接到远程TIME-WAIT状态的连接(比如FPM连redis、memcache之类),都会同享相同的profile,这个特性会非常快的按照顺序找到一个新的空闲端口。
四、其他解决办法
net.ipv4.tcp_tw_reuse
TIME-WAIT状态是为了防止不相关的延迟请求包被接受。
但在某些特定条件下,很有可能出现,新建立的TCP连接请求包,被老连接(同样的四元组,暂时还是TIME-WAIT状态,回收中)的连接误处理。
RFC 1323 实现了TCP拓展规范,以保证网络繁忙状态下的高可用。除此之外,另外,它定义了一个新的TCP选项–两个四字节的timestamp fields时间戳字段,第一个是TCP发送方的当前时钟时间戳,而第二个是从远程主机接收到的最新时间戳。
启用net.ipv4.tcp_tw_reuse后,如果新的时间戳,比以前存储的时间戳更大,那么Linux将会从TIME-WAIT状态的存活连接中,选取一个,重新分配给新的连接出去的TCP连接。连出的TIME-WAIT状态连接,仅仅1秒后就可以被重用了。
如何确通讯安全性?
TIME-WAIT的第一个作用是避免新的连接(不相关的)接收到重复的数据包。由于使用了时间戳,重复的数据包会因为timestamp过期而丢弃。
第二个作用是确保远程端(远程的不一定是服务端,有可能,对于服务器来说,远程的是客户端,我这里就用远程端来代替)是不是在LAST-ACK状态。因为有可能丢ACK包丢。远程端会重发FIN包,直到
放弃(连接断开)
等到ACK包
收到RST包
如果 FIN包接及时收到,本地端依然是TIME-WAIT状态,同时,ACK包也会发送出去。
一旦新的连接替换了TIME-WAIT的entry,新连接的SYN包会被忽略掉(这得感谢timestramps),也不会应答RST包,但会重传FIN包。
FIN包将会收到一个RST包的应答(因为本地连接是SYN-SENT状态),这会让远程端跳过LAST-ACK状态。 最初的SYN包将会在1秒后重新发送,然后完成连接的建立。看起来没有啥错误发生,只是延迟了一下。
另外,当连接被重用时,TWrecycled计数器会增加的。「译者注:见/proc/net/netstat 中TWrecycled的值」
net.ipv4.tcp_tw_recycle
这种机制也依赖时间戳选项,这也会影响到所有连接进来和连接出去的连接。「译者注:Linux上tcp_timestamps默认开启」
TIME-WAIT状态计划更早的过期:它将会在超时重发(RTO)间隔后移除(底层会根据当前连接的延迟状况根据RTT来计算RTO值,上篇《PHP-FPM中backlog参数变更的一些思考》也有提到过,比较复杂的算法)。
可以执行ss指令,获取当前存活的TCP连接状态,查看这些数据。「译者注:Linux指令ss的结果中rto,rtt值单位均为ms」
当远程端主机HOST处于NAT网络中时,时间戳在一分钟之内(MSL时间间隔)将禁止了NAT网络后面,除了这台主机以外的其他任何主机连接,因为他们都有各自CPU CLOCK,各自的时间戳。这会导致很多疑难杂症,很难去排查,建议你禁用这个选项。另外,对方上TCP的LAST-ACK状态是体现本机net.ipv4.tcp_tw_recycle的最好数据。
五、总结
最合适的解决方案是增加更多的四元组数目,比如,服务器可用端口,或服务器IP,让服务器能容纳足够多的TIME-WAIT状态连接。
在我们常见的互联网架构中(NGINX反代跟NGINX,NGINX跟FPM,FPM跟redis、mysql、memcache等),减少TIME-WAIT状态的TCP连接,最有效的是使用长连接,不要用短连接,尤其是负载均衡跟web服务器之间。
在服务端,不要启用net.ipv4.tcp_tw_recycle,除非你能确保你的服务器网络环境不是NAT。在服务端上启用net.ipv4.tw_reuse对于连接进来的TCP连接来说,并没有任何卵用。
在客户端(尤其是服务器上,某服务以客户端形式运行时,比如上面提到的nginx反代,连接着redis、mysql的FPM等等)上启用net.ipv4.tcp_tw_reuse,还算稍微安全的解决TIME-WAIT的方案。再开启net.ipv4.tcp_tw_recycle的话,对客户端(或以客户端形式)的回收,也没有什么卵用,反而会发生很多诡异的事情(尤其是FPM这种服务器上,相对nginx是服务端,相对redis是客户端)。
最后引用一下W. Richard Stevens在《UNIX网络编程》的一句话:
The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it.
译者:存在即合理,勇敢面对,而不是逃避。
在Linux 4.1内核中,net.ipv4.tcp_tw_recycle参数将被移除。
六、参考
记一次TIME_WAIT网络故障
https://blog.huoding.com/2012/01/19/142
Coping with the TCP TIME-WAIT state on busy Linux servers
https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux
不要在Linux上启用net.ipv4.tcp_tw_recycle参数
https://www.cnxct.com/coping-with-the-tcp-time_wait-state-on-busy-linux-servers-in-chinese-and-dont-enable-tcp_tw_recycle/