1. 凌晨 80 负载背后的教训为什么要搞 pstack-claude先讲一个真实场景。上周二凌晨两点我负责的一台 16 核机器突然出现大面积卡顿load average 直接冲到 80SSH 能连上但每次敲命令都肉眼可见地要延迟好几秒。核心服务的健康检查全部超时线上已经开始告警。我当时的第一个动作是保存现场用 pstack 把主进程的用户态堆栈打下来再把/var/log/messages和最近的业务日志按时间窗口切出来。600 多行原始堆栈拿到手里我连续看了快一小时只得出一个模棱两可的结论某个线程似乎长时间阻塞但阻塞在哪一层、跟哪个资源竞争完全没有头绪。后来我换了个思路把堆栈文件、进程元数据和日志片段整理成一个目录直接让本机安装的 Claude Code 以只读方式去分析。它给我的第一份报告里就指出了我一直忽略的一条链路线程 A 在futex上等待锁线程 B 持有同一把锁但已陷入内核态无法退出而锁对象的地址和连接池初始化代码里的全局变量正好对得上。顺着这条线索我确认是连接池在极端情况下出现了锁顺序倒置随后调整了池大小和超时参数重启服务二十分钟后负载恢复正常。这次经历让我重新审视了这套排查链路pstack 这类堆栈工具是疑难问题的关键取证手段但堆栈规模一大、线程数量一多人肉分析效率就极低而 Claude Code 真正有价值的地方并不只是能读你贴上去的文本而是它可以自己读取本地文件、执行命令、反复修正分析路径。它处在能动手检查的位置而不是被动的聊天窗口。于是我把这套流程固定下来起了个项目名叫 pstack-claude。pstack-claude 本质上是一套轻量工作流上游用 pstack、gdb、系统日志采集现场数据下游用 Claude Code 做结构化分析和验证。它适合两类人一类是做运维或 SRE 的同学经常处理进程卡死、线程阻塞、内存抖动问题另一类是负责底层服务开发的工程师想借助 AI 快速解读崩溃现场。这篇文章完整记录这套流程的搭建过程包括安装环境里的坑、堆栈采集姿势、提示词设计、脚本封装以及一次真实内存抖动案例的复盘。1.1 为什么不是把堆栈直接粘贴到网页对话里很多人第一反应是把堆栈贴给网页端 AI 不就行了我在早期也这么干但很快发现三个致命问题。第一网页对话没有现场感知能力。堆栈里出现0x7f8c2a3b4c50这种地址如果 AI 能查看/proc/pid/maps或加载符号文件就能还原出具体函数而网页对话只能对着文件名和偏移量猜。第二粘贴文本会丢失大量上下文。一次完整诊断往往要交叉看堆栈、日志、系统参数、进程启动参数四类信息一次性贴完容易超出上下文限制而且格式全乱。第三对话式 AI 无法验证自己的假设。Claude Code 在终端里可以执行cat /proc/pid/status去核对线程状态可以跑一个短小的统计脚本去判断锁竞争频率而网页对话只能等你把结果再贴回去。所以我最终选择在 Claude Code 里跑整条分析链路。它本质上是一个能访问本地环境的代理而不是一个聊天窗口。这个区别在处理线程为什么卡住这类问题时非常关键因为卡住的原因往往不在堆栈本身而在堆栈之外的进程状态和资源占用数据里。1.2 澄清一个词pstack 到底是什么pstack 是一个 Linux 命令行工具核心功能是打印指定进程的用户态线程堆栈。用法很简单pstack 12345它会输出进程内每个线程当前的函数调用栈包括函数名、偏移量和对应的二进制文件。C/C 服务、Redis、Nginx 这类进程卡死或性能抖动时pstack 的输出可以告诉我们现在大家都在哪一行代码上停留。但要注意pstack 并不是万能的它看不到内核态栈看不到锁的持有者信息对进程里某些 JIT 生成的代码也只能显示原始地址。所以我在实际使用中几乎总会搭配 gdb 一起抓栈后面会专门讲。之所以给这项工作起名 pstack-claude是因为 pstack 代表采集现场的那一半Claude 代表解读现场的另一半。对于没有现成 APM 平台的小团队来说这套组合可能是最快能落地的 AI 辅助排障方案。2. 安装与运行环境npm、WSL2、VS Code 里的各种隐性门槛先说安装这件事。Claude Code 最常见的安装方式是 npm 全局安装npm install -g anthropic-ai/claude-code装完以后执行claude --version确认版本。之后在任意项目目录里敲claude它就会进入交互模式你可以让它读文件、跑命令、修改代码。要实现 pstack-claude 里的一键分析能力更重要的一种用法是claude -p也就是非交互模式后面章节会展开。2.1 auto-update failednpm 全局目录没有写权限怎么办我第一次安装完第二天打开终端就遇到一个很典型的报错Auto-update failed: No write permission to npm prefix.原因很简单Claude Code 会自动检查更新并尝试把新版写入 npm 的全局目录而那个目录通常是/usr/lib/node_modules或/usr/local/lib/node_modules普通用户没有写权限。默认情况下如果你是用sudo npm install -g装的全局目录归 root 所有后续自动更新就会失败。我最推荐的解法是先看一下当前的 npm prefixnpm config get prefix如果输出是/usr/local那要么配置用户级 npm 目录要么修正目录权限。但我不太建议直接把整个全局目录改成当前用户所有污染面太大。更稳妥的做法是用 nvm 管理 Node.js 版本这样 npm 全局目录就在用户目录下自动更新不会遇到权限问题nvm install 22 nvm use 22 npm install -g anthropic-ai/claude-code不过要提醒一点用 nvm 之后如果你同时在用 WSL2两边是两套独立的 Node 环境全局命令行工具不会自动互通。别问我是怎么知道的我在 Windows 原生终端里装了 Claude Code切进 WSL 后发现命令不存在又装了一遍。2.2 Windows 上的 virtual machine platform 报错怎么处理如果你在 Windows 上运行 Claude Code可能会遇到一个看起来有点吓人的报错Claudes workspace requires the virtual machine platform on Windows. Enable it...这个报错的意思是Claude Code 的某些功能尤其是涉及隔离执行环境的特性依赖 Windows 的虚拟机平台组件。对这个报错不用太紧张按下面的顺序处理就行。首先确认 Windows 版本已开启两项可选功能适用于 Linux 的 Windows 子系统和虚拟机平台。在控制面板的启用或关闭 Windows 功能里勾选然后重启机器。如果重启后还报这个错最简单的做法是直接把开发环境迁移到 WSL2 里Windows 上保留 VS Code 连接 WSL 远程开发即可。我在 WSL2 里跑 Claude Code 之后稳定性好了很多文件 IO 更快命令执行也更接近生产环境的 Linux 语义。这里有个容易忽略的点WSL2 默认使用虚拟化平台如果你本身是在虚拟机内部再跑 WSL2可能会遇到嵌套虚拟化不开启导致的功能缺失。这种情况需要先在宿主机的虚拟机设置里打开对应的虚拟化选项否则在上面排查半天也找不到原因。2.3 在 VS Code 里配置 Claude Code很多读者问我在 VS Code 里怎么用。我的习惯是在 VS Code 的终端里直接运行claude而不是额外装一堆插件。这样 Claude Code 能直接看到当前工作区的文件结构分析代码时路径不会错。如果你希望在 VS Code 中更顺手地调用可以配置终端的shellIntegration或者使用官方提供的 Claude Code 扩展。这里有一个实操细节Claude Code 会读取当前目录下的CLAUDE.md作为项目级指令文件。你可以把一个通用的调试分析指令放在CLAUDE.md里这样不管从 VS Code 终端还是普通终端启动它都会自动加载。我们后面会专门给 pstack-claude 写一份这样的指令。3. 堆栈采集姿势pstack、gdb 批量线程栈与现场保留到了最关键的取证环节。我不打算只讲敲一个 pstack 命令就完事因为实际排障中堆栈采集的姿势决定了后续 AI 分析的质量。同样一份证据采集得全还是不全分析结论可能完全不同。3.1 pstack 的常用用法与三个限制pstack 的基本用法前面已经写过这里补充几个实际经验。第一权限问题。普通用户执行pstack pid时如果目标进程不是你启动的大概率会遇到Operation not permitted。此时需要切换到 root 或使用sudo。如果是容器里的进程还要注意宿主机和容器内的 PID 命名空间不同pstack 的 pid 需要以宿主机视角为准。第二线程数很多的进程pstack 输出会非常长。一个 200 线程的 C 服务堆栈文件轻松超过 1000 行。此时直接全量丢给 Claude Code既消耗大量上下文又容易让分析失焦。我建议先用grep或awk做预处理比如只保留每个线程栈顶的前 15 帧丢弃重复度极高的帧序列这样核心信息不会丢体积却能小一多半。第三pstack 默认只输出用户态栈进程卡在内核态时它可能只显示到某个系统调用入口比如futex、epoll_wait、poll。这时候需要进一步看内核态线索比如查/proc/pid/stack或者用cat /proc/pid/status看进程状态是否处于不可中断的 D 状态。3.2 gdb 批量线程栈比 pstack 更完整的现场在我的经验里如果只看一个工具首选的往往不是 pstack而是 gdb 的批处理模式。命令如下gdb -p pid -batch \ -ex set pagination off \ -ex thread apply all bt \ -ex info threads \ stack_gdb.txt 21这条命令会把所有线程的完整回溯栈 dump 出来同时输出线程 ID、状态、锁信息如果 gdb 能识别 libc 的 pthread 结构等附加信息。对 AI 分析来说info threads的输出尤其重要因为它明确标出了哪些线程处于运行态、哪些处于睡眠态哪些正在等待条件变量。这比一张平铺的调用栈表格有信息量得多。当然gdb 也有代价附加到进程过程中被调试进程会短暂暂停这对于不能中断的线上服务是种风险。所以我不是上来就 gdb而是先 pstack发现问题需要更多细节时再快速用 gdb dump 一次整个过程控制在几秒内然后立刻恢复服务。实际用下来这个短暂停顿对绝大多数业务服务影响可以忽略如果你实在不放心可以先在预发环境演练一遍。3.3 现场保留排障第一步永远是别让证据跑了很多人在服务卡死时急着重启这是最忌讳的。进程一旦重启所有线程堆栈、内存状态、锁信息全部消失后面想复盘就只能靠零散日志。我的习惯是准备一个排障脚本目录比如/tmp/investigate/遇到疑似故障时先做一组快照mkdir -p /tmp/investigate/$(date %Y%m%d_%H%M%S) cd /tmp/investigate/$(date %Y%m%d_%H%M%S) ps -eo pid,ppid,stat,wchan:32,cmd | grep -E app|redis ps.txt pid$(pgrep -f your-app-name | head -1) pstack $pid pstack.txt cat /proc/$pid/status status.txt cat /proc/$pid/stack kernel_stack.txt sysctl vm.overcommit_memory sysctl.txt dmesg | tail -200 dmesg.txt这个快照目录就是后面 pstack-claude 要分析的输入。我强烈建议把时间戳写进目录名因为后续可能要对比多个时间点的快照没有时间戳会出现文件覆盖问题。这个教训我在刚开始排查时也踩过顺手把两次采集都放在同一个目录下第二次抓到一半才发现旧文件被覆盖线索直接断掉。4. 把 Claude Code 变成堆栈分析师提示词与一键脚本的设计取证这半边准备好了接下来是分析这半边。我所说的分析师不是一个临时拼凑的提示词而是一套让 Claude Code 稳定输出可验证结论的规则。如果你只是随手丢一句帮我看看这个堆栈它给出来的东西往往不可控一旦把角色边界、阅读顺序、输出模板都定下来结果质量会完全不一样。4.1 用 CLAUDE.md 固定分析人设与工作边界我项目根目录下有一个CLAUDE.md内容经过多次迭代核心是这几条# pstack-claude 项目说明 ## 你的角色 你是一名具备内核与分布式系统背景的资深 SRE负责根据现场快照复原故障根因。 ## 工作方式 1. 用户会把现场快照放在 /tmp/investigate/ 下对应的子目录中。 2. 你应先阅读目录中的 ps.txt 和 status.txt了解进程整体状态。 3. 再阅读 pstack.txt 或 stack_gdb.txt识别每个线程的停留点和锁等待关系。 4. 对于可疑结论用用户提供的可执行命令去验证不要只停留在推测。 5. 最终输出必须包含三部分主线程与业务线程状态、锁等待/异常栈分析、候选根因列表。 6. 候选根因列表中的每一条都要附带验证命令例如 ss -tnp、cat /proc/pid/smaps。 ## 禁止 - 不要在缺少数据时强行下结论应明确列出缺失的信息。 - 不要忽略进程状态为 D 或 Z 的线程这类状态往往对应内核态阻塞或僵尸资源。这份指令的价值在于让 Claude Code 的输出结构稳定。没有它的约束时它的回答比较发散加上了角色设定和输出框架每次的分析报告都能直接成为团队内部复盘材料。你可能注意到我特意写了应先用哪些文件、后看哪些文件的顺序这是为了控制上下文和推理质量。先建立全局状态认知再看局部的栈和直接盯着栈硬猜结果差距很大。4.2 一键脚本从快照到分析报告手动操作太累所以我封装了一个脚本叫pstack-claude.sh。它的作用是获取指定进程的快照调用 Claude Code 的非交互模式生成分析报告并输出到当前目录。#!/usr/bin/env bash set -euo pipefail PID${1:-} if [ -z $PID ]; then echo usage: $0 pid exit 1 fi OUT/tmp/investigate/$(date %Y%m%d_%H%M%S)_$PID mkdir -p $OUT ps -eo pid,ppid,stat,wchan:32,cmd | grep -w $PID $OUT/ps.txt cat /proc/$PID/status $OUT/status.txt cat /proc/$PID/stack $OUT/kernel_stack.txt 21 || true pstack $PID $OUT/pstack.txt 21 || true # 如果存在大量线程预裁剪每个线程栈保留前20帧控制在合理长度内 if [ $(wc -l $OUT/pstack.txt) -gt 800 ]; then awk /Thread/{count; depth0} depth20{print} /^[[:space:]]*#/{depth} \ $OUT/pstack.txt $OUT/pstack_trimmed.txt else cp $OUT/pstack.txt $OUT/pstack_trimmed.txt fi claude -p 请分析 $OUT 目录下的现场快照按 CLAUDE.md 要求输出排查报告。 \ $OUT/report.md echo report saved to $OUT/report.md这里有几个细节我想特别解释一下。claude -p是非交互模式很适合在脚本里调用。它会把提示词作为参数传入执行完成后返回文本不会卡在交互界面上。理解了脚本采集数据 - 生成快照 - 触发 AI 分析 - 拿回报告这条流水线后续你要改成订阅、定时扫描都很容易。另外我在把 pstack 输出喂给 Claude Code 之前加了裁剪这一步。当堆栈文件超过 800 行我会把每个线程的栈深度截到 20 帧。这是因为线上故障分析最关心的往往是最上层的停留点而不是每个帧的完整历史。这样既保留了关键信息又能避免上下文被重复的 libc 内部帧淹没。4.3 控制上下文量的两个实用策略实际用下来上下文管理是最影响效果的点。堆栈文件 1000 行全给它丢进去得到的分析报告往往又长又散而且费用也高。我的策略通常有两条。第一参考文件分开给。Claude Code 有文件读取能力我不需要把所有内容都塞进 prompt只需要让它知道目录路径它自己会去读。上面的脚本就是只传递目录路径分析过程由它调用工具读文件这样可以控制每次读取的颗粒度。第二先用一个较窄的问题获取初步判断再针对可疑点请求深入分析。比如第一次问列出所有处于 D 状态和锁等待的线程得到结果后再问针对线程 204 的 futex 等待查看锁地址对应的分配位置。这种分层提问比一次性问帮我找出根因可靠得多也更容易避免 AI 在信息不足时强行编造结论的问题。我在文章后面会用真实案例示范这套分层提问的具体写法。5. MCP 扩展与兼容模型切换让诊断链路再往前一步pstack-claude 跑通以后你会发现它有一个天然瓶颈眼睛只能看到我主动抓到的堆栈和日志看不到实时的系统指标、数据库连接池状态、Nginx 访问日志。要突破这个瓶颈MCP 是一个很自然的方向。5.1 用 npx 拉起 MCP 服务扩展数据源MCP 的全称是 Model Context Protocol它让 Claude Code 可以通过标准协议调用外部工具。在 Claude Code 里新增一个 MCP 服务非常方便命令格式类似claude mcp add perf -- npx -y your-scope/perf-mcp-server加完之后用claude mcp list看一下是否注册成功。以后 Claude Code 在交互中就可以调用这个 MCP 暴露的工具比如读取实时 CPU 采样数据、查看某个进程的打开文件数、拉取 systemd journal 片段。这里有一个容易搞错的点MCP server 需要在claude命令启动前先能被 npx 拉取成功。如果你的开发环境网络连接本身不稳定可以先手动执行一次该 npx 包的安装确保不因为首次拉取超时导致注册失败。不要问我为什么要先确认这一步问就是我曾在离线演示时当场卡住。我给的示例里用的是占位符your-scope/perf-mcp-server实际使用时要替换成真实可用的 MCP 包。如果你暂时不想引入第三方包也可以用 Claude Code 支持的自定义工具方式把它指向一个本地的 shell 脚本让 Claude Code 通过执行脚本获取数据。这种方案更保守效果也不差。5.2 将 Claude Code 切到 DeepSeek 等兼容模型Claude Code 默认使用的是 Anthropic 官方模型但在某些场景下你可能想让整个分析流程跑在别的模型上比如成本更低的 DeepSeek 或本地部署的模型。很多模型服务商提供了 Anthropic 兼容的 HTTP 接口所以切换通常只需要设置三个环境变量export ANTHROPIC_BASE_URLhttps://api.deepseek.com/anthropic export ANTHROPIC_AUTH_TOKENyour_token_here export ANTHROPIC_MODELdeepseek-chat设置完之后再运行claude --version或claude -p ping确认它确实能正常工作。我在 pstack-claude 的日常使用中会把这套环境变量写进 shell 配置文件这样不同项目之间可以快速切换模型供应商而不需要改代码。这里想提醒一句模型切换之后分析质量不会完全一致尤其是复杂的锁竞争推理和内核态分析不同模型的差异会比较明显。所以我在项目里有一个约定默认跑官方模型只有在对速度要求高或做批量预扫描时才切到别的模型。这不是说替代模型不行而是说需要基于实际效果做选择别盲目为了省钱就全面替换。5.3 配置文件的版本管理细节CLAUDE.md、shell 脚本、MCP 配置最终都应该纳入 Git 管理。我的做法是单独建一个infra/pstack-claude/目录把脚本、示例快照和文档都放进去。这样团队里的同事拉下来就能用不用你手把手教。特别注意不要把包含密钥的配置文件提交上去令牌信息一定要用 Git 忽略规则排除掉。有一次团队同学把我给的脚本复制到自己的服务器上跑完发现报告内容出现了一些文件路径理解错误。后来一查是他把旧版本的 CLAUDE.md 留在了项目目录里和新的冲突了。所以我现在会把CLAUDE.md放在一个独立的分析工作区避免和业务代码的CLAUDE.md互相干扰。这个细节不算大但很影响结果一致性。6. 内存抖动复盘pstack-claude 一次完整实战最后用一个真实但脱敏的案例把整个过程串一遍。这样可以让你看到在前几节设计的脚本和流程在面对实际问题时是如何运转的。为了避免暴露具体业务细节这里对进程名、路径和数值都做了模糊处理但排查链路是完整真实的。6.1 现象与第一反应故障现象是某台应用服务器的内存占用持续上涨但业务量并没有明显增加。到了傍晚内存接近上限系统开始频繁 swap服务响应时间飙升。我登录上去时已经能感觉到命令执行明显变慢。第一反应不是急着重启而是执行现场快照脚本采集了 ps、status、dmesg 和 pstack 文件。当时的status.txt里显示VmRSS高达 12GB远超正常值而VmSwap还在增长说明内存压力已经传导到交换设备。我再看了下dmesg.txt发现内核日志里没有任何 OOM 记录。这说明问题不是单次暴增触发的 OOM而是缓慢泄漏或缓存异常。这种场景靠日志很难定位堆栈证据的重要性就出来了。6.2 喂给 Claude Code 的分析请求我用的 prompt 不是一把梭而是分了三步。第一步让它概述进程状态和采样到的线程分布claude -p 读取 /tmp/investigate/20250603_183022_2045 目录先归纳进程状态标出所有非 Running 线程并列出各自栈顶 5 帧。第二步针对锁等待和内存相关的线程深挖claude -p 基于刚才的快照找出与内存分配相关的调用路径检查是否有线程长时间停留在 malloc/brk/mmap 相关栈上。第三步让它生成可验证的根因假设并给出验证命令claude -p 根据快照和进程启动参数给出两个最可能的内存泄漏候选根因为每个根因提供一条验证命令。最终报告指出大量线程在malloc内部的锁上等待而持锁线程来自一个第三方库的内存池扩展路径它的栈上反复出现pthread_mutex_lock和一个并非业务直接调用的函数。根据这个线索我在生产上通过/proc/pid/smaps统计了匿名大页和堆区变化确认泄漏源指向那个第三方库的缓存没有释放而不是业务主链路的问题。6.3 结论验证与后续改进验证方式很简单联系该库的维护方确认缓存策略同时发布一个临时参数把缓存上限调低观察内存曲线。半小时后VmRSS开始下降一天内回到正常水位。整个过程从采集到定位大约花了四十分钟其中有近一半时间花在等待验证数据上真正的人工分析时间不超过十分钟。这次实战之后我给 pstack-claude 又加了一个步骤分析完成后自动生成一份简短的验证清单列出需要人工确认的数据点。因为 AI 的根因判断再合理最终还是要以真实系统指标或代码逻辑为准否则就可能出现判断很精彩但方向错了的情况。把判断与验证分开是我后来最坚持的设计原则。7. 最后想分享的几个实操细节到这儿pstack-claude 的整体方案就讲完了。按惯例我最后说几个自己反复用到的细节这些不写进任何文档但对实际效果影响很大。第一个是时间窗口。抓堆栈和抓日志的时间窗口必须对齐。比如进程从 18:00 开始卡顿你只抓了 18:30 的堆栈但日志文件却切到了 19:00 之后AI 很可能被无关信息带偏。我现在的脚本会强制在 prompt 里注明只分析故障窗口内的数据并且把日志文件的--since参数预设好避免信息交叉污染。第二个是伪结论的预防。Claude Code 在分析堆栈时有时会推断出看似合理的函数行为但堆栈文件里并没有直接证据。所以我在 CLAUDE.md 里明确要求凡是在堆栈文件里没有直接出现的符号必须在报告中标注为推断并提供验证方式。这条规则救过我很多次至少避免了把错误结论直接发给业务方。第三个是保留每次分析的完整记录。我的脚本输出目录里除了report.md还会把 prompt 原文存成prompt.txt。这样当同一个故障被分析两次却得到不同结论时可以回头比较 prompt 差异而不是怀疑 AI 行为不稳定。这个习惯看似多此一举但在长期使用中能显著提升排查流程的可复现性。pstack-claude 并不是什么复杂的系统它更像是把取证和解读这两个步骤用现代工具重新组织了一遍。如果你目前也在维护没有完善 APM 的系统手头又有 Claude Code 这样的终端助手不妨从今天开始在服务器上准备一个快照目录和一两个脚本。等到下一次凌晨出故障时你会感谢半小时前准备好的这套流程的。