随笔 - 137  文章 - 1  trackbacks - 0
<2020年5月>
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

常用链接

留言簿

随笔分类

随笔档案

收藏夹

调试技巧

搜索

  •  

最新评论

阅读排行榜

评论排行榜

关于TCP连接的TIME-WAIT状态,它是为何而生,存在的意义是什么?

    让我们回忆一下,什么是TCP TIME-WAIT状态?如下图

    TCP连接关闭之前,首先发起关闭的一方会进入TIME_WAIT状态(也就是主动关闭连接的一方才会产生TIME_WAIT,另一方可以快速回收连接。可以用

ss -tan

来查看TCP 连接的当前状态(注:ss命令要比netstat命令速度更快,并且功能更详细,使用可参考:http://www.cnphp6.com/archives/66361

1. TIME-WAIT状态的作用

    对于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. 另外一个作用是,防止最后一个对FINACK丢失当最后一个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时间改为一个可以自定义配置的参数 ,但被拒绝了,其实,这对TCP规范,对TIME-WAIT来说,是利大于弊的。

2. TIME_WAIT状态的影响

    那么问题来了我们来看下,为什么这个状态能影响到一个处理大量连接的服务器,从下面三个方面来说:

新老连接(相同四元组)在TCP连接表中的slot复用避免

内核中,socket结构体的内存占用

额外的CPU开销

注:TIME_WAIT状态的连接数可以使用: ss -tan state time-wait|wc -l ,查看

2.1. 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出现在服务端,问题会非常复杂,因为这里并没有日志记录,也没有计数器参考。不过,可以列出服务器上当前所有四元组连接的数量来确认

[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) 增加服务端端口,多监听一些端口,比如818283这些,web服务器前有负载均衡,对用户友好。

3) 增加客户端IP,尤其是作为负载均衡服务器时,使用更多IP去跟后端的web服务器通讯。

4) 增加服务端IP

5) 当然了,最后的办法是调整 net.ipv4.tcp_tw_reuse  net.ipv4.tcp_tw_recycle 。但千万别这么做,稍后再讲。

2.2. 内存

    保持大量的连接时,当多为每一连接多保留1分钟,就会多消耗一些服务器的内存。举个栗子,如果服务器每秒处理了1W个新的TCP连接,那么服务器一分钟将会1W/s*60s = 60WTIME_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连接哈希表的最大数目)的改动而将其覆盖。

    hashbucket中,每个TIME-WAIT状态socket对应一个tcp_timewait_sock结构体,其他状态的socket则对应tcp_sock结构体。

点击(此处)折叠或打开

  1. struct tcp_timewait_sock {
  2.     struct inet_timewait_sock tw_sk;
  3.     u32 tw_rcv_nxt;
  4.     u32 tw_snd_nxt;
  5.     u32 tw_rcv_wnd;
  6.     u32 tw_ts_offset;
  7.     u32 tw_ts_recent;
  8. long tw_ts_recent_stamp;
  9. };
  10. struct inet_timewait_sock {
  11.     struct sock_common __tw_common;
  12.     int tw_timeout;
  13.     volatile unsigned char tw_substate;
  14.     unsigned char tw_rcv_wscale;
  15.     __be16 tw_sport;
  16.     unsigned int tw_ipv6only : 1,
  17.                  tw_transparent : 1,
  18.                  tw_pad : 6,
  19.                  tw_tos : 8,
  20.                  tw_ipv6_offset : 16;
  21.     unsigned long tw_ttd;
  22.     struct inet_bind_bucket *tw_tb;
  23. struct hlist_node tw_death_node;
  24. };

(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 bytesinet_bind_socket结构体为48bytes所以,当服务器上有4W个连进来的连接进入TIME-WAIT状态时,才用了10MB不到的内存。如果作为客户端4W个连接到远程的连接进入TIME-WAIT状态时,才用了2.5MB的内存。再来看下slabtop的结果,这里测试数据是5WTIME-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状态连接的内存占用,简直可以无视。

2.3. CPU

那么TIME_WAIT状态对CPU的消耗影响如何呢?

TIME_WAIT状态的增加也只是多占用了一些端口,使这些端口短时间内得不到释放,但是hash的存储结构会使系统在需要新端口时很快找到一个空闲端口,所以对CPU的开销也不会明显增大。

3. 如何避免或者减小TIME_WAIT的影响

虽然通过以上分析,TIME_WAIT状态对系统的影响不大,但如果你还是执意想减小这些影响,可以有以下三个方法:

禁用socket延迟关闭

禁用net.ipv4.tcp_tw_reuse

禁用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.5RTO(超时重传时间),当然这个时间是随网络状态动态变化的,有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

4. 总结

1. tw_reusetw_recycle 必须客户端和服务端timestamps 开启时才管用(默认打开)

2. tw_reuse 只对客户端起作用,开启后客户端在1s内回收

3. tw_recycle 客户端和服务器同时起作用,开启后在 3.5*RTO 内回收,RTO 200ms~ 120s 具体时间视网络状况。

对于客户端

1) 作为客户端因为有端口65535问题,TIME_OUT过多直接影响处理能力,打开tw_reuse 即可解决,不建议同时打开tw_recycle,帮助不大

2) tw_reuse 帮助客户端1s完成连接回收,基本可实现单机6w/s请求,需要再高就增加IP数量吧。

3) 如果内网压测场景,且客户端不需要接收连接,同时tw_recycle 会有一点点好处。

4) 业务上也可以设计由服务端主动关闭连接

对于服务端

1) 打开tw_reuse无效

2) 线上环境 tw_recycle 最好不要打开

    服务器处于NAT 负载后,或者客户端处于NAT后(这是一定的事情,基本公司家庭网络都走NAT);公网服务打开就可能造成部分连接失败,内网的话到时可以视情况打开;像我所在公司对外服务都放在负载后面,负载会把timestamp 选项都给关闭所以就算打开也不起作用

3) 服务器TIME_WAIT 高怎么办

   不像客户端有端口限制,处理大量TIME_WAIT Linux已经优化很好了,每个处于TIME_WAIT 状态下连接内存消耗很少,而且也能通过tcp_max_tw_buckets = 262144 配置最大上限,现代机器一般也不缺这点内存。

posted on 2020-05-09 17:06 长戟十三千 阅读(635) 评论(0)  编辑 收藏 引用 所属分类: 编程技巧随笔

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理