资讯详情

资讯详情

pstack实战:快速定位线上进程卡死与CPU飙升的线程堆栈技巧

排查线上进程卡死、CPU 异常飙升、服务无响应这类问题的时候pstack 是我第一批摸上服务器就要敲的命令。它只需要你传入一个进程号几秒钟就能把所有线程当前执行到的函数调用堆栈完整抛出来相当于把进程在“出事瞬间”的地图拍下来后面再怎么排查都有据可依。这篇文章我不打算照着 man page 念而是把我日常用 pstack 的思路、底层原理、执行细节、典型故障场景和踩过的坑一起讲清楚。1. 为什么我坚持用 pstack 而不是第一时间上 gdb线上环境出问题的时候时间窗口非常短几秒钟之后现场可能就变了。很多人第一反应是“挂上 gdb 看看”但 gdb 的交互式操作在服务器应急场景里其实很别扭批处理模式又需要你记住一串参数容易在紧张的时候卡壳。pstack 的真正价值在于它把“抓堆栈”这个动作压缩成了一句话。1.1 pstack 到底是个什么工具pstack 本质上不是一个全新的进程分析器它是站在调试器后端之上的简化入口。它做三件事根据 PID 找到目标进程通过调试接口附加到该进程上再把该进程每个线程的回溯栈批量输出到终端。整个过程不需要你写脚本不需要你想“该附到哪个线程上”也不需要额外指定堆栈深度。对熟悉 Linux 底层的人来说pstack 的核心动作相当于帮你自动执行了“attach thread apply all bt detach”这一串调试指令。它把这些繁琐步骤封装成一个命令而且输出格式比交互式调试器更适合直接截屏存档。1.2 为什么说它适合应急现场我维护过不少高并发的服务进程处理过几十次线上故障。关键时刻时间就是钱一个要等待 gdb 交互、需要人肉判断的排查流程不如一个能在三条命令内给出结论的工具。pstack 快就快在它不需要你提前配置 core 文件不需要接入复杂的监控平台一条命令就能把线程栈落到日志文件里后续可以慢慢分析。它解决的痛点是进程还活着但不再响应进程 CPU 高到离谱但不知道是哪个代码路径在跑线程池里的线程不知道卡在哪些地方。这三类问题用 pstack 都能给出直接线索。2. 动手前先看清 pstack 的工作原理我见过不少人把 pstack 当黑盒用输出异常了却不知道怎么排查。这里我把它的实现原理拆开讲讲明白原理之后你遇到各种诡异输出就知道问题大概出在哪一层。2.1 pstack 的后台结构在常见 Linux 发行版里pstack 通常是通过包管理器安装的一个程序它会调用系统自带的调试器作为后端。它发送给调试器的核心指令是附加到指定 PID对进程内所有线程执行回溯打印然后自动退出。换句话说pstack 的“后端”负责真正的内存读取和符号解析pstack 自身负责组织调度。正因为有这样的结构pstack 的依赖环境里必须存在对应的调试后端。如果系统里缺少这个后端或者后端版本不兼容pstack 就会报错。多数场景下只要基础开发工具链完整pstack 就能正常跑。2.2 从附加到读栈要经过哪些环节我们以为 pstack 只是在读 /proc 里的文件实际上没那么简单。它要先让目标进程进入一种可被检查的状态。Linux 内核提供了一套完整的进程追踪机制操作上表现为追踪者可以暂停目标进程的运行读取它的寄存器、内存、调用栈信息然后恢复运行。pstack 获取线程列表时会扫描 /proc/ /task 目录每个线程对应一个子目录。之后对每个线程分别收集栈回溯数据。栈回溯的本质是从当前指令指针出发根据栈帧里的返回地址一层一层向上找调用关系。这个过程如果是动态链接的二进制还需要映射到共享库的符号表如果没有符号表就只能看到原始地址。2.3 为什么要理解它是“碰一下”目标进程pstack 在附加的瞬间目标进程会短暂暂停等栈信息采集完成后再恢复。对于生产环境的在线服务这种暂停一般很短但如果你用 pstack 去抓一个正在高频处理数据的进程代理层、数据库连接、网络调用都可能感知到微小抖动。我自己的做法是在非高峰期采集或者抓一次就走不要反复在同一进程上连续执行。理解这一点很重要。当你在高负载服务器上连跑几十次 pstack不是在“无损观测”而是在给目标进程制造可以被感知到的停顿。所以正确的采集姿势是快、准、一次到位拿数据讲究“够用就行”。3. pstack 的基础操作与执行细节说完了原理直接进入实操。这一节我会把常用命令、参数选择、权限准备和输出组织方式全部过一遍方便你直接把命令记下来。3.1 拿到 PID 先确认身份使用 pstack 之前先确认你要抓的进程确实存在并且已经稳定运行了一段时间。用 ps 或者 pidof 拿到 PID然后最好 ps -fp 确认进程名没有看错。误抓一个同名但不同实例的进程整个堆栈分析方向就会跑偏。命令很简单pstack pid如果你运行的账号和目标进程不是同一个用户一般要加 sudosudo pstack pid如果进程在容器里而你是从宿主机去抓的还要先搞清楚这个 PID 是宿主机视角的 PID 还是容器内的 PID。用 docker top 或者 nsenter 进入容器命名空间再看比在宿主机上瞎抓要靠谱得多。日常运维中我见过太多因为 PID 命名空间没对齐导致报“No such process”的情况。3.2 把输出保存成文件再解析pstack 的输出可能很长尤其是线程数几百上千的服务进程直接刷在屏幕上根本没法看。我会把结果做两层处理第一层原始输出完整落盘第二层只统计线程栈顶几句快速判断群体现状。sudo pstack 17230 /tmp/pstack_17230_$(date %s).txt这样存的好处是留有现场。后期想更精确地做统计还能自己写个脚本把每个线程的栈顶函数提取出来按出现次数排序。哪个函数出现在栈顶最多哪个位置最可能是卡点或热点所在。3.3 不用 pstack 的时候虽然标题是 pstack 指南但我也要提醒不是任何时候都该用它。如果进程已经卡到 D 状态也就是不可中断睡眠状态附加操作可能长时间拿不到响应如果栈空间被破坏得很严重回溯可能直接失败。还有一种情况是进程正在快速创建和销毁线程pstack 抓到的瞬间列表可能判读不完整此时可以间隔几秒多抓几次做对比。4. 读懂 pstack 输出的关键信息pstack 抓出来的东西不是给人直接念的里面有很多底层符号如果不会做语义映射很容易被一堆地址和内部函数名吓住。我建议你抓完堆栈之后按照“线程归属、栈顶位置、锁等待状态”三个角度去读。4.1 线程编号和归属关系pstack 输出的最开头一般会标出线程序号每个线程会注明对应的线程 ID。通常标记为 Thread 1 的代表进程的主线程也就是最先启动的那个执行流。其他线程的堆栈可能长得密密麻麻要注意先看它们各自所在的业务模块。主线程卡在某处和某个工作线程卡在某处分析方向是完全不同的。主线程卡住往往意味着事件循环、调度入口、初始化等全局流程出问题工作线程卡住则更倾向于锁竞争、依赖等待、外部调用阻塞。拿到堆栈后先分清角色再往下看。4.2 栈顶决定当前位置栈底决定业务入口每一帧堆栈最上面的几行代表线程当前正在执行的指令以及它所在的函数越往下越接近业务调用链的入口。例如像这样的输出片段Thread 7 (Thread 0x7f1a2b3c4700 (LWP 18223)): #0 0x00007f1a2e8f54e1 in pthread_cond_waitGLIBC_2.3.2 () from /lib64/libpthread.so.0 #1 0x0000559a1e3c12bb in customer_thread_run (arg0x0) at consumer_task.c:84 #2 0x00007f1a2e8e8dc5 in start_thread () from /lib64/libpthread.so.0 #3 0x00007f1a2e81773d in clone () from /lib64/libc.so.6栈顶停在 pthread_cond_wait说明这个线程当前正挂在某个条件变量上等待。test at consumer_task.c:84告诉你业务代码在文件里的位置。如果同一时刻其他线程都停在这个等待点说明进程并不是死锁而是没有可执行任务在休眠如果只有部分线程等在这里另一部分在争锁那判断方向又要调整。4.3 常见的“栈顶信号”对照我习惯把栈顶出现的函数归类快速匹配问题模式。以下是我常用的判断思路栈顶关键符号常见含义后续要查的方向pthread_cond_wait / pthread_mutex_lock线程在等待条件变量或互斥锁谁在持锁持锁线程卡在哪futex_wait / __lll_lock_wait底层锁等待结合整个线程组看是不是死锁__poll / select / epoll_wait在等待事件或超时事件循环是否还在正常运转sched_yield / __cpu_relax忙等或自旋是否有自旋锁/无锁算法死循环malloc / free 相关在堆分配/释放路径上是否有内存分配竞争或全局堆锁recv / read / write 相关在系统调用上等待 I/O下游依赖是否变慢这套信号要配合业务代码一起看。比如栈顶是等待读 socket但超时时间设得很长那服务无响应就可能是下游拖慢如果栈顶全落在锁等待而持锁的线程又停在网络阻塞上那定位起来就非常清晰了。5. 典型故障排查案例复盘下面我用三个经历过的高频场景把 pstack 在真实排查中的用法走一遍。这三个场景基本覆盖了线上服务最常见的三类故障CPU 高、无响应、死锁。5.1 场景一CPU 飙升但业务量平稳有一次我遇到一个服务CPU 使用率突然从 20% 一路冲到 90%但流量监控曲线几乎是平的。想都不用想直接抓堆栈。我执行了top -Hp pid先用 top 的线程视图找到占 CPU 最高的几个线程 ID再把这些线程的 ID 转成十六进制之后抓 pstack 做关联。虽然 pstack 本身没直接给你线程的 CPU 占用率但你可以把 top 里看到的高 CPU 线程和堆栈里对应的 LWP 对上。我那次抓到的高占用线程全部停在一个自旋等待的函数上栈顶反复出现 sched_yield 和某个自定义的空转循环。业务代码的确执行路径很短但高并发下大家都在重复进出这个空转逻辑CPU 当然被打爆。修复后占用立刻回归正常。5.2 场景二进程无响应但没退出服务对外不响应但进程还活着ps 看状态是 SCPU 也不高。这种“活死人”状态最典型的原因是某个全局锁被持有后没有释放或者某个核心线程早就在等待一个永远等不到的信号。pstack 一抓整个线程组里绝大多数线程都停在 pthread_mutex_lock 的等待序列上而其中有一个线程堆栈停在一个业务函数里那一帧指向的代码是普通处理逻辑按道理不应该这么久。再配合业务日志一看那个线程在等待一个外部资源而该资源的超时时间设成了永久等于持有锁以后直接休眠。一根链条卡住了所有工作线程。这种问题的价值就在于 pstack 能把“谁在等锁”“谁在持锁”直接摆在一起。你只需要找出唯一没有停在等待函数上的线程基本就能定位到真正的阻塞源头。5.3 场景三线程池任务堆积线程池一般都有一个任务队列正常情况下任务进队出队保持平衡。哪天你会看到队列积压越来越多但线程数没有增长服务吞吐掉得厉害。pstack 在这种场景下能告诉你两个信息线程是在干活还是在等待如果是在干活干活的内容是否是同一类任务。如果所有线程都停在一个耗时长的业务函数上且这个函数按日志来看调用量并不大那很可能是某个入口参数导致调用路径退化成慢路径。如果线程都在等任务队列那问题往往不在线程池本身而在上游生产任务的速度或者任务分发逻辑有没有异常。6. pstack 的局限性与兜底手段用 pstack 十年我可以很负责地说它并非万用工具。有些场景它会花很长时间有些场景它直接报错。提前知道局限可以避免把宝贵的时间耗在错误的方向上。6.1 权限和内核限制普通用户只能抓自己启动的进程。跨用户抓取基本上会被权限系统拦住。生产机上建议通过 sudo 或让服务所属账号来执行。还有一种情况是内核里开了更强的追踪保护导致任何人都只能 attach 自己的子进程这时候你需要临时调整相关内核参数或在服务启动脚本里预先放开权限。另外内核线程本身不能通过 pstack 抓取。内核线程往往没有用户态栈pstack 会直接给出空结果。你要是想排查内核态的问题需要另找专门的内核分析工具不要在 pstack 上钻牛角尖。6.2 D 状态与不可中断等待当进程处于不可中断的磁盘 I/O 等待中时pvstack 抓取可能卡住。因为进程没法响应用户态命令附加动作迟迟得不到回包。我在处理一例存储设备故障时pstack 命令整整挂了几分钟都没返回最后还是靠多台机器配合从外围线索定位到了问题。所以遇到 D 状态先判断是否存储设备故障再决定要不要继续等 pstack。6.3 没有符号表时的处理线上二进制如果被 strip 过没有调试符号pstack 打印出来只能看到模块名和地址函数名全是问号或原始地址。这种情况可以安装对应的 debuginfo 包或者临时部署一份带符号的二进制用于映射。需要说明的是没有符号时堆栈并非完全没用函数地址的前后偏移和模块范围依然能辅助定位到具体函数区间。7. 常见问题速查与排障记录最后把我在答疑和实际运维中经常遇到的一些问题整理成速查表。如果你在操作中遇到相同的报错直接对号入座会快很多。7.1 执行报错对照表报错信息常见原因处理办法No such processPID 错误或已在不同命名空间检查宿主机 vs 容器 PID确认进程是否存活Operation not permitted权限不足或内核追踪保护启用换 sudo或调整权限配置ptrace: Input/output error目标进程是内核线程或状态异常换普通用户态进程再抓或检查内核线程Cannot attach to process同一时间已被其他调试器附加等待或找出谁在调试占住期间不可并发gdb not found / 缺少后端系统缺少依赖安装对应调试后端或基础工具链7.2 采集过程注意事项采集 pstack 不要贪多。对同一进程连续抓几百份只会干扰现场。我建议是每 5 到 10 秒抓一次连抓 3 到 5 份先看变化趋势。如果每份结果都一样说明线程稳定停顿在某处如果栈顶在变说明进程仍在快速推进问题可能出在瞬时逻辑上。另外抓堆栈的文本里如果出现非常多的重复等待函数或者大量线程栈顶完全相同不要急着下死锁结论。还要看锁持有者在哪里。有一种情况是锁没死只是持有时间特别长表现为所有工作线程集体等待。pstack 看起来和死锁很像但解决方向完全不同。7.3 与其他工具的配合思路pstack 适合看线程函数的“静止照片”但如果要了解事件时序就要配合系统调用跟踪工具和网络监控一起用。比如发现线程栈停在 read 上再用网络监控确认对端是否还有数据发送判断到底是本地卡住还是下游不通。pstack 解决的是定位“停在哪”至于“为什么停”往往要结合日志、网络、资源监控等多维度数据才能形成完整判断。我个人在实际操作中的体会是pstack 是性价比极高的一类工具。它不依赖复杂平台不需要提前埋点一条命令就能让进程在你面前“裸奔”。它最大的成就感来自一种场景所有人围着服务无响应的问题一筹莫展你一条 pstack 下去直接看到线程全部堵在同一把锁上而持锁线程停留在某个可疑函数接着你去翻那一段日志一击命中。这种爽快感大概是每个排障人最享受的时刻。最后再分享一个小技巧抓完堆栈不要随手删把 pstack 输出按“应用名时间戳”归档。很多问题不是一次就能看清的尤其是那种周期性出现的卡顿多留几份历史堆栈后面做趋势对比时你会庆幸自己当时存了文件。这还是整个系列的第一部分下一篇我打算重点讲堆栈结果的二次分析、批量采集线上多实例堆栈方法以及如何把 pstack 融入日常监控体系让它从“救火工具”变成“预防工具”。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →