aliclouddb mongodb在开发过程中遇到一个无法正常退出的bug,由于是release版本的编译(-o3),debuginfo已经不能很好的展现出堆栈的情况。这时又该如何确定问题所在呢?本篇文章完整的记录了整个排查过程。
kill命令正常执行,但进程迟迟没有退出。非必现场景,在批量回收资源时比较容易出现,平时开发测试时没有遇到。从场景上看出现的概率并不高,可能是在某种极端条件下才能触发,由于第一次收到报告后没有保留现场,先到官方jira平台上去搜相关的bug,无果,又盲目的尝试了几个场景后只能先hold住,等待下次出现。
很幸运,第二天bug再次出现,僵尸进程?死循环?死锁?没有收到kill信号?无数想法蹦出来,迅速登陆机器,查看现场,先从最简单的可能性开始开始排查。
<code>ps</code>`top`第一套组合拳,排除了僵尸进程可能性,并且top显示cpu使用率为0并不高;strace继续跟进查看,也没有发现有系统调用,最后在补一个pstack打印堆栈信息,全部线程都在wait。bug的排查方向大致确定:线程间资源同步的问题(当然也不能排除是其他的可能性)。
详细分析pstack内容,从堆栈信息中看一个长相特别的(其他大部分的线程堆栈都是雷同的):
从函数名上看起来是mongodb退出的关键路径,就从这里入手,人肉翻下源码:
这么多的join,到底是哪个呢。上gdb,我们来看看thread 46到底在等谁。先加载symbol-file,失败,加载后堆栈变得更乱了,换<code>disassemble</code>命令,显示汇编信息:
查看<code>0x0000000000c8eeb7</code>地址的上下文,通过前后指令的函数符号名确定了目前代码是在<code>_applierthread->join()</code>,这里可以思考下是否还有的其他方法获得代码行。
<code>_applierthread</code>同样也是个线程,如果shutdown在等它,那它又在等谁呢,回头继续查pstack输出,找到相关的堆栈:
注意这里与shutdown的等待是不同的,shutdown是在等待线程退出,而这里是在等待某个条件变量,再次上gdb,查看thread 34 & backtrace 1, <code>info locals</code>:
看看代码怎么写的吧:
<code>_pause</code>变量一直都是0,所以会hang在这里。继续查看代码,跟踪<code>_pausedcondition</code>的调用,有两个函数会去唤醒,一个是stop,另一个是shutdown,但shutdown的调用应该在线程退后调用,以便释放资源。
同时,再次回过来在pstack中查看<code>backgroundsync</code>的堆栈信息,想看看到底卡在了哪里。结果找不到<code>backgroundsync</code>线程,问题更清晰了,所有<code>_pausedcondition</code>条件变量的唤醒,都是在该线程中完成的,如果<code>backgroundsync</code>已经不存在了,一定会hang住。
再反复阅读代码,<code>backgroundsync</code>在判断到<code>inshutdown()</code>时会自动结束生命周期,但结束后并没有更改<code>_pause</code>状态。
解决办法是线程最后退出前执行stop函数,修改<code>_pause</code>状态,(shutdown会提前释放资源),查看官方最最新代码,确认有同样的修改,反向追踪提交,找到相关jira,抱怨jira的搜索弱爆了。
为何该bug出现的频率会非常低呢,主要在判断是否等待的条件上:
也就是说刚刚赢得了选举后会产生<code>_iswaitingfordraintocomplete == true</code>状态,恰巧这个时间窗口内进程接受到退出信号。复现的办法就是在<code>kactionwinelection</code>的后面加上sleep,以此来延长时间窗口,当然也可以通过gdb block的方式来复现。