TCP关闭时
- 主动关闭: 发FIN(FIN_WAIT_1) --> 收ACK(FIN_WAIT_2) --> 收FIN(TIME_WAIT) --> 发ACK, TIME_WAIT会持续 2*MSL(1-4分钟)
- 被动关闭: 收FIN(CLOSE_WAIT) --> 发ACK --> 发FIN(LAST_ACK) --> 收ACK(CLOSED), 如果程序不主动调用 close(fd) 关闭套接字,就不会主动发送FIN,就会一直在 CLOSE_WAIT 状态
所以 CLOSE_WAIT 存在于被动关闭连接的情况,一般是服务器被动关闭,所以一般需要服务器去解决。
2. 产生的原因主要有两点:
- 代码中没有写关闭连接的代码,存在bug;
- 该连接的业务代码处理事件太长,代码还在处理,对方已经发起断开连接的请求了;这时候会存在一段时间的 CLOS_WAIT,直到服务端处理到这里。
总的来说,就是服务端没有及时调用close()
关闭套接字。
内核会定时探测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连接设置。
CLOSE_WAIT状态的端口表示正在被占用,如果没有设置端口复用,这个端口就变得不可用,过多的CLOSE_WAIT会耗尽系统的可用端口,新的连接进不来,服务变得不可用。
5. 如何解决- 程序方面,需要及时处理连接被动关闭的状态,特别是
recv()
收到0字节,表示对端关闭了; - 系统方面,可以将 keepalive 相关的参数设置的小一些,让系统尽快探测到TCP不可用,尽快回收资源。
- FIN_WAIT_2: 等待对端发送 FIN
- TIME_WAIT: 等待2MSL
调用了shutdown()
,但是没有调用close()
,就会等在FIN_WAIT_2状态。跟这个状态有关的内核参数是:
- net.ipv4.tcp_fin_timeout(30): 注意,这个参数只对调用了
close()
的孤儿tcp有效,如果程序只是调用了shutdown()
,但是没有close()
,该参数并不起作用。
一般主动关闭连接的一方较常见 TIME_WAIT,产生的原因是主动关闭的时候,要等着对端收到了最后一个ACK才回收相应资源,防止对端没有收到ACK又重新发送最后一个FIN。但这也是占用了一个端口,跟这个状态有关的参数是:
- net.ipv4.tcp_max_tw_buckets(180000): TIME_WAIT的最大数量,超过这个会被立刻清理,并打印警告信息
- net.ipv4.tcp_tw_timeout(60): TIME_WAIT的过期时间
- net.ipv4.tcp_tw_reuse(0): TIME_WAIT状态重用
- net.ipv4.tcp_recycle(0): 开启 TIME_WAT 快速回收。不再是2MSL,而是一个RTO(动态计算的数据包重传超时时间)。还需要tcp_timestamps保证旧连接的数据包不会被新连接接收。NAT环境可能导致drop掉SYN包,无法发起新的连接;
- net.ipv4.tcp_timestamps(1): 检查请求数据包的时间戳,快速回收需要配合这个时间戳选项;
简单来说就是NAT会修改目的IP,但有的不修改tcp头里的timestamp,导致不同的客户端通过NAT过来的数据包时间戳不一样,时间戳小的就接不进server,所以server一般不开启 net.ipv4.tcp_recycle。