大家好,我是小林。上周有位读者微信采访时,被问到既然net.ipv4.tcp_tw_reuse参数可以快速重用TIME_WAIT状态下的TCP连接,为什么linux默认是关闭的?好家伙,真是求详细啊!看到读者的这个问题,我也是一头雾水。想了想,终于知道怎么回答这个问题了。其实这个问题是变相的在问,“如果TIME_WAIT状态的持续时间太短或者没有,会有什么问题?”因为tcp_tw_reuse参数可以快速重用处于TIME_WAIT状态的TCP连接,相当于缩短了TIME_WAIT状态的持续时间。可能有同学会问,使用tcp_tw_reuse快速重用TIME_WAIT状态的TCP连接时,需要保证net.ipv4.tcp_timestamps参数开启(默认开启),tcp_timestamps参数可以避免老的延迟上报连接。文,这不就解决了没有TIME_WAIT状态的问题吗?它解决了部分问题,但没有完全解决。接下来,我就和大家聊一聊这个问题。什么是TIME_WAIT状态?TCP中四次挥手的过程,如下图所示:客户端打算关闭连接。这时,它会发送一条TCP头中FIN标志设置为1的报文,即FIN报文,然后客户端进入FIN_WAIT_1状态。服务器收到消息后,向客户端发送ACK响应消息,然后服务器进入CLOSED_WAIT状态。客户端收到服务器的ACK响应报文后,进入FIN_WAIT_2状态。等待服务器处理完数据后,也向客户端发送FIN报文,然后服务器进入LAST_ACK状态。客户端收到服务器的FIN报文后,返回ACK响应报文,然后进入TIME_WAIT状态。服务器收到ACK响应报文后,进入CLOSE状态,服务器完成连接的关闭。经过一段时间的2MSL,客户端自动进入CLOSE状态,客户端也完成了连接的关闭。如您所见,两个方向都需要FIN和ACK,因此通常称为四波。这里需要注意的一点是,TIME_WAIT状态只有在连接被主动关闭时才可用。可见TIME_WAIT是“主动关闭方”断开连接时的最后一个状态。这个状态会持续2MSL(MaximumSegmentLifetime),然后进入CLOSED状态。MSL是指TCP协议中任何数据包在网络上的最大生存时间,任何超过这个时间的数据都会被丢弃。虽然RFC793规定MSL为2分钟,但实际执行时会有所不同。例如,如果Linux默认为30秒,那么2MSL就是60秒。MSL由网络层IP包中的TTL保证。TTL是IP报头中的一个字段,用来设置一个数据报可以通过的路由器数量的上限。路由器每转发一次数据包,IP头中的TTL字段就会减1,当减到0时,数据包就会被丢弃。MSL和TTL的区别:MSL的单位是时间,而TTL是路由跳数。所以MSL应该大于等于TTL为0的时间,以保证数据包已经自然销毁。TTL的值一般为64,Linux设置MSL为30秒,也就是说Linux认为一个数据包通过64台路由器的时间不会超过30秒。如果超过,则认为该数据包在网络中消失了。为什么要设计TIME_WAIT状态?设计TIME_WAIT状态主要有两个原因:防止历史连接中的数据被同四元组的后续连接错误接收;确保“被动关闭连接”的一方能够正确关闭;原因一:防止历史连接中的数据被后面同四元组的连接误接收为了更好的理解这个原因,我们先来了解一下序列号(SEQ)和初始序列号(ISN)。序列号是TCP的一个头域,它标识了从TCP发送方到TCP接收方的数据流的一个字节。因为TCP是字节流的可靠协议,为了保证报文的顺序和可靠性,TCP在每个传输方向上的每个字节都分配了一个编号,以方便传输成功后确认,丢失后重传,保证有接收端不会乱码。序列号是一个32位的无符号数,所以达到4G后循环回0。初始序列号,当TCP建立连接时,客户端和服务端都会产生一个初始序列号,这是一个根据时钟产生的随机数,以保证每个连接都有不同的初始序列号。初始化序号可以看作是一个32位的计数器,每4微秒加1,循环一次需要4.55小时。我给每个人都拿了一个包裹。下图中的Seq为序号,红框内分别为客户端和服务端生成的初始序号。从上面我们知道,序号和初始化序号不是无限递增的,会回绕到初始值,也就是说不能根据序号来判断新旧数据。假设TIME-WAIT没有等待时间或者时间太短,延迟数据包到达后会发生什么?服务器在关闭连接之前发送的SEQ=301消息被网络延迟。然后,服务端重新开启一个新的相同4元组的连接,之前延迟的SEQ=301此时到达客户端,数据包的序号刚好在客户端的接收窗口内,所以客户端会正常收到这条数据报文,但是这条数据报文是上次连接遗留下来的,造成数据混乱等严重问题。为了防止历史连接中的数据被后续同一个四元组的连接误接收,TCP设计了TIME_WAIT状态,这个状态会持续2MSL,这个时间足够双向的数据包被丢弃,使得原来连接的数据包自然会在网络中消失,重新出现的数据包必须是新建立的连接产生的。原因二:确保“被动关闭连接”的一方能够正确关闭。如果客户端(主动关闭方)的最后一个ACK报文(第四次挥手)在网络中丢失,那么根据TCP可靠性原则,服务端(被动关闭方)会重发FIN报文。假设客户端没有TIME_WAIT状态,而是发送完最后一个ACK报文后直接进入CLOSED状态。如果ACK报文丢失,服务器会重传FIN报文,此时客户端已经进入CLOSED状态。在关闭状态下,收到服务器重传的FIN报文后,会返回一个RST报文。服务器收到此RST并将其解释为错误(对等方重置连接),这不是可靠协议的正常终止。为了防止这种情况发生,客户端必须等待足够长的时间以确保对端收到ACK。如果对端没有收到ACK,就会触发TCP重传机制,服务器会重发一个FIN。正好是两个MSL的时间。但是你可能会说,重发的ACK可能还是丢了,是的,不过TCP等了这么久,也算是仁慈了。什么是tcp_tw_reuse?在Linux操作系统下,TIME_WAIT状态的持续时间是60秒,也就是说客户端会在60秒内一直占用这个端口。要知道端口资源也是有限的。一般可以打开的端口为32768~61000,也可以通过以下参数设置指定范围:net.ipv4.ip_local_port_range那么如果连接方的TIME_WAIT状态是主动关闭的,则为full。所有端口资源,将导致无法创建新的连接。但是Linux操作系统提供了两个系统参数来快速回收处于TIME_WAIT状态的连接,这两个参数默认都是禁用的:net.ipv4.tcp_tw_reuse,如果启用这个选项,客户端(连接发起者)在调用connect()函数,内核会随机找一个TIME_WAIT状态超过1秒的连接重用为新的连接,所以这个选项只适用于连接发起者。net.ipv4.tcp_tw_recycle,如果启用该选项,则允许处于TIME_WAIT状态的连接被快速回收。这个参数在NAT网络下是不安全的!详见这篇文章:字节访谈:SYN报文什么时候会在某些情况下被丢弃?要使以上两个参数生效,有一个前提,就是开启TCP时间戳,即net.ipv4.tcp_timestamps=1(默认为1)。当tcp_timestamps参数打开时,TCP标头将使用时间戳选项。它有两个优点。一是方便准确计算RTT,二是防止序列号回绕(PAWS)。先介绍一下这个功能吧。序号为32位无符号整数,上限为4GB。4GB之后,需要将序列号进行包装,以便重复使用。在互联网速度较慢的过去,这不是问题,但是当通过足够快的网络传输大量数据时,序列号环绕的时间会更少。如果sequencenumberwraparound的时间很短,我们又会面临sequencenumber在之前延迟的packet到达后仍然有效的问题。为了解决这个问题,需要TCP时间戳。试试下面的例子,假设TCP的发送窗口是1GB,并且使用了时间戳选项,发送端会给每条TCP报文分配一个时间戳值,我们假设每条报文的时间加1,然后使用此连接传输大小为6GB的流。32位序列号在时间D和E之间回绕。假设在时间B一个数据包丢失并重新传输,并假设该段在网络上传播了很长一段距离并在时间F重新出现。如果TCP不识别此环绕消息,则数据完整性受到损害。使用时间戳选项可以有效防止上述问题。如果丢失的消息将在时间F重新出现,由于其时间戳为2,小于最新的有效时间戳(5或6),反环绕序列号算法(PAWS)将丢弃它。反绕包序号算法要求双方维护最后收到的数据包的时间戳(RecentTSval)。每接收到一个新的数据包,就会读取数据包中的时间戳值,并与RecentTSval值进行比较。如果发现接收到的数据包中的时间戳没有递增,则说明该数据包过期,直接丢弃该数据包。为什么tcp_tw_reuse默认关闭?经过这么多的准备,我们终于可以说说这个问题了。开启tcp_tw_reuse有什么风险?我认为会有2个问题。对于第一个问题,我们知道在开启tcp_tw_reuse的同时,还需要开启tcp_timestamps,也就是说wrappingsequencenumber的历史包可以通过时间戳的方式进行有效的判断。但是看了反绕包序号算法的源码后发现,即使RST报文的时间戳已经过期,只要RST报文的序号在对方的接收窗口内,它可以被接受。下面的tcp_validate_incoming函数是验证接收到的TCP报文是否合格的函数。第一步是执行PAWS检查,它负责tcp_paws_discard函数。staticbooltcp_validate_incoming(structsock*sk,structsk_buff*skb,conststructtcphdr*th,intsyn_inerr){structtcp_sock*tp=tcp_sk(sk);/*RFC1323:H1.ApplyPAWScheckfirst.*/if(tcp_fast_parse_options(sock_net(sk),p,th,t)&&tp->rx_opt.saw_tstamp&&tcp_paws_discard(sk,skb)){if(!th->rst){....gotodiscard;}/*ResetisacceptedevenifitdidnotpassPAWS.*/}当tcp_paws_discard返回true时,表示该包是历史包packet因此,消息将被丢弃。但是在丢弃这条消息时,会先判断它是否是RST消息,如果不是RST消息,则丢弃该消息。也就是说,即使RST包是历史包,也不会被丢弃。假设有这样一个场景,如下图所示:客户端向一个没有被服务器监听的端口发起HTTP请求,然后服务器会返回一个RST报文给对方。不幸的是,RST消息被网络阻止了。由于客户端还没有收到第二次TCP握手,所以重新发送SYN包。同时,服务端已经启动了服务,并监听了相应的端口。于是接下来,客户端和服务端进行了一次TCP三次握手,数据传输(HTTP响应-响应),四次挥手。因为client开启了tcp_tw_reuse,所以很快重用了处于TIME_WAIT状态的端口,和之前一样的四倍体建立了和server的连接。然后,RST报文在到达客户端之前经过网络延迟,RST报文的序号在客户端的接收窗口内。由于反回绕序列号算法不会阻止过期的RST,因此RST消息将被客户端接收。客户端接受了,于是客户端连接断开。上述场景是开启tcp_tw_reuse的风险,因为处于TIME_WAIT状态的端口很快被重用,导致新连接被包裹序号的RST报文断开。该消息将不会出现在下一个新连接中。你可能会有这样的疑问,为什么PAWS会检查放过期的RST消息。我读了RFC1323,里面有一句话:建议RST段不携带时间戳,并且无论时间戳如何,RST段都是可以接受的。旧的重复RST段应该是极不可能的,它们的清理功能应该优先于时间戳。粗略的意思:建议RST段不要携带时间戳,无论其时间戳如何,RST段都是可以接受的。旧的重复RST段应该是极不可能的,并且它们的清理应该优先于时间戳。RFC1323提到,接收历史RST数据包是极不可能的。之所以有这种想法,是因为TIME_WAIT状态的2MSL时间足以让连接中的数据包在网络中自然消失,所以认为是正常运行。说它不会发生,所以认为清除连接优先于时间戳。我前面提到的情况发生是因为启用了tcp_tw_reuse状态并跳过了TIME_WAIT状态。有同学会说,一个HTTP请求后,延迟的RST报文还能存活吗?HTTP请求实际上非常快。比如下面的抓包只用了0.2秒,远远少于MSL,所以延迟的RST包是有可能存活下来的。第二个问题是使tcp_tw_reuse能够快速重用处于TIME_WAIT状态的连接。如果第四次挥手的ACK报文丢失,可能会导致被动关闭连接的一方无法正常关闭,如下图:总结一下tcp_tw_reuse的作用就是让客户端快速重用端口处于TIME_WAIT状态相当于跳过了TIME_WAIT状态,这可能会导致两个问题:历史的RST包可能会终止同一个四元组的后续连接,因为PAWS会检查即使RSTs过期也不会被丢弃。如果第四次挥手的ACK报文丢失,有可能被动关闭连接的一方无法正常关闭;虽然TIME_WAIT状态持续的时间有点长,看起来很不友好,但它是为了避免Messy事情的发生而设计的。《UNIX网络编程》这本书说:TIME_WAIT是我们的朋友,它对我们有帮助,不要试图回避这种状态,而应该弄清楚。
