TCP连接中TIME_WAIT状态及作用及优化 - 图1

linux服务器开发相关视频解析:

支撑互联网的基石tcpip,5个方面全面解析
徒手实现网络协议栈,请准备好环境,一起来写代码

1.为什么需要TIME_WAIT状态?为什么TIME_WAIT的时长是2*MSL?

1. 防止连接关闭时,四次挥手的最后一次ACK丢失

TCP需要保证每一包数据都可靠的到达对端,包括正常连接状态下的业务数据报文,以及用于连接管理的握手、挥手报文,这其中在四次挥手中的最后一次ACK报文比较特殊,TIME_WAIT状态就是为应对最后一条ACK丢失的情况

TCP保证可靠传输的前提是收发两端分别维护关于这条连接的状态信息,当发生丢包时进行ARQ重传。如果连接释放了,就无法进行重传,也就无法保证发生丢包时的可靠传输。

对于最后一条ACK,如果没有TIME_WAIT状态,主动关闭一方(客户端)就会在收到对端(服务端)的FIN并回复ACK后 直接从FIN_WAIT_2 进入CLOSER 状态,并释放连接,销毁TCB实例。此时如果一条ACK丢失,那么服务器重传的FIN将无人处理,最后导致服务器长时间处于LAST_ACK 状态而无法正常关闭(服务器只能等到达到FIN的最大重传次数后关闭)

将TIME_WAIT的时长设置为 2MSL,是因为报文在链路中的最大生存时间为MSL(Maximum Segment Lifetime),超过这个时长后报文就会被丢弃。TIME_WAIT的时长则是:最后一次ACK传输到服务器的时间+服务器重传FIN的时间,即为2MSL。
**

2.防止新连接收到旧连接的TCP报文

TCP使用四元组区分一个连接(源端口、目的端口、源IP、目的IP),如果新、旧连接的IP与端口完全一致,则内核协议栈无法区分这两天连接。

2*MSL 的时间足以保证两个方向上的数据都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定是新连接上产生的。

MSL(Maximum Segment Lifetime)报文最大生存时间在不同操作系统中的具体值:

  1. Windows : 2 min
  2. Linux(Ubuntu) : 60 s
  3. Unix : 30 s

2.TIME_WAIT对连接并发数的影响(TIME_WAIT过多的危害)

在Linux系统中,MSL = 60s,2*MSL = 120s,所以一条待关闭的TCP 连接会在TIME_WAIT状态等待120s(2分钟)

当连接处于TIME_WAIT状态时仍会占用系统资源(fd、端口、内存),当系统的并发连接数很大时,过多的TIME_WAIT状态连接会对系统的并发量造成影响。

1.对服务器的影响

由于服务器一般只需要监听一个固定的端口,所以服务器所能支持的最大并发数的上限取决于系统套接字描述符 fd 的大小,以及服务器内存大小

fd:

Linux中的一个进程,所能打开的fd的最大数量默认为1024个,可通过“ulimit -n(+指定数量)” 进行修改

Linux系统所能支持的fd 最大值在/proc/sys/fs/fd-max 文件中可以查看,系‘统当前的fd 使用情况可以通过 /proc/sys/fs/fd-nr 查看。(本机上的fd-max 的值为:1221842,即100W 级。实际上fd 的最大值同样也取决于内存的大小)

内存

假设每一个TCP连接需要开辟“4K的接收缓冲区 + 4K的发送缓冲区= 8K”,1W的并发连接需要80M 内存,10W并发需要 800M,100W 并发需要 8G 内存。

综上,服务器的并发数主要受限于系统内存的大小,当TIME_WAIT状态的连接过多时,会导致消耗的内存增加,这一点可以通过扩展服务器的内存来解决。

2.对客户端的影响

客户端的并发数主要受限于端口数量
一种典型的场景是:高并发短连接(“短连接”表示“业务处理+传输数据”的时间远远小于TIME_WAIT超时的时间)。

在这种场景下,客户端可能会消耗大量的端口(例如取一个Web网页,1s 的HTTP短连接处理完业务数据,却需要2分钟的TIME_WAIT等待时间,在这段时间内客户端上的这个端口是无法被其他连接使用的,如果新建连接则需要使用另外的端口号),Linux系统的最大端口为6553,除去系统使用的端口号,假设网络进程可使用的端口有6W个,由于TIME_WAIT状态下在2*MSL(120s)内无法再被使用,这就限制了客户端的连接速率为 60000/120s = 500次/s,这是一个非常低的并发率

同时,大量的TIME_WAIT连接同样会消耗客户端的内存,所以客户端的最大并发数取决于端口号与内存二者中的最小值。

3.优化TIME_WAIT的方法

1.修改内核参数tcp_tw_reuse

  1. net.ipv4.tcp_tw_reuse = 1;
  2. net.ipv4.tcp_timestamp = 1;

注意:tcp_tw_reuse内核参数只在调用connect() 函数时起作用,所以只能用于客户端(主动连接一方)

tcp_tw_reuse的作用是:在调用connect()函数时,内核会随机找一个处于TIME_WAIT状态超过1s 的连接给新连接复用(超时时间由tcp_timestamp设置,默认为1s)

这种方式可以缩短TIME_WAIT的等待时间

2.修改内核参数tcp_max_tw_buckets

net.ipv4.tcp_max_tw_buckets 参数的默认值为18000,当系统中处于TIME_WAIT 状态的连接数超过阈值,系统会将后面的TIME_WAIT连接重置。

由于这种方法会重置连接,因此需要慎用。

3.设置套接字选项SO_LINGER

SO_LINGER选项用于设置调用close()关闭TCP连接时的行为,注意SO_LINGER选项会使用RST 复位报文段取代 FIN-ACK 四次挥手的过程,设置了SO_LINGER选项的一方在调用close() 时会一直发送一个RST,接收端收到复位连接,不会回复任何响应。

这样做的弊端是导致TCP缓冲区中的数据被丢弃。

正常情况下,调用close 后的缺省行为是:如果有待发送的数据残留在发送缓冲区中,内核协议栈将继续将这些数据发送给接收端后才关闭连接,走正常的四次挥手流程

设置SO_LINGER后,立即关闭连接,通过RST分组,发送缓冲区如果有未发送的数据,将会被丢弃,主动关闭的一方跳过TIME_WAIT状态,直接进入CLOSED(也跳过FIN_WAIT_1和FIN_WAIT_2)

SO_LINGER的另一种更温和的实现方式是设置一个超时时间(so_linger.l_linger),而不是直接关闭。

综上,如果只是单纯为了规避TIME_WAIT状态,使用SO_LINGER并不是一个好主意,因为他会在调用close关闭连接时,使用RST强制关闭连接,这可能会导致发送缓冲区、接收缓冲区中还未处理完的数据被丢弃

总结

对TIME_WAIT状态的优化思路是尽量缩小等待时长,而不是暴力的直接关闭。
所以修改内核参数tcp_tw_resue参数是最保险的方式,通过根据实际网络情况和应用场景适当的调解tcp_timestamp的值,可以达到缩小TIME_WAIT等待时长。