一、问题描述
1.我们的系统是通过K8S部署的,架构为spring + dubbo+ zookeeper这一套微服务架构。
2.线上用户反馈我们的部分功能不可用,查看K8S内的容器日志,是由于ZOOKEEPER连接不上,导致提供端不能注册到ZOOKEEPER,然后消费端也无法正常调用。
二、分析过程
1.我们在有问题的容器宿主主机上,使用telnet命令连接ZK服务器,发现连上又被服务端拒绝。然而在其它宿主主机上,连接ZK服务器正常。由此分析应该是此台客户端连接ZK的最大连接数已到达,所以ZK服务器限制了此台客户端的远程连接。
2.我们在ZK服务器上使用以下命令,查看ZK服务器2181端口的连接情况。
netstat -na | grep 2181 | grep 172.19.4.135 | grep TIME_WAIT| wc -l
通过统计发现,有问题的宿主主机上有4500个与ZK服务器的连接 处于TIME_WAIT状态,而这台宿主主机上的POD服务只有10个。
普及一下TCP知识
- FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
-
- FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
-
- TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
-
- CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
-
- CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
-
- LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。
推测发现,是服务端(主动断开方)主动断开连接,是由于TCP关闭连接时的四次挥手中的,收到客户端(被动断开方)最后一个FIN结束包,并且返回ACK给客户端后,等待一段时间才回到CLOSED状态。
表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
至于为什么会有大量的TIME_WAIT,应该是连接数达到ZK的单台客户端(60)连接数,ZK客户端不断重连,服务端不断拒绝 断开连接,所以会出现大量的临时TIME_WAIT。
三、处理方案
1.开启linux的TIME_WAIT状态,快速回收,默认时间比较长,改为30S。
vi /etc/sysctl.conf 编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30 然后执行
/sbin/sysctl -p让参数生效。
修改之后,发现大量的TIME_WAIT很快消失了。
2.这时到有问题的宿主主机上,使用TELNET远程连接ZK服务器,发现还是连接被拒绝。
3.发现应该是旧的连接数没有被释放,所以将这台宿主主机的POD全部驱逐,相当于释放进程。
kubectl drain 172.19.4.135--delete-local-data --ignore-daemonsets --force
4.驱逐完毕后,等待有问题的宿主主机的POD都移动到另外的宿主主机上。然后再使用TELNET远程连接ZK服务器,发现可以正常连接上ZK.
5.将有问题的宿主主机恢复调度
kubectl uncordon nodename
6.又有一些POD移到此主机上,然后连接 ZK,注册和服务调用均正常。
7.至此,问题临时解决,后面又对ZK的单台客户端最大连接数,调大为600。
8.原因初步分析,是由于可能部分的网络原因导致 ZK客户端与服务端连接不稳定,然后服务端断开,客户端不断重连,而服务端又不断处于TIME_WAIT的大量连接存在,导致最终达到ZK的最大连接数限制,进入死循环。
四、服务器有大量CLOSE_WAIT状态
1.1 原因
如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程序自己没有进一步发出ACK信号。换句话说,就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。个人觉得这种情况,通过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。
2.2 解决措施
首先考虑程序是否有BUG,是否有socket泄露(遗漏close())。
查看当前系统中的tcp socket状态信息:
# netstat -n | awk '/^tcp/ {++X[$NF]} END {for(i in X) print i, X[i]}'?
另外一个命令:lsof
比如搜索系统中IP为192.168.100.20的远程链接所有打开的套接字:
# lsof -i@192.168.100.20
查代码是解决大量CLOSE_WAIT问题的主要思路。
3、CLOSE_WAIT和TIME_WAIT的区别:
假如,服务器A是一台爬虫服务器,它使用简单的HttpClient去请求资源服务器B上面的apache获取文件资源,正常情况下,如果请求成功,那么在抓取完资源后,服务器A会主动发出关闭连接的请求,这个时候就是主动关闭连接,服务器A的连接状态我们可以看到是TIME_WAIT。如果一旦发生异常呢?假设请求的资源服务器B上并不存在,那么这个时候就会由服务器B发出关闭连接的请求,服务器A就是被动的关闭了连接,如果服务器A被动关闭连接之后程序员忘了让HttpClient释放连接,那就会造成CLOSE_WAIT的状态了。