runtime.Stack会触发STW,导致可感知的程序暂停;传true时耗时随goroutine数量增长而显著上升,5k+时可达100ms以上,可能引发HTTP超时和CPU突降。

runtime.Stack 会触发 STW,不是“小开销”而是“有感知的暂停”
runtime.Stack 不是纯内存读取操作,它需要 runtime 暂停所有 goroutine(Stop-The-World 片段),以确保堆栈状态一致。这个暂停时间取决于当前活跃 goroutine 数量和调用栈深度,**不是常量,也不能忽略**。
常见错误现象:runtime.Stack(buf, true) 在 goroutine 数量达 5k+ 时,耗时可能从 0.1ms 跳到 100ms 以上,HTTP 请求直接超时;监控看到 CPU 突降、GC Pause 报警联动触发。
- 传
false(仅当前 goroutine):通常 - 传
true(全部 goroutine):1k goroutine ≈ 5–10ms;10k goroutine ≈ 50–200ms(实测数据) - 缓冲区过小导致重试(比如先用 4KB 失败,再扩到 1MB)会叠加两次 STW,开销翻倍
缓冲区大小直接影响 STW 时长和结果完整性
缓冲区太小不会报错,但会静默截断——runtime.Stack 返回实际写入长度 n,若 n == len(buf),说明内容被砍了。而重试扩容需再次调用,等于多一次 STW。
使用场景:线上 panic 恢复中想存全栈,却因 buf 只开了 4KB,只记下 runtime.goexit 一行,误判为“无调用上下文”。
- 保守起见,当前 goroutine 用
make([]byte, 1(1MB) - 获取全部 goroutine 时,建议按预期最大 goroutine 数预估:每 goroutine 平均栈约 2–8KB,10k 就预留 64MB 缓冲(但更要优先避免真去调)
- 别用
nil当 buf:虽然函数会自动分配,但返回切片底层数组可能被 GC 回收,日志里出现乱码或 panic
线上禁用 runtime.Stack(true) 的三个硬性理由
不是“不推荐”,是生产环境应视为危险操作。它和 pprof/goroutine?debug=2 的行为不可等同看待——后者是采样式、异步、受限流保护的接口。
- 它阻塞调度器:STW 期间新 goroutine 无法启动,
select{default:}可能意外跳过,定时器延迟,netpoll 响应卡顿 - 它放大毛刺:在高负载时段调用,可能把原本 50ms 的 P99 拉高到 300ms,触发下游熔断
- 它不可控:没有超时、没有限流、没有 fallback,一旦卡住只能等它自己结束
真正需要查 goroutine 泄漏时,应该 curl /debug/pprof/goroutine?debug=2,而不是在代码里埋 runtime.Stack(buf, true)。
debug.Stack() 和 runtime.Stack() 的性能差异其实很小,但用途完全不同
很多人以为 debug.Stack() 更轻量,其实它底层就是封装了一次 runtime.Stack(buf, false),开销几乎一致。区别在于返回值类型和使用方式:
-
debug.Stack()返回[]byte,适合直接存日志、发告警、写临时文件——**控制权在你手上** -
debug.PrintStack()直接往os.Stderr写,如果服务把 stderr 重定向(如 systemd、容器日志驱动),就彻底看不见 - 别混用:
string(debug.Stack())没问题;但string(runtime.Stack(nil, false))是危险写法,nil 分配的内存生命周期不可控
最易被忽略的一点:哪怕只用 false,也不该放在高频路径(如每个 HTTP middleware、每条 Kafka 消息处理)。它不是零成本,只是成本较低——而低频异常路径(panic defer、测试失败钩子)才是合理位置。











