Title Loading...
- Time Loading...
- Wordcount Loading...
Catalogue
使用gdb调试Python多线程程序占用 CPU 100% 的问题
前言
最近发现了 teelebot 构建的 Bot 在长时间运行后会出现 占用 100% CPU 的情况。便乘问题复现的时候用 gdb
分析一波。
工具准备
gdb
: 由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。python3-dbg
: CPython 调试符号,用来在调试的时候看到python源代码的call stack。装上这个才能用gdb
调试 Python程序
sudo apt install gdb python3-dbg -y
使用上面的命令安装它们。
开始调试
一、获取Python程序进程PID
ps -aux | grep -w teelebot | grep -v grep | awk '{ print $2 }'
二、获取占用 100% CPU 的线程PID
top -H -p PID
该命令会动态显示程序所有线程信息,输出如下:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3208 user 20 0 822964 69152 14252 S 100 3.5 4:21.35 python3
3437 user 20 0 822964 69152 14252 S 0.0 3.5 0:04.85 python3
3444 user 20 0 822964 69152 14252 S 0.0 3.5 0:04.33 python3
3473 user 20 0 822964 69152 14252 S 0.0 3.5 0:02.17 python3
3474 user 20 0 822964 69152 14252 S 0.0 3.5 0:04.43 python3
3485 user 20 0 822964 69152 14252 S 0.0 3.5 0:04.37 python3
3486 user 20 0 822964 69152 14252 S 0.0 3.5 0:03.80 python3
PID为 3208
的线程即是我们要找的问题线程。
三、进入gdb调试环境
sudo gdb python3 -p PID //此处PID为进程PID
看到下方的输出就代表成功进入调试环境
Reading symbols from /usr/lib/x86_64-linux-gnu/libsqlite3.so.0...Reading symbols from /usr/lib/debug/.build-id/78/5ce90f42d13a60a368533ef.debug...done.
我们可以生成一个 coredump 文件来进行分析,以防程序突然重启导致的问题消失
(gdb) gcore PID //此处PID为进程PID
然后退出 gdb
用以下命令进入文件
gdb python3 core.PID //此处PID为进程PID
四、使用 info threads
查看程序所有线程
(gdb) info threads
输出如下:
Id Target Id Frame
1 Thread 0x7f19b6 (LWP 3208) "python3" 0x00007f19b6 in __GI___poll (fds=0x7f19b6,
nfds=1, timeout=500) at ../sysdeps/unix/sysv/linux/poll.c:29
* 2 Thread 0x7f19b4 (LWP 3437) "python3" futex_abstimed_wait_cancelable (private=0, abstime=0x0,
expected=0, futex_word=0x18fe750) at ../sysdeps/unix/sysv/linux/futex-internal.h:205
3 Thread 0x7f19ae (LWP 3444) "python3" futex_abstimed_wait_cancelable (private=0, abstime=0x0,
expected=0, futex_word=0x18fe750) at ../sysdeps/unix/sysv/linux/futex-internal.h:205
4 Thread 0x7f19ad (LWP 3473) "python3" futex_abstimed_wait_cancelable (private=0, abstime=0x0,
expected=0, futex_word=0x18fe750) at ../sysdeps/unix/sysv/linux/futex-internal.h:205
5 Thread 0x7f19ac (LWP 3474) "python3" futex_abstimed_wait_cancelable (private=0, abstime=0x0,
expected=0, futex_word=0x18fe750) at ../sysdeps/unix/sysv/linux/futex-internal.h:205
...
*
代表当前线程
五、切换到问题线程
由上一步我们知道了问题线程的编号为 1
,通过以下命令切换到问题线程:
(gdb) thread 1
输出如下:
[Switching to thread 1 (Thread 0x7f19b6 (LWP 3208))]
#0 futex_abstimed_wait_cancelable (private=0, abstime=0x0, expected=0, futex_word=0x7f19b6)
at ../sysdeps/unix/sysv/linux/futex-internal.h:205
205 ../sysdeps/unix/sysv/linux/futex-internal.h: No such file or directory.
六、使用 py-list
查看当前应用程序上下文
(gdb) py-list
输出如下:
348 for i in range(4): #生成答案列表
349 if answer == i:
350 options.append(captcha_text)
351 else:
352 options.append(shuffle_str(captcha_text))
>353 if len(options) == len(set(options)):
354 break
...
比较幸运的是,第一次使用py-list
就定位到了问题代码。若第一次未找到,则需要结合 py-up
和 py-down
寻找。
七、使用 py-bt
查看应用程序调用堆栈
初步定位问题代码后,我们还可以用 py-bt
查看程序的调用堆栈
(gdb) py-bt
输出如下:
Traceback (most recent call first):
File "/teelebot/plugins/Guard/Guard.py", line 353, in reply_markup_dict
if len(options) == len(set(options)):
File "/teelebot/plugins/Guard/Guard.py", line 167, in Guard
reply_markup = reply_markup_dict(captcha_text=captcha_text)
File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run
result = self.fn(*self.args, **self.kwargs)
File "/usr/lib/python3.8/concurrent/futures/thread.py", line 80, in _worker
work_item.run()
File "/usr/lib/python3.8/threading.py", line 865, in run
self._target(*self._args, **self._kwargs)
File "/usr/lib/python3.8/threading.py", line 917, in _bootstrap_inner
self.run()
可以看出问题确实出在 Guard插件 的 reply_markup_dict
函数上。
八、使用 py-locals
查看当前scope的变量
我们可以再看看当前使用的变量的值:
(gdb) py-locals
输出如下:
captcha_text = 'bsbmg'
options = ['sbbmg', 'bsgbm', 'bsbmg', 'bsgbm', 'msbgb', 'sbbmg', 'bsbmg', 'gmsbb', 'bsgbm', 'bbgsm', 'bsbmg', 'msbgb', 'sbgmb', 'bbgsm', 'bsbmg', 'bbmgs', 'smbgb', 'gbmsb', 'bsbmg', 'bbgsm', 'bmbgs', 'bmsgb', 'bsbmg', 'gmbbs', 'bmgbs', 'msbgb', 'bsbmg', 'bgmsb', 'sbbmg', 'bbmgs', 'bsbmg', 'msbgb', 'bgsbm', 'bbmsg', 'bsbmg', 'sbgbm', 'bbmsg', 'mgbsb', 'bsbmg', 'bgbms', 'bmgbs', 'bsbmg', 'bsbmg', 'bbgsm', 'sbgbm', 'sgbmb', 'bsbmg', 'bgmsb', 'sbbmg', 'bsgbm', 'bsbmg', 'bbgms', 'sgbbm', 'mbgbs', 'bsbmg', 'mbbgs', 'bmgsb', 'gbsmb', 'bsbmg', 'bmgsb', 'msbbg', 'mgsbb', 'bsbmg', 'gsbmb', 'bsgmb', 'bmbsg', 'bsbmg', 'gsmbb', 'mbsgb', 'gbmbs', 'bsbmg', 'bmgbs', 'bmsbg', 'bbgms', 'bsbmg', 'bbmsg', 'bgmbs', 'bsbmg', 'bsbmg', 'gbsbm', 'smgbb', 'bgmsb', 'bsbmg', 'mbbgs', 'bmgbs', 'mbgbs', 'bsbmg', 'bmgbs', 'mbgbs', 'mbgsb', 'bsbmg', 'mbgsb', 'bsbmg', 'mgsbb', 'bsbmg', 'gbbms', 'sbbgm', 'bmbsg', 'bsbmg', 'bsmbg', 'gbsmb', 'mbbgs', 'bsbmg', 'gbbms', 'bsmbg', 'gmbsb', 'bsbmg', 'smgbb', 'gbmsb', 'bmsgb', 'bsbmg', 'gbmsb', 'sgmbb', 'sgmbb...(truncated)
answer = 2
i = 3
九、解决问题
由上面的步骤分析,可以判断问题是由线程内函数死循环无法退出导致的。接下来在源文件中定位到问题代码:
options = []
while True:
for i in range(4): #生成答案列表
if answer == i:
options.append(captcha_text)
else:
options.append(shuffle_str(captcha_text))
if len(options) == len(set(options)):
break
可以看到这是一个死循环,退出条件为当 options
中不存在重复的元素。我们可以试着用文字复述问题出现的整个流程:当遭遇第一次循环后, options
中存在重复的元素,退出条件不会被触发,会继续进行循环,但由于变量 options
定义在循环之外,下一次循环变量 option
并不会被清空,因而 options
中永远存在重复的元素,退出条件永远不满足,循环永远不会退出,线程也永远等待在这里。
因此解决问题的办法就是: 进入下一次循环之前手动清空 options
的值。
options = []
while True:
for i in range(4): #生成答案列表
if answer == i:
options.append(captcha_text)
else:
options.append(shuffle_str(captcha_text))
if len(options) == len(set(options)):
break
else:
options = [] #手动清空options
奇怪的是内存为什么没有占满。。
最后
感谢
感谢他们的帮助: