出现大量close_wait
近日遇到一个线上服务 socket 资源被不断打满的情况。通过各种工具分析线上问题,定位到问题代码。这里对该问题发现、修复过程进行一下复盘总结。
先看两张图。一张图是服务正常时监控到的 socket 状态,另一张当然就是异常啦!
图一:正常时监控
图二:异常时监控
从图中的表现情况来看,就是从 04:00 开始,socket 资源不断上涨,每个谷底时重启后恢复到正常值,然后继续不断上涨不释放,而且每次达到峰值的间隔时间越来越短。
重启后,排查了日志,没有看到 panic ,此时也就没有进一步检查,真的以为重启大法好。
情况说明
该服务使用Golang开发,已经上线正常运行将近一年,提供给其它服务调用,主要底层资源有DB/Redis/MQ。
为了后续说明的方便,将服务的架构图进行一下说明。
图三:服务架构
架构是非常简单。
问题出现在早上 08:20 左右开始的,报警收到该服务出现 504,此时第一反应是该服务长时间没有重启(快两个月了),可能存在一些内存泄漏,没有多想直接进行了重启。也就是在图二第一个谷底的时候,经过重启服务恢复到正常水平(重启真好用,开心)。
将近 14:00 的时候,再次被告警出现了 504 ,当时心中略感不对劲,但由于当天恰好有一场大型促销活动,因此先立马再次重启服务。直到后续大概过了1小时后又开始告警,连续几次重启后,发现需要重启的时间间隔越来越短。此时发现问题绝不简单。这一次重启真的解决不了问题老,因此立马申请机器权限、开始排查问题。下面的截图全部来源我的重现demo,与线上无关。
发现问题
出现问题后,首先要进行分析推断、然后验证、最后定位修改。根据当时的表现是分别进行了以下猜想。
ps:后续截图全部来源自己本地复现时的截图
推断一
socket 资源被不断打满,并且之前从未出现过,今日突然出现,怀疑是不是请求量太大压垮服务
经过查看实时 qps 后,放弃该想法,虽然量有增加,但依然在服务器承受范围(远远未达到压测的基准值)。
推断二
两台机器故障是同时发生,重启一台,另外一台也会得到缓解,作为独立部署在两个集群的服务非常诡异
有了上面的的依据,推出的结果是肯定是该服务依赖的底层资源除了问题,要不然不可能独立集群的服务同时出问题。
由于监控显示是 socket 问题,因此通过 netstat 命令查看了当前tcp链接的情况(本地测试,线上实际值大的多)
/go/src/hello # netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' LISTEN 2 CLOSE_WAIT 23 # 非常异常 TIME_WAIT 1
发现绝大部份的链接处于 CLOSE_WAIT 状态,这是非常不可思议情况。然后用 netstat -an
命令进行了检查。
图四:大量的CLOSE_WAIT
CLOSED 表示socket连接没被使用。 LISTENING 表示正在监听进入的连接。 SYN_SENT 表示正在试着建立连接。 SYN_RECEIVED 进行连接初始同步。 ESTABLISHED 表示连接已被建立。 CLOSE_WAIT 表示远程计算器关闭连接,正在等待socket连接的关闭。 FIN_WAIT_1 表示socket连接关闭,正在关闭连接。 CLOSING 先关闭本地socket连接,然后关闭远程socket连接,最后等待确认信息。 LAST_ACK 远程计算器关闭后,等待确认信号。 FIN_WAIT_2 socket连接关闭后,等待来自远程计算器的关闭信号。 TIME_WAIT 连接关闭后,等待远程计算器关闭重发。
然后开始重点思考为什么会出现大量的mysql连接是 CLOSE_WAIT 呢?为了说清楚,我们来插播一点TCP的四次挥手知识。
TCP四次挥手
我们来看看 TCP 的四次挥手是怎么样的流程:
图五:TCP四次挥手
用中文来描述下这个过程:
Client: 服务端大哥,我事情都干完了,准备撤了
,这里对应的就是客户端发了一个FIN
Server:知道了,但是你等等我,我还要收收尾
,这里对应的就是服务端收到 FIN 后回应的 ACK
经过上面两步之后,服务端就会处于 CLOSE_WAIT 状态。过了一段时间 Server 收尾完了
Server:小弟,哥哥我做完了,撤吧
,服务端发送了FIN
Client:大哥,再见啊
,这里是客户端对服务端的一个 ACK
到此服务端就可以跑路了,但是客户端还不行。为什么呢?客户端还必须等待 2MSL 个时间,这里为什么客户端还不能直接跑路呢?主要是为了防止发送出去的 ACK 服务端没有收到,服务端重发 FIN 再次来询问,如果客户端发完就跑路了,那么服务端重发的时候就没人理他了。这个等待的时间长度也很讲究。
Maximum Segment Lifetime 报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃
这里一定不要被图里的 client/server 和项目里的客户端服务器端混淆,你只要记住:主动关闭的一方发出 FIN 包(Client),被动关闭(Server)的一方响应 ACK 包,此时,被动关闭的一方就进入了 CLOSE_WAIT 状态。如果一切正常,稍后被动关闭的一方也会发出 FIN 包,然后迁移到 LAST_ACK 状态。
既然是这样, TCP 抓包分析下:
/go # tcpdump -n port 3306 # 发生了 3次握手 11:38:15.679863 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [S], seq 4065722321, win 29200, options [mss 1460,sackOK,TS val 2997352 ecr 0,nop,wscale 7], length 0 11:38:15.679923 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [S.], seq 780487619, ack 4065722322, win 28960, options [mss 1460,sackOK,TS val 2997352 ecr 2997352,nop,wscale 7], length 0 11:38:15.679936 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 1, win 229, options [nop,nop,TS val 2997352 ecr 2997352], length 0# mysql 主动断开链接 11:38:45.693382 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [F.], seq 123, ack 144, win 227, options [nop,nop,TS val 3000355 ecr 2997359], length 0 # MySQL负载均衡器发送fin包给我 11:38:45.740958 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 124, win 229, options [nop,nop,TS val 3000360 ecr 3000355], length 0 # 我回复ack给它... ... # 本来还需要我发送fin给他,但是我没有发,所以出现了close_wait。那这是什么缘故呢?
src > dst: flags data-seqno ack window urgent options src > dst 表明从源地址到目的地址flags 是TCP包中的标志信息,S 是SYN标志, F(FIN), P(PUSH) , R(RST) "."(没有标记)data-seqno 是数据包中的数据的顺序号ack 是下次期望的顺序号window 是接收缓存的窗口大小urgent 表明数据包中是否有紧急指针options 是选项
结合上面的信息,我用文字说明下:MySQL负载均衡器 给我的服务发送 FIN 包,我进行了响应,此时我进入了 CLOSE_WAIT 状态,但是后续作为被动关闭方的我,并没有发送 FIN,导致我服务端一直处于 CLOSE_WAIT 状态,无法最终进入 CLOSED 状态。
那么我推断出现这种情况可能的原因有以下几种:
- 负载均衡器 异常退出了,
这基本是不可能的,他出现问题绝对是大面积的服务报警,而不仅仅是我一个服务
- MySQL负载均衡器 的超时设置的太短了,导致业务代码还没有处理完,MySQL负载均衡器 就关闭tcp连接了
这也不太可能,因为这个服务并没有什么耗时操作,当然还是去检查了负载均衡器的配置,设置的是60s。
- 代码问题,MySQL 连接无法释放
目前看起来应该是代码质量问题,加之本次数据有异常,触发到了以前某个没有测试到的点,目前看起来很有可能是这个原因
查找错误原因
由于代码的业务逻辑并不是我写的,我担心一时半会看不出来问题,所以直接使用 perf
把所有的调用关系使用火焰图给绘制出来。既然上面我们推断代码中没有释放mysql连接。无非就是:
- 确实没有调用close
- 有耗时操作(火焰图可以非常明显看到),导致超时了
- mysql的事务没有正确处理,例如:rollback 或者 commit
由于火焰图包含的内容太多,为了让大家看清楚,我把一些不必要的信息进行了折叠。
图六:有问题的火焰图
火焰图很明显看到了开启了事务,但是在余下的部分,并没有看到 Commit 或者是Rollback 操作。这肯定会操作问题。然后也清楚看到出现问题的是:
MainController.update 方法内部,话不多说,直接到 update 方法中去检查。发现了如下代码:
func (c *MainController) update() (flag bool) {o := orm.NewOrm()o.Using("default")o.Begin()nilMap := getMapNil()if nilMap == nil {// 这里只检查了是否为nil,并没有进行rollback或者commitreturn false}nilMap[10] = 1nilMap[20] = 2if nilMap == nil && len(nilMap) == 0 {o.Rollback()return false}sql := "update tb_user set name=%s where id=%d"res, err := o.Raw(sql, "Bug", 2).Exec()if err == nil {num, _ := res.RowsAffected()fmt.Println("mysql row affected nums: ", num)o.Commit()return true}o.Rollback()return false }
至此,全部分析结束。经过查看 getMapNil 返回了nil,但是下面的判断条件没有进行回滚。
if nilMap == nil {o.Rollback()// 这里进行回滚return false }
总结
整个分析过程还是废了不少时间。最主要的是主观意识太强,觉得运行了一年没有出问题的为什么会突然出问题?因此一开始是质疑 SRE、DBA、各种基础设施出了问题(人总是先怀疑别人)。导致在这上面费了不少时间。
理一下正确的分析思路:
- 出现问题后,立马应该检查日志,确实日志没有发现问题;
- 监控明确显示了socket不断增长,很明确立马应该使用
netstat
检查情况看看是哪个进程的锅; - 根据
netstat
的检查,使用tcpdump
抓包分析一下为什么连接会被动断开(TCP知识非常重要); - 如果熟悉代码应该直接去检查业务代码,如果不熟悉则可以使用
perf
把代码的调用链路打印出来; - 不论是分析代码还是火焰图,到此应该能够很快定位到问题。
那么本次到底是为什么会出现 CLOSE_WAIT 呢?大部分同学应该已经明白了,我这里再简单说明一下:
由于那一行代码没有对事务进行回滚,导致服务端没有主动发起close。因此 MySQL负载均衡器 在达到 60s 的时候主动触发了close操作,但是通过tcp抓包发现,服务端并没有进行回应,这是因为代码中的事务没有处理,因此从而导致大量的端口、连接资源被占用。在贴一下挥手时的抓包数据:
# mysql 主动断开链接 11:38:45.693382 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [F.], seq 123, ack 144, win 227, options [nop,nop,TS val 3000355 ecr 2997359], length 0 # MySQL负载均衡器发送fin包给我 11:38:45.740958 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 124, win 229, options [nop,nop,TS val 3000360 ecr 3000355], length 0 # 我回复ack给它
希望此文对大家排查线上问题有所帮助。为了便于帮助大家理解,下面附上正确情况下的火焰图与错误情况下的火焰图。大家可以自行对比。
- 正确情况下的火焰图 : https://dayutalk.cn/img/right.svg
- 错误情况的火焰图 : https://dayutalk.cn/img/err.svg
我参考的一篇文章对这种情况提出了两个思考题,我觉得非常有意义,大家自己思考下:
- 为什么一台机器几百个 CLOSE_WAIT 就导致不可继续访问?我们不是经常说一台机器有 65535 个文件描述符可用吗?
- 为什么我有负载均衡,而两台部署服务的机器确几乎同时出了 CLOSE_WAIT ?
参考文章:
- 又见CLOSE_WAIT
- TCP 4-times close : https://wiki.wireshark.org/TCP%204-times%20close
出现大量close_wait相关推荐
- TCP端口状态说明ESTABLISHED、TIME_WAIT、 CLOSE_WAIT
一. 首先说下tcp端口的几种状态: 1.LISTENING状态 FTP服务启动后首先处于侦听(LISTENING)状态. 2.ESTABLISHED状态 ESTABLISHED的意思是建立连接.表示 ...
- 系统调优,你所不知道的TIME_WAIT和CLOSE_WAIT
https://my.oschina.net/fdhay/blog/638631 高性能网络 | 你所不知道的TIME_WAIT和CLOSE_WAIT 2016-02-18 大房 大房说 本文是我将最 ...
- java项目close wait_服务器TIME_WAIT和CLOSE_WAIT详解和解决办法
昨天解决了一个HttpClient调用错误导致的服务器异常,具体过程如下: 里头的分析过程有提到,通过查看服务器网络状态检测到服务器有大量的CLOSE_WAIT的状态. 在服务器的日常维护过程中,会经 ...
- CLOSE_WAIT 和TCP_KEEPALIVE
之前面试 曾经被问到 CLOSE_WAIT 状态意味这什么(服务端收到FIN 包后 还没有close fd,存在fd 泄漏的风险) 问题现象: 代理报错 too many open files, ul ...
- 再谈应用环境下的TIME_WAIT和CLOSE_WAIT
昨天解决了一个HttpClient调用错误导致的服务器异常,具体过程如下: http://blog.csdn.net/shootyou/article/details/6615051 里头的分析过程有 ...
- TIME_WAIT和CLOSE_WAIT状态
下图了解各种状态: 如果服务器出现了异常,很大的可能是出现了以下两种情况: 服务器保持了大量的TIME_WAIT状态. 服务器保持了大量的CLOSE_WAIT状态. TIME_WAIT状态产生原因: ...
- CLOSE_WAIT状态的原因与解决方法 --转
转自:http://blog.chinaunix.net/uid-20357359-id-1963662.html 这个问题之前没有怎么留意过,是最近在面试过程中遇到的一个问题,面了两家公司,两家公司 ...
- python close_wait_线上大量CLOSE_WAIT原因深入分析
这一次重启真的无法解决问题了:一次 MySQL 主动关闭,导致服务出现大量 CLOSE_WAIT 的全流程排查过程. 近日遇到一个线上服务 socket 资源被不断打满的情况.通过各种工具分析线上问题 ...
- 通讯系统经验谈【一】TCP连接状态分析:SYNC_RECV,CLOSE_WAIT,TIME_WAIT
摘自:http://maoyidao.iteye.com/blog/1744277 面试时看到应聘者简历中写精通网络,TCP编程,我常问一个问题,TCP建立连接需要几次握手?95%以上的应聘者都能答对 ...
- TCP close_wait 状态的解释
CLOSE_WAIT状态的生成原因 首先我们知道,如果我们的Client程序处于CLOSE_WAIT状态的话,说明套接字是被动关闭的! 因为如果是Server端主动断掉当前连接的话,那么双方关闭这个T ...
最新文章
- 阿里巴巴测试环境稳定性提升实践
- 分享做LOGO 的方法和思维方式 -liuleihai
- unity 文本输入框_unity实战项目[我是食神黑长直]1星难度.含源码
- input Type
- Win10 Build9926 更新问题解决
- bitmap存入mysql,[MySQL] mysql中bitmap的简单运用
- 搞定Prometheus普罗米修斯监控系统
- (转)Silverlight 巧用文件生成时间强制客户端载入最新xap文件
- 查看电脑系统基本信息
- 禁用迅雷极速版自动升级为迅雷x
- 算法的时间复杂度 递推
- 每日算法 - 列出24点游戏的所有解法
- 2-- 关机/重启 、 创建 / 切换目录
- 产品经理学项目管理05:项目管理的目标
- 网络连通性测试ping和tracert命令
- [译]C++ 协程:理解 co_await 运算符
- 俞敏洪励志演讲稿【三】
- Android移动游戏之崛起
- 《 线性代数及其应用 (原书第4版)》——1.4 矩阵方程
- kesioncms (科讯cms) 6.x-8.x版本写入任意内容文件漏洞
热门文章
- 对于求职的一些个人看法及总结
- Sql Server数据库由于未知原因丢失的恢复方法
- 多项式算法3:多项式除法
- 1.24版本k8s集群安装部署rook-ceph存储集群
- Popper.js 使用方法
- 电表远程抄表系统如何保证数据传输的准确性
- 数据库周刊54丨2020 年度报告:PingCAP、腾讯云数据库、人大金仓、GoldenDB ;CPU 100% SQL优化案例;Mysql内存溢出处理;避免删库跑路黑天鹅……
- ROS下进行串口通讯
- JS的字符串操作和各种格式转换
- 新风作浪博客学习(八)代码实现UIPickerView .