注意看,这个男人叫小帅,他正在排查一个偶发的 coredump 问题。只见他熟练的 gdb -c core elf 打开 core 文件,bt 了一下,发现 core 在了 absl::btree::find 方法中。按理说,Abseil 库久经考验,core 在了他们代码里面,大概率是上层的业务代码出现了并发行为。

带着这个猜想,小帅便想看看其他线程的栈,希望能找到一个活跃在事故代码附近的堆栈。于是,他 thread all apply bt 了一下,但发现输出足足有一万多行,其中包含了 1076 个线程的堆栈。想要靠肉眼找出可疑的栈,小帅的眼睛怕是要瞎…

不要问为什么一个服务会开一千多个线程,代码复杂度把控不住,每个模块都往越来越臃肿的方向演进,最终合力形成一座屎山。

1. 上工具

简单的扫描后发现,这一千多个堆栈中包含大量的重复内容:

1
2
3
4
Thread 64 (Thread 0x7f64219ac700 (LWP 172)):
#0  pthread_cond_wait@@GLIBC_2.3.2 () at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
#1  0x00005629f8ef64d8 in ConditionVariable::Wait (this=this@entry=0x7f644cc05170) at cond.cc:40
#2  0x00005629f8f57105 in BaseThreadPool::GetPendingTask (this=this@entry=0x7f644cc05140, tasks=tasks@entry=0x7f64219a7d30) at base_thread_pool.cc:48

也就是说,大部分线程处于非活跃状态,在等待唤醒,他们的堆栈都是类似的。如果能把这些重复堆栈 group 到一起,折叠成一个,那就能去除不少干扰信息了。但这些重复的堆栈由于入参的不同,他们字面上的字符串并不相等,所以简单的 sort | uniq -c 组合已经解决不了这个需求了,这个时候,只能自己动手写 python 了。

在 GPT 这个废物的帮助下,小帅很快写出了gdb_bt_group.py工具:

它的工作原理很简单——将堆栈的第二列,即所有栈帧 PC 寄存器的组合作为栈的唯一标识字段。有了这个抽象的标识之后,工具就可以将相似的堆栈聚合到一起,并输出统计信息了:

1
2
3
4
Thread 64 (Thread 0x7f64219ac700 (LWP 172)): [Total 320]
#0  pthread_cond_wait@@GLIBC_2.3.2 () at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
#1  0x00005629f8ef64d8 in ConditionVariable::Wait (this=this@entry=0x7f644cc05170) at cond.cc:40
#2  0x00005629f8f57105 in BaseThreadPool::GetPendingTask (this=this@entry=0x7f644cc05140, tasks=tasks@entry=0x7f64219a7d30) at base_thread_pool.cc:48

这里的 [Total 320] 代表当前堆栈聚合了 320 个线程,他们都有相同的堆栈,只需要看一个就够了。

1.1 用法

将 gdb 的 bt 信息导出到文本后,用工具读取这个文本,它就能输出聚合后的堆栈信息了:

1
2
gdb -c core elf -batch -ex 'thread apply all bt' > back_trace.txt
gdb_bt_group.py back_trace.txt > grouped_back_tract.txt`

2. 结论

经过工具聚合后,原来的 1076 个堆栈压缩成了 28 个堆栈。小帅仔细的检查了下其余堆栈,果然发现了一个可疑线程,它正在执行同一个 btree 容器的 emplace 方法,但外部只加了个读锁(估计代码抄错了?)。

在这个工具的帮助下,小帅后续又成功的帮同事排查了几个多线程并发造成的 coredump 问题。所以,如果你也遇到了类似的问题,可以试试这个工具——多看几个堆栈,运气好的话,说不定就能找到可以线程呢~

另外,小帅计划,等有空了可以把这段 python 脚本封装成一个 gdb.Command,比如叫 grouped_bt?这样就可以直接在 gdb 里面调用,而不需要执行额外的导出再读取的操作了。