…
服务器经常出现的两个长时间占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 产生的原因
主要有两点:
- 代码中没有写关闭连接的代码,存在bug;
- 该连接的业务代码处理事件太长,代码还在处理,对方已经发起断开连接的请求了;这时候会存在一段时间的 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 如何解决
- 程序方面,需要及时处理连接被动关闭的状态,特别是
recv()
收到0字节,表示对端关闭了; - 系统方面,可以将 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重传超时测量等。
参考: