【翻译】如何应对繁忙linux服务器上的TCP TIME-WAIT状态

译者注:一篇能够真正带你搞懂TIME-WAIT问题的好文章!

原文:https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux

                     《如何应对繁忙linux服务器上的TCP TIME-WAIT状态》

给嫌太长看不下去的人:简单来说就是不要开启 net.ipv4.tcp_tw_recycle—这玩意甚至已经从linux4.12开始已经废弃了。大多数情况下,TIME-WAIT状态的socket都是无害的,但如果你不幸遇到了少数情况,则可以直接去看看文章最后的总结,寻找建议的解决办法。

 

前言

linux的内核文档对理解net.ipv4.tcp_tw_recycle 和 net.ipv4.tcp_tw_reuse 没什么帮助,写的很含糊,这种权威文档上的缺失导致一些个调优指南上建议开启这俩参数为1,来减少TIME-WAIT连接的数量。但是,正如tcp(7)手册上说的,net.ipv4.tcp_tw_recycle这个选项对于面向公众的服务器来说是一个相当大的问题,因为它无法处理来自同一NAT设备后面的两台不同计算机的连接(译者注:指的2个客户端通过NAT设备访问后面的1个服务器这样一种情况),这是一个很难检测到并等待解决的问题:  

Enable fast recycling of TIME-WAIT sockets. Enabling this option is not recommended since this causes problems when working with NAT (Network Address Translation). 开启TIME-WAIT socket的快速回收,不建议开启这个选项,因为它会导致NAT网络环境下的大问题。

我将会提供更详细的关于如何正确处理TIME-WAIT状态。同时请记住我们是在讨论linux的tcp协议栈,这与Netfilter connection tracking完全没有关系,后者可能会有其他的调节方式。

 

目录

       About the TIME-WAIT state
    Purpose
    Problems
      Connection table slot
      Memory
      CPU
  Other solutions
    Socket lingering
    net.ipv4.tcp_tw_reuse
    net.ipv4.tcp_tw_recycle
  Summary

 

关于TIME-WAIT状态

TIME-WAIT状态是什么,可以看下面的tcp状态图

只有主动关闭连接的一方才能到达TIME-WAIT状态,另一端则会允许快速的关闭掉连接。可以通过ss -tan命令看一下当前的连接状态:

ss -tan | head -5
LISTEN     0  511             *:80              *:*
SYN-RECV   0  0     192.0.2.145:80    203.0.113.5:35449
SYN-RECV   0  0     192.0.2.145:80   203.0.113.27:53599
ESTAB      0  0     192.0.2.145:80   203.0.113.27:33605
TIME-WAIT  0  0     192.0.2.145:80   203.0.113.47:50685

 

 

这个状态存在的作用

TIME-WAIT状态主要有两个作用:

1、最为熟知的是防止一个连接中延迟的网络包,被稍后的具有相同四元组的另一个连接接收。sequence也需要在一个特定的范围被接收。这缩小了一点问题,但它仍然存在,特别是在大接收窗口的快速连接上。RFC 1337 详细解释了如果缺少TIME-WAIT状态会发生什么。下面是一个例子,关于保留TIME-WAIT状态持续时长会避免什么问题:

2、另一个作用是确保对端能够最终关闭连接。当最后一个ACK包丢失,对端将会停在LAST-ACK状态,如果没有TIME-WAIT状态,那么本地可能会马上重新打开同一个四元组连接(译者注:本地端这时候以为4次挥手的关闭过程已经成功结束了、不知道ACK包对端没收到),但是对端这个时候认为原来的连接还未关闭成功,仍然是有效的。所以,当对端收到新的SYN包的时候,会回复一个RST、意思是没有收到期望的包。新的连接会引发error并被丢弃:

RFC 793 建议TIME-WAIT状态持续2MSL时间,而在linux里,这个时长是不能修改的,写死在include/net/tcp.h 里,为1分钟。

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                                  * state, about 60 seconds     */

有人提议把它变成一个可调的值,但被拒绝了,理由是TIME-WAIT状态是件好事。

TIME-WAIT可能带来的问题

在处理大量连接的服务器上,这个状态可能会带来的问题主要有3点:

1、占用connection table上的slot,在状态不结束之前、相同四元组的连接是无法建立的。(比如8080-12345端口表示的连接tw了,那么相同client是没法再通过12345端口建立到服务器8080的连接了。所以服务端的tw也是有影响的,并不是服务端tw完全可以无视。而客户端tw比较容易理解,直接占用本地可用端口、而本地端口可用range最大也就65535)

2、这些socket数据结构在内核里边会占用一些内存

3、这些连接会带来额外的CPU使用率

ss -tan state time-wait | wc -l 命令的结果本身不是问题!

 

connection table slot的限制

TIME-WAIT状态的连接在connection table里边会保存1分钟。因为相同四元组(源IP,源port,目标IP,目标port)的连接只能有一个,所以在服务端目标IP,目标port固定,且服务端在7层负载均衡后边的话、那么源IP也是固定的,那么就剩下源port了。在linux上,客户端端口默认大约可用范围大约是3万个,通过调整net.ipv4.ip_local_port_range可以修改这个范围。这意味着服务器和负载均衡器之间每分钟大约仅仅可以建立3万个左右的连接,也就是每秒大约500个。

如果tw socket是在客户端,问题比较容易发现也比较容易理解,直观很容易理解就是本地端口不够用了,connect() 调用会返回EADDRNOTAVAIL,发起调用的应用大概率也会报错日志。但是如果tw是在服务端,问题就负载一些了,因为首先服务端不会有报错日志和计数统计这样的错误。如果怀疑出现了服务端tw问题,首先应该列出当前使用的四元组的数量:

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

至于解决办法,就是增加更多的四元组。可以用以下几种办法(难度由简到繁):

把net.ipv4.ip_local_port_range改大

让服务端开放更多的端口来提供服务,比如(81, 82, 83, …)

在负载均衡器层提供更多的客户端IP、然后轮询使用

在服务端配置多个IP

最后一招是开net.ipv4.tcp_tw_reuse 和net.ipv4.tcp_tw_recycle,先别着急这样做,待会会介绍这些设置。

内存消耗

处理大量连接的时候,每个socket由于tw状态会多开1分钟,会消耗服务器一些额外的内存。例如,如果每秒钟要处理1万个新连接,就要处理大约60万的TIME-WAIT状态的socket,那么这些连接会消耗多少内存呢?其实没那么多。

首先,站住应用的角度,tw socket不消耗任何内存:因为socket被应用关闭了。应用级别的内存应该都已经可以回收了。而站在内核的角度来看,一个tw socket会存在三个数据结构(具有3种不同的作用):

1、内核里有一个hash table用来存tcp连接,这个hash table的每个bucket都包含两个strut list,一个用来放tw连接( struct tcp_timewait_sock),另一个用来放常规的活动连接( struct tcp_sock)。这个hash table的大小取决于系统内存,并且会在系统boot的时候打印出来:

dmesg | grep "TCP established hash table"
[ 0.169348] TCP established hash table entries: 65536 (order: 8, 1048576 bytes)

通过调整thash_entries内核参数可以修改hash table的entry数。

struct tcp_timewait_sock {
    struct inet_timewait_sock tw_sk;
    u32    tw_rcv_nxt;
    u32    tw_snd_nxt;
    u32    tw_rcv_wnd;
    u32    tw_ts_offset;
    u32    tw_ts_recent;
    long   tw_ts_recent_stamp;
};

struct inet_timewait_sock {
    struct sock_common  __tw_common;

