首页 文章 类别 VISION 关于


Time_wait详解(译文)

在构建基于 TCP 协议的 C/S 系统的时候,经常会因为一些简单的错误而导致严重的影响系统的可扩展性。 其中一些错误是因为对TIME_WAIT状态不理解导致的。 在本文中,我将会讲解为什么要存在TIME_WAIT 状态,它的存在所造成的一些问题以及如何解决这些问题。

TIME_WAIT在 TCP 的状态流程图中是一个很容易被误解的状态。它是表示socket可以进入和留存相当长一段时间的状态,如果你的系统中有很多 socket 处于TIME_WAIT状态,那么当你需要创建新的 socket 连接的时候可能会受到影响,这也会影响到你的程序的扩展性。很多人对于如何以及为何 socket 关闭连接的时候要在第一个地方以TIME_WAIT状态结束感到困惑,这里没有什么神秘的,从下面的 TCP 状态流程图中可以看出,TIME_WAIT状态是TCP 客户端断开连接之后的最终状态。

TCP-StateTransitionDiagram-NormalTransitions

尽管状态图显示的 TIME_WAIT 是客户端结束连接的最终状态,但这并不是说一定是客户端的结束状态才是TIME_WAIT,实际上,这是主动关闭连接(active close)的设备(不管是服务端还是客户端)的最终状态。什么是主动关闭连接呢?

如果一个 TCP 的终端(peer)首先对这个连接调用 Close() 关闭连接,就说这个终端发起了主动关闭。在很多协议和 C/S 系统中,这是指的客户端。在 HTTP 和 FTP 服务器上,通常指的是服务端。实际的终端以 TIME_WAIT状态终止的事件序列图如下所示。

TCP-StateTransitionDiagram-ClosureTransitions

现在我们已经知道socket如何以TIME_WAIT状态结束连接,接下来理解为什么要存在这个状态以及为什么它可能造成一些潜在的问题是非常有用的。

TIME_WAIT通常也称为2MSL等待状态。这是因为切换到TIME_WAIT状态的socket会保持2倍的最大段生命周期(MSL)的延迟时间。MSL是TCP协议数据报中,任意一段数据在网络上被丢弃之前保持可用的最大时间。这个时间使用用于传输TCP段的IP数据报中的TTL字段进行设置,不同的实现为MSL设置了不同的值,通常为 30s一分钟 或者 两分钟RFC793 指出MSL2分钟 ,在Windows下默认为该值,当然,可以通过修改注册表项TcpTimedWaitDelay设置该值。

之所以TIME_WAIT能够影响系统的扩展性是因为在一个TCP连接中,一个Socket如果关闭的话,它将保持TIME_WAIT状态大约 4分钟 。如果很多连接快速的打开和关闭的话,系统中处于TIME_WAIT状态的socket将会积累很多,你可以使用netstat命令查看处于TIME_WAIT状态的socket。由于本地端口数量的限制,同一时间只有有限数量的socket连接可以建立,如果太多的socket处于TIME_WAIT状态,你会发现,由于用于新建连接的本地端口太缺乏,将会很难再建立新的对外连接。 但是为什么要有TIME_WAIT状态呢?

对于TIME_WAIT的存在,有两个理由。一个原因是为了防止一个连接中延迟的数据段会被后序的连接错误的解析。当一个连接处于2MSL状态的时候,任何到达的数据段都将会被丢弃。

TIME_WAIT-why

在上图中,我们有从终端1到终端2的两个连接。在每个连接中,每个终端的地址和端口是相同的。第一个主动关闭的连接是由终端2主动发起的。如果终端2没有保持在TIME_WAIT状态足够长的时间以确保先前的连接中所有的数据段(每个数据段都有自己相应的序列号)都已经不可用了的话,可能会错误的成为第二个连接的一部分。

