0%

CLOSE_WAIT和TIME_WAIT

服务器经常出现的两个长时间占socket连接的TCP状态。

TCP关闭时:

  • 主动关闭: 发FIN(FIN_WAIT_1) –> 收ACK(FIN_WAIT_2) –> 收FIN,发ACK(TIME_WAIT), TIME_WAIT 会持续 2*MSL(1-4分钟)
  • 被动关闭: 收FIN(CLOSE_WAIT) –> 发ACK –> 发FIN(LAST_ACK) –> 收ACK(CLOSED), 如果程序不主动调用 close(fd) 关闭套接字,就不会主动发送FIN,就会一直在 CLOSE_WAIT 状态

1 CLOSE_WAIT

1.1 是什么

CLOSE_WAIT存在于被动关闭连接的情况,一般是服务器被动关闭,所以一般需要服务器去解决。

1.2 产生的原因

主要有两点:

  1. 代码中没有写关闭连接的代码,存在bug;
  2. 该连接的业务代码处理事件太长,代码还在处理,对方已经发起断开连接的请求了;这时候会存在一段时间的 CLOS_WAIT,直到服务端处理到这里。

总的来说,就是服务端没有及时调用close()关闭套接字。

1.3 一直不关闭,最多会存在多长时间

内核会定时探测TCP连接是否存在,如果不存在,会自动关闭该TCP,回收系统资源。内核依靠如下参数来探测:

  • tcp_keepalive_time(7200): 如果在该参数指定的秒数内,TCP连接一直处于空闲,内核就开始向对端发起探测,看对端是否还在;
  • tcp_keepalive_intvl(75): 发起探测时,每隔这么多秒数,探测一次;
  • tcp_keepalive_probes(9): 总共探测这么多次;

所以CLOSE_WAIT状态最多维持 tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes = 7200 +75*9 = 7875s,约130分钟,2小时多一点。

这只是内核的限制,还可以在程序里使用 setsocketopt(fd, SOL_TCP, TCP_KEEPIDLE...) 对每个TCP连接设置。

1.4 有什么影响

CLOSE_WAIT状态的端口表示正在被占用,如果没有设置端口复用,这个端口就变得不可用,过多的CLOSE_WAIT会耗尽系统的可用端口,新的连接进不来,服务变得不可用。

1.5 如何解决

  1. 程序方面,需要及时处理连接被动关闭的状态,特别是 recv() 收到0字节,表示对端关闭了;
  2. 系统方面,可以将 keepalive 相关的参数设置的小一些,让系统尽快探测到TCP不可用,尽快回收资源。

2 TIME_WAIT

2.1 是什么

主动关闭的时候, 收到对端发送FIN, 发送最后的ACK, 进入TIME_WAIT状态,会持续2*MSL(1-4分钟)。MSL(Maximum Segment Lifetime)是TCP协议中建议的值,为2分钟,表示一个TCP数据包在网络中可以存活的最长时间,目的是防止最后的ACK没有被对端收到,对端重发了最后的FIN,如果回收了四元组,就无法处理这个重发的FIN。这个是主动关闭一方必定会进入的状态。

2.2 影响

CLOSE_WAIT一样,过多的TIME_WAIT状态会占用四元组,导致新的连接进不来。

2.3 内核参数控制

相关参数:

net.ipv4.tcp_tw_reuse = 1

net.ipv4.tcp_tw_reuse = 1开启TIME_WAIT状态重用,允许处于该状态的四元组重新用于新TCP连接,默认为0。理想情况是,开启后,处于TIME_WATI状态的连接如果收到数据包,都应该是新建连接,但在某些情况下,会收到旧的连接延迟发来的数据包,造成误处理。为了解决该问题,在开启了tw复用的时候,内核会利用RFC 1323增加的两个4字节的时间戳选项(用于标记数据包的发送时间),在内部记录之前连接的时间戳,收到新的数据包后,如果新的时间戳比之前存储的更大,说明是新的连接,否则就拒绝该连接。

如果是客户端主动关闭连接,开启重用后,间隔1s就可以马上使用处于TIME_WAIT的连接来新建连接。

如果是服务端主动关闭连接,开启重用后,因为本地ip和端口固定,如果客户端使用之前的ip和端口重连,并且时间戳比之前的新,则可以立刻重用。有种情况是,如果多个客户端处于NAT之后,NAT转换出来数据包没有更新时间戳,导致服务端看到的是1个客户端使用不同的端口发起多个连接,而且可能重用之前的端口,服务端收到的时间戳也比之前的旧,导致四元组无法重用,无法新建连接。

net.ipv4.tcp_tw_recycle = 1

开启TIME_WAIT状态快速回收,默认是2*MSL(1-4分钟),开启后可以在1个RTO(Retrasmission Timeout)时间就回收,该时间是动态计算的,可以在毫秒级别;默认是0关闭;

禁用时,内核不会检查时间戳;开启后,会检查时间戳,如果发来的时间戳乱跳,会当成旧连接的重传数据,而不是新的连接,直接丢掉,造成大量丢包,无法新建连接等。时间戳乱掉的情况一般也是由于多个客户端在同一NAT之后,导致服务端看到的是1个客户端的多个连接。现象是客户端发送了SYN,但服务端认为是旧连接的数据,直接丢掉,不应答ACK

tcp_tw_recycle是比tcp_tw_reuse更激进的配置,会立刻释放服务器资源,但也会造成大量丢包。

net.ipv4.tcp_timestamps = 1

开启内核时间戳检查,会影响前两个设置。

3 FIN_WAIT_2

主动关闭时,如果没有收到对端FIN,会进入FIN_WAIT_2状态, 可以设置net.ipv4.tcp_fin_timeout=30调整等待时间,如果超过这个时间,回收该连接。

4 总结

不管是CLOSE_WAIT还是TIME_WAIT, 都是保证TCP正常使用的必要状态,但在高并发的情况下,需要做些取舍,以适应一些极端情况:

  • net.ipv4.tcp_tw_reuse = 1
    可以快速重用TIME_WAIT状态的四元组,允许更多短连接,但在开启net.ipv4.tcp_timestamps = 1会导致有时无法新建连接;并且会降低TCP安全性;

  • net.ipv4.tcp_tw_recycle = 1
    net.ipv4.tcp_tw_reuse更激进,快速回收TIME_WAIT资源,允许更多短连接,但也会引起有时无法新建连接的情况;

  • net.ipv4.tcp_timestamps = 1
    关闭后可以简化TCP握手流程,但对一些依赖时间戳的功能,比如拥塞控制算法或RTO重传超时测量等。

参考:

  1. https://nestealin.com/2e257c1d/
  2. https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux