调 Python CLI 时,可以用 faulthandler 留一份崩溃现场

46 次浏览4 条回复

遇到 Python 进程卡住或收到致命信号时,标准库的 faulthandler 能把线程栈打出来,排查起来比只看一行异常更有用。环境:Python 3.8+。

最简单的用法是给脚本加启动参数:

python -X faulthandler app.py

也可以在代码里开启,并把输出写到文件:

import faulthandler

with open("fault.log", "w") as log:
    faulthandler.enable(file=log, all_threads=True)
    run_app()

如果主要想抓住进程长时间没响应的情况,可以设置超时转储:

faulthandler.dump_traceback_later(30, repeat=True)

这个调用会每 30 秒打印一次所有线程的栈,恢复后记得用 faulthandler.cancel_dump_traceback_later() 取消。它只记录 Python 层的 traceback,不能替代 native debugger,不过查死锁、阻塞 I/O 这类问题时挺顺手。

补一个容易忽略的点:enable(file=log) 依赖这个文件句柄一直有效,别在开启后立刻离开 with,否则后面的超时转储可能没地方写。实际包一层 try/finally 会稳一点,退出前同时调用 cancel_dump_traceback_later(),再关闭日志文件。线上还可以给 dump_traceback_later 设 exit=True 做最后兜底,但要确认它符合进程的退出策略。

还有个实践上的小坑:如果把输出写到轮转日志或临时目录,最好先确认目录和文件权限,另外给 dump_traceback_later 配一个明确的超时,别让调试转储无限期跑在线上。需要抓 native 扩展问题时,再把这份 Python 栈和 core dump 的时间对起来,定位会更准。

如果不想在线上固定每 30 秒刷一次,还可以在 Unix 环境用 faulthandler.register(signal.SIGUSR1, file=log, all_threads=True),需要时给进程发 SIGUSR1 再抓现场。这样对偶发卡顿更友好,不过要把日志句柄的生命周期管好,并确认容器或进程管理器不会拦截这个信号。

还有个边界值得说明:faulthandler 的转储是在解释器信号处理路径里完成的,能写出的内容比较有限,适合先确认线程都卡在哪里,不适合在里面做复杂日志格式化。要和应用日志对齐的话,可以在转储前后各打一条带时间戳的普通日志;如果怀疑是 C 扩展卡住,再把它当线索配合 core dump 或 native debugger 看。