runtime.stack(buf, true) 是死锁分析起点,因它能获取全量 goroutine 堆栈,结构与 panic 时自动 dump 一致;传 false 仅得当前 goroutine 栈,无法定位死锁。

runtime.Stack 不能代替死锁自动检测,但它是 panic 后唯一能拿到全量 goroutine 状态的可靠手段;传 false 只抓当前 goroutine,线上排查基本没用。
为什么 runtime.Stack(buf, true) 是死锁分析的起点
Go 运行时在触发 fatal error: all goroutines are asleep - deadlock! 时,会强制 dump 所有 goroutine 的调用栈——这和你手动调用 runtime.Stack(buf, true) 输出的内容结构一致。区别在于:panic 时的输出是运行时自动触发、带上下文快照的“终局证据”,而手动调用是你在程序还能跑时主动抓取的“现场快照”。
- 必须传
true:否则只返回当前 goroutine 栈,对死锁毫无价值 - 缓冲区要够大:
buf := make([]byte, 1024*1024)是较安全的起始值;若n == len(buf),说明被截断,需重试更大 buffer - 别在 panic handler 里做字符串拼接或 log.Printf:直接用
os.WriteFile或http.ResponseWriter.Write(buf[:n])落盘,避免 GC 压力放大故障
runtime.Stack 输出里哪些堆栈状态值得立刻关注
拿到 stack dump 后,不是逐行读,而是按阻塞原语分类扫描。重点关注以下几类调用帧:
-
runtime.gopark+runtime.chansend或runtime.chanrecv:说明 goroutine 卡在 channel 操作上;若大量 goroutine 都停在这两个函数,且收发方不匹配(比如全是 send 没 recv),就是典型 channel 死锁 -
sync.runtime_SemacquireMutex或runtime.semacquire:表示卡在mu.Lock();检查是否 defer unlock 被跳过、或 panic 后 unlock 没执行 -
sync.runtime_notifyListWait或runtime.netpollblock:前者常出现在sync.Cond.Wait(),后者是系统调用阻塞(如文件读写),属正常态,不用优先处理 -
runtime.gopark+runtime.selectgo:说明卡在select上;若所有 goroutine 都卡在这里且没 default 分支,大概率是退出条件缺失
线上环境调用 runtime.Stack 的三个硬限制
本地能跑通,不等于线上能拿到有效信息。真实部署中容易忽略的三点:
- 二进制被 strip 过:
go build -ldflags="-s -w"会删掉 DWARF 调试信息,stack 输出只剩地址(如0x456789),无法对应源码行;生产 debug 包建议保留 DWARF,发布前用strip单独处理 - /proc/self/maps 不可读:容器中若挂载了
procfs但权限受限,部分符号解析失败,显示??:0;这不是runtime.Stack的问题,但你会误判为“没拿到栈” - goroutine 处于系统调用中:比如卡在
read、epoll_wait,stack 停在runtime.gopark是正常行为,不代表卡死;重点看是否集中在同一业务逻辑点(如全部卡在某个 DB 查询)
比 runtime.Stack 更早发现问题的替代手段
等 panic 再查 stack,已经是故障发生后。真正有效的线上防控,得靠前置信号:
- 用
GODEBUG=schedtrace=1000观察调度器状态:如果连续几秒都是goroutines: N runnable: 0,说明已实质卡死,比等 panic 更早预警 - 启动时加
import _ "net/http/pprof"并起http.ListenAndServe("localhost:6060", nil),再用go tool trace抓 trace.out;它能还原 goroutine 的阻塞链路(谁在等谁、等了多久),尤其适合“没 panic 但程序不动”的灰度场景 - 对关键同步点埋点:比如在
mu.Lock()前记录 goroutine ID 和时间戳,在mu.Unlock()后清除;超时未清除即告警,比事后翻 stack 更主动
真正难的不是拿到 stack,而是从几百行 goroutine 列表里快速识别出哪几个在互相等待。每次看到 chan send 和 chan recv 出现在不同 goroutine 但指向同一个 channel 变量名,基本就可以收工了。











