pstack+Claude Code:AI辅助定位C++服务线程卡死与死锁
做后端服务维护的朋友大概率都碰过这种场景线上进程还挂着端口能通业务却像被按了暂停键请求全部卡住。我第一次被拉去处理这类问题时第一反应就是掏出 pstack 打一份线程栈结果面对 80 多行原始输出盯了半个多小时才勉强找出一个可疑线程。后来我把 Claude Code 引入到分析链路里攒出了 pstack-claude 这套工作流——用 pstack 采集进程栈让命令行 AI 工具结合源代码辅助解读定位效率明显上了一个台阶。这篇文章就把这套方法完整拆开讲清楚包括采集脚本、上下文组织、实际案例以及我踩过的一些坑适合正在维护 C/C 服务、或者经常处理线上疑难问题的朋友参考。1. 项目背景一次差点通宵的线上卡顿排查1.1 事故现场进程活着但业务全部挂起先说个典型场景。某个用 C 写的数据转发服务某天下午突然有人工反馈说订单处理延迟飙到几十秒。我登录上去看了一眼进程还在CPU 占用率不高内存正常端口也能连上但业务请求就是出不来。用ss -lntp看监听队列backlog 已经堆满这说明 socket 层已经接受了连接但业务线程没有及时处理。这种“进程活着却什么都干不了”的状态最怕的就是瞎猜。有人怀疑网络问题有人怀疑磁盘 IO还有人怀疑是流量突增导致线程池打满。最后稳定下来的排查手段还是得看线程在干嘛——也就是把每个线程的调用栈打出来看它到底卡在哪个函数里。这就是 pstack 的经典用法。那次排查我连续打了好几次栈对比之后才确认是某个队列加锁顺序出问题两个线程互相等待形成死锁。整个过程花了将近一个通宵很大一部分时间都耗在“读栈”上。1.2 pstack 传统打法的三个痛点第一输出非常原始。pstack 打印出来的是一串函数名加内存地址比如一行是#0 0x00007f7a2b3d7c32 in pthread_cond_wait ()没有调用关系说明没有源码行号更没有“这个线程在等哪个锁”这种结论。要读懂它你得在脑子里把栈帧串起来然后回源代码里比对。第二单次采样不够。一个瞬间的栈只能说明“这一刻线程在哪”不能区分“正常工作”和“卡死”。比如一个线程停在 epoll_wait 里那是它在等事件很正常但如果它连续三次采样都停在同一个 mutex 的加锁调用上那问题就大了。没有对比你根本不敢下结论。第三多线程并行分析太烧脑。死锁很少是单个线程的问题多数是两三组线程互相卡住。你得同时看多个线程栈在脑子里画“谁在等谁”的图。几个线程还行几十个线程时人脑真的不够用。1.3 把 AI 拉进排查链路的想法是怎么冒出来的后来我在日常开发里开始用 Claude Code 这个命令行 AI 工具。它不像 IDE 插件那样只做补全而是能理解整个项目的上下文能读源码文件、能定位函数定义、能按我的要求输出推理过程。当时我就琢磨pstack 这种“原始素材多、但结论要靠人推理”的场景不正是 AI 擅长的吗于是我做了一次实验把两次 pstack 采样喂给 Claude Code告诉它项目路径然后问它“哪些线程的栈完全没变、这些线程大概率卡在什么地方、结合源码看它们在等什么”。结果它很快给出了一个很靠谱的判断——不仅列出了可疑线程还指出了一个我在人工排查时忽略的锁顺序问题。打那以后pstack-claude 这套工作流就逐渐成型了。2. 先搞懂 pstack 在干什么再谈 pstack-claude2.1 pstack 的工作机制用户态栈与 gdb attachpstack 并不是一个读内核数据的工具它本质上是 gdb 的一个封装脚本。执行pstack PID时它大致做了三件事检查目标进程是否存在、是否有权限附加。调用 gdb 以-p PID方式附加到进程上。执行thread apply all bt打印所有线程的用户态调用栈然后退出。换句话说pstack 的结果来自 ptrace 机制获取的是用户态线程栈。它能看到你的业务函数调用链比如WorkerThread::Run - ProcessMessage - Queue::Pop - pthread_mutex_lock也能看到系统库的等待调用比如pthread_cond_wait、epoll_wait。和 pstack 容易搞混的是/proc/PID/task/TID/stack这个文件它记录的是内核栈需要 root 权限且内核开启CONFIG_STACKTRACE才能读通常用来排查 D 状态进程卡在内核哪里。pstack 一般用不上它但后面我讲容器里的 D 状态案例时会提到。还有一个细节pstack 附加进程的瞬间目标进程会被暂停。对绝大多数服务来说这只是一瞬间的事但生产环境务必意识到这个副作用后面我会专门讲。2.2 别输在采样姿势上多时间点对比才算数我第一次用 pstack 时犯的错误是只打一次栈然后对着输出瞎猜。后来才明白单次快照信息量极其有限。假设你看到一个线程停在pthread_cond_wait你能说它卡住了吗不能。它很可能只是在等下一个条件变量信号属于正常工作。真正的做法是“多次采样 对比栈帧”。我一般间隔 2 到 3 秒连续打两次关键场景打三次。然后看每个线程的栈帧内容是否完全一致如果某个线程两次采样栈完全不变说明它在这几秒内纹丝不动极可能卡在锁、条件变量、IO 等阻塞点上。如果栈一直在变说明它还在运行或处于正常的事件等待循环中问题大概率不在它身上。这一招看起来简单却极其有效。pstack-claude 这套工作流的核心就是先把这种对比逻辑自动化再把对比结果交给 AI 做深度解读。2.3 pstack 的盲区和不适用场景pstack 也不是万能的下面这些情况它帮不上忙你心里要有数。内存泄漏。泄漏是慢慢积累的栈快照根本体现不出来你应该用valgrind、jemalloc的 prof 接口或者监控 RSS 曲线。CPU 飙高但线程栈很活跃。这通常是热点代码频繁执行单看栈定位不了性能瓶颈得上perf top或者perf record。进程已经崩溃。栈都打不出来了这时候应该回头找 core dump用 gdb 分析崩溃现场。内核态卡死。比如进程处于 D 状态用户态栈只能看到它卡在某个系统调用上再往内部看就得靠/proc/PID/task/TID/stack。理解这些边界很重要。pstack-claude 不是全能诊断工具它解决的是“进程还活着线程却卡住”的那一类问题适用面已经很广了但没必要硬套到所有故障上。3. pstack-claude 的完整落地实现3.1 整体链路采集、清洗、组织、分析四段式我最终沉淀下来的流程是四段式采集、清洗、组织、分析。每一步都可以脚本化整个链路下来基本是半自动的。采集用 pstack 按固定间隔采样两次产出原始栈文件。清洗过滤掉系统库函数、内存地址、行号噪音把项目自有函数提取出来。组织把两个时间点的栈按线程 ID 对齐生成一份“哪些线程栈不变、哪些栈在变”的对比摘要。分析把摘要和项目路径交给 Claude Code让它结合源码给出结论。一开始我也犯过把原始输出直接丢给 AI 的错。后来发现AI 虽然能读原始栈但输出里混杂了大量无关系统帧会严重干扰判断。所以清洗和组织这两个步骤不是可有可无而是整个方案能不能落地的前提。3.2 采样与清洗脚本从原始栈到分析素材采集脚本很简单我直接贴出来。#!/usr/bin/env bash set -euo pipefail PID${1:?usage: $0 pid [interval]} INTERVAL${2:-3} for i in 1 2; do echo sample $i at $(date %F %T) pstack $PID | tee stack_${i}.txt echo if [ $i -eq 1 ]; then sleep $INTERVAL fi done执行方式就是./collect-stack.sh 12345 3得到stack_1.txt和stack_2.txt两份原始输出。要注意 PID 别搞错建议先用pgrep -f 服务名确认一下多实例部署时尤其要小心。接下来是清洗。pstack 输出里大量重复的libc、libpthread、libstdc帧还有一堆十六进制地址对 AI 分析来说全是噪音。我会先做一个粗过滤# 过滤掉系统库帧和地址保留项目函数调用链 for f in stack_1.txt stack_2.txt; do grep -E ^#|^Thread $f \ | grep -vE libc|libpthread|libstdc|ld-|0x[0-9a-f] \ ${f%.txt}.clean.txt done这里没有把全部地址删掉因为后面定位时可能还要用addr2line反查源码行号所以原始文件要保留。清洗后的文件专门用来给 AI 做快速判断。3.3 构造向 Claude Code 投喂的上下文包这一步是整个流程里最重要的。清洗后的栈文件依然是一堆函数名如果直接丢给 Claude Code它缺少一个关键维度两份采样之间的对比。我一般会先在终端里生成一份对比摘要格式类似下面这样进程信息:>这是两份间隔 3 秒的 pstack 采样对比摘要。服务是>Thread 3: #0 0x00007f97cb5c088f in pthread_cond_wait () #1 0x000055e7c8b0b942 in ?? () #2 0x000055e7c8b0b8a0 in ?? ()?? ()意味着那段地址没有符号信息可能是项目二进制被 stripped 了也可能某些静态库没带调试符号。这种情况下 AI 再强也没用你喂给它的函数名都是空的。我的处理办法先确认二进制是否带符号用file binary或者nm binary看如果全被 strip 了想办法装对应的debug包或者找 CI 构建时保留符号版本的产物。如果只是个别库丢符号可以用addr2line把地址还原成文件名和行号addr2line -e /usr/local/bin/data-forwarder -f -C 0x555e7c8b0b942还原出来的结果再补充进清洗后的栈文件AI 才能正常工作。记住一个原则先给 AI 能吃的东西再让它发挥推理能力这个顺序不能反。5.2 生产环境 attach 的副作用与时间窗口pstack 靠 gdb attach而 gdb attach 会让进程短暂停下。绝大多数时候这是毫秒级影响但极端情况下会有风险如果进程正持有着某个锁而且正在做关键操作暂停可能让它的下游调用方超时甚至引发雪崩。我第一次在生产环境用 pstack-claude 时就遇到过一次连续采样三次每次停顿叠加导致某几个依赖方报了很多超时告警。虽然有惊无险但后来我给自己定了几条规矩采样前先和业务负责人确认时间窗口尽量选低峰期。默认采两次间隔控制在 3 秒以内最多三次。如果一次采样已经看出端倪立刻停手不要再贪多。容器环境更要注意非特权容器可能根本没有 ptrace 权限pstack 会直接报错这时候要么让容器具备SYS_PTRACE能力要么在宿主机上对容器进程做采集。另外提一个容易被忽略的点如果你的生产环境配置了 systemd 的 coredump 策略频繁 attach/中断进程可能触发 coredump 线程的额外行为采样前看一眼系统配置更稳妥。5.3 上下文组织错误会让 AI 给出误导性结论最开始的几次尝试里我犯过一个很典型的错误把stack_1.txt和stack_2.txt两坨原始文件直接丢给 Claude Code不做对齐、不做摘要。结果它抓不住重点一会儿说线程 5 可疑一会儿又说线程 19 可疑两边输出矛盾反而浪费我时间。后来我把流程改成了前面说的“对比摘要”方式。这个摘要的作用相当于告诉 AI “请重点关注栈完全不变的线程”省去它自己逐帧比对的过程。AI 出错率明显下降。还有另一个细节告诉 AI 哪些函数是项目自有的。C 项目经常依赖 Boost、libevent、gRPC 这些第三方库它们的内部栈帧也会出现在输出里。如果不对这些帧做标注AI 可能在第三方库的内部函数上纠结很久。我的做法是在清洗阶段就直接把boost::、grpc::、event::等前缀的帧标为“第三方库”让 AI 跳过只在项目代码范围内找卡点。 提示投喂 AI 的上下文永远要先做“降噪”和“对齐”两步。原始数据再真实直接堆给 AI 也只会稀释它的注意力。6. 这套方法的边界以及还能怎么扩展6.1 它擅长什么、不擅长什么pstack-claude 解决的核心问题是“进程活着但线程卡住”时快速给出可疑点排序。我用一张表总结一下它的适用范围场景pstack-claude 是否适用说明死锁、锁顺序问题适用多线程栈对比 源码分析定位效率很高条件变量等待适用栈不变 代码路径可定位等待条件线程池耗尽、连接不处理适用多个工作线程栈聚集在排队点一眼可见D 状态、IO 卡死部分适用用户态栈信息有限需配合/proc内核栈内存泄漏不适用栈快照看不到应使用内存分析工具CPU 飙高但无阻塞不适用用 perf 做热点采样更合理进程已崩溃不适用走 core dump 分析路线这套工具是“排查链路的加速器”不是“代替所有诊断工具的万金油”。6.2 复制到 jstack 与 goroutine dumppstack-claude 的最大价值其实是“栈采集 AI 推理”这套方法论它完全可以迁移到别的语言生态Java 服务用jstack PID抓线程栈两次采样对比后喂给 Claude Code。Java 线程栈信息比 pstack 更丰富自带锁状态描述比如locked、waiting to lockAI 判断死锁会更容易。Go 服务向进程发送SIGQUIT触发 goroutine dump或者用go tool pprof goroutine导出。goroutine 栈的模式化很强AI 很快能发现“大量 goroutine 阻塞在同一个 channel send/receive”这种问题。Python 服务用py-spy dump --pid PID抓线程栈同样可以复刻这个链路。只要栈是文本格式数据能采集AI 就能参与分析。你不需要为每种语言重新设计方法论只需要换一个采集命令。6.3 从手动分析走向半自动巡检到了这一步我自然而然地想把它变成半自动巡检工具。思路也很直接写一个巡检脚本每隔固定时间采样一次 pstack连续三次栈完全一致的线程打上“可疑”标签再自动把摘要推给 Claude Code 生成报告。目前我这个巡检脚本的原型逻辑大致是每 5 分钟执行一次 pstack 采样 保留最近 3 份采样结果 对每个线程做栈帧 hash 对比 连续 3 次 hash 完全一致 - 标记为 stuck 候选 将 stuck 候选和对应栈帧摘要汇总发送给 ClCode 生成分析建议这种半自动模式的好处是不需要等线上事故爆发后再登录机器手动排查平时就能积累“可疑线程”的观测记录。将来如果再发生卡死历史数据还能用来对比是突发问题还是渐进劣化。写在最后一个对我帮助很大的使用习惯用了这么久的 pstack-claude我最大的体会是AI 分析能力强但它的输入质量完全取决于你怎么组织信息。不要拿最原始的日志去考验它先自己完成采集、对齐、降噪、摘要这些脏活AI 才能真正发挥推理优势。另一个深刻体会是AI 分析的结论永远是假设不是最终答案——它每次给我一个“最可能卡点”后我都会回到源码、加上日志或者 gdb 单步去验证确认无误才敢下结论。工具能把人从大量低价值阅读里解放出来但最终对该做的事负责的仍然是你自己。