pstack-claude:基于MCP让Claude自动分析线程堆栈
凌晨两点服务毫无征兆地卡死CPU 被打满接口全部超时。ps确认了 PIDpstack一把抓出线程堆栈剩下的就是漫长的人肉读栈一个个帧翻过去查锁、查系统调用、查业务代码。那一晚我翻了快两个小时才定位到一个非常隐蔽的锁竞争。天快亮的时候我一直在想这种事为什么不交给 Claudepstack 负责把现场拍下来Claude 负责看图说话我只需要做个胶水层把它们接起来。于是就有了 pstack-claude 这个项目一个挂在 Claude Code 上的 MCP 工具让 Claude 能直接执行 pstack失败了还会自动用 gdb 兜底然后基于堆栈输出给你做诊断。这篇文章是完整的落地记录要解决什么问题、底层怎么设计、代码长什么样、实际排障效果如何以及一路踩过的坑。适合两类人一类是天天跟进程卡死、CPU 飙高打交道的开发或运维另一类是正在研究 Claude Code 工具调用、想给 AI装手装脚的折腾党。1. 这个项目到底解决什么问题1.1 传统 pstack 工作流的真实痛点先说一个非常典型的排障场景线上服务突然不响应top一看某个进程 CPU 飙到 100%。常规操作是ps -L -p pid列出线程然后pstack pid把现场抓出来。问题是抓出堆栈只是开始读堆栈才是真正的体力活。我见过太多堆栈输出的实际内容了。一个多线程服务pstack打印出来经常是几十个线程的栈每个栈从内核态到用户态有十几层帧。你得知道哪些线程是空闲等待的哪些线程真的在干活哪些帧是锁等待哪些是死循环。这需要经验更需要耐心。更麻烦的是很多情况下你抓完一次堆栈还不够需要隔几秒再抓一次对比线程状态是否一直在原地打转才能判断是不是真的卡死。而且传统工作流是割裂的抓堆栈是一套命令分析是纯人肉劳动最后写排查结论又是另一件事。工具和思考之间没有任何衔接。pstack-claude 想解决的就是把抓取堆栈和分析堆栈这两段用 Claude 串起来。你要做的只是输入一句话比如PID 4321 卡住了帮我看看它在干什么剩下的由 Claude 自己调用工具、读取输出、组织判断。1.2 Claude 在流程里的定位不是替代是代读我得先说清楚pstack-claude 不是要替代 pstack也不打算替代工程师的判断。pstack 是 Linux 下抓取进程线程堆栈的经典命令它负责把现场完整记录下来Claude 在这里扮演的是代读的角色把堆栈里那些晦涩的函数名、锁等待、内核帧翻译成人类能快速理解的诊断结论。实际用下来Claude 最有价值的地方在于它不会漏看。人读堆栈的时候扫到第 20 个线程往往已经烦了注意力会下降。Claude 会把每一个线程的栈都过一遍主动标出可疑的线程、可疑的调用链然后告诉你最可能需要关注的点。它不会吐槽你也不会越读越烦躁这在我看来就是 AI 替人干活最理想的状态。当然前提是你得让它能调用 pstack。这就是 MCPModel Context Protocol出现的意义Claude Code 支持通过 MCP 加载外部工具pstack-claude 本质上就是这样一个工具服务器。2. 核心设计pstack 的封装与 Claude 的工具调用2.1 pstack 命令的真实面纱pstack 在多数 Linux 发行版里其实是一个脚本底层经常是调 gdb 的批处理模式来获取栈。比如在 CentOS 系统上pstack大致会执行这样一条命令gdb -quiet -batch -ex thread apply all bt -p pid它依赖的是一个核心的系统调用ptrace。pstack 通过 ptrace 挂到目标进程上去读取每个线程的寄存器、栈地址和符号信息再整理成人类可读的调用栈。这决定了它的两个天然限制。第一个限制是权限。ptrace 不是你想 attach 谁就 attach 谁的。Linux 有个 Yama LSM 模块/proc/sys/kernel/yama/ptrace_scope这个参数如果为 1你只能 ptrace 自己的子进程。所以线上抓栈经常要 root或者用sudo -u 运行用户 pstack pid。第二个限制是依赖。如果系统里没有 gdbpstack 大概率会失败这时候你需要一个替代方案比如直接调 gdb或者安装 gdb 包。在设计 pstack-claude 的 MCP 工具时我必须把这两个限制都考虑进去。不能只封装一个裸的pstack命令否则实际使用中遇到一次权限问题或者 pstack 缺失整个工具就废了。2.2 MCP 工具调用的实现逻辑MCP 全称是 Model Context Protocol它提供了一套标准协议让大模型可以调用外部工具。你可以把它理解成给 AI 定义了一组APIAI 在对话中判断需要用什么工具按约定的 JSON 格式发出调用请求工具执行完把结果返回给 AIAI 再基于结果继续推理。Claude Code 支持 MCP 服务器传输方式里最常用的是 stdio也就是 Claude Code 直接启动一个子进程通过标准输入输出和 MCP server 通信。对本地命令封装来说很干净没有网络端口不需要额外启动服务。pstack-claude 就是实现一个 MCP server暴露一个get_thread_stack工具。它的核心逻辑分三步先校验 PID 是否存在然后尝试用 pstack 抓栈如果失败自动换 gdb 兜底最后把堆栈文本返回给 Claude。Claude 拿到文本后会自己分析和总结。这里有个关键设计决策工具只做抓取和返回原始文本不替 Claude 做任何总结。为什么不直接在工具里做诊断因为模型在工具调用循环里对文本的上下文理解更充分。你把未经修饰的堆栈交给 Claude它可以根据当前对话目标给出针对性解读而不是服务器端提前固化一套分析规则。分析规则如果写死在工具里那这工具就永远不可能分析出你预设之外的诡异问题。2.3 为什么选择 MCP 而不是直接 Shell 管道有朋友问我抓个堆栈而已直接把 pstack 输出复制粘贴给 Claude 不就行了为什么要多此一举搞 MCP区别在于被动投喂和主动调用。Shell 管道方式下是你替 Claude 做决策你自己决定要抓谁的堆栈、什么时候抓、要不要附加 gdb。MCP 方式下决策权在 Claude 手里你跟它说那个卡死的进程它自己会先问你要 PID然后调用工具抓取发现抓不到还会尝试兜底整个过程是模型自主驱动的。还有一个实际原因MCP 是一次性的能力注册。pstack-claude 挂上之后后续任何一个会话里 Claude 都可以直接调用不用每次在对话上下文里粘贴工具说明。整个流程更接近给 AI 装上一只手它想用就能用。另外MCP 是可以叠加的。后面把日志查看、strace 分析、健康检查等工具做成更多的 MCP serverClaude 就能在一个会话里自主组合排障链路。这是直接复制粘贴永远做不到的。pstack-claude 只是这个思路的第一块积木。3. 从零到一pstack-claude 的完整落地过程3.1 环境准备Claude CLI 与 MCP SDKpstack-claude 依赖两个东西Claude Code 命令工具以及 MCP 的 Node.js SDK。Claude Code 的安装走 npm 是最直接的npm install -g anthropic-ai/claude-code装完先确认版本claude --version然后登录或者配置密钥。我习惯用环境变量方式把 Anthropic API Key 配到 shell 配置里export ANTHROPIC_API_KEY你的密钥之后就进入项目目录初始化 MCP server。MCP SDK 也以 npm 包形式存在建议在独立目录里做避免和全局环境搅在一起mkdir -p /opt/pstack-claude cd /opt/pstack-claude npm init -y npm install modelcontextprotocol/sdk这里多一句嘴Claude Code 对 Node 版本有要求尽量用 Node 18 以上。如果后续启动 MCP 时报语法错误八成是 Node 太老了先检查node -v。3.2 核心代码实现一个能自己兜底的 MCP Server我直接贴核心实现一个server.js文件就能跑import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { exec } from node:child_process; import { promisify } from node:util; const execAsync promisify(exec); const server new McpServer({ name: pstack-claude, version: 0.1.0 }); async function runPstack(pid) { const candidates [ pstack ${pid}, gdb -p ${pid} -batch -ex thread apply all bt 2/dev/null ]; let lastError ; for (const cmd of candidates) { try { const { stdout } await execAsync(cmd, { timeout: 30000 }); if (stdout stdout.trim()) { return stdout; } lastError 命令执行成功但没有输出; } catch (err) { lastError err.message; } } throw new Error(无法获取堆栈: ${lastError}); } server.tool( get_thread_stack, 获取Linux系统上指定进程的线程堆栈信息用于排查进程卡死、死锁、CPU飙高、请求不响应等问题, { pid: { type: number, description: 目标进程的PID } }, async ({ pid }) { try { await execAsync(kill -0 ${pid}); } catch { return { content: [{ type: text, text: 进程 ${pid} 不存在请确认PID是否正确 }] }; } try { const output await runPstack(pid); return { content: [{ type: text, text: output }] }; } catch (err) { return { content: [{ type: text, text: 抓取堆栈失败: ${err.message} }] }; } } ); const transport new StdioServerTransport(); await server.connect(transport);代码不多但有几个细节值得讲。第一kill -0 ${pid}是个很妙的探活方式。kill -0不会真的发信号杀进程只是检查进程是否存在、当前用户有没有权限操作它。如果这一步就失败后面的 ptrace 大概率也失败提前返回能给 Claude 一个更明确的错误信号。第二兜底策略很重要。pstack在很多精简容器里根本没有但 gdb 可能存在反过来也有系统装了大把诊断工具却没有 gdb 的情况。候选命令按优先级排列哪个能出结果就用哪个这是经验里长出来的设计。第三超时控制。execAsync里必须加 timeout。pstack 挂到一个大进程上有时候会慢得出奇不加超时可能让整个 Claude 会话卡在那里等半天。30 秒是一个平衡值既能覆盖大部分正常情况又不至于把对话拖死。3.3 配置与启动让 Claude长出手脚server.js 写好后要让 Claude Code 识别它。Claude Code 的 MCP 配置有两种方式我更推荐命令行方式claude mcp add pstack -- node /opt/pstack-claude/server.js这条命令会把名为pstack的 MCP server 注册为用 node 执行/opt/pstack-claude/server.js。之后在 Claude Code 会话里这个工具就会被加载。也可以直接编辑配置文件。Claude Code 的配置文件一般是~/.claude.json或项目里的.mcp.json把 server 注册信息写进去等价于上面命令。配置文件的写法大致是这样{ mcpServers: { pstack: { command: node, args: [/opt/pstack-claude/server.js] } } }改完配置文件后需要重启 Claude Code 会话MCP 工具才会被重新加载。3.4 先做一次冒烟测试配置好别急着上生产排障先找个无害的进程试一下。比如随便找一个当前 shell 的 PIDecho $$然后在 Claude Code 里直接说用 get_thread_stack 工具看一下进程 12345 的堆栈如果一切正常Claude 会调用工具返回堆栈内容然后向你说明这个进程目前的调用情况。如果用在 bash 这种进程上你会看到它大概率卡在read相关的系统调用上这是正常的说明工具链路通了。冒烟测试时最容易翻车的点是 Node 环境。如果 Claude Code 启动 MCP server 时静默失败先手动跑一遍node /opt/pstack-claude/server.js看能不能正常启动。如果手动启动有报错通常错误信息会直接告诉你缺什么依赖、语法哪里不对。还有一个坑如果你的系统 node 命令不在 Claude Code 子进程能拿到的 PATH 里配置里的command: node可能找不到。这时候可以把 command 换成 node 的绝对路径比如/usr/local/bin/node省去一堆 PATH 的麻烦。4. 实战用 Claude 排查一次 CPU 飙升4.1 现象与前置定位纸上谈兵没用直接看真实排障流程。假设有一台线上机器某个 nginx worker 进程 CPU 占满请求大量超时。传统流程下我们通常会这么走top -H -p nginx_worker_pidtop -H能看到进程内每个线程的 CPU 占用。如果发现某个线程 CPU 特别高基本能断定问题出在它身上就可以抓堆栈看它到底在跑什么。但这里的问题是你得先看懂现象再决定下一步而这正是 Claude 能介入的环节。实际做的时候我先手动拿到 nginx worker 的 PID比如 4321然后用 pstack-claude 把所有线程的堆栈抓了出来再让 Claude 分析。下面是 pstack-claude 抓回来的堆栈内容我截取了两段关键线程Thread 3 (Thread 0x7f8b2c0d0700 (LWP 4321)): #0 0x00007f8b2c5d69a3 in epoll_wait (epfd8, events0x7f8b2c0cfad0, maxevents512, timeout5000) #1 0x000000000042e29d in ngx_process_events_and_timers (cycle0x1b7e0a0) #2 0x0000000000434d32 in ngx_worker_process_cycle (cycle0x1b7e0a0, data0x0) #3 0x0000000000434a81 in ngx_worker (cycle0x1b7e0a0, data0x0) #4 0x000000000043348a in ngx_spawn_process (cycle0x1b7e0a0, proc0x434a20) #5 0x00000000004346ae in ngx_master_process_cycle (cycle0x1b7e0a0) #6 0x00000000004075d9 in main (argc3, argv0x7ffd2c3aff18) Thread 1 (Thread 0x7f8b2c2d0740 (LWP 4320)): #0 0x00007f8b2c5e0a45 in sched_yield () #1 0x0000000000441b12 in ngx_shmtx_lock (mtx0x7f8b2c6d4040) #2 0x0000000000432f2a in ngx_event_accept (ev0x7f8b2c6d4000) #3 0x0000000000434d07 in ngx_process_events_and_timers (cycle0x1b7e0a0) #4 0x0000000000434d32 in ngx_worker_process_cycle (cycle0x1b7e0a0, data0x0) #5 0x0000000000434a81 in ngx_worker (cycle0x1b7e0a0, data0x0) #6 0x000000000043348a in ngx_spawn_process (cycle0x1b7e0a0, proc0x434a20) #7 0x00000000004346ae in ngx_master_process_cycle (cycle0x1b7e0a0) #8 0x00000000004075d9 in main (argc3, argv0x7ffd2c3aff18)这种原始堆栈对不常看的人就是天书。我把这堆文本丢给 Claude让它分析。4.2 让 Claude 自己抓栈并分析在 Claude Code 会话里我只需要说进程 4321 的 CPU 飙到了 100%请求大量超时。请用 get_thread_stack 抓一下它的堆栈帮我看看问题出在哪。Claude 会先调用工具拿到堆栈然后很快给出解读。它的回答风格大概是这样从堆栈看线程 1LWP 4320卡在了ngx_shmtx_lock这是 nginx 的共享内存锁调用路径是ngx_event_accept - ngx_shmtx_lock - sched_yield。线程 3LWP 4321正常阻塞在epoll_wait。关键点ngx_shmtx_lock内部在反复调用sched_yield这不是普通的阻塞等待而是自旋锁在空转。通常意味着某个持有 accept 锁的线程没有及时释放其他 worker 拿不到锁只能在这里死等。CPU 飙高的原因就在这里。建议下一步用strace -p 4320 -e futex跟踪系统调用确认锁等待的行为模式检查 nginx 配置里accept_mutex是否开启如果开启且压测场景下出现锁竞争可以考虑关闭或调优重点排查持有锁之后慢路径上的业务逻辑比如内置的 Lua 脚本或上游代理是否在长时间阻塞。说实话这个分析水平已经超过了相当一部分初级运维。它没有说废话直接锁定了最可疑的帧和调用链并且给出了下一步操作建议。对人来说可能要先搜ngx_shmtx_lock是什么、在什么场景下会调用Claude 不需要这一步它训练时看过足够多的相关内容能直接建立帧和语义之间的关联。4.3 Claude 给出的诊断结论Claude 除了指出表面现象还能帮你做一层推理自旋和阻塞的区别。同样是sched_yield出现在这个调用路径上和出现在普通业务代码里含义完全不同。它结合上下文判断这是 nginx accept 锁的竞争而不是业务代码死循环这个判断方向很关键。我又追问了它一句如果抓第二次堆栈这个线程还在这里是不是能确认是持锁线程卡死Claude 回答如果两次抓取间隔 2 秒以上线程 1 依然停留在ngx_shmtx_lock同时其他 epoll 线程有事件进来却处理不动基本可以确认是持锁线程长时间占用了 accept 锁。这时候应该去排查持锁线程而不是再看等待锁的线程。这个思路是对的。多抓几次堆栈看线程是否原地不动本身就是判断卡死的经典方法。Claude 能把工具调用和这个方法论连起来pstack-claude 实际用起来就不只是个命令封装而是一个有分析行为的助手。5. 常见问题与排查记录5.1 pstack 权限不足、命令不存在实际用得最多的是坑就是权限和工具缺失。报错通常是Operation not permitted或者command not found。前者基本是 ptrace 权限不够可以按下面的顺序排查cat /proc/sys/kernel/yama/ptrace_scope如果是 1非 root 用户只能 attach 自己的子进程。想临时放开可以echo 0 /proc/sys/kernel/yama/ptrace_scope但这只是临时生效重启后恢复。生产环境更靠谱的方式是用目标进程的同款用户来执行比如 nginx 是 nobody 跑的那就sudo -u nobody pstack pid后者command not found说明 pstack 没装。pstack 在 CentOS 上是 gdb 依赖带出来的但 Ubuntu 这类系统不一定预装。我之前在 Ubuntu 20.04 上就踩过一次解决办法不是硬装 pstack而是直接用 gdb 命令抓栈。这也是为什么 pstack-claude 要做 gdb 兜底——你永远猜不到客户的服务器上有什么工具。pstack-claude 遇到这个问题时你可以直接问 Claude为什么抓不到堆栈它会告诉你工具执行失败的原因。很多情况下跟着错误信息走就能找到答案。5.2 Claude Code 升级与 npm 权限问题Claude Code 自己的问题我遇到的第二大坑。有段时间它启动时一直报auto-update failed: no write permission to npm prefix原因是全局 npm 包的目录属于 root当前用户写不进去自动更新就失败了。解决办法是给 npm 换一个用户级的前缀目录npm config get prefix mkdir -p ~/.npm-global npm config set prefix ~/.npm-global export PATH~/.npm-global/bin:$PATH然后重新安装 Claude Code 到用户目录npm install -g anthropic-ai/claude-code注意的是换完前缀之后要让claude命令指向新路径也就是~/.npm-global/bin/claude。这个改完所有依赖 npm 全局工具的习惯都会舒服很多。如果你之前用 root 安装过旧版本可能还残留在系统目录里必要时which claude检查一下命令实际路径。还有人在 Windows 上遇到 Claude 相关桌面端提示需要开启虚拟机平台功能这是因为它的沙箱组件依赖 Windows 的虚拟化底层。这类问题通常去启用或关闭 Windows 功能里把虚拟机平台勾上重启系统即可。但如果你是直接用 Claude Code命令行在 Linux 服务器上基本碰不到这种桌面端限制。5.3 MCP 工具没有出现在会话里配置好 MCP server 后在会话里找不到get_thread_stack这种情况我排查过不少次。最常见的原因是 server.js 启动时报错但错误被 Claude Code 吞掉了。这时候手动执行一下配置里的命令看能不能正常冒烟node /opt/pstack-claude/server.js如果手动执行没有输出且不退出说明 server 正常。如果手动执行立刻报错按报错去修依赖。还有一个容易忽略的点MCP server 是独立于 Claude Code 的进程它启动的时机很重要。如果你是先启动 Claude Code 再做 MCP 配置需要重启会话才能生效。另外配置文件如果放在项目目录内只在那个项目下才加载换个目录就不认识了。这时候claude mcp list会告诉你当前有哪些可用工具先跑一下这个命令做验证。5.4 堆栈符号缺失Claude 也难为无米之炊这个坑我相信所有用过 pstack 的人都有体会堆栈打印出来一堆??或者只有十六进制地址没有函数名。原因很简单目标进程的符号表被剥离了或者 debuginfo 没装。pstack-claude 能做的只是把抓到的原文给 Claude。如果原文全是地址没符号Claude 能给你的信息就非常有限。它也许会告诉你这些地址段可能属于哪些模块或者建议你安装 debuginfo 包后重新抓。这里我的体会是堆栈质量决定了 AI 能发挥的上限工具层能做的只有保证传递链路干净。所以如果你的服务有排障需求打包部署时最好保留符号表文件至少保证核心服务的 debuginfo 是安装的。否则再强的模型面对一堆裸地址也只能摊手。写在最后的一点实践体会pstack-claude 做出来以后我最大的感受是工具本身并不复杂真正的价值在于它改变了排障的交互方式。以前是我读堆栈、我下命令、我写结论现在是我描述现象、AI 抓取现场、AI 给方向我在中间做最后一道验证。AI 根据堆栈给出判断这一步的质量确实超出了我最初预期而且它能保持耐心的态度把所有线程全扫一遍再总结。但我也得说句实在话AI 的结论不能直接照抄。它在锁竞争这种比较标准的问题上表现很好但遇到你业务里特有的奇怪状态它的知识可能跟不上你的代码。每次 Claude 给出诊断后我都会用后面的实际操作去验证比如补一次堆栈对比、看一眼日志、跑一下 strace。工具是放大你的效率不是替代你的判断。这个项目后续的扩展空间其实很大。我已经在计划给它加一个strace工具让 Claude 能直接观察进程的系统调用再加一个日志摘要工具把最近一段时间的应用日志也纳入分析上下文。当这些工具串起来一个真正自主的排障助手会慢慢成形。pstack-claude 只是第一步但这一步走通之后后面的路就顺了。