TIME_WAIT 发生的场景


TCP 连接终止时,主机1 先发送 FIN 报文,主机2 进入 CLOSE_WAIT 状态,并发送一个 ACK 应答,同时,主机2 通过 read 调用获得 EOF,并将此结果通知应用程序进行主动关闭操作,发送 FIN 报文。主机1 在接收到 FIN 报文后发送 ACK 应答,此时主机1 进入 TIME_WAIT 状态。
主机1 在 TIME_WAIT 停留持续时间是固定的,是最长分节生命期 MSL(maximum segment lifetime)的两倍,一般称之为 2MSL。Linux 系统里有一个硬编码的字段,名称为TCP_TIMEWAIT_LEN,其值为 60 秒。也就是说,Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds  */
注:只有发起连接终止的一方会进入 TIME_WAIT 状态

TIME_WAIT 的作用

为什么不直接进入 CLOSED 状态,而要停留在 TIME_WAIT 这个状态?
(1) 为了确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
如果图中主机1 的 ACK 报文没有传输成功,那么主机2 就会重新发送 FIN 报文。如果主机1 没有维护 TIME_WAIT 状态,而直接进入 CLOSED 状态,它就失去了当前状态的上下文,只能回复一个 RST 操作,从而导致被动关闭方出现错误。现在主机1 知道自己处于 TIME_WAIT 的状态,就可以在接收到 FIN 报文之后,重新发出一个 ACK 报文,使得主机2 可以进入正常的 CLOSED 状态。
(2) 为了让旧连接的重复分节在网络中自然消失
在网络中,经常会发生报文经过一段时间才能到达目的地的情况,如路由器重启,链路突然出现故障等。如果原连接中断后,又重新创建了一个原连接的“化身”,说是化身其实是因为这个连接和原先的连接四元组完全相同,如果迷失报文经过一段时间也到达,那么这个报文会被误认为是 连接“化身”的一个 TCP 分节,这样就会对 TCP 通信产生影响。
注:TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的分组都被丢弃,使得原来连接的分组在网络中都自然消失,再出现的分组一定都是新化身所产生的。

TIME_WAIT 的危害

  • 对内存资源的占用,这个目前看来不是太严重,基本可以忽略。
  • 对端口资源的占用,一个 TCP 连接至少消耗一个本地端口。一般可以开启的端口为 32768~61000 ,也可以通过net.ipv4.ip_local_port_range指定,如果 TIME_WAIT 状态过多,会导致无法创建新连接

如何优化 TIME_WAIT?

(1) 通过 sysctl 命令,将 net.ipv4.tcp_max_tw_buckets 调小
这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的 TIME_WAIT 连接状态重置,并且只打印出警告信息。这个方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用

(2) 调低 TCP_TIMEWAIT_LEN,重新编译系统
这个方法是一个不错的方法,缺点是需要“一点”内核方面的知识,能够重新编译内核。

(3) 设置 SO_LINGER 
“linger”的意思为停留,我们可以通过设置套接字选项,来设置调用 close 或者 shutdown 关闭连接时的行为。
int setsockopt(int sockfd, int level, int optname, const void *optval,  socklen_t optlen);

struct linger {
     int  l_onoff;    /* 0=off, nonzero=on */
     int  l_linger;    /* linger time, POSIX specifies units as seconds */
}
  • 如果l_onoff为 0那么关闭本选项。l_linger的值被忽略,这对应了默认行为,close 或 shutdown 立即返回。如果在套接字发送缓冲区中有数据残留,系统会将试着把这些数据发送出去
  • 如果l_onoff为非 0, 且l_linger值也为 0:那么调用 close 后,会立该发送一个 RST 标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了 TIME_WAIT 状态,直接关闭。这种关闭的方式称为“强行关闭”。 在这种情况下,排队数据不会被发送,被动关闭方也不知道对端已经彻底断开。只有当被动关闭方正阻塞在recv()调用上时,接受到 RST 时,会立刻得到一个“connet reset by peer”的异常
  • 如果l_onoff为非 0, 且l_linger的值也非 0:那么调用 close 后,调用 close 的线程就将阻塞,直到数据被发送出去,或者设置的l_linger计时时间到
第二种可能为跨越 TIME_WAIT 状态提供了一个可能,不过是一个非常危险的行为,不值得提倡

(4) 更安全的设置:net.ipv4.tcp_tw_reuse
从协议角度理解 如果是安全可控的,可以复用处于 TIME_WAIT 的套接字 为新的连接所用。
什么是协议角度理解的安全可控呢?主要有两点:
  • 只适用于连接发起方(C/S 模型中的客户端)
  • 对应的 TIME_WAIT 状态的连接 创建时间超过1秒才可以被复用
注:使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即 net.ipv4.tcp_timestamps=1(默认即为 1)。
TCP选项中的 两个4字节的时间戳字段,用于记录TCP发送方的当前时间戳 和从对端接收到的最新时间戳。由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为 时间戳过期 被自然丢弃

总结

  • TIME_WAIT 的引入是为了让 TCP 报文得以自然消失,同时为了让被动关闭方能够正常关闭;
  • 不要试图使用SO_LINGER设置套接字选项,跳过 TIME_WAIT;
  • 现代 Linux 系统引入了更安全可控的方案,可以帮助我们尽可能地复用 TIME_WAIT 状态的连接。

思考题

1. 最大分组 MSL 是 TCP 分组在网络中存活的最长时间,你知道这个最长时间是如何达成的?换句话说,是怎么样的机制,可以保证在 MSL 达到之后,报文就自然消亡了呢?
一来一回,去时ACK的最大存活时间(MSL)+ 来时FIIN的最大存活时间(MSL) =  2MSL
2. RFC 1323 引入了 TCP 时间戳,那么这需要在发送方和接收方之间定义一个统一的时钟吗?
需要,使用NTP服务