    int                     tw_timeout;
    volatile unsigned char  tw_substate;
    unsigned char           tw_rcv_wscale;
    __be16 tw_sport;
    unsigned int tw_ipv6only     : 1,
                 tw_transparent  : 1,
                 tw_pad          : 6,
                 tw_tos          : 8,
                 tw_ipv6_offset  : 16;
    unsigned long            tw_ttd;
    struct inet_bind_bucket *tw_tb;
    struct hlist_node        tw_death_node;
};

 2、一个connection list集合,被称为death row,用来记录tw连接的失效。 按照还剩多长时间失效进行排序。

 It uses the same memory space as for the entries in the hash table of connections. This is the struct hlist_node tw_death_node member of struct inet_timewait_sock.

它使用跟上面说过的连接hash table的entry一样的内存空间:struct inet_timewait_sock里的struct hlist_node tw_death_node 

3、一个放bound端口的hash table,存放本地端口极其相关参数,用来检查是否可以用这些端口来监听或者用来作为动态出站端口。这个hash table的大小跟存放连接的hash table是一样的:

dmesg | grep "TCP bind hash table"
[ 0.169962] TCP bind hash table entries: 65536 (order: 8, 1048576 bytes)

里边的element是一个struct inet_bind_socket,每个本地端口对应一个element。一个web服务器的TIME-WAIT connection在本地绑定80端口,跟80上的TIME-WAIT连接共享相同的entry。另一方面,出站的本地随机端口则不共享entry。

我们只关注struct tcp_timewait_sock 和struct inet_bind_socket占用的内存空间,每个tw状态的连接都有一个struct tcp_timewait_sock不管是出站还是入站,而struct inet_bind_socket则只有出站连接有、入站连接是没有的。

一个struct tcp_timewait_sock只有168个字节大小,一个struct inet_bind_socket是48字节:

sudo apt-get install linux-image-$(uname -r)-dbg
[…]
$ gdb /usr/lib/debug/boot/vmlinux-$(uname -r)
(gdb) print sizeof(struct tcp_timewait_sock)
 $1 = 168
(gdb) print sizeof(struct tcp_sock)
 $2 = 1776
(gdb) print sizeof(struct inet_bind_bucket)
 $3 = 48

所以如果有4万个入站连接处于tw状态的话,仅仅占用10M内存不到。如果是4万出站tw连接,则需要2.5M的额外内存。如下是一个服务器,当有5万tw连接、其中4.5万是出站时候的情况:

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


可见,tw连接占用的内存真的是很少的,如果您的服务器每秒需要处理数千个新连接,则需要更多的内存才能有效地将数据推送到客户端。相比之下TIME-WAIT 连接的内存开销可以忽略不计了。

CPU开销

CPU用来查找本地空闲端口的开销相对内存来说有些大,cpu是用inet_csk_get_port() 这个调用来完成上述工作,包含对本地端口的lock和遍历、直到找到一个空闲的端口。

当有大量出站连接处于tw状态时(比如使用短链接去连mencached服务),对应的hash table里边会有大量的entry,不过这通常不是什么大问题:这些连接通常会共享相同的profile,inet_csk_get_port() 按顺序进行遍历会很快的找到空闲的port

 

其他解决方案

如果看了上面的章节仍然感觉有TIME-WAIT连接的问题,那么还有3个额外的手段:

禁用socket lingering

net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_recycle

 

Socket lingering

当close()被调用时,任何内核buffer里边的残留数据都会被在后台继续发送,并且socket最终会变为TIME-WAIT状态。这样应用程序可以立即继续工作,并且认为所有的数据最终会安全的送达。

但是,应用程序也可以选择禁用这种socket延迟关闭机制,有两种方式(flavor):

1、第一种,缓冲区里存留的数据马上丢弃,并且不是通过正常那样通过4次挥手关闭连接,而是直接通过发送RST、并马上关闭本地连接来结束这一过程(对端会收到RST而触发一个error)。这样就根本没有TIME-WAIT状态了。

