ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI 辅助线程堆栈分析:pstack+Claude 快速定位死锁与性能瓶颈

AI 辅助线程堆栈分析:pstack+Claude 快速定位死锁与性能瓶颈

CPU 飙到 99%,服务卡死,我第一时间跑上机器执行pstack <pid>,拿到了几千行线程堆栈。盯着终端看了半小时,能看出的只有“它在等某把锁”,至于哪把锁、谁持有、谁应该释放,完全没有头绪。后来我把同一条堆栈文本丢给 Claude,几分钟后拿到了一份带证据链的排查报告,从锁等待链到可疑调用点,甚至给出了下一步该执行哪些命令。这就是我写pstack-claude这个工具的起因:不是让 AI 替我做运维,而是让 AI 帮我把堆栈里最耗时间的那部分——辨认模式、梳理调用链、圈定嫌疑区域——直接压缩成可执行的判断。

这篇内容我会完整拆解这个工具的设计思路、采集与上下文处理的细节、完整的落地步骤,以及我在实际使用中踩过的一堆坑。适合后端开发、SRE、性能分析和所有被线程堆栈折磨过的人,读完你可以直接把这套方案搬到自己的项目里。

1. 项目定位:为什么我需要一个“会读堆栈的AI”

1.1 pstack 到底给了我们什么

pstack是 Linux 上最朴素的堆栈采集工具,它通过ptrace挂到目标进程上,利用 gdb 读取进程的内存和符号信息,把每个线程当前执行到的函数调用链打印出来。对于 C/C++ 程序,pstack <pid>是最直接的现场取证手段;Java 进程通常用jstack;Python 进程可以用py-spy dump;Go 程序则直接发一个SIGQUIT信号,运行时自己会把所有 goroutine 的堆栈吐出来。

堆栈信息的本质是“某个瞬间程序在执行什么”的快照。它非常诚实,不会撒谎,但也正因如此,原始输出极度不便阅读。真实线上的堆栈长这样:

Thread 0x7f8c1c000700 (LWP 12345): #0 0x00007f8c1b2d6c51 in __lll_lock_wait_private () from /lib/x86_64-linux-gnu/libc.so.6 #1 0x00007f8c1b2d4e3a in _L_lock_101 () from /lib/x86_64-linux-gnu/libc.so.6 #2 0x00007f8c1b2d4c37 in __GI___libc_malloc () from /lib/x86_64-linux-gnu/libc.so.6 #3 0x00007f8c1a3f9822 in operator new(unsigned long) () #4 0x00007f8c1a2a1f45 in std::string::_M_construct() ...

信息密度低、地址偏移一长串、重复的模式反复出现。人脑处理这种数据非常吃力,尤其是堆栈超过几百行时,整个排查过程就变成了在噪音里找信号。

1.2 堆栈离“诊断结论”还有几步

把原始堆栈直接丢给大模型,效果往往也不好,原因不是模型不行,而是你给的材料本身没有加工。堆栈分析这件事,真正的价值在“聚合”和“关联”,而不是“逐行翻译”。

我总结下来,从原始堆栈到可诊断的结论,至少要经历四步加工:

  • 清洗:去掉地址偏移、时间戳、编译路径这些对定位问题没有帮助的噪音,保留函数名、文件、行号、所在模块。
  • 分组:相同调用栈的线程合并在一起,统计出现次数。十个线程卡在同一个锁上是一回事,一个线程卡在某处是另一回事。
  • 模式识别:在清洗后的堆栈里标记“等待锁”“Native 方法调用”“GC 线程”“网络 IO 阻塞”这类高价值特征。
  • 关联上下文:把进程状态、线程总数、CPU 占用、采样时间这些信息一起打包,让分析者知道当前是什么场景。

打个比方:给你一千份员工打卡记录,让你找出谁在摸鱼,你的眼睛看不过来;但如果有人先统计出“连续五天十点以后到岗”的名单,再让你盯着这几个人看,效率完全不一样。pstack-claude做的就是这层“先统计、再分析”的加工,然后把加工结果交给 Claude 去判断。

1.3 这个工具适合谁,应用场景与能力边界

pstack-claude适合以下场景:

  • 线上应用突然 hang 住,需要快速判断是不是死锁或者锁竞争,没时间人工翻几百行堆栈。
  • Java 进程频繁 Full GC 导致线程停滞,jstack输出中堆满了GCLocker相关调用。
  • Python 程序出现全局解释锁(GIL)竞争,需要py-spy dump配合 AI 定位热点函数。
  • 定时巡检,每天自动采几轮堆栈,让 Claude 对比前后差异,提前发现隐患。

但它不是万能的。堆栈只能告诉你“此刻程序在哪个函数里”,没法告诉你“那台机器的网络是不是已经断了半小时”。所以这个工具的输出定位是“排查入口”而不是“最终结论”。它擅长把一块难啃的骨头变成可执行的下一步,但真正的根因确认,仍然需要结合日志、监控和分布式追踪。

2. 整体设计与工具链选型

2.1 采集层:多语言进程的堆栈采集方案怎么选

设计这个工具时,第一个问题就是“堆栈从哪里来”。不同语言、不同运行时的采法完全不同,我在工具里做了一个简单的自动检测:根据进程命令行特征判断该用哪个采集器。

工具适用场景典型命令注意事项
pstackC/C++、任意带符号的本地进程pstack <pid>依赖 gdb,符号不全时只能看到地址
jstackJava 进程(HotSpot JVM)jstack <pid>JDK 18 以上需要处理新 attach 机制
py-spyPython 进程py-spy dump --pid <pid>不需要重启程序,但需要 ptrace 权限
gdb attach通用兜底gdb -p <pid> -batch -ex "thread apply all bt"最重,适合其他方式都失效时
SIGQUITGo 程序kill -SIGQUIT <pid>Go 运行时自带堆栈 dump 能力

为什么不用单一采集器?因为实际环境太杂了。我曾经遇到过一个混合部署的节点,同一个容器里既有 Java 服务又有 Python sidecar,如果只做 jstack,另一个进程的问题就完全盲区了。所以工具的设计原则是“能采到就行,优先级分好”,具体顺序可以在命令行参数里覆盖。

2.2 上下文工程:给 Claude 看的堆栈要过几道筛子

很多人在类似工具上翻车,不是 API 调用有问题,而是 Prompt 上下文构造得太粗糙。把pstack <pid>的原始输出原封不动发给模型,模型会在几千行地址偏移里被噪音干扰,产出自然不理想。

我在这部分做了一个轻量级上下文工程,流程是:

  1. 规范化:用正则把0x00007f8c...这类地址偏移替换成#索引,把重复的from /lib/x86_64-linux-gnu/...简化成模块名。
  2. 去重与聚合:按“完整调用栈”做 hash,相同栈只保留一条,并在头部标注出现次数。比如[x12]表示同一个栈出现在 12 个线程里。
  3. 特征标注:对等待锁(__lll_lock_wait、pthread_cond_wait、Object.wait)、IO、GC、网络请求相关帧打上高亮标记。
  4. 裁剪和截断:单条栈超过 30 帧时截掉重复的通用底层帧,保留用户代码入口和当前正在执行的帧。
  5. 附上元数据:进程 PID、进程名、线程总数、采样时间、运行时长、系统负载。这些信息 Claude 不会自己猜,必须显式提供。

处理后我传给模型的内容大概长这样:

进程: order-service (PID 25891) 线程总数: 52 | 采样时间: 2025-06-01 14:30:22 可疑聚合栈 Top 3: 1. [x15] 线程卡在 java.sql.DriverManager.getConnection -> 调用链: HttpHandler -> OrderService.create -> DBClient.getConnection 2. [x8] 线程处于 TIMED_WAIT 状态 -> 调用链: ThreadPoolExecutor.getTask -> ... 3. [x2] GC 线程活跃,G1 并发标记阶段

这种结构对 Claude 来说,直接省去了“大海捞针”的过程,它可以把精力集中在真正的分析上。上下文工程是整个工具跑得顺不顺的关键,比模型选哪个还重要。

2.3 分析接入:Claude API 与 Claude Code 两条路径

pstack-claude的“分析引擎”有两种接入方式,我两个都做了,因为使用场景完全不同。

第一种是 Claude API 直连。适合完全自动化的场景,比如定时巡检、故障自愈流程里自动触发分析。工具内部用anthropic的 Python SDK 拼装消息,把加工后的堆栈作为用户消息传入,system prompt 设定“你是一名有 10 年经验的 SRE 专家”之类的角色约束,并把输出格式固定为 Markdown 报告,包含摘要、热点现象、可能原因、建议命令四个部分。这种方式不需要人工介入,拿到报告直接入库或告警。

第二种是 Claude Code 交互式接入。适合人在现场的排查场景。我在终端里启动claude,让它读取已经生成好的堆栈分析报告,然后自然语言追问“第二个锁等待的持有者是谁”“这个 GC 线程是不是导致 CPU 飙升的元凶”等。Claude Code 的好处是它能在同一个会话里执行命令、读取文件、跑诊断脚本,等于把整个排查过程变成了对话式协作。

这两条路径在代码上共享同一套 Prompt 模板和堆栈加工逻辑,只是调用方式不同。API 路径适合“无人值守”,Claude Code 路径适合“人在环上”。

2.4 扩展为 MCP Server:让 Claude Code 直接操作整个排查流程

既然 Claude Code 已经能自己干活了,那pstack-claude作为老式脚本就有点浪费。MCP(Model Context Protocol)协议出现后,我在项目里加了一个 MCP Server 包装层,把“采集堆栈 + 预处理 + 生成报告”变成一个可被 Claude Code 直接调用的工具。

这样一来,你在 Claude Code 里直接说“帮我看看 PID 25891 这个进程是不是卡在锁上了”,Claude 会自己判断需要调用analyze_process工具,然后自动执行采集、分析、写报告,最后把结论返回给你。

添加一个本地 MCP Server 到 Claude Code 里的命令大概是这样:

claude mcp add --transport stdio --scope user pstack-claude -- node /usr/local/bin/pstack-claude-mcp.js

如果你在项目里只想让协作同事也能用,可以通过.mcp.json声明:

{ "mcpServers": { "pstack-claude": { "command": "node", "args": ["/usr/local/bin/pstack-claude-mcp.js"] } } }

注意,MCP Server 和 API 调用并不冲突:MCP 让 Claude Code 具备操作能力,API 让无人值守脚本具备分析能力。实际使用中我把两者组合起来,效果比单用任何一个好很多。

3. 实操全流程:从一条命令到一份诊断报告

3.1 安装与环境准备

先说环境要求。pstack-claude是 Python 脚本,分析端依赖 Claude API 或 Claude Code。全套流程准备如下:

  • Python 3.10+,建议用虚拟环境隔离依赖。
  • pip install anthropic psutil。anthropic是官方 SDK,psutil用来读取进程元数据。
  • Claude Code 不是硬性要求,但如果要交互式排查,建议装:npm install -g @anthropic-ai/claude-code,要求 Node.js 18+,安装后执行claude --version验证。
  • 配置 API Key:不要硬编码进代码里,直接 export 到环境变量。
export ANTHROPIC_API_KEY=sk-ant-xxxxxx

提示:API Key 是敏感信息,生产环境建议放进密钥管理服务,而不是写在 shell 历史里。

由于我经常在容器里排查,容器内要保留pstack或gdb,否则采集层会直接失败。Debian/Ubuntu 系可以用apt install gdb装齐。

3.2 快速开始:采集一个进程的堆栈并生成报告

核心代码不长,我贴一个精简版,保留最关键的流程:

import os import sys import subprocess import psutil from anthropic import Anthropic STACK_COMMANDS = { "java": "jstack {pid}", "python": "py-spy dump --pid {pid}", "c": "pstack {pid}", } def collect_stack(pid: int, runtime: str) -> str: cmd = STACK_COMMANDS.get(runtime, "pstack {pid}").format(pid=pid) return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, text=True) def simplify(stack: str) -> str: lines = [] for line in stack.splitlines(): line = line.strip() # 去掉地址偏移 line = line.replace("0x00007f", "#") if line and "from /lib/" not in line: lines.append(line) return "\n".join(lines) def analyze(pid: int, runtime: str, content: str) -> str: client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) proc = psutil.Process(pid) meta = f"进程: {proc.name()} (PID {pid}) | 线程数: {proc.num_threads()}" prompt = f"""{meta} ---- 堆栈信息 ---- {content} 请分析当前进程是否存在异常:死锁、锁竞争、线程卡死、GC问题或其他可疑点。 输出 Markdown 格式报告,包含:1. 摘要 2. 热点问题 3. 证据链 4. 建议命令。 """ resp = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2000, system="你是一名有 10 年经验的后端性能分析专家,擅长线程堆栈定位问题。回答要具体、克制、不臆测。", messages=[{"role": "user", "content": prompt}], ) return resp.content[0].text

使用方式:

python pstack_claude.py --pid 25891 --runtime java --output report.md

脚本内部流程是:探测进程类型(也可以通过--runtime强制指定)-> 执行采集命令 -> 清洗和聚合 -> 构造 Prompt -> 调用 Claude API -> 把返回的 Markdown 报告写入指定文件。

执行后生成的报告大概长这样:

## 摘要 进程 order-service 存在明显的数据库连接获取阻塞,15 个线程卡在 getConnection。 ## 热点问题 - 线程池连接耗尽:多个线程在等可用连接,疑似连接池太小或连接泄漏。 ## 证据链 - 15 个线程的栈同时包含 `DBClient.getConnection`,且调用栈上层都是业务接口入口。 ## 建议命令 1. 确认连接池配置:`jstat -gcutil <pid>` 2. 抓取两轮 jstack 间隔 10 秒对比线程是否还在等待 3. 检查数据库端活跃连接数

这份报告的价值在于它把“结论”“证据”“下一步动作”分开,你拿到后不是被一个问题牵着走,而是有了一条清晰的排查路径。

3.3 在 Claude Code / VS Code 里配合使用

有经验的工程师排查问题时不会满足于一次性报告,总要追问几轮。pstack-claude和 Claude Code 结合后的体验比纯 API 顺畅很多。

终端里启动 Claude Code:

claude

然后在交互界面里说:

请读取 report.md 和 stack_raw.txt,重点分析第 2 个热点。 接着检查当前进程的网络连接状态,看是否有大量 TIME_WAIT。

Claude Code 会自己安排工具调用顺序,比如先cat report.md,再ss -antp | grep 25891,最后给你一个综合判断。整个过程像带了一个实习生在帮你跑腿,你只需要把关方向。

VS Code 用户可以在 IDE 内置终端里跑:

  1. 安装 Claude Code 的 VS Code 扩展。
  2. 在侧边栏或终端面板打开 Claude Code 会话。
  3. 在会话里直接执行python pstack_claude.py --pid 25891 --runtime java,然后继续追问结果。

实际上 VS Code 的本质是给你一个顺手的终端,真正干活的是 Claude Code 的 Agent 能力。它可以读文件、执行命令、解析输出,所以排查完堆栈之后还能顺手帮你查日志、看监控曲线截图,整个闭环非常顺。

3.4 看懂结果:结论与证据要分开看

pstack-claude输出的报告我很在意一点,就是“哪些是确定的,哪些是推测的”。AI 在生成文本时天然倾向自信表达,如果它告诉你“这就是连接池泄漏”,你把它当实锤去改代码,可能浪费时间。

所以在 Prompt 模板里我做了约束:报告必须区分“证据链”(从堆栈里直接看到的事实)和“推测原因”(基于经验的判断)。实际操作时,我会再追问一句:“以上判断中,哪些是堆栈直接证明的?置信度打分。”这样能明显降低误判率。

另外,尽量不要只采样一次就下结论。线程调度具有随机性,一次采样可能恰好错过了真正卡住的线程。我一般建议连续采样 3 次、间隔 10 到 30 秒,对比不同时间点的堆栈差异,稳定性会好很多。

4. 常见坑与排查技巧实录

4.1 采集端:attach 权限与容器隔离

堆栈采集的第一步就经常遇到权限问题。Linux 内核的ptrace_scope安全机制限制了普通用户 attach 到非子进程。报错通常是:

Operation not permitted

检查方式:

cat /proc/sys/kernel/yama/ptrace_scope

如果输出1,只有父进程或具备 CAP_SYS_PTRACE 的进程才能 attach。临时放开可以执行:

echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

但这只是临时方案,生产环境不建议直接改内核参数,更稳妥的办法是用 sudo 运行采集命令,或者把采集进程以 root 权限跑。容器环境下还有一个坑:默认容器看不到宿主机全局的进程表,需要给采集容器挂--pid=host,跑 Java 进程的容器还要注意 jstack 必须能 attach 到目标 JVM,有时候需要--cap-add=SYS_PTRACE才能成功。

4.2 Claude Code 安装与自动升级时的 npm 权限问题

Claude Code 的安装一直是大多数人翻车的高发地,尤其是 npm 全局目录权限不对时,会看到:

auto-update failed: no write permission to npm prefix

这个问题的根源是 npm 的全局安装目录属于 root,当前用户没有写权限。最干净的解决方式是把 npm 全局目录改到自己用户目录下:

npm config set prefix ~/.npm-global export PATH=$PATH:~/.npm-global/bin

设置完重新安装:

npm install -g @anthropic-ai/claude-code

然后验证:

claude --version

不要在有写权限问题的路径下直接sudo npm install -g,虽然能装上,但后续自动升级还是会反复报权限错误,而且会造成文件归属混乱。另外,如果你不想全局安装,临时跑一次也可以用npx @anthropic-ai/claude-code,不过每次启动速度会略慢,体验不如全局安装。

4.3 Windows 上桌面版与虚拟机平台依赖问题

如果你在 Windows 上安装或启动 Claude 桌面版,有时会碰到类似下面这种提示:

Claude's workspace requires the virtual machine platform on Windows. Enable it...

这个问题的本质是新版桌面端使用了虚拟化工作区隔离机制,Windows 必须开启虚拟化功能才能运行。两步处理:

  1. 在 BIOS 中确保 CPU 虚拟化已开启(Intel VT-x 或 AMD-V)。
  2. 以管理员身份打开 PowerShell,执行:
Enable-WindowsOptionalFeature -Online -FeatureName HypervisorPlatform

执行完重启操作系统。如果你不想动系统组件,还有一个替代方案:放弃桌面版,改用 Claude Code 命令行工具,在终端里完成对话和任务执行,它不依赖 Windows 虚拟机平台。还有不少人选择在 WSL2 里跑 Claude Code,这也是可行的,WSL2 本身依赖虚拟化平台,如果系统没开,先执行上面的命令把基础打牢。

顺带一提,这类“Windows 功能缺失”的问题和网络、地区无关,纯粹是本机组件开关的问题,检查顺序建议是:BIOS 虚拟化 -> Windows 可选功能 -> 重启,按这个顺序排查最快。

4.4 分析端:采样时机、符号缺失与 AI 误判

堆栈采集得再准确,分析端照样有坑。最常见的三个问题:

第一,采样时机不对。进程可能只是偶发卡顿,你采样的那一刻恰好恢复了,堆栈看起来一切正常。解决办法是连续采样两到三次,间隔 10 秒以上,并把“多次采样结果一致”作为可靠判断的前提。

