脚本跑着跑着就卡住?用 faulthandler 打出线程栈快速定位

举报
yd_235270612 发表于 2026/10/30 09:01:26 2026/10/30
【摘要】 定时脚本偶发卡死,用 faulthandler dump 线程栈定位阻塞点

一、问题


定时脚本原本几分钟跑完,某天起开始"卡死":进程还在,CPU 接近 0,日志停在最后一行不动,只能手动 kill。重启后又正常,过几天再来一次。这种偶发、不可复现的卡顿,靠加 print 几乎没法定位——卡住的时候,恰恰是没有日志可打的时候。


二、原因分析


大多数"卡住"不是算子写错,而是阻塞在某个等待上:网络请求没设超时、数据库连接被锁、父进程等子进程却不读它的 stdout,缓冲区写满后双方互等、多线程里同一把锁被重复获取。这时候线程停在系统调用里,解释器根本没有机会执行你的日志代码。于是翻日志只能看到"最后一步做了什么",看不到"现在停在哪里",而这两件事完全不同。


要在现场拿到调用栈,就得用 faulthandler。它由 C 层实现并直接写 fd,注册信号后即使解释器卡在 socket recv 里,收到信号也能把所有线程的 Python 栈打印出来——不需要 GIL 空闲,也不需要你的代码配合。


三、做法


第一步,启动时打开 faulthandler,并挂一个"定时炸弹":


import faulthandler, sys


# 段错误、线程崩溃先打栈再退出

faulthandler.enable(file=sys.stderr, all_threads=True)


# 跑满 60 秒还没结束,就 dump 一次全部线程栈(不杀进程)

faulthandler.dump_traceback_later(60, repeat=True)


第二步是治本:给所有外部等待设上限。没有超时的调用,等于把整条流水线的控制权交给对端。


import requests

from subprocess import run, PIPE, TimeoutExpired


resp = requests.get(url, timeout=(3.05, 10)) # 连接、读取分开设


try:

    run(cmd, stdout=PIPE, stderr=PIPE, timeout=120, check=True)

except TimeoutExpired:

    log.warning("cmd timeout: %s", cmd)


第三步,把"卡住"变成可比较的证据:连续两次 dump 都停在同一个函数,基本就是元凶。可以在卡顿时把 stderr 存两份再对比重复帧:


def frames(path):

    with open(path, encoding="utf-8", errors="replace") as f:

        return [l.strip() for l in f if l.strip().startswith("File")]


common = set(frames("dump1.txt")) & set(frames("dump2.txt"))


第四步,用 timeout 兜底,防止进程永久占着调度窗口。宁可让它被杀掉、下一轮重跑,也不要它半死不活地挂着:


timeout -k 10 1800 python3 job.py


四、小结


三句话:日志告诉你走到哪一步,线程栈才告诉你停在哪里;凡是可能阻塞的调用都必须有超时,否则一次网络抖动就能拖垮整条流水线;观测手段要提前埋,等出事再加就来不及。faulthandler 不用装依赖、几行代码就能上线,值得写进每个常驻脚本的头部。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。