TIME_WAIT 状态分析

一般情况下,http server 主动断开连接即 "avtive close",而浏览器等客户端则是 "passive close"。
server                client
       --- FIN --->
   
       <--- ACK ---
   
       <--- FIN ---
   
       --- ACK --->


这里盗用了网上的一张图,不知出处,结合上面看应该更清楚。

1. server 进行 "active close",server 这边的工作已经完成,发送 FIN 给 client,进入 FIN_WAIT_1 状态
2. client 收到 FIN 标志,发送 ACK 以回应,进入 CLOSE_WAIT 状态;当 server 接收到 ACK 后,进入 FIN_WAIT_2 状态。现在是则是半关闭
3. client 目前则是 "passive close",这意味着他在等待使用浏览器的 app 关闭,当该进程关闭后,client 发送 FIN 给 server,释放 client 端的 socket
4. server 收到 FIN,发送 ACK 确认,接下来就进入了"著名"的 TIME_WAIT 状态

注意:主动关闭的一端会才会进入 TIME_WAIT 状态,被动关闭的一端进入 CLOSED 状态。

进入 TIME_WAIT 状态,主要有两种方式:
1: 常规的,FIN_WAIT_2 -> TIME_WAIT
server 收到 client 的 FIN, server 发送 ACK,进入 TIME_WAIT

2: FIN_WAIT_1 -> TIME_WAIT
当 server 还在 FIN_WAIT_1 时,client 发送同时带 FIN 和 ACK 的报文,server 回复 ACK,将直接进入 TIME_WAIT,不经过 FIN_WAIT_2

为啥要有 TIME_WAIT 这个状态,原因有二:

1. 体现 TCP 是可靠的全双工连接,既然有可靠的三次握手来开始数据传输,也必须要有可靠的四次握手结束连接
2. 防止网络上的残余数据报文对接下来有可能产生的新连接产生影响

对于第一点,很好理解,假使 server 发了最后一个 ACK,但是 client 没有收到,client 会发送第二个带有 FIN 标志的包,如果没有 TIME_WAIT 就直接关闭,server 在收到这个 FIN 后会直接回应 RST ,表示应该立即结束这次 TCP 连接,并且会 discard 接下来的所有与之有关的报文,这个显然不是我们想看到的。

对于第二点,假使没有该状态,一个新的连接被建立起来,巧的是使用的 ip、port 都跟之前的一模一样。而不幸的是,在上次连接过程中,由于网络路由选路的等原因,导致上次的包依然存在于网络中。而新的连接已经建立,这就出现了一个问题,新连接有这样的可能会收到那个残留在网络中的报文,这会造成服务器端混乱。因此规定了 TIME_WAIT 的时间是 2MSL,MSL (maximum segment lifetime) 表示报文单向从一端到另一端的最长时间,超过该时间即被丢弃。2MSL 的时间确保了先前的报文都已丢失,避免对新的连接造成干扰。

总结起来就是上面的两点,可靠的关闭 TCP 连接,防止原先的报文对新连接产生影响。理解了上面这个,也就发现了 TIME_WAIT 这个状态在 TCP 中是不可缺少的。

rfc 规定的默认 MSL 时间为 120s,然后,要维持这 2MSL 的时间,是需要付出代价的。每个 socket 的状态都由一个叫做 TCP Control Block(TCB) 的数据结构维持,而 IP 包需要关联到相应的 TCB,随着 TCB 的增多,搜索时间也会增长。另外肯定会占用了相当一部分的端口。但是,即使 TCB 的搜索很快,并且依然剩余大量的空闲端口,还要考虑到一个问题,众多的 TCB 是需要占用内存的。因此,要解决大量 TIME_WAIT 其实是一个比较复杂的问题,这也是 HTTP 1.1 长链接产生的一部分原因。

TIME_WAIT 过多有什么负面影响,根据这篇论文的描述,主要如下的影响:
1. TCB 的增多会导致更多内存的消耗
2. 降低 avtive connection 的连接速度
3. 降低 MSL 可能导致系统的不稳定

要解决此状态过多的问题,上面那位论文哥的论文以及他总结的幻灯片里面提到了下面三种方式:

1. TIME-WAIT Negotiation,需要对 TCP 的状态做比较大的改动
2. TCP Level solution,需要修改 RST 包的含义,并且性能会受 RST 的影响
3 .Application level solution,需要对客户端做改动,需要增加 CLIENT_CLOSE 这个状态

一图表万语:

上面三种方式需要达到的目的都一样,最佳的结果就是让 TIME_WAIT 状态出现在 client 端,但是都需要涉及到协议的修改,不是很切实际。作者自己最后结尾了也说了:需要修改 TCP 其能够支持 TIME-WAIT negotiation,作者的希望就在 IPv6 身上了。

为此我们可以降低 TIME_WAIT 值,Ubuntu(10.04.3) 以及 CentOS(5.6) 的有个参数用于 FIN_WAIT_2 状态的控制,叫 tcp_fin_timeout ,默认为 60s,但实践下来,缩短该时间没有很大的改变,并且越是缩短该时间,引起问题的可能性就越大。
对于 HTTP 1.1 协议,开启 keep alived 可以缓解此类问题,但是不根本。如果想有立竿见影的效果,可以降低 tcp_max_tw_buckets 的值,该参数表示系统处理的最大的 TIME_WAIT 状态 socket 数目,如果超过该值,多余的会被立即销毁,官方建议不要降低此值,可以增加,但是会有内存的消耗。但是由此又会引发一个问题,messages 中会出现大量如下信息:
TCP: time wait bucket table overflow

对此,cu 上一篇帖子很好的解释了原因,这里的情况就是由于降低了 tcp_max_tw_buckets 的值导致的,所以需要一个比较平衡的数值。
另外,可以开启 tcp_tw_reuse,用于处在 TIME_WAIT 状态的 socket 的重用,还有叫 tcp_tw_recycle 的参数,在 LB 情况下会出问题,谨慎使用。

ref:
http://www.isi.edu/touch/pubs/infocomm99/infocomm99-web/
http://stackoverflow.com/questions/337115/setting-time-wait-tcp
http://blog.port80software.com/2004/12/07/hurry-up-and-time_wait/
http://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implications-for-protocols-and-scalable-servers.html