2、第二种方式,任何数据仍然会在socket发送缓存区,进程在调用close()时会sleep直到所有数据都被发送并被对端确认收到、或者达到了延迟关闭时间。

一个进程可能不会sleep如果socket io是non-blocking的,这种情况,类似的过程将会在后台发生。它会允许剩余数据在超时时间允许的时间内发送,但是如果数据最后发送成功,正常的关闭流程将会执行,这样最后仍然会得到TIME-WAIT状态。另一方面,如果在超时时间内剩余数据没有成功发送,那么将会使用RST的方式来关闭连接,剩余数据会被丢弃。

以上两种情况,禁用socket延迟关闭都不是一种万能的解决方案,有些应用程序比如HAProxy或Nginx可能会在能够从协议层面确保安全的情况下禁用socket lingering。千万不要无脑的去禁用它。

net.ipv4.tcp_tw_reuse

TIME-WAIT避免延迟了的segment被不相关的连接接收。但是,某些特殊情况下,是可以认为新连接的包不会与旧连接的包错乱在一起的。

RFC 1323提供了一组TCP扩展来提高高带宽路径上的性能。除此之外,它还定义了一个新的TCP选项,其中包含两个4字节的时间戳字段。第一个是发送的TCP的时间戳时钟的当前值,而第二个是从远程主机接收的最近的时间戳。

启用net.ipv4.tcp_tw_reuse,linux会复用一个存在的tw连接来当作新的出站连接——如果新的时间戳大于前一次连接的最后时间戳:之后,一个tw状态的出站连接将会在1秒之后被复用。

这么做安全吗?TIME-WAIT状态的第一个目的是避免延迟包发送给不相干的连接,依靠时间戳的使用,这样的包会带着已经过期的时间戳,因而会被丢弃。

第二个目的是确保对端正确关闭,而不是因为没有收到最后一个ACK而一直处于LAST-ACT状态。如果没有收到最后一个ACK,对端会重新发送FIN包直到:

1、对端主动放弃,然后关闭连接

2、对端收到了ACK,然后正常关闭连接

3、对端收到了RST,然后关闭连接

如果对端发来的FIN包及时的被收到,本地端socket将会仍然保持TIME-WAIT状态,并回复最后一个ACK包。

Once a new connection replaces the TIME-WAIT entry, the SYN segment of the new connection is ignored (thanks to the timestamps) and won’t be answered by a RST but only by a retransmission of the FIN segment. The FIN segment will then be answered with a RST (because the local connection is in the SYN-SENT state) which will allow the transition out of the LAST-ACK state. The initial SYN segment will eventually be resent (after one second) because there was no answer and the connection will be established without apparent error, except a slight delay:

一旦一个新连接替换了TIME-WAIT连接,新连接的SYN包就会被忽略(由于时间戳),并且不会由RST应答,而只是通过FIN段的重新传输来应答。FIN段将用RST应答(因为本地连接处于SYN-SENT状态),这将允许对端从LAST-ACK状态转换出来。初始SYN段最终将重新发送(一秒钟后),因为没有应答,连接将在没有明显错误的情况下建立,但有一点延迟:(过程比较复杂,参考下图)

If the remote end stays in LAST-ACK state because the last ACK was lost, the remote connection will be reset when the local end transition to the SYN-SENT state.

应该注意的是,当一个连接被重用时,TWRecycled计数器会增加(不管它的名称如何)。

 

net.ipv4.tcp_tw_recycle

This mechanism also relies on the timestamp option but affects both incoming and outgoing connections. This is handy when the server usually closes the connection first.

这个机制也是要开启timestamp选项才行,并且能够同时作用于入站和出站的连接(译者注:tw_reuse仅作用于出站连接、也就是客户端)。当服务端先关闭连接的时候这比较有用。(When the server closes the connection first, it gets the TIME-WAIT state while the client will consider the corresponding quadruplet free and hence may reuse it for a new connection 当服务端先关连接,它进入tw状态,但是客户端会认为相关的四元组已经空闲,因此可能会复用这个四元组来建立一个新连接)

