首页 / 资讯中心 / 文章详情

用Claude解读pstack输出:AI辅助定位进程死锁与线程卡死

用Claude解读pstack输出:AI辅助定位进程死锁与线程卡死 ★ FEATURED ARTICLE
1. 项目起源当进程卡死不再需要人肉看栈做过线上排查的兄弟应该都懂这个场景服务没崩、没日志、CPU 忽高忽低进程就像被人点了穴一样卡在那里。这时候 pstack 是大家最熟悉的一招——把进程所有线程的调用栈 dump 出来看卡在哪个函数。但 pstack 只是给你一堆栈帧怎么从几千行输出里定位到真正的死锁点往往是体力活。我做的 pstack-claude 这个项目就是把 pstack 的采集能力和 Claude 的理解能力串起来脚本自动抓栈、收集上下文再让 Claude 按结构化格式输出诊断结论把人肉看栈变成人审 AI 结论。这套东西适合谁后端开发、SRE、以及所有需要半夜爬起来处理进程挂起的人。你可以把它当成一个诊断加速器它不替代你的判断但能帮你把从看到栈到想明白怎么回事的时间从半小时压到三五分钟。如果你平时排查问题习惯先pstack一把梭再对着函数名发呆那这篇文章剩下的内容应该对你有用。1.1 一个典型的查不出原因现场我印象很深的一次某天晚上线上一个 C 网关服务突然所有请求超时进程还活着ps里状态是S但业务日志已经十分钟没动弹了。第一反应是看日志没有第二反应是top -H看线程发现 32 个线程里 30 个都睡在futex上。这时候常规操作就来了pstack pid stack.txt拿到的文件 500 多行全是__lll_lock_wait、pthread_mutex_lock、pthread_cond_wait这些底层函数夹着一堆业务函数名。我盯着看了半小时只能隐约觉得好像有几个线程互相等锁。真正确认死锁关系还是靠把每个线程的栈顶函数列出来、手工画等待关系图才看明白。这还是在符号齐全的情况下要是碰上 strip 过的二进制连函数名都没有基本就是灾难。那次之后我就在琢磨pstack 的输出结构其实非常适合做自动化分析——它有明确的线程编号、栈帧顺序、函数名。而分析这段输出、判断是否构成死锁这件事正好是语言模型擅长的。于是就有了 pstack-claude一个用 Claude 解读 pstack 输出的诊断脚本集。1.2 它能替你省下哪一段工作pstack-claude 做的事情不复杂就是一条流水线采集自动对目标 PID 执行 pstack同时抓/proc/pid/status、进程 CPU/内存信息、最近日志、内存映射等上下文。组织把栈和上下文拼成一个结构化的诊断 prompt。分析调用 Claude Code 的 headless 模式要求它按固定 JSON 格式输出卡死原因一句话、属于哪种模式死锁/自旋/正常阻塞、关键线程、判断依据、风险等级、下一步可验证动作。验证把 AI 给的下一步动作拿来在 gdb 里执行人工确认后再动手。换句话说它替代的只是人工把栈读一遍并形成假设这个环节。真正改代码、做变更还是人来。我在实战中发现这个定位非常重要——千万别指望 AI 直接给结论就上生产后面我会专门讲为什么。2. 核心设计拆解采集与理解分开各干各擅长的事整个项目最核心的设计决策是把采集和理解拆成两个独立环节。这个拆法看起来平凡但决定了工具的稳定性。2.1 pstack 输出里到底藏着哪些线索先说 pstack 本身。它不是一个多玄乎的工具在多数发行版里它的本质就是非交互式调用 gdb等价于执行gdb -p pid -batch -ex thread apply all bt也就是把目标进程每个线程的调用栈一次性打出来。典型输出长这样Thread 1 (Thread 0x7f8a2c000700 (LWP 12234)): #0 0x00007f8a2f6b4bcd in epoll_wait () from /lib/x86_64-linux-gnu/libc.so.6 #1 0x0000558a1e5a3f2a in MyServer::EventLoop::Run () at event_loop.cc:118 #2 0x0000558a1e5a412d in main () at main.cc:42 Thread 2 (Thread 0x7f8a2bfff700 (LWP 12235)): #0 0x00007f8a2f6b4bcd in epoll_wait () from /lib/x86_64-linux-gnu/libc.so.6 #1 0x0000558a1e5a3f2a in Worker::Idle () at worker.cc:31 #2 0x0000558a1e5a412d in main () at main.cc:42 Thread 3 (Thread 0x7f8a29ffd700 (LWP 12236)): #0 __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:52 #1 0x00007f8a2f6c2e05 in pthread_mutex_lock () from /lib/x86_64-linux-gnu/libc.so.6 #2 0x0000558a1e5a4a1b in TaskQueue::Pop () at task_queue.cc:56 #3 0x0000558a1e5a4c31 in Worker::Run () at worker.cc:78读这种输出是有套路的栈顶是当前正在干什么。epoll_wait、poll、nanosleep这类出现在栈顶是健康的说明线程在等 IO 或定时器。__lll_lock_wait、pthread_mutex_lock、futex出现在栈顶说明线程在等一把锁。如果大量线程都等在同一把锁上那不是死锁就是锁竞争需要再看是谁持有这把锁。pthread_cond_wait是在等条件变量要确认有没有线程负责notify。栈帧里的函数名如果带着源码路径和行号那就是金矿直接把代码位置暴露了。这些规律写成规则也不难但难的是组合判断。单独看每个线程都很正常组合起来才知道线程 A 拿着锁 X 等锁 Y线程 B 拿着锁 Y 等锁 X。这种跨线程的模式匹配正是我想让 Claude 干的活。2.2 为什么用 Claude 而不是写死规则一开始我也考虑过纯脚本方案grep 到__lll_lock_wait就报警看到pthread_mutex_lock就高亮。做了一版 demo 后发现太脆——不同语言运行时、不同锁实现的栈帧完全不一样。C 的std::mutex、Rust 的parking_lot、Go 的 runtime lock栈底函数名各不相同规则根本写不完。更关键的是规则脚本无法回答这个死锁是怎么形成的。AI 的优势在于能把线程间的等待关系串联起来还能结合日志和进程状态做交叉印证。比如日志里最后一条是进入批量任务栈里显示 16 个 worker 全在 Pop 空队列等待AI 就会把这两个线索关联起来给出生产者线程退出导致消费者永久等待这种假设而不是只报一句检测到 futex 等待。还有一个现实原因Claude Code 本身能读代码。你把仓库路径给它它不光能分析栈还能直接定位到源码里的加锁顺序甚至给出修复补丁。这一点对从诊断到修复的闭环非常有用。2.3 为什么是 pstack 而不是 gdb、gcore选择 pstack 做采集入口是权衡了侵入性、产出物和上手成本之后的结果gdb 交互式太繁琐虽然也能-batch -ex写成脚本但 pstack 本来就是这个封装没必要重复造轮子。gcore 会 dump 整个进程的内存镜像一个 Java/C 服务动辄几个 GB生成慢、占用磁盘大适合事后深入分析不适合现场快速定位。pstack 输出轻量几百 KB 的文本足够做第一轮判断如果不够再上 gcore 做离线深挖。注意pstack 和 gdb attach 一样会在采集瞬间让目标进程短暂停顿本质是给所有线程发 SIGSTOP抓完再继续。对已经卡死的进程没所谓但如果对健康的高并发服务频繁执行是会造成请求抖动甚至超时的。这也是我把 pstack-claude 设计成按需触发而不是定时轮询的原因之一。后面实操部分我会给一个带条件触发的方案。如果你实在担心停顿还有一个替代工具eu-stack它用 elfutils 实现对进程的干扰通常更小但输出格式和 pstack 略有差异需要微调 prompt。3. 从零实现 pstack-claude 的关键环节下面进入实操。整个项目我拆成四块环境准备、采集脚本、Prompt 设计、headless 调用。每一块都有值得注意的细节。3.1 环境准备命令、权限、模型三个前置条件先检查 pstack 是否可用which pstack || which gdb如果没有 pstackDebian/Ubuntu 系直接装包或者用 gdb 兜底。接下来是权限问题。现代 Linux 发行版默认开启ptrace_scope限制非父进程 attach 会被拒绝报错通常是Could not attach to process. If your uid matches the uid of the target process, check the setting of /proc/sys/kernel/yama/ptrace_scope.查看当前限制cat /proc/sys/kernel/yama/ptrace_scope如果是 1只有进程的父进程或者有 CAP_SYS_PTRACE 的进程能 attach。临时放开的命令是sudo sysctl kernel.yama.ptrace_scope0但放开是有安全代价的生产环境建议只在排查窗口临时改或者用 sudo 直接跑别把整机永久改成 0。容器场景更要注意容器里 attach 别的容器进程还需要SYS_PTRACEcapability启动的时候带上docker run --cap-addSYS_PTRACE ...然后是 Claude Code 的准备工作。按官方安装器装好之后验证能不能跑通 headless 模式claude -p say ok --output-format json这里有两个环境问题容易踩一是 npm 全局目录权限不足报错类似auto-update failed: no write permission to npm prefix这种一般是因为当初用 npm 装的且 npm prefix 指向了系统目录。解法是把 npm 全局目录挪到用户目录或者干脆用官方安装脚本重装省心很多。二是在 Windows 上配 WSL 时提示需要启用 Virtual Machine Platform这是 Windows 功能没开用管理员 PowerShell 执行dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart重启后问题就解决了。如果你手上有兼容 Anthropic API 格式的其他模型服务Claude Code 也支持通过ANTHROPIC_BASE_URL、ANTHROPIC_MODEL这类环境变量做模型路由。pstack-claude 的 prompt 对模型并不挑只要输出 JSON 稳定、能读长文本就行。3.2 采集脚本把上下文和栈一起打包我最开始只喂给 Claude 一份 pstack 输出结果它给出的分析经常是疑似死锁建议检查代码。后来我意识到AI 缺的不是分析能力而是现场情报。于是把采集脚本改成打包上下文的形态每个现场案例生成一个目录#!/usr/bin/env bash set -euo pipefail PID${1:?Usage: $0 pid} TS$(date %Y%m%d%H%M%S) OUTcase-${PID}-${TS} mkdir -p $OUT # 1. 进程基础信息状态、CPU、内存、启动时间、完整命令行 ps -o pid,ppid,stat,pcpu,pmem,etime,cmd -p $PID $OUT/process.txt # 2. 每个线程的状态和内核等待通道快速判断卡点 for tid in /proc/$PID/task/*; do echo $(basename $tid) $(cat $tid/wchan 2/dev/null) $OUT/wchan.txt done # 3. 栈采集注意会让进程短暂停顿 timeout 10 pstack $PID $OUT/stack.txt 21 || \ gdb -p $PID -batch -ex thread apply all bt $OUT/stack.txt 21 # 4. 最近日志给 AI 提供业务侧线索 journalctl -n 200 --no-pager $OUT/recent-logs.txt 2/dev/null || true # 5. 内存映射符号缺失时可以配合地址做二次分析 cat /proc/$PID/maps $OUT/maps.txt echo collected: $OUT几个设计点解释一下timeout 10很重要。attach 一个线程特别多的进程偶尔会很慢不加超时会把现场采集脚本本身也卡住。采集wchan很快是基于/proc的不需要 attach。它显示每个线程在内核里的等待点比如mutex_lock、futex_wait_queue_me用它做快速预判确认值得跑 pstack 再跑减少对进程的打扰。日志一定要采。你会发现 Claude 的很多灵光一现其实来自日志里最后一条业务信息栈只是验证它的判断。目录化的好处是每个案例都能保留现场事后可以做复盘也能当样本继续优化 prompt。3.3 Prompt 设计别让 AI 蒙答案这是整个项目里我最想强调的部分。直接甩一份栈给 Claude它确实能给出像模像样的分析但经常出现一本正经胡说八道。我后来把 Prompt 改成强制结构化输出 强制标注证据情况好了很多。当前用的模板长这样你是一名资深的 Linux 服务端故障诊断专家。下面是一次线上进程疑似卡死的现场数据 - 进程信息来自 process.txt注意 STAT、%CPU、etime - 线程等待通道来自 wchan.txt - 全部线程栈来自 stack.txt - 最近日志来自 recent-logs.txt - 内存映射来自 maps.txt可选 请完成以下诊断严格输出 JSON不要输出任何额外解释 { summary: 一句话描述卡死原因, pattern: deadlock 或 spin 或 blocked_on_io 或 normal_wait 或 unknown, keyThreads: [关键线程编号及栈顶三层帧], evidence: [你依据哪一条栈帧或日志得出上述结论], confidence: high 或 mid 或 low, riskLevel: high 或 mid 或 low, nextSteps: [至少两条可验证的下一步操作可以是 gdb 命令或具体代码位置] } 要求 1. 如果证据不足以判断confidence 写 lowpattern 写 unknown并说明需要补充什么数据。 2. 不允许凭猜测补充细节。假设你是一名严谨的排查人员而不是讲故事的人。 3. nextSteps 必须是可以执行的动作不要写建议进一步排查这种空话。 4. 如果存在跨线程锁等待关系请显式写出线程A持有X等待Y线程B持有Y等待X这样的链。这样约束有几个好处JSON 结构让后续脚本能直接解析自动把结果发到 IM 或者生成工单。evidence 字段逼着 AI 把结论和输入数据绑定大幅降低瞎猜概率。confidence/unknown 选项给了 AI 一个合法的不知道出口。我实测发现只要明确允许它说不知道它反而更可靠不允许它说不知道它就编。3.4 跑起来headless 模式与结果解析调用侧很简单Claude Code 支持-p参数直接执行一次性任务claude -p $(cat diagnostic_prompt.txt) \ --output-format json \ --model sonnet \ case-${PID}-${TS}/context.txt case-${PID}-${TS}/result.json注意我习惯把prompt 模板和现场数据分开prompt 模板是固定的诊断指令现场数据是采集目录里的所有文件拼接后的内容。这样模板可以在 git 里维护成 playbook每来一个现场直接把数据接在后面。拿到返回的 JSON 后用 jq 提取关键字段看重点jq -r .result case-*/result.json | jq .summary, .pattern, .confidence, .nextSteps实际用的时候我会让脚本自动打印三样东西summary、evidence、nextSteps。summary 给自己扫一眼evidence 用于判断可信度nextSteps 直接复制到终端跑。我在实际使用中发现这套流程比在 Claude Code 交互式会话里粘贴一整份栈要稳定得多——交互式对话容易跑偏每次都要重新解释背景而固定 prompt 的输出质量是可复现的。4. 实战记录一次多线程死锁的定位全过程空谈设计没意思我拿一次真实的排查过程完整走一遍。为保护业务细节函数名会做脱敏但流程是原样的。4.1 现象进程活着业务全停那天值班收到告警某个网关服务健康检查失败15 分钟后仍没恢复。top看进程还在CPU 占用不高但请求成功率是零。ps -eLf看到 32 个线程其中 26 个状态是S可中断睡眠全部集中在内核锁等待上。我按 pstack-claude 的流程先执行采集脚本传到那个 PID 上。采集过程大约 3 秒对已经卡死的服务没什么影响。生成的 case 目录里有 process.txt、stack.txt、wchan.txt 和最近日志。4.2 Claude 给出的诊断摘要跑完 headless 分析结果里 summary 是这样的疑似死锁线程 7 持有一把业务互斥锁 A在 Router::Send 中继续等待锁 B 线程 11 持有锁 B在 Context::Destroy 中等待锁 A。 其余 24 个工作线程全部阻塞在获取锁 A 上形成锁 convoy。 confidence: highkeyThreads 里列了线程 7 和线程 11 的具体栈顶帧evidence 里对应写明了是从哪几行栈帧推断出持有/等待关系的。说实话看到 summary 的第一反应是跟我昨晚手工画的等待图结论一致但它只花了不到十秒我昨晚花了半小时。4.3 人工验证的环节AI 说 high confidence我不可能直接信。用它的 nextSteps 里的 gdb 命令来验证。它建议的其中一条是确认两个关键线程确实停在预期函数上gdb -p pid -batch \ -ex thread 7 -ex bt \ -ex thread 11 -ex bt跑出来的栈帧和 pstack 一致确认线程 7 的当前函数是Router::Send线程 11 的当前函数是Context::Destroy。接着用另一条建议查锁对象——打印pthread_mutex_t的__owner字段能看到互斥锁当前持有着的线程 LWP。把线程 7 等待的锁对象的 owner 打出来结果显示正是线程 11线程 11 等待的锁对象 owner 正是线程 7。这一下闭环了死锁关系实锤。修复其实很小代码里Send路径和Destroy路径对这两把锁的加锁顺序相反改成一顺的顺序就解决了。但 pstack-claude 真正帮上忙的点在于它把哪两个线程互相等锁这个最耗时的判断自动化了剩余时间我只需要验证不用从头开始拼图。4.4 复盘它帮了什么、没帮什么帮到的是检索和关联。25 个线程的栈打印出来让一个人脑快速扫出线程 7 等锁 B、线程 11 等锁 A这种关系其实不难但慢容易看漏。AI 做这种跨线程匹配是天然的强项而且它给出的 evidence 还能反过来加速人工复核。没帮到的是业务语义判断。它知道两把锁形成了环但不知道为什么会在不同路径以不同顺序加锁——这需要对业务代码演进的了解。还有一点要承认它也会误报。后面常见问题部分我会专门讲一次误报经历。5. 常见报错与避坑清单这块内容全部来自我实际踩过的坑按环境类和使用类分开。先用一张速查表再展开讲教训。5.1 环境类问题速查表现象原因处理方式pstack: command not found发行版默认没装 pstack 包安装 pstack 包或直接用 gdb -p -batch 方式兜底Could not attach to processyama ptrace_scope1 限制非父进程 attach临时sysctl kernel.yama.ptrace_scope0用后恢复attach 时报 Operation not permitted容器缺少 SYS_PTRACE capability启动容器加--cap-addSYS_PTRACE或从宿主机 attach栈里全是对地址没有函数名二进制被 strip缺 debuginfo安装对应 dbgsym/debuginfo 包后再抓栈claude 自动升级失败提示 no write permission to npm prefixnpm 全局目录不可写npm config set prefix $HOME/.npm-global或改用官方安装器Windows/WSL 下提示需要启用 Virtual Machine PlatformWindows 功能未开启管理员执行 dism 命令启用并重启采集脚本对健康服务造成抖动gdb attach 的 SIGSTOP 影响所有线程只在进程疑似卡死时触发不要高频定时抓栈5.2 使用层面的失败教训先讲一次误报经历。有一次进程卡住我照流程跑完Claude 给了 high confidence 的死锁结论还标了两个线程。但 gdb 验证时发现这两个线程等的是两把完全不同的、毫无交集的锁只是恰好栈长得很像。后来我补跑分析它自己承认这两把锁没有证据构成环。问题出在哪我当时采集时没带 wchan.txt而且只给了 stack.txt进程的整体状态信息太少。AI 看到一堆pthread_mutex_lock就倾向于推测死锁这是它的先验倾向。这个教训让我做了两处调整一是采集脚本强制带上 wchan 和 ps 状态二是 prompt 里明确写了禁止在没有跨线程锁等待链证据时下死锁结论。之后误报率明显下降。说白了栈只是快照状态信息才是判断死锁的关键上下文。第二个教训是符号缺失问题。有一回服务用的第三方库是 strip 过的pstack 里好几个线程栈顶都是??AI 分析直接抓瞎。这种情况光靠 pstack 不够还得把/proc/pid/maps里的加载基址拿出来用addr2line把地址换算成可读的函数名。所以脚本里采集 maps.txt 不是可选项是必须项。第三个教训是别追短命进程。曾经想对一个频繁启动的 worker 进程做排查脚本还没 attach 上进程已经退出了。后来我改成先采集/proc/pid/task/*/wchan做快速判断确认目标值得抓再跑 pstack整体命中率高很多也少打扰线上。5.3 几个我反复用的实战技巧先cat /proc/pid/wchan能秒看出进程卡在内核哪个等待点上。futex_wait_queue_me和mutex_lock是两个完全不同的方向这一步能帮你决定要不要跑完整 pstack。如果进程已经崩溃成 core 文件pstack 就派不上用场了但我发现同样的 prompt 模板可以直接用在coredumpctl gdb的thread apply all bt full输出上AI 的分析效果一样好。pstack-claude 本质上是喂栈给 Claude的模板输入换成 core 栈也能用。把 diagnostic_prompt.txt 放进 git 维护。每踩一次坑就补一条规则进去比如禁止无证据断言死锁必须列出跨线程等待链。一个月下来这个 prompt 就是你团队的排查经验沉淀比文档好用。生产环境做定时巡检的话别直接定时跑 pstack而是用健康检查结果做条件触发。比如连续两次健康检查失败再自动执行采集脚本这样既保证现场新鲜又不会频繁打扰正常进程。6. 做完这个项目后的几点体会我最初以为难点在写采集脚本做完才发现难点在调试 prompt、在教 AI 什么时候该说不知道。pstack-claude 做出来后我自己用它排查过死锁、条件变量不通知、还有一次纯粹是磁盘 IO 卡住的假死进程——后者 AI 正确识别为 blocked_on_io没有误报死锁靠的就是 wchan 和日志里的 IO 报错时间戳。最后分享一个正在做的扩展方向把采集脚本做成 systemd 定时任务配合健康检查失败事件自动触发跑完分析后把 summary 和 nextSteps 直接推到团队 IM 群附上一句这是 AI 的初步假设请人工验证后再操作。已经在小范围试跑了几周团队的值班体验改善很明显至少深夜接警的时候打开 IM 就有了一份带证据链的初步诊断而不是对着裸栈发呆。这套东西的思路不限于 pstack任何有结构化输出的诊断工具——jstack、gdb、perf的采样结果——都可以套用同样的采集AI 解读人工验证模式。我自己的下一个目标就是把 perf 的火焰图数据也接进来试试。
阅读完成 · 觉得有帮助?
咨询建站