Linux上暂停指定的多个运行线程
Linux上暂停指定的多个运行线程
开篇问题
有一个Linux的多线程程序,在某一台服务器上运行10天后,该程序进程产生了120个线程,此时业务的性能变得很差(可以容易获取业务性能表现的指标)。 我们检查了同时启动服务的其他10台服务器,这10台服务器的业务表现都正常。 我们使用了各种手段分析排查产生性能下降的原因,但没有找到。 不过经过对比,我们对其中一个业务产生了比较大的怀疑,这个业务本身很独立,别的流程对它没有依赖。 我们怀疑这个业务的线程运行过程中执行了某些特殊代码,对系统整体性能产生了影响。 这个业务一共启动了12个线程,而且这个业务也没有运行时开关。 但是我们不能重启程序,因为担心无法复现问题。 现在,怎么来确认这个怀疑是对的还是错的?
问题跟踪思路
验证这个猜想大概有这几种途径:
- 方法1:看看这个业务的12个线程在干什么?看看有什么异常的系统调用或者影响系统硬件的操作;
- 方法2:改变程序的输入,让输入的数据被过滤掉,使这个被怀疑的业务没有数据需要处理;
- 方法3:修改运行时的指令码,让这几个线程都退出掉,然后再来分析系统性能表现;
- 方法4:我们想办法去复现这个问题,修改业务代码或者调整配置后关闭该业务流程,重启复现;
- 方法5:如果能够从外部直接将这业务的所有线程都暂停了,那么再来观测一下系统的性能表现有没有恢复,就能确认怀疑对不对了。
方法1可以比较容易实施,例如我们可以用gdb/pstack/strace等相应的工具去分析线程的调用栈,但问题是我们对系统的理解大都是不全面的,看到函数调用后也只能是猜想,无法直接证实。 方法2比较间接,如果这个业务本身的代码实现上存在漏洞,例如在没有数据需要处理时也执行各种操作,那么使其无数据输入的方法就无法排除它的嫌疑,而且有时构造特定的输入数据也很困难。 方法3属于Linux的特定产物,我们没有通用的使线程退出的手段,所以想要去修改执行的机器码让线程退出,但这个操作难度较大,而且容易造成程序崩溃,属于风险较高的操作,需要谨慎使用。 方法4有不确定性,有可能需要很长的时间,甚至找不到复现问题的有效手段。 方法5在实施成本就比较低,影响面小,可快速验证猜想,是非常有效的初步手段。
如果使用方法5快速验证了是这个业务的问题,那么就可以开展下一步,使用方法1在更小的范围内去寻找线索。 如果使用方法5验证的结果是系统的性能没有受到影响,那么我们的怀疑就不成立了,需要继续努力寻找其他线索了,即便如此,我们也只是支付了很小的代价而已,只是暂停了一小会儿线程,出问题的现场环境大概率还存在,还可以接着分析。
既然方法5是有效的手段,那么我们如何实施?即,我们如何在Linux上仅仅暂停一个进程中的一部分线程的运行?
说实话,这个问题能难倒一大片经验丰富的工程师。
使用gdb暂停指定的几个线程
我们猜想gdb应该具有这个能力。 但是,你在浏览器中去寻找“如何使用gdb暂停几个线程时”,你就会发现,这个问题好像大家不怎么会遇到。 连广受好评的《100个gdb小技巧》中也没有记录如何达成这个目的。 经过几番探索后,找到了方法,这里分享给大家。 方法出奇的简单,就是使用gdb的attach命令。
attach process-id
This command attaches to a running process—one that was started outside GDB. (info files shows your active targets.) The command takes as argument a process ID. The usual way to find out the process-id of a Unix process is with the ps utility, or with the ‘jobs -l’ shell command.
我们容易被这个“process-id”误导了,以为只能传入进程ID,实则不然。
以暂停mysqld为例,如下是mysqld的线程情况:
现在,我们暂停线程3697,像这样:
[root@localhost tmp]# gdb -q --pid=3697
Attaching to process 3697
Reading symbols from /usr/sbin/mysqld...
Reading symbols from /lib64/libpthread.so.0...
(No debugging symbols found in /lib64/libpthread.so.0)
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Reading symbols from /lib64/libaio.so.1...
Reading symbols from .gnu_debugdata for /lib64/libaio.so.1...
(No debugging symbols found in .gnu_debugdata for /lib64/libaio.so.1)
Reading symbols from /lib64/libnuma.so.1...
Reading symbols from .gnu_debugdata for /lib64/libnuma.so.1...
(No debugging symbols found in .gnu_debugdata for /lib64/libnuma.so.1)
Reading symbols from /lib64/libcrypt.so.1...
(No debugging symbols found in /lib64/libcrypt.so.1)
Reading symbols from /lib64/libdl.so.2...
(No debugging symbols found in /lib64/libdl.so.2)
Reading symbols from /lib64/libstdc++.so.6...
(No debugging symbols found in /lib64/libstdc++.so.6)
Reading symbols from /lib64/libm.so.6...
(No debugging symbols found in /lib64/libm.so.6)
Reading symbols from /lib64/libgcc_s.so.1...
(No debugging symbols found in /lib64/libgcc_s.so.1)
Reading symbols from /lib64/libc.so.6...
(No debugging symbols found in /lib64/libc.so.6)
Reading symbols from /lib64/ld-linux-x86-64.so.2...
(No debugging symbols found in /lib64/ld-linux-x86-64.so.2)
Reading symbols from /lib64/libfreebl3.so...
Reading symbols from .gnu_debugdata for /lib64/libfreebl3.so...
(No debugging symbols found in .gnu_debugdata for /lib64/libfreebl3.so)
Reading symbols from /lib64/libnss_files.so.2...
(No debugging symbols found in /lib64/libnss_files.so.2)
0x00007f453f4bf644 in __io_getevents_0_4 () from /lib64/libaio.so.1
(gdb)
或者像这样:
[root@localhost tmp]# gdb -q
(gdb) attach 3697
Attaching to process 3697
Reading symbols from /usr/sbin/mysqld...
Reading symbols from /lib64/libpthread.so.0...
(No debugging symbols found in /lib64/libpthread.so.0)
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Reading symbols from /lib64/libaio.so.1...
Reading symbols from .gnu_debugdata for /lib64/libaio.so.1...
(No debugging symbols found in .gnu_debugdata for /lib64/libaio.so.1)
Reading symbols from /lib64/libnuma.so.1...
Reading symbols from .gnu_debugdata for /lib64/libnuma.so.1...
(No debugging symbols found in .gnu_debugdata for /lib64/libnuma.so.1)
Reading symbols from /lib64/libcrypt.so.1...
(No debugging symbols found in /lib64/libcrypt.so.1)
Reading symbols from /lib64/libdl.so.2...
(No debugging symbols found in /lib64/libdl.so.2)
Reading symbols from /lib64/libstdc++.so.6...
(No debugging symbols found in /lib64/libstdc++.so.6)
Reading symbols from /lib64/libm.so.6...
(No debugging symbols found in /lib64/libm.so.6)
Reading symbols from /lib64/libgcc_s.so.1...
(No debugging symbols found in /lib64/libgcc_s.so.1)
Reading symbols from /lib64/libc.so.6...
(No debugging symbols found in /lib64/libc.so.6)
Reading symbols from /lib64/ld-linux-x86-64.so.2...
(No debugging symbols found in /lib64/ld-linux-x86-64.so.2)
Reading symbols from /lib64/libfreebl3.so...
Reading symbols from .gnu_debugdata for /lib64/libfreebl3.so...
(No debugging symbols found in .gnu_debugdata for /lib64/libfreebl3.so)
Reading symbols from /lib64/libnss_files.so.2...
(No debugging symbols found in /lib64/libnss_files.so.2)
0x00007f453f4bf644 in __io_getevents_0_4 () from /lib64/libaio.so.1
(gdb)
这样操作后,不要退出gdb,对应的线程就被暂停住了。 注意,gdb启动后不能退出,因为gdb退出后线程就恢复执行了。
要暂停多个线程就只需要启动多个gdb进程即可。
在2个gdb进程中分别暂停了线程3697和线程3698后,如下: 可以看到线程3697和3698的状态都变成了“t”。
但是我要暂停12个线程,岂不是要启动12个gdb? 对,我没有在gdb中找到方法,目前较新版本的gdb,一个gdb进程也只能同时attach一个线程。 这很不合理呀。
如果需要暂停的线程多,我们就要换一个工具,这个工具就是fthread,推荐给大家。
使用fthread暂停指定线程
fthread简介
fthread就是用来专门解决批量暂停线程的工具,正好符合我们目前的需求。
获取fthread
fthread目前只提供了linux x64版本的可执行文件,其二进制下载路径如下:
https://github.com/EmptyWatson/fthread/releases/download/v0.0.1-release/fthread_x64
使用fthread
其官方文档上有详尽的说明
我们以暂停mysqld中的几个线程(htop中标黄的线程)为例:
[root@localhost tmp]# ./fthread_x64 -p $(pidof mysqld)
2022-01-29 15:26:02,589 INFO --------->fthread is starting......
2022-01-29 15:26:02,590 INFO Welcome to fthread
Press 'tab' to view autocompletions
Type '.help' for help
Type '.quit' or '.exit' to exit
fthread> f 3697-3706
freeze threads ....
freeze threads finish, succ cnt 10
fthread> f 3836,3837
freeze threads ....
freeze threads finish, succ cnt 2
fthread> list u
unfreeze u
fthread> list freezed
IDX Thread Id Status Name
[2 ] 3697 * freezed mysqld
[3 ] 3698 * freezed mysqld
[4 ] 3699 * freezed mysqld
[5 ] 3700 * freezed mysqld
[6 ] 3701 * freezed mysqld
[7 ] 3702 * freezed mysqld
[8 ] 3703 * freezed mysqld
[9 ] 3704 * freezed mysqld
[10 ] 3705 * freezed mysqld
[11 ] 3706 * freezed mysqld
[14 ] 3836 * freezed mysqld
[15 ] 3837 * freezed mysqld
fthread>
fthread提供了自动补全的能力:
暂停完成后,htop中的线程状态:
暂停线程的原理
暂停线程的原理很简单,就是借助linux提供的ptrace系统调用:
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
ptrace(PTRACE_ATTACH, pid, 0, 0)
ptrace的说明如下:
The ptrace() system call provides a means by which one process
(the "tracer") may observe and control the execution of another
process (the "tracee"), and examine and change the tracee's
memory and registers. It is primarily used to implement
breakpoint debugging and system call tracing.
A tracee first needs to be attached to the tracer. Attachment
and subsequent commands are per thread: in a multithreaded
process, every thread can be individually attached to a
(potentially different) tracer, or left not attached and thus not
debugged. Therefore, "tracee" always means "(one) thread", never
"a (possibly multithreaded) process". Ptrace commands are always
sent to a specific tracee using a call of the form
ptrace(PTRACE_foo, pid, ...)
where pid is the thread ID of the corresponding Linux thread.
当指定的线程被attach后,线程的执行就暂停了。
参考资料
- [1] gdb手册 https://sourceware.org/gdb/onlinedocs/gdb/Attach.html
- [2] fthread https://github.com/EmptyWatson/fthread
- [3] ptrace 维基百科 https://en.wikipedia.org/wiki/Ptrace
- [4] ptrace man https://man7.org/linux/man-pages/man2/ptrace.2.html