调试Python程序卡住,寻找原因

最近大模型平台有个知识库模块,在对文件进行加工处理,进行embedding,逻辑上不复杂,基本就是对文本切片,再embedding,然后落库milvus。

不过出现一个现象就是当文本过大的时候,knowledge based的处理过程会卡住,且knowledge based停止服务,请求处于等待状态,由于knowledge based的整个服务是部署在docker中的,于是查看卡在哪儿,只能在docker中进行了。

首先通过:

docker exec  --privileged -it ID /bin/bash

进入容器,查找到PID:

root@815eb533c377:/app# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Jun12 ? 00:00:00 /bin/sh -c cd /app/src && poetry run alembic upgrade head && poetry run python main.py
root 57 1 0 Jun12 ? 00:05:24 /root/.cache/pypoetry/virtualenvs/knowledge-base-9TtSrW0h-py3.10/bin/python main.py

可以看到PID分别为1和57,这里需要调试的是PID 57。

通过:

gdb python 57

进行调试,碰到一个问题:

Attaching to process 57 
ptrace: Operation not permitted

看了下docker container里面的/proc/sys/kernel/yama/ptrace_scope 的值是1,1 意味着无法attach,对于docker来说,这个值是跟随宿主机走的,也就是需要修改宿主机的值,也就是执行:

echo 0 > /proc/sys/kernel/yama/ptrace_scope

对于默认是zsh的会碰到错误:

zsh: permission denied: /proc/sys/kernel/yama/ptrace_scope

这时候可以通过切换bash解决:

sudo bash -c "echo 0 > /proc/sys/kernel/yama/ptrace_scope"

再通过:

gdb python 57

进行调试,通过py-list查看当前执行的位置:

(gdb) py-list
700 self._checkReadable()
701 if self._timeout_occurred:
702 raise OSError("cannot read from timed out object")
703 while True:
704 try:
>705 return self._sock.recv_into(b)
706 except timeout:
707 self._timeout_occurred = True
708 raise
709 except error as e:
710 if e.errno in _blocking_errnos:

gdb -p pid
pid是指进程号,可以通过 ps -ef 查看,请替换为实际的进程号

步骤5: 打印信息
py-bt
解释: 查看当前进程的堆栈信息,这是很重要的命令,一般可以看到进程运行到哪里发生死锁或者卡住,进而可以回到python源码确定最终问题在哪里

py-list
解释: 显示python进程当前代码运行到哪里

py-locals
解释: 显示当前进程中的局部变量

py-print
解释: 打印python变量的值
用法: 例如
py-print self

info threads
thread
解释: 先查看线程信息,然后切换线程想要查看的线程

thread apply all py-list:
解释:查看所有进程执行位置

通过py-bt查看当前堆栈,除了使用gdb之外,还可以使用其他调试工具,比如pystack

pip install pystack-debugger

执行pystack 57能看到:

 root@815eb533c377:/app# pystack 57
Dumping Threads....


File "/usr/local/lib/python3.10/threading.py", line 973, in _bootstrap
self._bootstrap_inner()
File "/usr/local/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
self.run()
File "/root/.cache/pypoetry/virtualenvs/knowledge-base-9TtSrW0h-py3.10/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 797, in run
item = self.queue.get()
File "/usr/local/lib/python3.10/queue.py", line 171, in get
self.not_empty.wait()
File "/usr/local/lib/python3.10/threading.py", line 320, in wait
waiter.acquire()

---------------

也可以使用hypno:

pip install hypno

这个库的目标不太一样,主要是为了 inject 到正在运行的 Python 进程里跑一些代码,所以灵活性比单纯打 callstack 更高,比如可以打印一些变量之类。我们用如下的命令来实现获取 callstack 的功能:

hypno "import traceback; traceback.print_stack()"

还有py-spy

pip install py-spy

获取 callstack 可以使用 dump 命令:

py-spy dump --pid 54

此外py-spy还支持top形式的实时 profiling,以及生成火焰图(操作稍麻烦)的功能,除了程序 hang 之外也能做性能优化的监控工具。不过易用性上来说比下面要介绍的austin还是稍微差了些。

conda install -c conda-forge austin-tui

这是一个集大成的 profiling 工具,不光可以看 callstack,还可以看对应的采样(近似理解为程序时间开销)百分占比信息,以及实时更新的 flamegraph 等,竟然还是跨平台的!官网上还有更多高级特性,感觉已经可以跟 JDK 里的 visualvm 来媲美了。

python 调试工具 pyrasite 可以附加到python进程中,在这个进程中打开一个python 命令行。然后再这个里面执行代码。

pyrasite-shell PID

(对于这种挂死问题通常是由于多进程和多线程混用导致的,多线程中如果有锁,在使用fork创建多进程的过程中fork出来的进程是单线程执行的,只会复制内存中的对象当前的信息,如果有一把锁被别的线程获取到,我们当前fork出的进程中的这个线程中,这把锁的状态只会是锁定状态,会导致子进程中再使用这个锁的时候会导致死锁。)

对于python进程中的挂死问题,我们需要对于进程中所有的线程查看它们卡死在了哪一个位置。

当我们进入这个进程中后,我们可以执行下面的代码查看进程的帧栈。

import sys
for threadid,stack in sys._current_frames().items():
print(threadid,stack)

对于内存泄漏问题,我们可以使用objgraph来查看内存泄漏,对于python 代码造成的内存泄漏,我们可以很容易得看出来泄漏对象的引用关系。
但是对于C代码造成的泄漏,我们通常只能看出对象类型来,并不能看出泄漏对象的引用关系。所以还要一步一步排除。

py-spy 也可以看出进程是否挂死

使用方法 pip install py-spy

py-spy dump -p $pid

py-spy dump -l -p $pid


扫码手机观看或分享: