当前位置: 首页 > Linux

TIME_WAIT及其设计启示

时间:2023-04-06 05:32:37 Linux

最近产品销量不错,却带来疯狂的服务器警告:TIME_WAIT连接过多。对TCP的一般协议有所了解,但对很多细节和中间状态的把握并不深刻。所以打算翻译这个TIME_WAIT及其对协议和可扩展客户端服务器的设计启示,边研究边思考如何解决服务器上的告警问题。以下为原文翻译:在建立一个使用TCP进行通信的客户端-服务器系统时,很容易因为一些小错误而严重影响系统的可扩展性。错误之一是没有考虑TIME_WAIT状态。在本文中,我将解释为什么存在TIME_WAIT状态,它可能导致的问题,以及围绕整个状态应该做和不应该做的一些事情。TIME_WAIT是TCP状态转换图中经常被误解的状态。它标志着某些套接字可能会进入或停留在一个比较长的状态。如果你的服务器有大量处于TIME_WAIT状态的套接字,很可能会影响你继续创建新的套接字连接,从而影响你的服务器扩展能力。首先,对于套接字如何以及为什么进入TIME_WAIT状态,通常存在一些误解。按理说这是不应该的,这个状态转换也没有什么神奇的。从下面的TCP状态转换图中可以看出,TCP客户端通常会以TIME_WAIT状态结束。虽然图中显示TIME_WAIT是客户端的最终状态,但并不代表客户端必须进入TIME_WAIT。在一个TCP连接对中,无论是客户端还是服务端,谁发起“主动关闭”,谁最终都会进入TIME_WAIT状态。那么问题来了,“主动关闭”指的是什么?在一个TCP连接中,一端发起的“主动关闭”意味着该端首先在连接中调用了Close()。在许多协议和客户端/服务器架构设计中,它是由客户端触发的。但在HTTP和FTP服务器上,通常是服务器发起。导致TCP端进入TIME_WAIT状态的实际过程如下图所示:现在我们知道了socket是如何进入TIME_WAIT状态的。这有助于我们理解为什么存在这种状态,以及为什么它可能隐藏潜在的问题。TIME_WAIT状态也常被称为2MSL等待状态,因为socket在转换到TIME_WAIT状态后会停留2倍的MaximumSegmentLifetime(MSL)时间。MSL是指构成TCP协议的任何数据包在被丢弃之前可以在网络上保留的最长时间。这个时间限制最终会绑定到传输TCP数据的IP包中的TTL字段。在不同的实现中,MSL会有不同的值,最常见的值是30s、1分钟或2分钟。该值在RFC793中定义为2分钟,Windows系统也将2分钟设置为默认值,但可以通过TcpTimedWaitDelay的注册设置进行调整。TIME_WAIT状态之所以会影响系统的可扩展性,是因为TCP连接中的socket一旦关闭,最多还会停留在TIME_WAIT状态达4分钟。如果不断有大量的连接被创建和关闭,处于TIME_WAIT状态的套接字就会开始堆积。您可以使用netstat观察处于TIME_WAIT状态的套接字。服务器同时可以建立的连接数是有限制的,其中之一就是服务器本地端口的数量。如果处于TIME_WAIT状态的连接太多,那么你会发现很难建立外部连接,因为没有足够的本地端口来支持新的连接。既然有这样的问题,为什么TIME_WAIT状态还存在呢?TIME_WAIT状态的设计有两个原因。第一个是防止在连接中迟到的数据包被误认为是后续的新连接数据包。当连接处于2MSL等待状态时,任何后续到达的数据包都将被丢弃。在上图中,可以看到从终端1到终端2建立了两条连接,每条连接的地址和端口都是相同的。第一个连接由终端2发起“主动关闭”。如果终端2没有在TIME_WAIT状态停留足够长的时间以丢弃先前连接中的所有后续数据包,则可以考虑后续传入的数据包(具有合理的序列号)作为一个新的连接。请注意,这种延迟包触发问题实际上是很难发生的。首先,它要求连接中的地址和端口相同,这本身概率很低,因为要使用的客户端端口是由操作系统从临时端口中选择的,通常不同的连接有不同的端口。其次,延迟报文中携带的序号在新连接中也很难判断是否合法。综上所述,当这两种情况都发生时,TIME_WAIT状态可以防止新连接中的数据受到延迟数据包的影响。TIME_WAIT状态存在的另一个原因是为了保证TCP全双工连接中断的可靠性。如果终端2发送的最后一个ACK包丢失,那么终端1将重发最后一个FIN包。但是如果此时终端2上的连接已经进入了CLOSED状态,则只会返回一个RST,因为此时收到的FIN包不是预期的。这将导致终端1收到错误回复,即使它已正确发送所有数据包。不幸的是,许多操作系统对TIME_WAIT状态的实现似乎有点简单。只有那些真正符合条件的套接字才需要被阻塞,以获得TIME_WAIT状态提供的保护。需要保护的是那些可以通过客户端地址和端口、服务器地址和端口的精确匹配来识别的连接。但是,一些操作系统实施了更严格的限制,处于TIME_WAIT状态的连接使用的本地端口号不能被复用。如果有足够多的套接字进入TIME_WAIT状态,系统将由于缺少可用的本地端口而无法创建新的出站连接。Windows操作系统并没有这样做,它只是限制建立新的与处于TIME_WAIT状态的现有连接的属性完全一致的出站连接。入站连接很少受TIME_WAIT状态的影响。当连接在服务器的“主动关闭”操作下进入TIME_WAIT状态时,服务器监听的本地端口将不会被阻止用于新的入站连接。在Windows操作系统中,可以使用服务器正在监听的TIME_WAIT连接的知名端口来组成一个新的连接,如果新连接中的远程地址和端口与原来的连接完全一致在TIME_WAIT状态被重用,那么这个连接只接受比TIME_WAIT连接中最后一个序列号大的序列号消息。虽然TIME_WAIT对入站连接的影响相对较小,但是TIME_WAIT连接在服务器上的累积会影响性能和资源,因为处理TIME_WAIT过期需要一些操作,连接会一直持续到TIME_WAIT状态最终结束然后关闭。使用系统资源(虽然不多)。由于TIME_WAIT状态会影响服务器创建出站连接,因为本地端口被占用,而创建连接时使用的本地端口是操作系统从临时端口范围中选择的,为了改善这种情况,首先你能做的就是确保你的临时端口范围足够大。在Windows操作系统中,您可以调整MaxUserPort注册表配置。请注意,默认情况下,Windows系统可用的临时端口范围约为4000,这对于许多客户端/服务器系统来说太小了。虽然有可能减少套接字在TIME_WAIT状态中花费的时间,但这种操作通常无济于事。因为只有大量的连接建立后主动关闭,才会进入TIME_WAIT状态而出现问题。调整2MSL等待时间通常只会让一段时间内创建和关闭更多连接,所以需要不断减少2MSL时间,直到TIME_WAIT保护功能失效,可能会触发延迟包出现在后续连接中,造成问题。当然,这种问题只会出现在几种情况下:你建立同一个远程地址和端口的连接,并在短时间内用完所有本地端口,或者你需要使用一个固定的本地端口连接到相同的远程端口地址和建立连接的端口。修改2MSL时间通常是一个全局有效的配置。除了修改这个配置,还可以尝试修改SO_REUSEADDR,在socket级别处理TIME_WAIT。此选项允许创建一个与现有套接字具有相同地址和端口的套接字,新套接字将劫持旧套接字。你可以允许SO_REUSEADDR允许一个处于TIME_WAIT状态的端口被用来创建新的套接字,但是你也需要承担拒绝服务攻击和数据包被盗的风险。在Windows平台上,另一种socket配置SO_EXCLUSIVEADDRUSE可以避免SO_REUSEADDR带来的一些问题。不过在我看来,与其改变这些配置,还不如重新设计系统,彻底避免TIME_WAIT问题。前面图中的TCP状态转换都显示了有序的TCP连接关闭。不过,还有一种关闭TCP连接的方式,叫做abortclose,就是发送一个RST包而不是FIN包。这通常可以通过将此套接字的SO_LINER设置为0来配置。这会导致直接用一个RST关闭连接,丢弃等待传输的数据,而不是像正常状态一样传输等待数据并发送FIN包结束连接。需要注意的是,一旦连接中断,会直接发送一个RST包,连接中的所有数据都会被丢弃。通常会生成错误标志“连接已被对等方重置”。那么对端就会知道连接中断了,双方都不会进入TIME_WAIT。当然,一个连接被RST终止后,也会受到TIME_WAIT保护的延迟消息的影响。但是,触发条件非常严格,几乎不可能。如果要避免在中断操作后受到延迟包的影响(例如连接被中间设备关闭,比如路由器),需要在TCP两端都进入TIME_WAIT状态。然而,这种情况很少发生,目前TCP两端都简单地关闭了连接。您有许多操作可以防止TIME_WAIT状态引起问题。其中一些操作假定您有能力更改客户端和服务器之间的交互协议。大多数自行设计的服务端场景都满足这个条件。对于从不主动建立外部连接的服务器,除了TIME_WAIT状态下维持连接所需的资源和性能外,不需要过多担心。对于一个既要接收连接,又要创建外部连接的服务器,金科玉律是确保连接在对端关闭。最好的方法是永远不要出于任何原因启动“主动关闭”。如果对端超时,使用RST终止连接而不是关闭它。如果对端发送了错误的数据,它也回复RST,以此类推。如果服务器从不主动发起“主动关闭”,那么连接就不会进入TIME_WAIT,也不会受到TIME_WAIT状态带来的问题的影响。这样一想,虽然我们很容易知道发生错误时应该做什么,但是一个正常的连接应该怎么关闭呢?理想的解决方案是在协议中协商由客户端发起的断开连接。当服务端认为应该断开连接时,从应用层发送“连接结束”消息,通知客户端主动关闭连接。如果客户端没有在合理的时间内关闭连接,则服务器终止连接。在客户端,事情要复杂得多。由于必须有一个端需要发起“主动关闭”来终止TCP连接,所以在客户端控制TIME_WAIT状态将有以下好处:第一,只有一个客户端会受到TIME_WAIT的累积和连接的影响会受到其他客户的影响。结束不会受到影响。其次,客户端快速打开和关闭与同一服务器的TCP连接没有意义。保持连接更长时间比处理TIME_WAIT问题更有意义。不要设计客户端每分钟都与服务器建立新连接的协议机制。相反,使用仅在连接断开时才重新连接的持久连接设计。如果中间路由器拒绝保持连接,可能需要实现一个应用层的心跳包,或者使用TCPkeepalive,或者接受路由器保持重置连接:好处是不会有sockets堆积在TIME_WAIT状态。如果你在连接中所做的事情是短暂的,那么可以考虑使用连接池设计来保持连接打开并重用连接。最后,如果你只是想让客户端连续快速的打开和关闭同一个服务器的连接,那么你可能需要设计一个应用层的关闭逻辑,关闭逻辑完成后直接终止连接(abortiveclose)。您的客户端发送“我完成了”,然后服务器响应“再见”,客户端终止了连接。TIME_WAIT状态的存在是合理的,但是缩短2MSL时间或者允许地址重用都不是很好的解决问题的方法。如果你可以设计你的协议来避免TIME_WAIT,你通常可以完全避免这个问题。如果您想了解更多关于TIME_WAIT实现及其工作原理的信息,您可以查看本文和本文。