第二,符号文件缺失。堆栈里如果出现大片的??或者[0x7f8c...],说明程序没有携带符号表或符号文件路径不对。对 C/C++ 程序,确保编译时带-g,线上环境可以保留单独的 debuginfo 包供 gdb 使用。对 Java 程序,注意 jstack 输出的 native 栈部分本来就可能只有地址,这是正常的,重点看 Java 栈帧即可。

第三,AI 过度推断。模型看到堆栈里出现getConnection就顺着猜测连接池问题,但它并不知道你的数据库连接池监控显示正常。我的处理方式是在 Prompt 里固定一句:“如果堆栈本身不能直接证明某个结论,标注为推测,不要使用肯定语气。”同时在追问时告诉它补充监控数据,哪怕只是贴一段日志,判断准确率都会有质的提升。

5. 进阶玩法:把 pstack-claude 变成巡检机器人的“手和眼”

5.1 定时采集与基线对比

工具做出来后,我第一个想到的用法不是故障时临时用,而是让它每天定时干活。通过 cron 或 systemd timer 每 5 分钟采集一次堆栈,并和上一次的聚合结果做对比:哪个调用栈的线程数量在上涨,哪个等待模式是新增的,哪个之前出现过但这次消失了。

基线很关键。第一次跑的时候,找一个服务稳定的时段,采集一份堆栈作为基线存档。之后每一次采样都和基线做 diff,AI 只需要关注“新增的、变化的”那一部分,而不是把整个堆栈重新分析一遍。这个思路和监控系统里的动态阈值很像,以常态为参照,异常才会真的跳出来。

5.2 告警触发与报告归档

在定时任务里加一层条件判断:如果聚合结果中有超过 20 个线程卡在同一栈帧,或者某个等待模式的占比超过历史统计的 3 倍标准差,就触发告警。告警不是简单喊一嗓子,而是自动跑一遍完整分析流程,生成 Markdown 报告,按时间戳命名归档:

report-20250601-143022.md

原始堆栈和分析报告放在同一个目录,方便事后回溯。这个做法在复盘故障时特别好用,因为你不用凭记忆还原现场,报告里的证据链就是当时最真实的记录。

5.3 更完整的现场:让数据库、日志和监控一起进上下文

堆栈分析的局限在于它只看得到“程序在干什么”,看不到“外部系统发生了什么”。所以我通过 MCP 生态把更多数据源接了进来:Claude Code 可以在一次会话里读取 MySQL 慢查询日志、拉取 Redis 延迟监控、查看网关错误率。

假设堆栈显示多个线程卡在JDBC executeQuery,传统做法是你还得手动去数据库查慢 SQL。但接入了 MCP 工具后,Claude Code 会自动把慢 SQL 日志 grep 出来,和你手里的堆栈做关联,直接告诉你“就是这条 SQL 引起的连接长时间占用”。这一步把“猜测然后验证”变成了“现场综合分析”,效率完全不一样。

配合常规的日志分析工具、APM 系统,pstack-claude的角色从“堆栈分析器”进化成“故障现场助理”,它不替代任何现有监控,而是把所有信号在 AI 会话里拼接成一个完整画面。

我在实际使用中最大的体会是:这工具的价值不在于替你做决策,而在于帮你把发现问题的速度从小时级压缩到分钟级。刚开始我还会逐帧去核对它的分析结果,后来跑的次数多了,发现它列出的证据链确实都对应得上,才敢让它在巡检任务里全自动出报告。如果你也想自己搭一个,建议从最朴素的想法开始:先写一个脚本,把堆栈采集下来,再用手头的 Claude API 试试直接分析原始输出,感受到噪音的影响后,你就会理解为什么预处理那一步如此重要。最后分享一个我踩过几次坑后养成的习惯:第一次跑一定要先采一份正常状态的堆栈当基线,往后所有排查都拿它做底子,AI 的判断准确率会高出一大截。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进