TCP TIME_WAIT状态

TIME_WAIT状态是TCP连接中主动关闭连接的一方会进入的状态,在发出最后一个ACK包之后,主动关闭方进入TIME_WAIT状态,从而确保:

  • ACK包到达对端
  • 等待网络中之前迷路的数据包完全消失,防止端口被复用的时候收到迷路包从而出现收包错误

TIME_WAIT状态会持续2MSL(max segment lifetime)的时间,一般1分钟到4分钟。在这段时间内端口不能被重新分配使用。

TIME_WAIT并不会占用过多的系统资源,但是可以通过修改内核参数/etc/sysctl.conf来限制TIME_WAIT数量。

四次挥手过程

先来了解TCP四次挥手的过程:

  • 第一次:主机 1(可以是客户端,也可以是服务器端),设置 Sequence NumberAcknowledgment Number,向主机 2 发送一个 FIN 报文段;此时,主机1进入 FIN_WAIT_1 状态;这表示主机 1 没有数据要发送给主机 2 了;

  • 第二次:主机 2 收到了主机 1 发送的 FIN 报文段,向主机 1 回一个 ACK 报文段,Acknowledgment NumberSequence Number 加 1 ;主机 1 进入 FIN_WAIT_2 状态;主机 2 告诉主机 1,我“同意”你的关闭请求;

  • 第三次:主机 2 向主机 1 发送FIN报文段,请求关闭连接,同时主机 2 进入LAST_ACK 状态;

  • 第四次:主机 1 收到主机 2 发送的 FIN报文段,向主机2发送 ACK 报文段,然后主机1进入 TIME_WAIT 状态;主机 2 收到主机 1 的ACK报文段以后,就关闭连接;此时,主机1等待 2 * MSL 后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。

注: MSL是指Max Segment Lifetime,即一个IP数据包能在网络中生存的最长时间,超过这个时间,IP数据包将在网络中消失。每种TCP协议的实现方法均要指定一个合适的MSL值,如RFC1122给出的建议值为2分钟,又如Berkeley体系的TCP实现通常选择30秒作为MSL值。这意味着TIME_WAIT的典型持续时间为1-4分钟。

TCP四次挥手过程中通信双方状态解析

  • FIN_WAIT_1
    其实FIN_WAIT_1FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:

    • FIN_WAIT_1状态实际上是当SOCKETESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)
  • FIN_WAIT_2
    实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)

  • CLOSE_WAIT
    表示在等待关闭。当对方close一个SOCKET后发送FIN报文给你,你自然会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)

  • LAST_ACK
    被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED状态了。(被动方)

  • TIME_WAIT
    表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED状态了。如果FIN WAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)

  • CLOSED
    表示SOCKET连接已中断

为什么会有TIME_WAIT状态

  • 可靠地实现TCP全双工连接的可靠终止
    TCP协议在关闭连接的四次握手过程中,最终ACK是由主动关闭连接的一端发出的,如果这个ACK丢失,被动方将重发最终的FIN,因此主机1就必须维护状态信息TIME_WAIT 允许它发送最终的ACK。如果主机1不维持TIME_WAIT的状态,而是处于CLOSED状态,那么主机1将响应RST(reset)数据包,主机2收到后将此数据报解释成一个异常(Java中会抛出connection reset的SocketException)。因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个数据包任何一个分节丢失的情况,主动关闭连接的主机1必须维持TIME_WAIT的状态。

  • 保证此次连接的重复数据段从网络中消失
    TCP数据包可能由于路由器异常而“迷路”,在“迷路”期间,TCP发送端可能因确认超时而重发这个分节,“迷路”的分节在路由器恢复正常后也会被发送到最终的目的地,这个迟到的“迷路”数据包到达时可能会引起问题。在关闭“前一个连接”之后,马上又建立起一个相同的IP和端口之间的“新连接”,这会导致“前一个连接”的迷路重复分组在“前一个连接”终止后到达,从而被“新连接”接收到了。

    为了避免以上情况,TCP/IP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,这就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消失

出现太多TIME_WAIT危害

高并发短连接的TCP服务器上,当服务器处理完请求后会立刻按照主动正常关闭连接。这个场景下,会出现大量socket处于TIMEWAIT状态。如果客户端的并发量持续很高,此时部分客户端就会显示连接不上。

解释下这个场景。主动正常关闭TCP连接,都会出现TIMEWAIT。为什么我们要关注这个高并发短连接呢?有两个方面需要注意:

  • ① 高并发可以让服务器在短时间范围内同时占用大量端口,而端口有个0~65535的范围,并不是很多,刨除系统和其他服务要用的,剩下的就更少了。

  • ② 在这个场景中,短连接表示“业务处理+传输数据的时间 远远小于 TIMEWAIT超时的时间”的连接。 这里有个相对长短的概念,比如,取一个web页面,1秒钟的http短连接处理完业务,在关闭连接之后,这个业务用过的端口会停留在TIMEWAIT状态几分钟,而这几分钟,其他HTTP请求来临的时候是无法占用此端口的。单用这个业务计算服务器的利用率会发现,服务器干正经事的时间和端口(资源)被挂着无法被使用的时间的比例是 1 :几百,服务器资源严重浪费。

说个题外话,从这个意义出发来考虑服务器性能调优的话,长连接业务的服务就不需要考虑TIMEWAIT状态。同时,假如你对服务器业务场景非常熟悉,你会发现,在实际业务场景中,一般长连接对应的业务的并发量并不会很高.

综合这两个方面,持续的到达一定量的高并发短连接,会使服务器因端口资源不足而拒绝为一部分客户服务

TIME_WAIT太多怎么解决

修改/etc/sysctl.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
# 表示开启重用
# 允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1

# 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
# net.ipv4.tcp_timestamps 开启时,net.ipv4.tcp_tw_recycle开启才能生效
net.ipv4.tcp_tw_recycle = 1

# 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_timestamps = 1

# 用来设置保持在FIN_WAIT_2状态的时间
net.ipv4.tcp_fin_timeout = 2

保存后sysctl -p生效

地址reuse问题

在写一个unix server程序时,经常需要在命令行重启它,绝大多数时候工作正常,但是某些时候会抛出异常 bind: address already in use,于是重启失败。

上面这个就是地址reuse问题,就是由于TIME_WAIT状态产生的,我们有以下方案来解决这个问题:

SO_REUSEADDR

这个socket选项通知内核:如果端口忙,但TCP状态位于TIME_WAIT,可以重用端口

一个socket由相关五元组构成: 协议、本地地址、本地端口、远程地址、远程端口SO_REUSEADDR仅仅表示可以重用本地本地地址、本地端口,整个相关五元组还是唯一确定的。所以,重启后的服务程序有可能收到非期望数据。必须慎重使用SO_REUSEADDR选项。

一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。

SO_REUSEADDR用于对TCP处于TIME_WAIT状态下的socket,才可以重复绑定使用。server程序总是应该在调用bind()之前设置SO_REUSEADDR选项。先调用close()的一方会进入TIME_WAIT状态。

SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。

SO_LINGER

Linux网络编程中,socket的选项很多。其中几个比较重要的选项就包括SO_LINGER。

在默认情况下,当调用close()关闭socket的使用,close()会立即返回,但是,如果send buffer中还有数据,系统会试着先把send buffer中的数据发送出去,SO_LINGER选项则是用来修改这种默认操作的。

SO_LINGER是一个socket选项,可以通过set sockopt API进行设置,使用起来比较简单,但其实现机制比较复杂,且字面意思上比较难理解。SO_LINGER的值用如下数据结构表示:

1
2
3
4
struct linger {
int l_onoff //0 = off, nonzero = on(开关)
int l_linger //linger time(延迟时间)
}

其取值和处理如下:

  • 设置 l_onoff 为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close()调用会立即返回给调用者,如果可能将会传输任何未发送的数据;

  • 设置 l_onoff 为非0,l_linger为0,当调用close()的时候,TCP连接会立即断开。send buffer中未被发送的数据将被丢弃,并向对方发送一个RST信息。值得注意的是,由于这种方式,不是以4次握手方式结束TCP连接,所以,TCP连接将不会进入TIME_WAIT状态,这样会导致新建立的可能和旧连接的数据造成混乱。这种关闭方式称为“强制”或“失效”关闭。通常会看到 Connection reset by peer 之类的错误;

  • 设置 l_onoff 为非0,l_linger为非0,在这种情况下,会使得close()返回得到延迟。调用close()去关闭socket的时候,内核将会延迟。也就是说,如果send buffer中还有数据尚未发送,该进程将会被休眠直到一下任何一种情况发生:

    a. send buffer中的所有数据都被发送并且得到对方TCP的应答消息;

    b.延迟时间消耗完。在延迟时间被消耗完之后,send buffer中的所有数据都将会被丢弃。这种关闭称为“优雅的”关闭。

因此,在正常情况下,在socket调用close()之前设置SO_LINGER超时为0都不是个好的选择。但也有些情况下需要使用SO_LINGER:

  • 如果server返回无效数据或者超时时,SO_LINGER有助于避免卡在CLOSE_WAIT或TIME_WAIT的状态

  • 如果必须启动有数千个客户端连接的app,则可以考虑设置SO_LINGER,从而避免数千个socket处于TIME_WAIT状态,从而减少可用端口在服务重启后,新客户端连接受到的影响

总结

通过上面的讨论,我们知道TIME_WAIT状态是友好的,并不是多余的。TCP要保证在所有可能的情况下使得所有的数据都能够正确送达。当你关闭一个socket时,主动关闭一端的socket将进入TIME_WAIT状态,而被动关闭的一方则进入CLOSED状态,这的确能够保证所有的数据都被传送。

当一个socket关闭的时候,是通过两端四次挥手完成的,当一端调用close()时,就说明本端没有数据要传送了,这好像看来在挥手完成以后,socket就可以处于CLOSED状态了,其实不然,原因是这样安排状态有两个问题:

  • 第一,我们没有任何机制保证最后的一个ACK能够正常传输;
  • 第二,网络仍然可能有残余的数据包,我们也必须能够正常处理。

TIME_WAIT状态就是为了解决这两个问题而生的。服务端为了解决这个TIME_WAIT问题,可选的方式有3种:

  • 保证由客户端主动发起关闭
  • 关闭的时候使用RST方式(set SO_LINGER)
  • 对处于TIME_WAIT状态的TPC允许重用(set SO_REUSEADDR)

参考:
TCP/IP中TIME_WAIT状态详解
TCP——-为什么会有TIME_WAIT状态 ?