将晦涩难懂的技术讲的通俗易懂
分类: LINUX
2016-07-31 19:02:50
如何避免TCP的TIME_WAIT状态
关于TCP连接的TIME-WAIT状态,它是为何而生,存在的意义是什么?
让我们回忆一下,什么是TCP TIME-WAIT状态?如下图
当TCP连接关闭之前,首先发起关闭的一方会进入TIME_WAIT状态(也就是主动关闭连接的一方才会产生TIME_WAIT),另一方可以快速回收连接。可以用
ss -tan
来查看TCP 连接的当前状态(注:ss命令要比netstat命令速度更快,并且功能更详细,使用可参考:
对于TIME-WAIT状态来说,有两个作用。
1. 人尽皆知的是,防止上一个TCP连接的延迟的数据包(发起关闭,但关闭没完成),被接收后,影响到新的TCP连接。(唯一连接确认方式为四元组:源IP地址、目的IP地址、源端口、目的端口),包的序列号也有一定作用,会减少问题发生的几率,但无法完全避免。尤其是较大接收windows size的快速(回收)连接。RFC1137解释了当TIME-WAIT状态不足「注3」时将会发生什么。如果TIME-WAIT状态连接没有被快速回收,会避免什么问题呢?请看下面的例子:
如图,序号为3的报文,由于某种原因在网络中发生了延时(并没有丢失),但是发送端因为超生又进行了3的重传,缩短TIME_WAIT的时间后,延迟的SEQ3 会被新建立的TCP连接接收。而如果采用正常的TIME_WAIT机制,可以保证SEQ3在网络中消失(为什么呢?因为TIME_WAIT的时间是2MSL,如果数据包没有丢失则可以充分让一个数据包在这个时间到达)。
2. 另外一个作用是,防止最后一个对FIN的ACK丢失,当最后一个ACK丢失时,远程连接进入LAST-ACK状态,如果没有TIME-WAIT状态,当远程仍认为这个连接是有效的,则会继续与其通讯,导致这个连接会被重新打开。当远程收到一个SYN 时,会回复一个RST包,因为这SEQ不对,那么新的连接将无法建立成功,报错终止。
如果远程因为最后一个ACK包丢失,导致停留在LAST-ACK状态,将影响新建立具有相同四元组的TCP连接。
RFC 793中强调TIME-WAIT状态必须是两倍的MSL时间(max segment lifetime),在linux上,这个限制时间无法调整,写死为1分钟了,定义在include/net/tcp.h
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT * state, about 60 seconds */
#define TCP_FIN_TIMEOUT TCP_TIMEWAIT_LEN
/* BSD style FIN_WAIT2 deadlock breaker.
* It used to be 3min, new value is 60sec,
* to combine FIN-WAIT-2 timeout with
* TIME-WAIT timer.
*/
曾有人提议将 ,但被拒绝了,其实,这对TCP规范,对TIME-WAIT来说,是利大于弊的。
那么问题来了,我们来看下,为什么这个状态能影响到一个处理大量连接的服务器,从下面三个方面来说:
l 新老连接(相同四元组)在TCP连接表中的slot复用避免;
l 内核中,socket结构体的内存占用;
l 额外的CPU开销;
注:TIME_WAIT状态的连接数可以使用: ss -tan state time-wait|wc -l ,查看
处于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出现在服务端,问题会非常复杂,因为这里并没有日志记录,也没有计数器参考。不过,可以列出服务器上当前所有四元组连接的数量来确认。
[root@localhost ~]#$ ss -tan 'sport = :80' | awk '{print $(NF)" "$(NF-1)}' | sed 's/:[^ ]*//g' | sort | uniq -c
696 10.24.2.30 10.33.1.64
1881 10.24.2.30 10.33.1.65
5314 10.24.2.30 10.33.1.66
5293 10.24.2.30 10.33.1.67
3387 10.24.2.30 10.33.1.68
2663 10.24.2.30 10.33.1.69
1129 10.24.2.30 10.33.1.70
10536 10.24.2.30 10.33.1.73
解决办法是,增加四元组的范围,这有很多方法去实现。(以下建议,可行性越来越小)
1) 修改 net.ipv4.ip_local_port_range 参数,增加客户端端口可用范围。
2) 增加服务端端口,多监听一些端口,比如81、82、83这些,web服务器前有负载均衡,对用户友好。
3) 增加客户端IP,尤其是作为负载均衡服务器时,使用更多IP去跟后端的web服务器通讯。
4) 增加服务端IP。
5) 当然了,最后的办法是调整 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_tw_recycle 。但千万别这么做,稍后再讲。
保持大量的连接时,当多为每一连接多保留1分钟,就会多消耗一些服务器的内存。举个栗子,如果服务器每秒处理了1W个新的TCP连接,那么服务器一分钟将会有1W/s*60s = 60W个TIME_WAIT状态的TCP连接,那这将会占用多大的内存么?别担心,少年,没那么多。
首先,从应用的角度来看,一个TIME_WAIT状态的socket不会消耗任何内存:socket已经关了。在内核中,TIME-WAIT状态的socket,对于三种不同的作用,有三个不同的结构。
(1) “TCP established hash table”的连接存储哈希表(包括其他非established状态的连接),当有新的数据包发来时,是用来定位查找存活状态的连接的。
该哈希表的bucket包含了TIME_WAIT状态的socket以及正常活跃的socket。该哈希表的大小,取决于操作系统内存大小。在系统引导时,会打印出来,dmesg日志中可以看到。
dmesg | grep "TCP established hash table"
[ 0.169348] TCP established hash table entries: 65536 (order: 8, 1048576 bytes)
这个数值,有可能被kernel启动参数thash_entries(设置TCP连接哈希表的最大数目)的改动而将其覆盖。
在该hash的bucket中,每个TIME-WAIT状态的socket,对应一个tcp_timewait_sock结构体,其他状态的socket则对应tcp_sock结构体。
点击(此处)折叠或打开
(2) 有一组叫做“death row”的链表,是用来终止TIME_WAIT状态的连接(socket)的,链表上的连接根据TIME_WAIT的剩余时间按照由小到大排序,链表中的元素则直接复用hash表中的对应元素(所以没有更多的消耗内存),即结构体inet_timewait_sock中的hlist_node tw_death_node成员,如上代码的倒数第二行。
(3) 另外一个相关结构称为“hash table of bound ports”,即存放调用后bind函数的port即其相关参数。这个hash表的主要作用就是当需要动态绑定端口时,提供一个可用的port。这个hash所用的内存也可从系统的启动日志中查到:
$ dmesg | grep "TCP bind hash table"[ 0.169962] TCP bind hash table entries: 65536 (order: 8, 1048576 bytes)
这个hash表的每个元素都是inet_bind_socket结构体。每个调用过bind的端口都会有一个元素。对于web服务器来说,它绑定的是80端口,其TIME-WAIT连接都是共享同一个entry的。对于连接远程服务器的客户端来说,他们的端口都是调用connect时随机分配的,并不在hash表中占用元素(没有调用过bind)。所以,和TIME_WAIT状态有关的结构只有结构体tcp_timewait_sock和结构体inet_bind_socket。每一个TIME_WAIT状态的连接都要消耗一个tcp_timewait_sock结构,而只有服务端的TIME_WAIT状态采用消耗一个inet_bind_socket结构。
tcp_timewait_sock结构体的大小只有168 bytes,inet_bind_socket结构体为48bytes。所以,当服务器上有4W个连进来的连接进入TIME-WAIT状态时,才用了10MB不到的内存。而如果作为客户端有4W个连接到远程的连接进入TIME-WAIT状态时,才用了2.5MB的内存。再来看下slabtop的结果,这里测试数据是5W个TIME-WAIT状态的连接结果,其中4.5W是连接到远程的连接:
$ sudo slabtop -o | grep -E '(^ OBJS|tw_sock_TCP|tcp_bind_bucket)'
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
50955 49725 97% 0.25K 3397 15 13588K tw_sock_TCP
44840 36556 81% 0.06K 760 59 3040K tcp_bind_bucket
命令执行结果原样输出,一个字符都没动。TIME-WAIT状态的连接占用内存非常的小。当然如果你的服务器上要处理每秒成千上万的新建TCP连接,你可能需要多一点的内存才能 正确无误的跟客户端做数据通信。但一般情况下TIME-WAIT状态连接的内存占用,简直可以无视。
那么TIME_WAIT状态对CPU的消耗影响如何呢?
TIME_WAIT状态的增加也只是多占用了一些端口,使这些端口短时间内得不到释放,但是hash的存储结构会使系统在需要新端口时很快找到一个空闲端口,所以对CPU的开销也不会明显增大。
虽然通过以上分析,TIME_WAIT状态对系统的影响不大,但如果你还是执意想减小这些影响,可以有以下三个方法:
l 禁用socket延迟关闭;
l 禁用net.ipv4.tcp_tw_reuse;
l 禁用net.ipv4.tcp_tw_recycle;
(1) 禁用socket延迟关闭
通常情况当close被调用时,SOCKET需要延迟关闭(lingering),在内核buffers中的残留数据将会发送到远程地址,同时,socket会切换到TIME-WAIT状态。如果禁用此选项,则调用close之后,底层也会关闭,不会将Buffers中残留数据未发送的数据继续发送。关于socket lingering 延迟关闭,会有以下两种行为(具体和设置参数有关):
① close函数后,并不会在发送FIN分节,取而代之的是发送RST分节,而在buffers任何残留数据都会被丢弃。在这种做法中,不会再有TIME-WAIT状态的SOCKET出现。
② 如果当调用close函数后,socket发送buffer中仍然有残留数据,此进程将会休眠,直到所有数据都发送完成并确认,或者所配置的linger计时器过期了。这个机制确保残留数据在配置的超时时间内都发送出去。 如果数据正常发送出去,FIN包也正常发送,那么将会转换为TIME-WAIT状态。其他异常情况下,则会发送RST。
(2) net.ipv4.tcp_tw_reuse
这个选项有什么作用呢?根据名称也能猜到,这个选项可以重用TIME_WAIT状态下的连接。默认情况下TIME_WAIT状态的时间是60s(linux中),而如果开启了这个选项,当系统需要发起新的outgoing connection时,如果新的时间戳比之前TIME_WAIT连接的时间戳大的话(大于1s),则可直接复用原有的TIME_WAIT连接。即:TIME-WAIT状态的连接,仅仅1秒后就可以被重用了。
这里要解释两个术语,一个是outgoing connection,即主动发起的对外连接,也就是作为客户端发起的连接,所以这个选项的开启仅对客户端起作用。另外一个是时间戳,RFC 1323 实现了TCP拓展规范,以保证网络繁忙状态下的高可用。它定义了一个新的TCP选项–两个四字节的timestamp fields时间戳字段,第一个是TCP发送方的当前时钟时间戳,而第二个是从远程主机接收到的最新时间戳。启用 net.ipv4.tcp_tw_reuse 后,如果新的时间戳,比以前存储的时间戳更大,那么linux将会从TIME-WAIT状态的存活连接中,选取一个,重新分配给新的 连接出去的TCP连接 。
那么开启这个选项对系统的安全性有何影响呢?我们从TIME_WAIT的两个作用来分析,首先TIME_WAIT可以有效的防止老的分节出现在新的连接中,而时间戳选项的激活可以很大程度的避免这一点。
另外一点,如果关闭连接的最后一个ACK丢失会怎么样,即如果新连接复用了之前的TIME_WAIT连接,但又收到了上个连接的FIN包会如何呢?如下图所示,会直接回复RST,并继续原有连接的建立。
(3) net.ipv4.tcp_tw_recycle
这个选项同样依赖时间戳选项,同样更加选项名称也能猜到,这个选项可以加快TIME_WAIT状态连接的回收时间(不开启默认是60s)。如果开启了这个选项,则TIME_WAIT的回收时间变为一个3.5个RTO(超时重传时间),当然这个时间是随网络状态动态变化的,有RTT计算而来。这个选项对所有的TIME_WAIT状态都有影响,包括incoming connections和 outgoing connections。所以开启这个选项,对客户和服务端都会产生影响。我们可以通过ss命令查看一个连接的RTO:
$ ss --info sport = :2112 dport = :4057State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 1831936 10.47.0.113:2112 10.65.1.42:4057 cubic wscale:7,7 rto:564 rtt:352.5/4 ato:40 cwnd:386 ssthresh:200 send 4.5Mbps rcv_space:5792
1. tw_reuse,tw_recycle 必须在客户端和服务端timestamps 开启时才管用(默认打开)
2. tw_reuse 只对客户端起作用,开启后客户端在1s内回收
3. tw_recycle 对客户端和服务器同时起作用,开启后在 3.5*RTO 内回收,RTO 200ms~ 120s 具体时间视网络状况。
l 对于客户端
1) 作为客户端因为有端口65535问题,TIME_OUT过多直接影响处理能力,打开tw_reuse 即可解决,不建议同时打开tw_recycle,帮助不大。
2) tw_reuse 帮助客户端1s完成连接回收,基本可实现单机6w/s请求,需要再高就增加IP数量吧。
3) 如果内网压测场景,且客户端不需要接收连接,同时tw_recycle 会有一点点好处。
4) 业务上也可以设计由服务端主动关闭连接
l 对于服务端
1) 打开tw_reuse无效
2) 线上环境 tw_recycle 最好不要打开
服务器处于NAT 负载后,或者客户端处于NAT后(这是一定的事情,基本公司家庭网络都走NAT);公网服务打开就可能造成部分连接失败,内网的话到时可以视情况打开;像我所在公司对外服务都放在负载后面,负载会把timestamp 选项都给关闭,所以就算打开也不起作用。
3) 服务器TIME_WAIT 高怎么办
不像客户端有端口限制,处理大量TIME_WAIT Linux已经优化很好了,每个处于TIME_WAIT 状态下连接内存消耗很少,而且也能通过tcp_max_tw_buckets = 262144 配置最大上限,现代机器一般也不缺这点内存。