0%

tcp的TIME_WAIT和CLOSE_WAIT

TIME_WAIT 是客户端(主动发起方)的状态,在发送第四次挥手后进入的一个状态。服务器也有可能出现TIME_WAIT,服务器也有可能是断开连接的主动发起方。

进入TIME_WAIT后,客户端等待2MSL时间(RFC793建议是两分钟),在 Linux 上 2MSL 的时长是 60 秒,也会进入 CLOSED 状态。

1. TIME_WAIT

1.1 作用

那么客户端为什么进入 TIME_WAIT 而不是直接关闭?保证「被动关闭连接」的一方,能被正确的关闭。

从 RFC 793 对 TIME_WAIT 状态的定义中,我们可以发现该状态的另一个重要作用,等待足够长的时间以确定远程的 TCP 连接接收到了其发出的终止连接消息 FIN 对应的 ACK

TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

1. 保证正确关闭

因为客户端发送挥手④的时候,有可能失败。

如果客户端直接关闭,服务器重发挥手③,客户端处于关闭不响应,服务器无法释放资源。

img

1、在①中,CLient1端主动发起关闭链接,Server针对Client1的FIN回执了ACK包,然后接着发送了自己的FIN包,等待Client1回执最终的ACK包。

2、在②中,这里假设TIME_WAIT的时间不足够充分,当Server还没有收到 ACK 消息时,Client1就主动变成CLOSED状态。

3、在③中,由于Server一直没有等到自己FIN包的ACK应答包,导致一直处于LAST_ACK状态。

4、在④中,因为 服务端因为没有收到 ACK 消息,当Client2重新与Server建立TCP链接,认为当前连接是合法的,CLient2重新发送 SYN 消息请求握手时会收到Server的 RST 消息,连接建立的过程就会被终止。

2. 防止相同四元组再次收到

防止历史连接中的数据,被后面相同四元组的连接错误的接收。

TCP 报文可能由于路由器异常而 “迷路”,在迷途期间,TCP 发送端可能因确认超时而重发这个报文,迷途的报文在路由器修复后也会被送到最终目的地,这个原来的迷途报文就称为 lost duplicate。在关闭一个 TCP 连接后,马上又重新建立起一个相同的 IP 地址和端口之间的 TCP 连接,后一个连接被称为前一个连接的化身,那么有可能出现这种情况,前一个连接的迷途重复报文在前一个连接终止后出现,从而被误解成从属于新的化身。

为了避免这个情 况, TIME_WAIT 状态需要持续 2MSL,因为这样就可以保证当成功建立一个 TCP 连接的时候,来自连接先前化身的重复报文已经在网络中消逝。

1.2 过多的原因

如果服务端出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务端主动断开了很多 TCP 连接。问题来了,什么场景下服务端会主动断开连接呢?

第一个场景:HTTP 没有使用长连接
第二个场景:HTTP 长连接超时
第三个场景:HTTP 长连接的请求数量达到上限

1. HTTP 没有使用长连接

从 HTTP/1.1 开始, 就默认是开启了 Keep-Alive,现在大多数浏览器都默认是使用 HTTP/1.1,所以 Keep-Alive 都是默认打开的。一旦客户端和服务端达成协议,那么长连接就建立好了。

客户端禁用了 HTTP Keep-Alive,服务端开启 HTTP Keep-Alive,谁是主动关闭方?

当客户端禁用了 HTTP Keep-Alive,这时候 HTTP 请求的 header 就会有 Connection:close 信息,这时服务端在发完 HTTP 响应后,就会主动关闭连接。不再重用这个连接的时机就只有在服务端了。

客户端开启了 HTTP Keep-Alive,服务端禁用了 HTTP Keep-Alive,谁是主动关闭方?

当客户端开启了 HTTP Keep-Alive,而服务端禁用了 HTTP Keep-Alive,这时服务端在发完 HTTP 响应后,服务端也会主动关闭连接。

当服务端出现大量的 TIME_WAIT 状态连接的时候,可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive。

2. HTTP 长连接超时

假设设置了 HTTP 长连接的超时时间是 60 秒,nginx 就会启动一个「定时器」,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,nginx 就会触发回调函数来关闭该连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

当服务端出现大量 TIME_WAIT 状态的连接时,如果现象是有大量的客户端建立完 TCP 连接后,很长一段时间没有发送数据,那么大概率就是因为 HTTP 长连接超时,导致服务端主动关闭连接,产生大量处于 TIME_WAIT 状态的连接。

可以往网络问题的方向排查,比如是否是因为网络问题,导致客户端发送的数据一直没有被服务端接收到,以至于 HTTP 长连接超时。

3. HTTP 长连接的请求数量达到上限

Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。

比如 nginx 的 keepalive_requests 这个参数,这个参数是指一个 HTTP 长连接建立之后,nginx 就会为这个连接设置一个计数器,记录这个 HTTP 长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则 nginx 会主动关闭这个长连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

keepalive_requests 参数的默认值是 100 ,意味着每个 HTTP 长连接最多只能跑 100 次请求,这个参数往往被大多数人忽略,因为当 QPS (每秒请求数) 不是很高时,默认值 100 凑合够用。

但是,对于一些 QPS 比较高的场景,比如超过 10000 QPS,甚至达到 30000 , 50000 甚至更高,如果 keepalive_requests 参数值是 100,这时候就 nginx 就会很频繁地关闭连接,那么此时服务端上就会出大量的 TIME_WAIT 状态。

针对这个场景下,解决的方式也很简单,调大 nginx 的 keepalive_requests 参数就行。

1.3 危害

过多的 TIME-WAIT 状态主要的危害有两种:

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源等;
  • 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range参数指定范围。

1.4 优化

TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。

如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT。

打开 sysctl.conf 文件,修改以下几个参数:

1
2
3
net.ipv4.tcp_tw_recycle = 1	 #表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1 # 重新使用TIME_WAIT状态的连接。
net.ipv4.tcp_timestamps = 1 # 需要打开对 TCP 时间戳的支持。

2. CLOSE_WAIT

CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。

所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。

当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close。

3. 头脑风暴

  • 避免提前close,服务器收不到重发。防止迷途报文重发到相同四元组。

  • 服务器过多原因,肯定是服务器主动关的。确认http keepalive使用,超时,数量限制。可以调整linux参数不建议。

4. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!