1. pstack-claude 到底在解决什么问题
1.1 一次让我决定写这个工具的事故现场
上个月我们有一台订单服务节点半夜 CPU 直接飙到 300%,监控报警把值班群里所有人炸醒了。我连上机器,习惯性先看进程,然后就是pstack <pid>抓堆栈。结果那一刻我是有点懵的:一个 Java 面试题式的"线上 CPU 飙高怎么排查"场景,落到真实生产环境,出来的是一份几万行的线程栈转储。密密麻麻的线程 ID、十六进制地址、各种at开头的调用帧,我盯着屏幕翻了十几分钟,眼睛都快花了,才勉强判断出有一个线程池的线程状态不太对。
那一晚我就在想,这种工作真的应该由人工来做吗?堆栈分析本质上是模式识别:哪几个线程卡在锁上、哪些调用链反复出现、哪些逻辑等一个资源等了很久。模式识别正好是当前这代大模型的强项,尤其是长上下文能力足够强的模型,能把整份堆栈当成一个长文档去读。后来我把 Claude Code 接进了这套流程,顺手做了一个本地小工具,名字就叫 pstack-claude。它做的事情很简单:采集进程堆栈、归一化成结构化文本、交给 Claude Code 做分析、输出一份能直接拿去开会的排查报告。
1.2 pstack 和 Claude Code 各自是什么,组合起来又能干什么
pstack 本身不是新东西,Linux 上用来 dump 一个进程的调用栈,相当于给运行中的程序拍一张"现在每行代码执行到哪了"的快照。它和jstack、gdb、py-spy本质上是同一类工具,只是覆盖的语言运行时不同。Claude Code 则是 Anthropic 官方的终端编程智能体,可以直接在终端里跑,也可以进 VSCode,本质上是把 Claude 模型嵌入到命令行工作流里,让它读文件、跑命令、写代码、做分析。
把这两个东西拼在一起之后,我得到的效果非常直接:以前我要从几千行堆栈里人肉找"哪个线程在等什么锁",现在只需要让 Claude Code 去读那几千行,它会在几十秒内给出一个有优先级排序的嫌疑清单。我不再是它的翻译官,而是它的复核员。这个项目真正解决的,不是"堆栈采集"——那一部分本来就很成熟,而是"堆栈的消费效率"问题:采集完之后,谁来看?人看得慢,那就让模型先看一遍,我再带着结论去验证。
1.3 这个工具适合谁,不适合谁
如果你经常处理线上服务抖动、Linux 进程卡死、Java 线程池耗尽、Python 进程 hang 住这类问题,而且已经忍够了"抓完 pstack 没人愿意看"的流程,那 pstack-claude 会很对你的胃口。它适合那种已经有基本运维手段、但想在分析环节提升效率的团队。
反过来,如果你刚接触 Linux,连ps、top都还不熟,我不建议一上来就搞这套。工具只能优化你已经会的流程,不会凭空教你看懂堆栈。另外,如果你所在团队有严格的代码安全红线,不允许把堆栈、日志这类运行信息交给外部 API,那么你可以只把 pstack-claude 当作一个本地格式化的脚手架,最后一步的人工分析部分自己留着,模型部分跳过即可。这是一套完全解耦的设计,采集、归一化、分析三个环节各自独立。
2. 采集层:先把散乱的堆栈归一成一种格式
2.1 pstack 命令的局限与替代方案
Linux 上pstack用起来很简单:找到进程号,执行pstack <pid>,就能看到进程里每个线程的调用栈。但麻烦也藏在这里面。首先,很多发行版默认根本没有pstack命令,你敲下去会收到command not found。这时候最通用的替代方案是直接用 gdb:
gdb -p <PID> -batch -ex "set pagination off" -ex "thread apply all bt"这条命令的意思是:附加到目标进程,关闭分页,然后把所有线程的 backtrace 全部打出来。实测下来,它比很多发行版自带的pstack脚本输出更稳定,因为后者内部实现的版本差异非常大,有的输出带符号,有的就是一堆裸地址。
第二个坑是权限。pstack 或者 gdb 附加到进程,本质上要走 ptrace 系统调用。现代 Linux 默认开启ptrace_scope限制,非 root 用户只能调试自己启动的子进程,其他用户的进程一律Operation not permitted。遇到"抓不到栈"的情况,先不要急着上 root,先看一眼这个文件:
cat /proc/sys/kernel/yama/ptrace_scope如果是 1 或 2,说明有权限限制。临时放开可以执行echo 0 > /proc/sys/kernel/yama/ptrace_scope,但这个操作在容器环境里不一定有效,而且生产环境改内核参数需要走变更流程。更干净的办法是用 root 身份执行采集,或者给排查账号单独授权。
第三个限制是符号缺失。如果生产环境的二进制做了 strip,堆栈里就只剩下函数地址,没有函数名。这种情况 pstack 输出的可读性会大打折扣。我自己的处理方法是把info sharedlibrary的输出也一起抓下来,再把地址转换成偏移量交给模型,让模型结合代码库结构去猜。不过说实话,符号缺失的情况下模型再强也发挥有限,所以线上发布时保留 symbol 才是治本之策。
2.2 面对不同技术栈的采集命令
pstack-claude 这个名字带 pstack,但实际项目里我只把它当作整个采集层的代表,因为一个线上系统通常混着多种技术栈。我们的服务里有 Java 写的订单中心,有 Python 写的报表任务,还有少量 C++ 模块,所以我给采集层做了一套统一入口,每个入口背后是不同的采集命令:
- Java 服务:用
jstack -l <pid>,-l会输出锁信息,对分析死锁和线程阻塞非常重要。 - Python 服务:用
py-spy dump --pid <pid>,它不需要暂停进程就能拿到栈,对线上服务友好得多。 - C/C++ 模块:用前面说的 gdb 全线程 backtrace。
- 通用兜底:直接
pstack <pid>,虽然格式简陋,但至少快。
这里我最想多说一句的是jstack -l。很多新手只会jstack <pid>,丢掉了锁信息,然后对着"不知道线程在等什么"的栈干瞪眼。带上-l之后,你能看到线程持有哪些 monitor,正在等待哪个 monitor,这才是 Java 堆栈分析里最有价值的部分。
采集层还需要注意时间窗。单份堆栈只是进程某一瞬间的快照,很多时候看不出"趋势"。我现在习惯隔 3 秒连续采两份,如果问题持续时间长,还会再隔 15 秒采一份。三份快照放在一起,模型能看出哪些线程一直卡着没动,哪些线程是被临时打进来的流量击穿的。这个习惯帮我解决过一次非常隐蔽的间歇性卡顿问题,后面在案例部分细说。
2.3 归一化脚本:地址脱敏、重复帧压缩、栈频次统计
原始堆栈不能直接丢给模型。一份包含多线程的 pstack 输出里,有大量噪声:线程 ID 每次采集都在变、Java 内部帧at java.lang.Thread.run出现几百次、十六进制地址毫无规律。如果不做归一化,模型读起来效率低,而且容易把注意力放在无关的地址差异上。
pstack-claude 的采集脚本做三件事。第一,脱敏:把 IP 地址、内网域名替换成[host],把进程 ID 统一替换成[pid],避免把生产信息直接抛出去。第二,压缩重复帧:同一个调用栈里连续重复的帧只保留一个,比如 Java 里常见的synchronized锁等待会在栈底反复出现同一条park帧,这种信息保留一个就够了。第三,统计栈频次:把所有线程的调用栈按"栈顶前五帧的路径"做一次聚类,输出count字段,让模型清楚地看到哪一类调用栈占据了绝大多数线程。
以下是归一化之后的结构我直接贴出来:
[进程] order-service (pid [pid]) [线程组] http-nio-8080-exec-12 RUNNABLE count=24 at java.util.concurrent.FutureTask.get (FutureTask.java:169) at com.example.service.PaymentService.submit (PaymentService.java:58) at com.example.controller.OrderController.create (OrderController.java:42) [线程组] pool-7-thread-2 WAITING count=3 at jdk.internal.misc.Unsafe.park (Unsafe.java:593) at java.util.concurrent.locks.LockSupport.park (LockSupport.java:211) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire这份结构是给后面 Claude Code 看的,跟传统的 pstack 输出长得完全不一样,但它保留了堆栈分析真正需要的信息:线程数量、状态分布、调用链、锁等待点。模型拿到这份东西,效率会高很多。
3. 分析层:让 Claude Code 真正看懂堆栈
3.1 为什么选 Claude Code 而不是手动翻栈
堆栈分析本身不是高深学问,但它非常吃耐心。几千行栈,人肉翻一遍需要半小时到一小时,而且翻完还不一定能有结论,因为人眼很不擅长跨线程对比"谁在等谁"。Claude Code 这类工具的核心价值是长上下文和指令跟随:它可以一次性读完整份归一化文本,并且严格按照你给的输出格式给结果。
我选 Claude Code 还有一个实际原因:它本身就活在终端里,跟我们的排查流程天然契合。我们排查问题一直都是 ssh 上机器,跑命令,看输出。Claude Code 是同一个界面里就可以直接运行,不用再开一个网页应用,也不用把堆栈复制粘贴到别的工具里。加上它支持 headless 模式,可以在脚本里用一行命令触发分析,这让整个"采集 → 分析 → 出报告"的流程实现了自动化。
不过我也要说句公道话:模型不是神。它给出的结论永远是"基于当前证据的嫌疑排序",不是"根因判定"。我在流程里刻意保留了一个人工确认环节:所有模型输出的可疑代码位置,我都会去代码仓库里再核对一遍核心的那一两个结论。这样既享受了模型的效率,又守住了正确性的底线。
3.2 给模型的输入:一份结构化堆栈包
pstack-claude 每次分析不是只丢一堆栈文本给 Claude,而是打包成一份"结构化堆栈包",里面包含四类信息:
- 系统概览:进程是什么、CPU 和内存大概什么水位、最近有没有发布。
- 栈分组结果:就是前面归一化脚本输出的线程组列表。
- 关键日志片段:采集时间窗口内,进程日志里 WARN/ERROR 级别的行。
- 监控指标快照:线程数、活跃连接数、GC 次数这类动态数据。
为什么要加日志和指标?因为堆栈是静态切片,它只能告诉你"当前这一刻代码执行到哪了",不能告诉你"这一刻之前发生了什么"。有一次我们的进程大量线程卡在 socketRead,Claude 只看栈只能得出"网络读取缓慢"的结论。我后来把监控指标里"到下游 Redis 的连接数已经拉满"这个事实一起丢进去,它立刻把嫌疑指到了连接池配置上。所以说,堆栈是骨骼,日志和指标是血肉,两者都齐了模型才能做有依据的推理。
打包成 markdown 文件之后,每一份都放在独立的目录里,以时间戳命名。这不仅方便 Claude Code 读取,也方便我们事后回溯:哪一次故障对应哪一份分析包,一目了然。
3.3 Prompt 设计:角色定义、观察点与输出格式
分析层的核心是一段系统提示词。我一开始的提示词写得很偷懒,就是"请分析以下堆栈",结果是模型给出了一堆正确的废话。后来反复迭代,改成现在这个模样,核心是三个约束。
第一个约束是角色定义。告诉 Claude 它现在是一个有十年经验的 SRE,专门做线上故障排查,不是代码老师也不是架构师。角色限定之后,它的输出风格会明显偏向"现场处置",而不是"课堂讲解"。
第二个约束是观察点清单。我会明确告诉它重点关注哪几类问题:锁等待、死锁、线程池耗尽、连接池耗尽、磁盘 I/O 阻塞、CPU 热点、GC 引起的停顿。模型在长文档里找异常,如果没有观察点指引,它会把注意力平均分配到每条栈上,效率很低。给了清单之后,它会主动做"筛选-聚焦"的工作。
第三个约束是输出格式。我要求它严格按四段输出:现象归纳、可疑位置、证据链、推荐动作。其中"证据链"是我自己加的,要求它必须引用堆栈或日志里的原文来说明为什么怀疑某个位置。这招非常有用,能显著减少模型胡编的概率,哪怕它真的猜错了,我复核时也能一眼看出它从哪里开始跑偏。
3.4 自动化批处理:一行命令出初步报告
分析层如果只靠人手动复制粘贴,那就白折腾了。我写了一个很薄的批处理脚本,把整个流程串起来:
#!/usr/bin/env bash PID=$1 echo "[1/3] 采集堆栈与指标" bin/collect --pid "$PID" --out /tmp/pstack-claude/$PID echo "[2/3] 归一化文本生成" bin/normalize --dir /tmp/pstack-claude/$PID echo "[3/3] 交给 Claude Code 分析" claude -p "请阅读 /tmp/pstack-claude/$PID/stack_brief.md,按照 SRE 排查规范输出报告。"这里的claude -p是 Claude Code 的非交互模式,执行完直接打印结果,非常适合嵌入脚本。跑完之后,我会把输出重定向到report.md,再人工过一遍核心结论。如果第一轮的结论不够明确,我会把监控指标文件路径加进 prompt 再跑第二轮。
这套自动化流程我跑了小半年,最大的感受是:把"采集"和"分析"串起来之后,整个排障的启动成本从半小时降到了五分钟。以前看到报警要先手动执行各种命令,现在一条命令就能拿到一份带初步结论的报告。报告不一定每句都对,但至少给我指了一个方向,而不是让我面对几千行栈发呆。
4. Claude Code 环境搭建:一次装到能用的完整记录
4.1 前置条件:Node 与 npm 的坑
要跑 Claude Code,第一步不是装 Claude Code 本身,而是把 Node.js 环境搞干净。官方推荐的安装方式是通过 npm 全局安装:
npm install -g @anthropic-ai/claude-code这里最坑的是 Node 版本。如果你机器上的 Node 太老,npm 安装过程可能不报错,但 claude 命令启动时会直接崩,或者表现成"没有反应"。我建议先用node -v确认版本,低于 18 的先用 nvm 升上去。nvm 是 Node 版本管理器,比手装 Node 省心太多,尤其后面要反复切换版本的时候。
另一个前置问题是 npm 全局目录。很多人的 Node 是用安装包直接装的,全局 bin 目录指向/usr/lib/node_modules这种系统目录,普通用户没写权限。这时执行npm install -g会失败,或者安装完执行claude --version提示找不到命令。我个人解法是把 npm 的全局前缀指到用户目录下:
npm config set prefix "$HOME/.npm-global" export PATH="$HOME/.npm-global/bin:$PATH"把 export 那行写进~/.bashrc,以后就不会再有权限问题了。现在回想起来,这一条至少帮我省了三次跟权限错误搏斗的时间。
4.2 Windows 上的 Virtual Machine Platform 与 WSL
如果你在 Windows 上跑 Claude Code,大概率会撞上一个很典型的报错,大意是 Claude 的工作区需要 Windows 虚拟机平台,要求去启用它。我第一次看到这个提示也很懵,一个终端工具为什么要虚拟化平台?后来才反应过来,Claude Code 在 Windows 上不是简单跑一个 Node 进程,它的 workspace 机制需要用到 Windows 的虚拟机平台能力,相当于一个轻量的隔离环境。
处理方式其实很简单。一种是打开"启用或关闭 Windows 功能",勾选"虚拟机平台",然后重启系统。另一种是直接用管理员权限执行命令:
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart执行完重启之后再跑 claude,这个报错就能过去。另外提一句,如果你准备长期在 Windows 上做开发,与其反复调 Windows 原生环境,不如直接把 WSL 装上。Claude Code 在 WSL 的 Linux 环境下跑得更顺,很多 Windows 特有的奇奇怪怪的问题根本不会碰到。
WSL 安装也很直接:
wsl --install -d Ubuntu-22.04装完之后进入 WSL 终端,在里头再装 Node 和 Claude Code。这里注意,WSL 的内核和 Windows 宿主的内核是独立的,你在 Windows 上下载的 Node 在 WSL 里不能用,必须在 WSL 内部重新装。这个细节我当初没注意,白白浪费了半个小时。
4.3 VSCode 集成与模型配置
装好 CLI 之后,下一步大概率就是把 Claude Code 集成进 VSCode。我自己的习惯是在 VSCode 的集成终端里直接跑claude,这样文件编辑、命令执行、看模型输出都在同一个界面里,非常顺手。官方也提供了 VSCode 插件,安装方式是扩展市场里搜 Claude Code,装好之后左侧会多出一个面板。
VSCode 集成主要解决两件事:一是把当前打开的项目目录作为 Claude 的工作上下文,让它能直接读项目代码;二是把密钥管理统一起来。密钥可以通过环境变量设置,也可以运行claude config set写入配置。我是推荐环境变量方式的,比如export ANTHROPIC_API_KEY=xxx,因为这样密钥不会写死在项目配置里,也就不容易误提交到代码仓库。
模型配置同样可以用 config 命令切换。不同时期的模型擅长点略有差异,有时候某个模型对长上下文的遵循度高一些,跑堆栈分析就更稳。我一般会在项目目录里放一份.claude/config,把默认模型、温度参数都固定下来,确保同事拉下项目跑的时候行为一致。
这段提一句第三方模型的事:Claude Code 本身支持通过环境变量指定模型端点,所以也有人把它接到别的模型上。但我这篇只聊官方链路,第三方接入的兼容性和行为差异我没有系统性验证过,不展开。
4.4 升级失败与 npm 权限问题
Claude Code 更新很勤,每次启动都可能触发自动升级。升级本身是好事,但如果你 npm 全局目录没配好,就会看到一条经典报错:
auto-update failed: no write permission to npm prefix这句话说白了就是:自动升级想往 npm 全局目录写新版本,但你当前用户没权限。我在 4.1 里讲的npm config set prefix "$HOME/.npm-global"就是治这个病的。如果你已经装完才发现这个问题,可以先手动把目录权限理干净,或者干脆重装一次:先用 nvm 装好 Node,再重新全局安装 Claude Code,让所有文件都落在用户目录下。
还有一个小经验:如果你的 Claude Code 长时间没升级导致自动升级失败,可以手动执行一次覆盖安装:
npm update -g @anthropic-ai/claude-code这条命令不会动你的配置,只更新程序本体,比折腾自动升级稳定得多。我每次遇到版本异常都是先跑这个,基本都能拉回正轨。
5. 常见问题速查与排障记录
5.1 高频报错速查表
这一节把我在搭建 pstack-claude 过程中真正遇到过的问题整理成一张表,方便你直接对着查。有些问题是环境问题,有些是权限问题,也有纯使用姿势问题,但每一条都是真实踩过的。
| 现象 | 根本原因 | 处理方式 |
|---|---|---|
| pstack: command not found | 发行版没装 pstack 包 | 改用gdb -p <pid> -batch -ex "thread apply all bt" |
| ptrace: Operation not permitted | yama ptrace_scope 限制或容器权限不足 | root 采集,或按流程调整 ptrace_scope |
| Claude's workspace requires Virtual Machine Platform on Windows | Windows 虚拟机平台未开启 | 启用 Windows 功能"虚拟机平台",或改用 WSL |
| auto-update failed: no write permission to npm prefix | npm 全局目录不可写 | npm config set prefix "$HOME/.npm-global",重装全局包 |
| VSCode 终端里找不到 claude 命令 | PATH 没包含 npm 全局 bin 目录 | 把$HOME/.npm-global/bin加进 PATH |
| 运行 claude 时提示 App unavailable 或区域不可用 | 账号所在区域不在官方支持范围 | 以官方文档与支持渠道说明为准,不做绕过 |
| 模型分析结果泛泛而谈 | 输入信息太少,只有裸堆栈 | 补上日志片段和监控指标,增加角色与观察点约束 |
最后一行虽然是"模型分析结果不准",但它其实是这整套工具里最影响使用体验的问题。我单独放在后面详细讲。
5.2 抓不到堆栈的权限排查流程
抓堆栈遇到权限问题是早晚的事。我总结了一个固定排查顺序,遇到"抓不到"就按这个顺序走,能少走很多弯路。
第一步,确认当前用户身份和目标进程身份。ps -o user,pid,cmd -p <pid>看一眼,如果目标进程是另一个用户跑的,先切到那个用户或者用 root。
第二步,查 ptrace 限制。cat /proc/sys/kernel/yama/ptrace_scope,如果是 1 或者 2,非 root 用户无法附加到其他进程。生产环境如果长期需要抓栈,建议把排查账号加入到目标服务启动用户的附加组,而不是直接关掉安全限制。
第三步,容器环境要额外看宿主机的限制。容器里即使 root,宿主机没有给 ptrace 权限,依然会Operation not permitted。这种时候需要在容器编排配置里给容器加上SYS_PTRACEcapability,或者用 sidecar 模式把进程和抓栈工具放到同一个 PID namespace 下。
第四步,如果以上都正常但 gdb 附加后输出为空,检查进程是不是卡在了不可中断的 D 状态。这类进程的栈在用户态往往拿不到有效信息,得靠/proc/<pid>/stack看内核栈,但那个文件通常只有 root 能读。这个问题超出了 pstack-claude 的常规处理范围,遇到时我一般直接看内核事件日志。
5.3 分析结果不准的三次校准
我第一次用 pstack-claude 分析一个线程池任务堆积的问题,Claude 给的结论是"线程池配置过小,建议增大核心线程数"。这个结论不算错,但它没抓到重点。真实情况是线程池里的线程全部阻塞在了一个下游 HTTP 调用上,线程池大小只是表象,本质是下游服务超时设置太长。模型只看了堆栈,没有看指标,所以它不知道"阻塞时间中位数已经超过 10 秒"这个关键事实。
第二次校准,我把监控指标快照加进了输入包,并在 prompt 里写明"如果某个线程组 WAITING 超过 3 秒,优先怀疑下游依赖超时而非线程数不足"。模型立刻给出了准确得多的结论:下游 HTTP 连接池耗尽,所有线程卡在等待连接。这个案例让我意识到,给模型看"动态证据"比单纯堆栈重要得多。
第三次校准是关于输出格式。早期模型的报告很长,读完需要五分钟,但里面真正有操作价值的可能就三句话。后来我把输出格式强行约束成"先给结论,再给证据,最多五个嫌疑点",并明确告诉它"这是给值班人快速处置看的,不是论文"。之后每次分析结果都干净利落,一眼就能决定下一步动作。
这三轮迭代给我留下一个很重要的原则:pstack-claude 不是把堆栈丢给模型就完事,它要做的是把堆栈转化成模型容易理解、且不会误导模型的形式。输入侧的组织方式,往往比模型本身的聪明程度更影响最终结果。
现在我的流程已经很固定了:线上出问题,先在 3 秒和 15 秒两个时间点采集两份栈,连同日志和指标一起归一化,跑一遍 pstack-claude,拿到初步嫌疑清单,然后我带着清单去代码仓库核对最核心的一两个点,确认之后再动手改。它不会替我做最终决策,但把排查时间从一小时压缩到十分钟,我已经很满意了。踩过 Node 权限、Windows 虚拟机平台、WSL 环境这一圈的坑之后,我越发觉得这类工具链的问题,九成是环境问题而不是工具本身的问题。逐步把环境配好,剩下的就是不断积累排障的 prompt 经验,让模型一次比一次更懂你手底下的系统。