golang 服务大量 CLOSE_WAIT 故障排查 (2)

一时蒙蔽,synapse-nhttp 这个是什么程序,当时不确定全是 tcp 网络连接的 fd,情急之下只顾着导出最全的网络信息执行了 netstat -a ,没有带上 -n -p 转换端口为数字同时输出执行程序。~_~

这个 synapse-nhttp 非常可疑,同时查看了其他 go 程序机器都没有这个连接,为了排查是否程序本身问题,查看了 pre、qa 机器的连接,均都是 synapse-nhttp 这个端口名字。

判断下来 synapse-nhttp 确实是我们自己的程序,但是为什么端口名字会是 synapse-nhttp,后来查询下来发现我们程序使用的 8280 端口就是 synapse-nhttp 默认端口,所以被 netstat 自动人性化解析了。~_~

由于请求链路经过 sidecar 进来,大量的 CLOSE_WAIT 被动关闭状态,开始怀疑 sidecar 问题,保险起见我们采用排除法先将一个机器的量切到走域名做灰度测试,看是 sidecar 问题还是程序本身问题。

我们发现一个有意思的现象,CLOSE_WAIT 是被动关闭连接的状态,主动关闭连接的状态应该是 FIN_WAIT1。比较了两种状态连接数不是一个数量级,CLOSE_WAIT 将近1w个,而 FIN_WAIT1 只有几个,同时 FIN_WAIT2 只有几十个,TIME_WAIT一个没有。

合理情况下,sidecar 连接的 FIN_WAIT1 状态和本机程序连接的 CLOSE_WAIT 状态应该是一个数量级才对。但是现在明显被动关闭并没有成功完成,要么是上游 sidecar 主动断开了连接,本机程序迟迟未能发送 fin ack,sidecar 端的连接被 tcp keepalive 保活关闭释放了。或者本机程序已经发出 fin ack 但是 sidecar 没有收到,还有一种可能就是,sidecar 端连接在收到 fin ack 前被回收了。

当然,这些只是猜测,为了搞清楚具体什么原因导致只能抓包看 tcp 交互才能得出最终结论。

【tcpdump 包分析】
我们准备好 tcpdump 脚本,定期抓取 tcp 包,现在就在等故障出现了,因为故障一定还会出现。果然在30号下午又出现了,我们一阵激动准备分析dump文件,但是端口抓错了,sidecar 和程序都是本机调用走的是本地环回 lo 虚拟网卡接口,调整脚本在耐心的等待。~_~

问题又如期而至,我们开始分析包。

vim

可以很清楚看到 HTTP 请求有进来没有返回的。第一个红框是请求超时,上游主动关闭连接,超时时间大概是1s,服务器正常返回了 fin ack。第二个红框大概是间隔了一分半钟,主动关闭连接,但是直接返回 RST 重置标志,原先的连接其实已经不存在了。

为了验证这个请求为什么没有返回,我们提取 tcpdump 中的 HTTP 请求到后端日志查看发现到了服务器,我们再从 Mysql 服务器请求 sql 中查看发现没有这个请求没有进来,同时我们发现一个规律,故障期间 DB 非活动连接数都有持续跑高现象,非常规律。

vim

基本上断定是 DB 连接池泄漏,开始排查代码。

发现代码中有一个方法有问题,这个方法之前一直没有业务规则命中,故障前一天26号有一个业务方开始走到这个方法。这个方法有一个隐藏bug,会导致 go 连接无法关闭。

这个bug其实也有go.sql原生库的一半责任。

var r *sql.Rows if r, err = core.GetDB().NewSession(nil).SelectBySql(query).Rows(); err != nil { return } for r.Next() { if err = r.Scan(&sum); err != nil { applog.Logger.Error(fmt.Sprint("xxx", err)) r.Close() // 由于没有主动close连接导致泄漏 return } }

sql.Rows 的Scan方法内部由于没有判断查询DB返回的空,就直接转换导致 converting panic 。在加上我们这个方法没有处理 panic 情况,所以命中隐藏bug就会泄漏。

这个方法为什么不主动关闭连接是因为 sql.Rows 扫描到最后会做关闭动作,所以一直以来都很好。

所以真正的问题是由于 连接池泄漏,导致所有的请求 goroutine block 在获取连接地方的地方,这一类问题排查起来非常困难。

总结

1.回顾这整个排查过程,我觉得让系统运行的健康状态透明化才是发现问题的最有效手段,代码不出问题不现实。

2._go.sql_ 库还谈不上企业级应用,整个连接消耗、空闲和工作时长都是没有监控的,这也是导致这个case无法快速定位的原因。包括go的其他方面都存在很多不完善的地方,尤其是企业级应用套件都很弱,包括_go_原生 dump 内存之后分析的套件。

3.整个排查还是受到了一些噪音干扰,没能坚定核心逻辑和理论。DB 连接跑高为什么没注意到,这一点其实是因为我们一般只看当时故障前后半小时后指标,没有拉长看最近一段时间规律是否有异样,包括 sidecar 流量持续下掉是因为都是存量请求,请求逐渐被 _hang_住,导致量持续下掉,所以看上去感觉请求变少了,因为并没有多出流量。

4.其实线上故障一旦被定位之后,问题本身都很简单,一行不起眼的代码而已。所以我们必须敬畏每一行代码。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zygffx.html