注意的是,延迟的数据段一般不会像这里这样造成问题。首先,每个终端的地址和端口号必须是相同的,这一点是可能性很小的,因为客户端的端口号通常由操作系统自动从可用端口范围中任意选择端口,并且在不同的连接中该端口通常是不同的。其次,延迟的片段的序列号需要在第二个连接中是可用的,这也是不太可能的。但是如果一旦这两个条件同时发生,TIME_WAIT状态可以防止新链接的数据出现问题。

第二个原因是为了实现TCP全双工连接的终止可靠性。如果来自终端2的最后一个ACK被丢弃,那么终端1将会重新发送最后的FIN,如果这时候终端2的连接状态已经转变到了CLOSED,那么唯一的响应将会是发送一个RST告诉它重发FIN是不被期望的,这样的结果会导致尽管所有的数据都已经正确的传输,但是终端1还是会接收到一个错误消息。

不幸的是有些操作系统对TIME_WAIT的实现太过简单(slightly natvie)。只有在一个连接中完全匹配的socket(一个连接使用客户端地址,端口,服务端地址,端口进行标识)才需要被TIME_WAIT保护,以减少TIME_WAIT造成的开销。然而,某些操作系统采用了更加严格的限制,并且防止重用处于TIME_WAIT状态的连接所包含的本地端口号。如果有太多的socket结束后处于TIME_WAIT状态的话,由于没有足够的新的本地端口分配给程序,因此无法建立新的对外的连接。

Windows下并不是这样做的,它只防止完全匹配的处于TIME_WAIT状态的出站连接的建立。

入站连接很少会被TIME_WAIT影响。虽然与客户端一样,服务器端主动关闭的连接会进入TIME_WAIT状态,但是服务端监听的端口并不会防止新建的入站连接请求的建立。在Windows下,服务器正在监听的知名端口可以作为后续接受的连接的一部分,如果要从一个远端地址和端口与当前正在处于TIME_WAIT状态的本地地址和端口建立新的连接,那么只要新的连接的序号大于当前处于TIME_WAIT状态的连接的最后的序号,连接就可以建立。但是,累积在服务端的处于TIME_WAIT状态的连接可能会影响性能和资源的使用,因为处于TIME_WAIT状态的连接最终都会超时,这就需要服务器对超时进行处理,并且在TIME_WAIT状态结束之前都会占用服务器的资源(少量)。

由于本地端口的缺乏,TIME_WAIT的存在影响的是出站连接的建立,这些本地端口由操作系统进行自动的分配,因此,优化的方法是增加本地端口的范围,在Windows下,你可以调整 MaxUserPort 注册表项。注意的是,很多Windows系统下默认的端口范围比较小,大约4000个左右,这对很多客户端服务器系统来说太少了。

虽然可以减少socket在TIME_WAIT状态花费的时间,但通常情况下这都是不会起到什么实际的帮助的。TIME_WAIT只会在服务器建立了很多连接并且主动关闭的情况下会产生影响,调整2MSL的时间只会让服务器可以建立更多的连接并且在给定的时间内关闭,所以你必须继续调整2MSL的时间更低以至于该值太小,导致遇到一些由于延迟片段成为后序连接的一部分而产生的问题,当然,这只会在连接到同一个远端地址和端口号并且非常频繁的使用本地端口,或者是你连接到同样的远程地址和端口,并且绑定了固定的本地端口的时候出现。

修改2MSL的值通常是机器全局的配置修改。你可以在socket级别使用SO_REUSEADDR socket选项解决TIME_WAIT的问题,这使得即使一个有着同样地址和端口的socket存在,也可以创建一个新的socket,新的socket最终将会劫持旧的socket。你可以使用SO_REUSEADDR选项,在一个有着同样端口的socket已经处于TIME_WAIT状态的时候创建新的socket,但这样做可能会造成一些问题,比如拒绝服务攻击或者数据窃取。在Windows平台下,有另一个socket选项SO_EXCLUSIVEADDRUSE,使用它可以避免SO_REUSEADDR选项的缺陷,但是依我之见,最好还是避免处理TIME_WAIT的问题,代之好好设计你的系统,让TIME_WAIT不再成为问题。

