线上进程假死?用gstack快速抓取线程调用栈,定位死锁与阻塞

发布时间:2026/10/13 18:41:06
线上进程假死?用gstack快速抓取线程调用栈,定位死锁与阻塞 1. 线上进程假死的时候我为什么第一个想到 gstack1.1 先从一次午夜故障说起我记得很清楚有一回凌晨两点被值班监控吵醒某个业务模块对外端口还开着健康检查也显示存活但所有请求全部超时。日志里干干净净没有报错没有崩溃CPU 占用率低得离谱内存也没异常。这种半死不活的状态最折磨人——进程没退出但和死了没区别。当时我第一个动作就是找 PID然后跑了一条命令gstack 26731几秒钟之后终端里刷出了几十个线程的调用栈。死锁的现场被完整保留下来两个线程各自持有一把锁都在等对方释放另一把。问题定位前后没超过十分钟。这就是我写这篇文章的初衷。很多人遇到进程假死第一反应是重启、是看监控、是翻日志唯独忘了去看一眼进程此刻到底在干什么。而gstack就是干这个用的。1.2 gstack 的真面目一个把 gdb 包成傻瓜相机的脚本如果你第一次听说gstack可能会以为它是什么高深的新工具。其实它不是它的本质是一个 Shell 脚本由 GDBGNU Debugger软件包附带安装。在多数 Linux 发行版里装完 gdb 就有了它。比如 Ubuntu 上它是/usr/bin/gstackCentOS 上它同样随 gdb 提供。它的全部核心逻辑可以浓缩成一句话用 gdb 以批处理模式 attach 到指定进程执行打印所有线程的调用栈然后自动退出。说白了gstack就是 GDB 的傻瓜模式。它把gdb -p PID进入交互式调试这个操作封装成了带 PID 参数的一条命令适合在故障现场快速抓取证据。虽然拆开看毫无技术含量但恰恰是这种简单到不需要思考的特性让它在紧急排障时比裸 gdb 好用得多。提示如果你的系统里找不到 gstack先确认有没有装 gdb 包。部分发行版也提供pstack很多环境里它们功能和用法几乎一样后面我会单独对比。2. gstack 的工作原理从 ptrace 到调用栈的全过程2.1 attach 那一刻进程其实被按了暂停键要理解 gstack必须先理解 gdb 的 attach 机制。这里的核心是 Linux 的ptrace系统调用。你用gdb -p PID附加到一个正在运行的进程时gdb 通过 ptrace 请求内核获得对该进程的调试权限。内核会做几件事检查调用者的权限你是否是目标进程的 owner或者是否有CAP_SYS_PTRACE权限检查ptrace_scope安全机制后面细说给目标进程发送SIGSTOP信号让进程暂停下来。暂停这一点极其关键。因为只有在进程完全静止、内存和寄存器状态不再变化的时候gdb 才能安全地读取它的运行时信息。你可以把 attach 想象成给正在跑步的人按了暂停键然后蹲下来仔细看他的姿势。接下来gdb 通过/proc/pid/mem和 ptrace 的读取接口拿到进程的内存映射、线程列表、寄存器值再结合可执行文件里的符号表和调试信息把一个个栈帧还原出来。这就是我们看到的调用栈。整个过程听起来复杂但 gdb 自己全包了。gstack做的事就更简单给 gdb 传几个参数让它跑完就退出。实际脚本核心逻辑大约是这样的不同发行版略有差异#!/bin/sh # gstack - print stack trace of a running process # 本质上是 gdb 的批处理封装 if [ $# -ne 1 ]; then echo Usage: gstack pid 2 exit 1 fi if [ ! -r /proc/$1/task ]; then echo Process $1 not found. 2 exit 1 fi exec gdb -q -batch -p $1 -ex thread apply all bt full注意最后一行thread apply all bt full是关键thread apply all表示对所有线程执行命令bt full是打印完整调用栈包括局部变量。这行命令决定了 gstack 的输出天然就是全线程视角的。2.2 一次抓栈的输出到底该怎么读拿一个典型的输出片段来说[Thread 0x7f0e2bfff700 (LWP 26738) is running] Thread 6 (Thread 0x7f0e2bfff700 (LWP 26738)): #0 0x00007f0e2f0a5e9d in syscall () at ../sysdeps/unix/sysv/linux/x86_64/syscall.S:43 #1 0x00007f0e2e89a6b6 in futex_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:33 #2 0x00007f0e2e8e627e in __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:61 #3 0x00007f0e2e830b63 in _L_lock_1054 () from /lib/x86_64-linux-gnu/libpthread.so.0 #4 0x00007f0e2e82f09d in pthread_mutex_lock () from /lib/x86_64-linux-gnu/libpthread.so.0 #5 0x0000000000401a2e in worker_process (arg0x0) at server.c:128 #6 0x00007f0e2e82e6db in start_thread () at pthread_create.c:477 #7 0x00007f0e2f0a1aaf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95逐行拆开看每行是一个栈帧#0是当前正在执行的函数#1是它的调用者往下依次是整条调用链。以#4为例pthread_mutex_lock说明这个线程正在尝试获取一把互斥锁而#5的worker_process是业务代码里真正发起加锁操作的位置server.c:128直接告诉你在哪一行。输出里还会标注每个线程的状态比如 is running、is sleeping 等。如果一个服务有几十个线程大部分都停在futex_wait或pthread_cond_wait上只有少数在跑那基本能推测出线程池大部分线程在等待任务这一层信息。2.3 为什么抓栈必须快、必须自动退出这里有个很多人忽略的细节gdb attach 会让目标进程暂停。如果在生产环境这意味着服务在那几秒里是真正停摆的。所以 gstack 用-batch模式让 gdb 执行完命令立即退出进程随即恢复运行。整个过程通常只有一两秒。这个设计提醒我一件事抓栈不是无所谓代价的。在 CPU 高负载的 12 核机器上attach 瞬间所有线程被 SIGSTOP如果此时有外部依赖比如数据库连接另一端可能就开始积累超时了。所以实际操作里我一般遵循三条原则一次抓栈尽量控制在 2 秒内完成gstack 默认就满足不要在高峰期对核心服务反复抓栈要抓就抓一两次拿到信息马上离开如果服务特别敏感可以先摘流量再抓抓完再恢复。3. gstack 实战哪些场景能救哪些场景救不了3.1 三个典型的抓栈现场我按实际排障频率排个序说说最值得用 gstack 的场景。场景一进程假死CPU 和内存都正常请求全部卡住。这是最典型的死锁现场。进程没在消耗资源说明它不是忙而是在等。等什么等锁、等 IO、等网络。gstack 一抓所有线程的等待位置全部现形。如果看到两个以上线程互相持有锁并互相等待死锁成立直接把server.c:128这种位置报给开发问题当场闭环。场景二某个线程 CPU 飙到 100%整体服务变慢。这时候光看 top 只知道有线程在烧 CPU不知道在烧什么代码。用top -H找出高 CPU 线程的 tid然后gstack pid输出里按LWP号码去对注意对应关系是十进制 tid就能看到那个线程卡在哪个函数里。有可能是死循环、正则回溯、序列化热点一眼就明白。场景三服务假死后自动恢复了想复盘。这种最郁闷——等你登录上去进程已经活过来栈早就变了。我的做法是在监控里预设一条触发命令当错误率或超时率超过阈值时自动执行gstack pid /tmp/stack_$(date %s).log把现场存下来。多来几次这种自动化抓栈比事后开会猜原因有用得多。3.2 抓栈救不了的场景与替代方案gstack 不是万能的下面几类情况它确实搞不定提前知道能省掉走弯路的时间。Java 进程别用 gstack。不是说完全不能用而是 Java 的线程模型跑在 JVM 之上gstack 看到的多是 JVM 内部的 native 栈业务方法栈你根本看不出来。这种情况直接用jstack pid拿到的才是 Java 线程的完整调用栈。进程处于不可中断睡眠D 状态。这类进程通常卡在底层 IO比如 NFS 挂死、磁盘故障gdb 想 attach 上去往往也读不到有效信息甚至可能长时间卡住。这时候优先看/proc/pid/stack和dmesg查内核层面的阻塞原因。内核线程或内核态死锁。gstack 面向的是用户态进程内核栈得用/proc/pid/stack需要 root 加上对应内核配置或者借助 crash 工具分析 vmcore。进程已经彻底崩溃。gstack 只能 attach 到活着的进程。如果是崩溃那就别指望它直接看 core dump用 gdb 分析 core 文件效果更好。还有一个不算少见的情况目标进程把 gdb 的 ptrace 权限封了比如某些安全加固程序主动调用了prctl(PR_SET_DUMPABLE, 0)。这时候 attach 会被拒绝gstack 直接报错。如果确认是这类情况从应用配置和日志入手可能更实际。4. gstack、pstack、裸 gdb我的选型建议4.1 三者到底差在哪很多初学者搞不清 gstack、pstack 和直接敲 gdb 的区别我做个表直接对比工具本质输出内容上手难度适用场景gstackgdb 批处理封装脚本所有线程调用栈极低一条命令快速抓现场、自动化采集pstack同类工具实现可能不同有的走 /proc有的也是 gdb 封装所有线程调用栈极低和 gstack 基本可互相替代gdb裸完整调试器不仅调用栈还能看变量、内存、寄存器、堆栈操作高需要深入交互式调试、排查复杂 bug从实践角度我自己的选择逻辑是这样的需要紧急确认进程卡在哪无脑用 gstack。它输出格式稳定结果里带着源码行号和函数名足够应付 80% 的假死类问题。需要在脚本里自动采集也用 gstack。因为它是非交互式的一条命令跑完自动退出成功和失败都有清晰退出码方便接进监控告警。需要进一步确认变量值、内存内容或者想在 attach 状态下做点操作比如修改内存那必须上裸 gdb。gstack 只是一个拍照工具不是手术工具。顺带提醒一句有些环境里 gstack 和 pstack 是同一个东西的不同软链有些发行版则各自独立行为也有细微差别。我建议你拿到一台新服务器时先跑一遍gstack --help看一眼版本避免在故障现场被莫名其妙的差异坑一把。4.2 容器环境里的权限与内核参数坑现在服务基本都在容器里跑这里有两个高频坑值得单独拿出来说。第一个是ptrace 权限受限。Linux 内核从 3.10 开始引入 YAMA 安全模块默认的ptrace_scope1只允许进程 trace 它的直接子进程。你在宿主机上想 attach 容器里的进程本来就不是父子关系如果容器没有给足权限gdb 会直接报Operation not permitted。解决办法分几种在容器启动参数里加--cap-addSYS_PTRACE或者在 Kubernetes 的 securityContext 里配置capabilities: add: [SYS_PTRACE]临时调整宿主机的/proc/sys/kernel/yama/ptrace_scope为 0但这对所有进程生效生产环境要谨慎用完记得改回来干脆进入容器内部再跑 gstack容器内 attach 容器内进程往往就是父子关系能绕开一部分限制。第二个是容器内没有 gdb。很多精简镜像为了控制体积连 gdb 都不装gstack 自然也不存在。我的经验是生产环境的基础镜像里提前装好 gdb体积增加不大但关键时刻能救命。如果实在不想装可以用一个折中的办法——在宿主机上抓容器的进程gstack $(docker inspect -f {{.State.Pid}} container_name)但请注意宿主机 attach 容器进程同样受 ptrace_scope 和容器权限约束不是每次都灵。最稳妥的仍然是镜像里预装 gdb 容器带 SYS_PTRACE 权限 从容器内执行 gstack三件套。5. 拿到调用栈之后根因分析的几条实战套路5.1 死锁、空转和 IO 阻塞的栈特征调用栈打印出来了接下来怎么判断问题性质我总结了三类最常见的栈特征遇到直接对号入座。死锁类。特征是多个线程同时卡在锁等待函数上比如pthread_mutex_lock、pthread_rwlock_wrlock而且每个线程的栈里都能往上看到它已经持有了某把锁。锁的持有和等待关系往往通过业务函数名就能看出来。比如线程 A 在handle_order里等inventory_lock线程 B 在update_inventory里等order_lock锁名、函数名一对上死锁闭环就清楚了。自旋/空转类。特征是某个线程的栈顶层停在一个业务函数内部而且 CPU 占用率极高。注意 gstack 只给你当前在哪不告诉你循环了几次。判断是不是死循环要靠二次采样——隔几秒再抓一次如果栈完全一样那基本可以断定是自旋空转如果栈在变化说明它在正常推进。IO 阻塞类。特征是大量线程卡在系统调用上比如read、write、ppoll、futex_wait常见于后端依赖数据库、消息队列变慢把线程池全部占住。这时候调用栈只能告诉你阻塞在 socket 读取上具体等的是哪个下游还得到业务日志或网络层去找。5.2 连抓三次判断线程是卡住还是在干活这里分享一个我实战中最常用的技巧——二次/三次采样法。单次 gstack 只能说明此刻线程在哪个函数但无法区分它是暂时停在这个函数还是已经卡死在这里。解决办法很简单隔 3 到 5 秒再抓一次对比两次输出。如果同一个线程两次的栈完全一致连栈帧地址都没变说明它在原地打转或彻底阻塞——这本身就是强烈的故障信号。如果两次的栈有变化哪怕只是栈帧地址变了说明它还在推进只是比较慢。三次采样基本能把活但慢和死但不自知彻底分开。我在脚本里是这么写的#!/bin/bash pid$1 for i in 1 2 3; do gstack $pid /tmp/gstack_${pid}_${i}.log 21 sleep 3 done # 用 diff 快速找差异 diff /tmp/gstack_${pid}_1.log /tmp/gstack_${pid}_3.log | head -50配合top -H看线程 CPU 占用这套组合拳基本能覆盖绝大多数服务假死的定位需求。提示抓栈间隔别太短。gdb attach 本身要让进程暂停连续高频抓栈对服务是干扰。3 秒是我实践下来比较平衡的间隔既能看到差异又不会太打扰业务。5.3 一个完整的排障时间线最后串一个完整的实战时间线方便你直接抄作业告警触发确认服务端口存活但请求超时pidof 服务名拿到主进程 PIDtop -H -p pid看线程 CPU 分布记录高占用线程的 tid立即执行gstack pid保存第一次栈快照隔 3 秒再抓一次保存第二次快照对比两次快照确认线程是静止还是移动用cat /proc/pid/status看进程状态cat /proc/pid/wchan看内核等待点结合业务日志确认调用链里的关键函数是什么业务逻辑定位后摘流量 → 重启或修复 → 恢复观察。这套流程我现在已经写成内部排查手册了。新人照着走哪怕不懂底层原理也有 80% 的概率能把卡在哪这个问题给定位出来。写在最后gstack 不是什么黑科技它只是把 gdb 这把大杀器封装成了一条简单的命令。但排障这件事很多时候比的不是工具多么高级而是你能不能在最乱的时刻用最顺手的方式拿到最关键的现场信息。对我来说gstack 就是那个最顺手的方式。如果你手头正好有台测试机我建议你现在就找个进程试一把gstack 任意进程PID看看输出长什么样感受一下非交互式抓栈的过程。等到哪天线上真的出了问题你的肌肉记忆会替你做出正确选择。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询