前言
大家好,我是路由器没有路。
在线上运行的系统中,问题的出现是不可避免的。如何快速、准确地排查问题,是每个技术人员都需要掌握的技能。
本文将分享一个线上问题排查的过程和总结,希望对大家有所帮助。
问题表现
我们的商城系统有一个问题:有用户反馈打开商城首页有偶现打不开的情况。
前端查了下反馈说接口超时导致,但是查 cgi 接口进程还在,也无相关报错日志。
假设导致问题的原因
我们首先对问题进行假设,以便更好地进行排查。我们认为问题可能是由以下几种情况导致的:
- 网络异常导致
- 底层服务异常导致
- 底层服务慢查询导致
- 服务查询数据量大,回包给 cgi 有问题导致
- 接口有 panic 然后重启导致
小心验证
接下来,我们需要对每一种假设进行验证,以排除不符合实际的情况。
- 在网络正常的情况下,自己进商城首页试了下,确实也偶现该问题;接着让运维查了下网络带宽,也是正常的;排除网络异常导致。
- 查了下底层服务日志,日志无异常报错,进程也无重启情况,排除服务异常导致。
- 运维有工具监控慢查询的情况,有慢查询的话会有统计和告警的,查了下慢查询日志没有对应的慢查询;排除慢查询导致。
- 查了下该服务对应的 pl 日志,即框架平台日志(平台会有记录每次请求的包大小情况),看请求的包大小在正常阈值范围内,也无报包太大的错误,排除回包太大导致。
- 在日志系统查了下该接口的日志,并没有发现有 panic 或者其它异常的报错信息;此时一筹莫展,想着再看下部署机器上的接口进程是否都正常,仔细看发现有个别机器上的接口服务进程有重启过的情况,问了下同事也没有重启和发布的情况;猜测是代码有 bug 导致,于是再仔细 review 下代码的实现,果真是代码有 bug 导致的。
Bug 分析
通过排查,我们发现问题是由代码 bug 导致的。
具体来说,平台框架代码有这么个实现:请求进来时 main goroutine
有一个 defer recover panic
的实现,如下所示:
正常如果接口有 panic
的情况会被框架 recover
掉,然后打印相应的错误日志,即在日志系统可以查到对应的报错信息。
但是在排查问题时,之所以没有查到有对应 panic
的日志的原因是:在 cgi 接口业务代码的一个方法中起了多个 goroutine
,即开了并发处理一些业务数据汇总的逻辑,而这些新开的 goroutine
中并没有做 defer recover
,也就是说当这些 goroutine
中发生异常 panic
的话,整个接口就会崩溃退出。
而这里的 bug 恰恰就是开多个 goroutine
并发写一个共享对象 map
(mapFloorId2Idx) 产生竞争导致的(会报 fatal error: concurrent map writes 的异常信息),然后这里程序会重启的原因是有后台监控进程监控到并拉起。实现代码如下:
大家可以想下为什么框架外层的 main goroutine 捕获不到其它 goroutine 的 panic?
原因很简单,就是 Golang 中的 goroutine 没办法跨协程 recover,从而导致程序崩掉。
Golang 在发生 panic 时,是先找到本 goroutine,再在这个 goroutine 里的 defer 函数里看看有没有 recover。
解决方案
通过分析,我们找到了问题的根本原因:开多个 goroutine 并发写一个共享对象 map 产生竞争导致的。为了解决这个问题,我们需要做两个方面的工作:
- 在多 goroutine 操作共享的对象 map 时上锁;
- 内部的 goroutine 函数要 defer recover。
总结
在排查线上问题时,我们需要做到以下几点:
- 快速响应:一旦发现问题,需要立即响应,不能拖延;
- 有条理地排查:需要有一定的排查思路和方法,按照假设进行验证,逐步排除不符合实际的情况;
- 细心仔细:需要仔细查看日志、代码等信息,不能遗漏任何细节;
- 总结归纳:需要将排查过程和结果进行总结归纳,以便在以后的工作中能够更好地应对类似问题。
通过本次排查,我们不仅解决了一个具体的问题,更重要的是积累了一定的排查经验和技巧,这对我们今后的工作将会有很大的帮助。