TCP连接中TIME_WAIT状态的作用及优化
1. 为什么需要TIME_WAIT状态?为什么TIME_WAIT的时长是2*MSL?
原因1:防止连接关闭时四次挥手中的最后一次ACK丢失:
TCP需要保证每一包数据都可靠的到达对端,包括正常连接状态下的业务数据报文,以及用于连接管理的握手、挥手报文,这其中在四次挥手中的最后一次ACK报文比较特殊,TIME_WAIT状态就是为了应对最后一条ACK丢失的情况。
TCP保证可靠传输的前提是收发两端分别维护关于这条连接的状态信息(TCB控制块),当发生丢包时进行ARQ重传。如果连接释放了,就无法进行重传,也就无法保证发生丢包时的可靠传输。
对于最后一条ACK,如果没有TIME_WAIT状态,主动关闭一方(客户端)就会在收到对端(服务器)的FIN并回复ACK后 直接从FIN_WAIT_2 进入 CLOSED状态,并释放连接,销毁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) 报文最大生存时间在不同操作系统中的具体值:
Windows : 2 min
Linux(Ubuntu) : 60 s
Unix : 30 s
2. TIME_WAIT对连接并发数的影响(TIME_WAIT过多的危害):
在Linux系统中,MSL = 60 s, 2 * MSL = 120 s,所以一条待关闭的TCP连接会在 TIME_WAIT 状态等待 120秒(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网页,1秒钟的HTTP短连接处理完业务数据,却需要 2分钟的TIME_WAIT等待时间,在这段时间内客户端上的这个端口是无法被其他连接使用的,如果新建连接则需要使用另外的端口号),Linux系统的最大端口为65535,除去系统使用的端口号,假设网络进程可使用的端口有 6W个,由于TIME_WAIT状态下在 2*MSL(120秒)内无法再被使用,这就限制了客户端的连接速率为 60000 / 120秒 = 500 次/秒, 这是一个非常低的并发率。
同时,大量的TIME_WAIT连接同样会消耗客户端的内存,所以客户端的最大并发数取决于 端口号与内存 二者中的最小值。
【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)
3. 优化TIME_WAIT的方法:
方法1:修改内核参数 tcp_tw_reuse:
net.ipv4.tcp_tw_reuse = 1;
net.ipv4.tcp_timestamp = 1;
注意: tcp_tw_reuse 内核参数只在调用 connect() 函数时起作用,所以只能用于客户端(主动连接的一端)。
tcp_tw_reuse 的作用是:在调用connect()函数时,内核会随机找一个处于TIME_WAIT状态 超过1秒 的连接给新连接复用。(超时时间由 tcp_timestamp设置,默认为 1秒)
这种方式可以缩短 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),而不是直接关闭。
应用程序调用close后进入睡眠,内核协议栈负责发送缓冲中残留的待发送数据,如果在 l_linger 超时时间内发送完毕,则走正常的四次挥手流程;如果超时未发送完,则发送RST强制关闭连接,并丢弃发送缓冲区中其余的数据(接收端收到RST后也会丢弃接收缓冲区中的数据),并且发送端close()函数返回 EWOULDBLOCK。
综上,如果只是单纯为了规避 TIME_WAIT 状态,使用 SO_LINGER并不是一个好主意,因为它会在调用close关闭连接时 使用RST强制关闭连接,这可能会导致 发送缓冲区、接收缓冲区 中还未处理完的数据被丢弃。
struct linger so_linger;
so_linger.l_onoff = 1; //0表示关闭,忽略l_linger的值;非0表示打开
so_linger.l_linger = 0; //设置等待时间,等于0则表示立即关闭
setsockopt(fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));
方法4:设置套接字选项 SO_REUSEADDR:
SO_REUSEADDR 选项用于通知内核:
如果端口忙,并且端口对应的TCP连接状态为TIME_WAIT,则可以重用端口;
如果端口忙,并且端口对应的TCP连接处于其他状态(非TIME_WAIT),则返回 “Address already in use” 的错误信息。
设置SO_REUSEADDR的风险是可能会导致新连接上收到旧连接的数据(复用了旧连接的端口,导致新旧连接的四元组完全一致,内核协议栈无法区分这两个连接)。
SO_REUSEADDR 选项并没像 tcp_tw_reuse 那样同时提供一个 tcp_timestamp 参数可以设置 TIME_WAIT的等待时长。
综上,对TIME_WAIT状态的优化思路是尽量缩小等待时长,而不是暴力的直接关闭(可能会引起新连接收到旧连接数据的风险),也不要直接发送RST复位连接(可能会引起发送、接收缓冲区中的数据丢失),所以使用修改内核参数 tcp_tw_reuse 参数是最保险的方式,通过根据实际网络情况和应用场景适当的调节 tcp_timestamp 的值,可以达到缩小 TIME_WAIT 等待时长,进而减少系统中同一时刻处于 TIME_WAIT 状态的连接数量的目的。
注:RTS报文:
一、到不存在的端口的连接请求
当向一个未使用的目的端口发送TCP连接请求时,目的服务器回应TCP复位报文。
PS: UDP返回ICMP端口不可达。
二、异常终止一个连接
有序释放: 通过发送FIN来终止连接
异常释放: 通过发送RST来终止连接 (优点: 如果有任何待发数据,就可以直接丢弃了)
三、检测半打开连接
半打开连接: 一方已经关闭或者异常终止连接,但是另一方还不知道。
半打开连接下,正常的一方向关闭的一方再次发送数据,接收方将回应RST报文。