Blog Email GitHub

11 Jun 2014
深入理解TCP的TIME-WAIT

TIME-WAIT简介

我在 TCP协议的哪些超时 中提到 TIME-WAIT 状态,超时时间占用了 2MSL ,在 Linux 上固定是 60s 。

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                              * state, about 60 seconds     */

之所以这么长时间,是因为两个方面的原因。

  1. 一个数据报在发送途中或者响应过程中有可能成为残余的数据报,因此必须等待足够长的时间避免新的连接会收到先前连接的残余数据报,而造成状态错误。

    duplicate-segment

    由于 TIME-WAIT 超时时间过短,旧连接的 SEQ=3 由于 路上太眷顾路边的风景,姗姗来迟 ,混入新连接中,加之 SEQ 回绕正好能够匹配上,被当做正常数据接收,造成数据混乱。

  2. 确保被动关闭方已经正常关闭。

    last-ack

    如果主动关闭方提前关闭,被动关闭方还在 LAST-ACK 苦苦等待 FIN 的 ACK 。此时对于主动关闭方来说,连接已经得到释放,其端口可以被重用了,如果重用端口建立三次握手,发出友好的 SYN ,谁知 热脸贴冷屁股,被动关闭方得不到想要的 ACK ,给出 RST 。

TIME-WAIT危害

主动关闭方进入 TIME-WAIT 状态后,无论对方是否收到 ACK ,都需要苦苦等待 60s 。这期间完全是 占着茅坑不拉屎 ,不仅占用内存(系统维护连接耗用的内存),耗用CPU,更为重要的是,宝贵的端口被占用,端口枯竭后,新连接的建立就成了问题。之所以端口 宝贵 ,是因为在 IPv4 中,一个端口占用2个字节,端口最高65535。正好从海龙和智平那里得到两个很好的例子。

cotton 服务器的 uWSGI 进程,uWSGI 进程中没有复用 Redis 连接。uWSGI 处理一个HTTP请求,便创建一个 Redis 连接,用完便释放。

+-------------------+         +----------+
|      uWSGI        | ---->   |  Redis   |            
|   Redis client    | <----   |  Server  |
+-------------------+         +----------+

移动支付的系统架构,nginx 接受到 HTTP 请求后, proxy 给上游的 uWSGI 服务器,采用 HTTP 短连接,uWSGI 作为上游服务器,返回 HTTP Response 后便关闭了连接。

+---------+         +----------+
|  nginx  | ---->   |  uwsgi   |            
|         | <----   |          |
+---------+         +----------+

这两种情况,都有一个共同的问题: 不停的创建TCP连接,接着关闭TCP连接 ,因为不可避免的造成了大量的TCP连接进入 TIME-WAIT 状态,如果在 60s 内处理完 65535个请求,必然造成端口不够用的情况。

但是又有很大的不同:

  • cotton例子中,uWSGI 作为 Redis client ,是主动关闭方。
  • 移动支付例子中,uWSGI 作为 HTTP Server,是主动关闭方。

一个是 client (连接发起主动方),一个是 Server (连接发起被动方),主动关闭造成的后果是一样的,但是解决起来的方法又是不一样的。后面会详细解释。

解决

TCP协议推出了一个扩展 RFC 1323 TCP Extensions for High Performance ,在 TCP Header 中可以添加2个4字节的时间戳字段,第一个是发送方的时间戳,第二个是接受方的时间戳。

基于这个扩展,Linux 上可以通过开启 net.ipv4.tcp_tw_reusenet.ipv4.tcp_tw_recycle 来减少 TIME-WAIT 的时间,复用端口,减缓端口资源的紧张。

如果对于是 client (连接发起主动方)主动关闭连接 的情况,开启 net.ipv4.tcp_tw_reuse 就很合适。通过两个方面来达到 reuse TIME-WAIT 连接的情况下,依然避免本文开头的两个情况。

  1. 防止残余报文混入新连接。得益于时间戳的存在,残余的TCP报文由于时间戳过旧,直接被抛弃。
  2. 即使被动关闭方还处于 LAST-ACK 状态,主动关闭方 reuse TIME-WAIT连接,发起三次握手。当被动关闭方收到三次握手的 SYN ,得益于时间戳的存在,并不是回应一个 RST ,而是回应 FIN+ACK,而此时主动关闭方正在 SYN-SENT 状态,对于突如其来的 FIN+ACK,直接回应一个 RST ,被动关闭方接受到这个 RST 后,连接就关闭被回收了。当主动关闭方再次发起 SYN 时,就可以三次握手建立正常的连接。

    last-ack-reuse

而对于 server (被动发起方)主动关闭连接 的情况,开启 net.ipv4.tcp_tw_recyle 来应对 TIME-WAIT 连接过多的情况。开启 recyle 后,系统便会记录来自每台主机的每个连接的分组时间戳。对于新来的连接,如果发现 SYN 包中带的时间戳比之前记录来自同一主机的同一连接的分组所携带的时间戳要比之前记录的时间戳新,则接受复用 TIME-WAIT 连接,否则抛弃。

但是开启 net.ipv4.tcp_tw_recyle 有一个比较大的问题,虽然在同一个主机中,发出TCP包的时间戳是可以保证单调递增,但是 TCP包经过路由 NAT 转换的时候,并不会更新这个时间戳,因为路由是工作在IP层的嘛。所以如果在 client 和 server 中经过路由 NAT 转换的时候,对于 server 来说源IP是一样的,但是时间戳是由路由后面不同的主机生成的,后发包的时间戳就不一定比先发包的时间戳大,很容易造成 误杀 ,终止了新连接的创建。

最后

最后的结论是:

  • 对于是 client (连接发起主动方)主动关闭连接 的情况,开启 net.ipv4.tcp_tw_reuse 就很合适的。
  • 对于 server (被动发起方)主动关闭连接 的情况,确保 client 和 server 中间没有 NAT ,开启 net.ipv4.tcp_tw_recycle 也是ok的。但是如果有 NAT ,那还是算了吧。

References & Resources