前面的TCP状态转换图都显示了连接断开的顺序,这里还有另外一种方式断开TCP连接。通过终止(abort)连接并发送一个RST代替FIN,这可以通过设置socket的SO_LINGER选项为 0 来实现。这样会使未处理的数据直接被丢弃并且连接被RST中断,而不是使用FIN的时候那样,未处理的数据继续完成传输。认识到当连接被RST中断的时候,任何在终端之间未处理的数据都将会被直接丢弃是非常重要的,通常这个RST代表了一个错误消息”connection has been reset by the peer“。远程终端知道连接是被中断还是进入了TIME_WAIT状态。

当然,一个已经使用RST终止的连接可能成为TIME_WAIT所预防的延迟段问题的受害者,但是这种可能性是非常小的,原因见上述的详细描述。要防止一个中断的连接受到延迟段问题的影响,两个终端必须转换到TIME_WAIT状态,因为连接的关闭可能会是由传输中介造成,比如路由器。然而,在没有发生这种情况时两端都会被简单的关闭。

要避免TIME_WAIT成为你的问题也是有办法的,这里假设你有能力修改你的客户端和服务端之间使用的协议,但是,通常情况下需要自己进行服务器设计。

对于从来都不会自身建立出站连接的服务器来说,除了会牺牲部分资源和性能去维护处于TIME_WAIT状态的连接外,你不需要过度的担心其它的问题。

对于需要同时建立出站连接和入站连接的服务器来说,黄金规则是如果需要TIME_WAIT的话,让远端来主动关闭连接而不是本服务器。最好的方式是无论什么原因,永远不要由服务器来初始一个主动关闭。如果你的终端超时了,使用RST中断连接代替关闭它。如果你的终端发送了不可用的数据,中断连接等。这种方法的思想是如果你的服务器永远都不初始发起主动关闭,那当前服务器就不会累积处于TIME_WAIT状态的socket,因此就不会造成扩展性的问题。虽然在出错的情况下中断连接是非常简单的,但是如果是正常连接的终止该如何做呢?李向情况下,你应该在你的服务器协议设计的时候有一种方法能够告诉客户端让客户端主动断开连接,而不是由服务器发起。所以,如果服务器需要中断一个连接的话,服务器发送一个应用级别的消息”we’re done”告诉客户端,客户端来关闭这个连接。如果客户端由于某些原因关闭连接失败了,然后服务器直接中断连接。

在客户端,事情就更加复杂一点,毕竟它需要初始发起一个主动关闭去终止TCP连接,如果客户端终止连接,它将以TIME_WAIT状态结束。但是,在客户端以TIME_WAIT终止连接有很多优点。第一,如果因为某些原因,客户端由于TIME_WAIT的问题而造成的连接问题不用影响到其它客户端。第二,如果频繁的打开关闭到同一个服务器的TCP连接的话是非常低效的。不要设计这种客户端每分钟都去连接服务器并且打开一个新的连接的协议。取而代之的是使用持久连接的设计并且只在连接失败的时候重新连接。如果中间的路由器拒绝保持没有数据流的连接的话,你可以实现一个应用级别的ping,使用TCP的keep alive或者接受路由器的重置连接;这样的好处是你不会积累很多TIME_WAIT的socket。如果你所做的操作的生存周期非常短,可以考虑一些连接池的设计,让连接一直保持打开和可重复使用。最后,如果你必须在客户端频繁的打开和关闭到同一个服务器的连接,可能你需要设计应用级别的关闭方式。你的客户端可以发送一个”I’m done”的消息,然后服务器可以发送一个”goodbye”的响应消息,然后客户端终止连接。

TIME_WAIT的存在是有它的理由的,通过缩短2MSL的时间或者使用SO_REUSEADDR允许连接重用并不总是好主意。如果你有能力去设计你的协议避免TIME_WAIT产生的问题的话,你就可以避免这里所有的问题。

你过你希望获取更多关于TIME_WAIT的实现和如何利用它的信息,参考这两篇文章:


原文: TIME_WAIT及其对协议和可扩展的客户端服务器系统设计的影响