开启tw_recycle之后,TIME-WAIT状态会被安排更快的结束,它将在根据RTT及其方差计算的重新传输超时(RTO)间隔之后被删除。可以使用ss命令为活动连接查看适当的值:

ss --info  sport = :2112 dport = :4057
State      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

为了在减少TIME-WAIT状态过期时间的同时保持所提供的相同功能,当一个连接进入TIME-WAIT状态后,最后的时间戳会在一个专用的结构体中记录,这个结构体包含上一个对端的各种信息。然后,Linux将丢弃任何远程主机发来的包、只要包里的时间戳不大于前面记录的那个最后时间戳,除非TIME-WAIT状态过期:

To keep the same guarantees the TIME-WAIT state was providing, while reducing the expiration timer, when a connection enters the TIME-WAIT state, the latest timestamp is remembered in a dedicated structure containing various metrics for previous known destinations. Then, Linux will drop any segment from the remote host whose timestamp is not strictly bigger than the latest recorded timestamp, unless the TIME-WAIT state would have expired:

if (tmp_opt.saw_tstamp &&
    tcp_death_row.sysctl_tw_recycle &&
    (dst = inet_csk_route_req(sk, &fl4, req, want_cookie)) != NULL &&
    fl4.daddr == saddr &&
    (peer = rt_get_peer((struct rtable *)dst, fl4.daddr)) != NULL) {
        inet_peer_refcheck(peer);
        if ((u32)get_seconds() - peer->tcp_ts_stamp < TCP_PAWS_MSL &&
            (s32)(peer->tcp_ts - req->ts_recent) >
                                        TCP_PAWS_WINDOW) {
                NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
                goto drop_and_release;
        }
}

当远程主机是NAT设备时,时间戳条件将禁止所有主机(NAT设备后面的主机除外)在一分钟内连接,因为它们不共享相同的时间戳时钟。毫无疑问,禁用此选项要好得多,因为它会导致难以检测和诊断的问题。

When the remote host is a NAT device, the condition on timestamps will forbid all the hosts except one behind the NAT device to connect during one minute because they do not share the same timestamp clock. In doubt, this is far better to disable this option since it leads to difficult to detect and difficult to diagnose problems.

(从Linux 4.10(commit 95a22caee396)开始,Linux将随机分配每个连接的时间戳偏移量,使这个选项完全破坏,不管是否使用NAT。它已从Linux 4.12中完全删除。)

另外, 关于LAST-ACK状态的处理的问题,与net.ipv4.tcp_tw_reuse类似,不再赘述。

总结

1、最通用的解决办法是增加可用的四元组的数量,比如开放更多的服务端口,这将使得你不必担心TIME-WAIT状态耗尽可用的连接。

2、在服务端,永远不要启用net.ipv4.tcp_tw_recycle ,因为它具有副作用。而net.ipv4.tcp_tw_reuse则对入站连接没有帮助,所以服务端也不要开启它。

      在客户端,开启net.ipv4.tcp_tw_reuse一般不会有太大问题,可以帮助提高并发连接数。与此同时开启net.ipv4.tcp_tw_recycle则没啥用处。

3、此外,在设计协议的时候,不要让客户端去主动关闭连接,客户端不必处理TIME-WAIT状态,而是将这个工作推给更适合的服务端去处理。

 

最后引用W. Richard Stevens大神的一句话作为结尾:

“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.”

                                      -------------------------------- 《Unix Network Programming》  W. Richard Stevens 

 

后来发现的另一个翻译:http://www.softyun.net/article/27512.html   有的地方翻译的比我好 :)

posted on 2021-04-01 17:03  肥兔子爱豆畜子  阅读(329)  评论(0编辑  收藏  举报

导航