pprof 默认不暴露 Goroutine 数量,需手动注册;/debug/pprof/goroutines 返回快照而非结构化数值,推荐用 runtime.NumGoroutine() 自建轻量接口,避免轮询开销与解析风险。

pprof 默认不暴露 Goroutine 数量,得手动注册
Go 的 net/http/pprof 默认只挂载了 /debug/pprof/ 下的标准路径(如 /goroutines、/heap),但这些端点本身不提供“实时数值”——它们返回的是文本或 pprof 格式快照。想在监控系统里拉取一个数字(比如每 10 秒查一次当前 goroutine 数),不能靠直接 GET /debug/pprof/goroutines?debug=1 再解析 HTML 或文本,因为:
• 它返回的是完整 goroutine stack dump,体积大、解析慢
• 没有结构化字段(如 {"count": 127})
• ?debug=1 输出可读文本,?debug=2 是 protobuf,都不适合自动化提取
真正轻量、可编程的方式是:用 runtime.NumGoroutine() 自己暴露一个 HTTP 接口。例如:
http.HandleFunc("/metrics/goroutines", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "%d", runtime.NumGoroutine())
})
这个值和 /debug/pprof/goroutines?debug=1 开头的 total: XXX 严格一致,且无额外开销。
别把 /debug/pprof/goroutines 当成监控接口用
直接轮询 /debug/pprof/goroutines?debug=1 看似省事,但实际踩坑不少:
- 每次请求会触发一次全量 goroutine stack trace 收集,高并发下可能引发短暂 STW(尤其 goroutine > 10k 时)
- 返回体大小随 goroutine 数线性增长(每个 goroutine 平均 ~200–500 字节),1w goroutine 就是 2–5MB 响应,浪费带宽也拖慢采集器
- 文本格式无版本保证,Go 新旧版本间
total:行位置或前缀可能微调(比如加空行、换提示语),导致正则解析失效 - 默认未开启 gzip,HTTP 层无法自动压缩大文本
所以,除非你明确需要分析阻塞点或死锁,否则不要把它当监控指标源。
如果非要从 pprof 接口取数,用 ?debug=2 + protobuf 解析
Go 1.19+ 支持 /debug/pprof/goroutines?debug=2 返回 protocol buffer(profile.Profile),比文本更稳定、更小。但要注意:
- 需引入
google.golang.org/protobuf和github.com/google/pprof/profile - 响应 body 是二进制,必须用
profile.Parse解析,不能直接 string 转换 -
profile.Sample里没有总数字段;总数藏在profile.PeriodType.Unit的注释里?错——其实根本不在 profile 结构中;真实总数仍得靠runtime.NumGoroutine(),?debug=2只是 dump 数据,不附带元信息
换句话说:?debug=2 适合做离线分析,不适合取数量。硬要走这条路,最后还是得 fallback 到 NumGoroutine()。
生产环境记得限制 pprof 访问权限
/debug/pprof/ 和自定义的 /metrics/ 都含敏感运行时信息,暴露在外网等于交出进程快照:
- 内网服务:用反向代理(如 Nginx)限制 IP 段,或加 basic auth
- 云环境(K8s):用 NetworkPolicy 禁止外部访问
6060端口,只允许 Prometheus ServiceMonitor 抓取 - 别用
http.ListenAndServe(":6060", nil)直接启动——务必显式传入路由树,避免意外暴露其他 handler - 自定义指标接口(如
/metrics/goroutines)建议和 pprof 分开端口,降低攻击面
最常被忽略的一点:很多人开了 pprof 就忘了关,上线后长期开着,既增加攻击面,又让恶意请求轻易触发大量 stack dump,反而成为 DoS 入口。











