https://github.com/singgel?tab=repositories
问题描述:
最近性能测试碰到的一个问题。客户端使用NIO,服务器还是一般的Socket连接。当测试进行一段时间以后,发现服务器端的系统出现大量未释放的网络连 接。用netstat -na查看,连接状态为CLOSE_WAIT。这就奇怪了,为什么Socket已经关闭而连接依然未释放。
注意根源是因为网络是双关的,双向概念
三次握手:
三次握手:
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,今天balabala……”
两次握手:
“喂,你听得到吗?”
“我听得到呀”
“喂喂,你听得到吗?”
“草,我听得到呀!!!!”
“你TM能不能听到我讲话啊!!喂!”
“……”
四次握手:
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,你能听到我吗?”
“……不想跟傻逼说话”
四次挥手:
四次挥手:
A:“喂,我不说了。”A->FIN_WAIT1
B:“我知道了。等下,上一句还没说完。Balabala…..”B->CLOSE_WAIT | A->FIN_WAIT2
B:”好了,说完了,我也不说了。”B->LAST_ACK
A:”我知道了。”A->TIME_WAIT | B->CLOSED
A等待2MSL,保证B收到了消息,否则重说一次”我知道了”,A->CLOSED
三次握手转自知乎四次挥手转自知乎
解决:
Google了半天,发现关于CLOSE_WAIT的问题一般是C的,Java似乎碰到这个问题的不多(这有一篇不错的,也是解决CLOSE_WAIT的,但是好像没有根本解决,而是选择了一个折中的办法)。接着找,由于使用了NIO,所以怀疑可能是这方面的问题,结果找到了这篇。顺着帖子翻下去,其中有几个人说到了一个问题—— 一端的Socket调用close后,另一端的Socket没有调用close.于是查了一下代码,果然发现Server端在某些异常情况时,没有关闭Socket。改正后问题解决。
分析:
上面我碰到的这个问题主要因为TCP的结束流程未走完,造成连接未释放。现设客户端主动断开连接,流程如下
Client 消息 Server
close()
------ FIN ------->
FIN_WAIT1 CLOSE_WAIT
<----- ACK -------
FIN_WAIT2
close()
<------ FIN ------
TIME_WAIT LAST_ACK
------ ACK ------->
CLOSED
CLOSED
如上图所示,由于Server的Socket在客户端已经关闭时而没有调用关闭,造成服务器端的连接处在“挂起”状态,而客户端则处在等待应答的状态上。此问题的典型特征是:一端处于FIN_WAIT2 ,而另一端处于CLOSE_WAIT. 不过,根本问题还是程序写的不好,有待提高。
TIME_WAIT状态:
根据TCP协议,主动发起关闭的一方,会进入TIME_WAIT状态,持续2*MSL(Max Segment Lifetime),缺省为240秒,在这个post中简洁的介绍了为什么需要这个状态。
值得一说的是,对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可 想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态,假如server一秒钟接收1000个请求,那么就会积压240*1000=240,000个 TIME_WAIT的记录,维护这些状态给Server带来负担。当然现代操作系统都会用快速的查找算法来管理这些TIME_WAIT,所以对于新的 TCP连接请求,判断是否hit中一个TIME_WAIT不会太费时间,但是有这么多状态要维护总是不好。
HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个 request/response,一个主要原因就是发现了这个问题。还有一个方法减缓TIME_WAIT压力就是把系统的2*MSL时间减少,因为 240秒的时间实在是忒长了点,对于Windows,修改注册表,在HKEY_LOCAL_MACHINE/ SYSTEM/CurrentControlSet/Services/ Tcpip/Parameters上添加一个DWORD类型的值TcpTimedWaitDelay,一般认为不要少于60,不然可能会有麻烦。
对于大型的服务,一台server搞不定,需要一个LB(Load Balancer)把流量分配到若干后端服务器上,如果这个LB是以NAT方式工作的话,可能会带来问题。假如所有从LB到后端Server的IP包的 source address都是一样的(LB的对内地址),那么LB到后端Server的TCP连接会受限制,因为频繁的TCP连接建立和关闭,会在server上留 下TIME_WAIT状态,而且这些状态对应的remote address都是LB的,LB的source port撑死也就60000多个(2^16=65536,1~1023是保留端口,还有一些其他端口缺省也不会用),每个LB上的端口一旦进入 Server的TIME_WAIT黑名单,就有240秒不能再用来建立和Server的连接,这样LB和Server最多也就能支持300个左右的连接。 如果没有LB,不会有这个问题,因为这样server看到的remote address是internet上广阔无垠的集合,对每个address,60000多个port实在是够用了。
一开始我觉得用上LB会很大程度上限制TCP的连接数,但是实验表明没这回事,LB后面的一台Windows Server 2003每秒处理请求数照样达到了600个,难道TIME_WAIT状态没起作用?用Net Monitor和netstat观察后发现,Server和LB的XXXX端口之间的连接进入TIME_WAIT状态后,再来一个LB的XXXX端口的 SYN包,Server照样接收处理了,而是想像的那样被drop掉了。翻书,从书堆里面找出覆满尘土的大学时代买的《UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI》,中间提到一句,对于BSD-derived实现,只要SYN的sequence number比上一次关闭时的最大sequence number还要大,那么TIME_WAIT状态一样接受这个SYN,难不成Windows也算BSD-derived?有了这点线索和关键字 (BSD),找到这个post,在NT4.0的时候,还是和BSD-derived不一样的,不过Windows Server 2003已经是NT5.2了,也许有点差别了。
做个试验,用Socket API编一个Client端,每次都Bind到本地一个端口比如2345,重复的建立TCP连接往一个Server发送Keep-Alive=false 的HTTP请求,Windows的实现让sequence number不断的增长,所以虽然Server对于Client的2345端口连接保持TIME_WAIT状态,但是总是能够接受新的请求,不会拒绝。那 如果SYN的Sequence Number变小会怎么样呢?同样用Socket API,不过这次用Raw IP,发送一个小sequence number的SYN包过去,Net Monitor里面看到,这个SYN被Server接收后如泥牛如海,一点反应没有,被drop掉了。
按照书上的说法,BSD-derived和Windows Server 2003的做法有安全隐患,不过至少这样至少不会出现TIME_WAIT阻止TCP请求的问题,当然,客户端要配合,保证不同TCP连接的sequence number要上涨不要下降。
more:
CLOSED状态:
起初每个socket都是CLOSED状态,当客户端初使化一个连接,他发送一个SYN包到服务器,客户端进入SYN_SENT状态。服务器接收到SYN包,反馈一个SYN-ACK包,客户端接收后返馈一个ACK包客户端变成ESTABLISHED状态,如果长时间没收到SYN-ACK包,客户端超时进入CLOSED状态。
当服务器绑定并监听某一端口时,socket的状态是LISTEN,当客户企图建立连接时,服务器收到一个SYN包,并反馈SYN-ACK包。服务器状态变成SYN_RCVD,当客户端发送一个ACK包时,服务器socket变成ESTABLISHED状态。
当一个程序在ESTABLISHED状态时有两种图径关闭它,第一是主动关闭,第二是被动关闭。如果你要主动关闭的话,发送一个FIN包。当你的程序closesocket或者shutdown(标记),你的程序发送一个FIN包到peer,你的socket变成FIN_WAIT_1状态。peer反馈一个ACK包,你的socket进入FIN_WAIT_2状态。如果peer也在关闭连接,那么它将发送一个FIN包到你的电脑,你反馈一个ACK包,并转成TIME_WAIT状态。TIME_WAIT状态又号2MSL等待状态。MSL意思是最大段生命周期 (Maximum+Segment+Lifetime)表明一个包存在于网络上到被丢弃之间的时间。每个IP包有一个TTL(time_to_live),当它减到0时则包被丢弃。每个路由器使TTL减一并且传送该包。当一个程序进入TIME_WAIT状态时,他有2个MSL的时间,这个充许TCP重发最后的ACK,万一最后的ACK丢失了,使得FIN被重新传输。在2MSL等待状态完成后,socket进入CLOSED状态。
被动关闭:当程序收到一个FIN包从peer,并反馈一个ACK包,于是程序的socket转入CLOSE_WAIT状态。因为peer已经关闭了,所以不能发任何消息了。但程序还可以。要关闭连接,程序自已发送给自已FIN,使程序的TCP socket状态变成LAST_ACK状态,当程序从peer收到ACK包时,程序进入CLOSED状态。
解决方案:
通常,CLOSE_WAIT 状态在服务器停留时间很短,如果你发现大量的 CLOSE_WAIT 状态,那么就意味着被动关闭的一方没有及时发出 FIN 包,一般有如下几种可能:
- 程序问题:如果代码层面忘记了 close 相应的 socket 连接,那么自然不会发出 FIN 包,从而导致 CLOSE_WAIT 累积;或者代码不严谨,出现死循环之类的问题,导致即便后面写了 close 也永远执行不到。
- 响应太慢或者超时设置过小:如果连接双方不和谐,一方不耐烦直接 timeout,另一方却还在忙于耗时逻辑,就会导致 close 被延后。响应太慢是首要问题,不过换个角度看,也可能是 timeout 设置过小。
- BACKLOG 太大:此处的 backlog 不是 syn backlog,而是 accept 的 backlog,如果 backlog 太大的话,设想突然遭遇大访问量的话,即便响应速度不慢,也可能出现来不及消费的情况,导致多余的请求还在队列里就被对方关闭了。
如果你通过「netstat -ant」或者「ss -ant」命令发现了很多 CLOSE_WAIT 连接,请注意结果中的「Recv-Q」和「Local Address」字段,通常「Recv-Q」会不为空,它表示应用还没来得及接收数据,而「Local Address」表示哪个地址和端口有问题,我们可以通过「lsof -i:<PORT>」来确认端口对应运行的是什么程序以及它的进程号是多少。
如果是我们自己写的一些程序,比如用 HttpClient 自定义的蜘蛛,那么八九不离十是程序问题,如果是一些使用广泛的程序,比如 Tomcat 之类的,那么更可能是响应速度太慢或者 timeout 设置太小或者 BACKLOG 设置过大导致的故障。
此外还有一点需要说明:按照前面图例所示,当被动关闭的一方处于 CLOSE_WAIT 状态时,主动关闭的一方处于 FIN_WAIT2 状态。 那么为什么我们总听说 CLOSE_WAIT 状态过多的故障,但是却相对少听说 FIN_WAIT2 状态过多的故障呢?这是因为 Linux 有一个「tcp_fin_timeout」设置,控制了 FIN_WAIT2 的最大生命周期。坏消息是 CLOSE_WAIT 没有类似的设置,如果不重启进程,那么 CLOSE_WAIT 状态很可能会永远持续下去;好消息是如果 socket 开启了 keepalive 机制,那么可以通过相应的设置来清理无效连接,不过 keepalive 是治标不治本的方法,还是应该找到问题的症结才对。
推荐两个案例:
time_wait案例
修改so_linger或者so_reusaddr解决
https://zhuanlan.zhihu.com/p/151749320
https://www.cnblogs.com/my_life/articles/5174585.html
close_wait案例
httpclient inputstream没有close案例:
https://blog.csdn.net/shootyou/article/details/6615051
netty 开启autoread案例:
https://www.jianshu.com/p/8f22675d71ac
tomcat中的业务线程阻塞案例: