如何在不中断程序运行的情况下,实时查询活跃的Goroutine数量?
- 内容介绍
- 文章标签
- 相关推荐
本文共计971个文字,预计阅读时间需要4分钟。
它返回的是当前已启动、尚未退出的所有+
常见错误是把它当成“用户创建的 goroutine 数”。实际上它包含 runtime 内部协程,比如:netpoll 的监听 goroutine、timer 管理协程、GC 辅助协程等,通常稳定在 2–5 个。别一看到值是 7 就怀疑自己漏了 defer wg.Done()。
- 在 HTTP handler 中暴露监控端点时,直接
return fmt.Sprintf("%d", runtime.NumGoroutine())即可 - 不要在每个请求里都调用并打日志——日志 IO 成本远高于
NumGoroutine()本身 - 该值不是流式指标,无法告诉你 goroutine 在哪卡住,只适合做数量趋势观察
用 /debug/pprof/goroutine?debug=2 查看真实堆栈
runtime.NumGoroutine() 返回一个整数,而 /debug/pprof/goroutine?debug=2 返回的是全部 goroutine 的完整调用栈快照,两者语义不同。pprof 默认(?debug=1)会过滤掉大量 idle 的 runtime 协程;加 ?debug=2 才能看到更全的视图,但依然可能不包含刚创建还没被调度的 goroutine。
典型误判场景:你看到 NumGoroutine() 是 1200,pprof 显示只有 800 行堆栈——差值大概率是 netpoll worker 或 GC mark assist 协程,它们处于 runtime.gopark 状态,在 pprof 中被默认归为 idle 并隐藏。
- 访问
http://localhost:6060/debug/pprof/goroutine?debug=2后,搜索关键词如select、chan receive、semacquire、net.(*pollDesc).wait,快速定位阻塞点 - 注意区分状态:
running可能是 CPU 密集型 bug;IO wait更可能是连接/句柄未释放导致 goroutine 挂起 - pprof 不是实时流,两次采样之间有窗口,别指望靠它抓到毫秒级瞬态 goroutine
统计特定业务 goroutine 要自己加原子计数
runtime.NumGoroutine() 统计的是全局总数,无法区分 processOrder 和 sendNotification 各自起了几个。要精确追踪某类逻辑的并发规模,必须在应用层埋点。
用 sync/atomic 是最轻量、无锁、panic-safe 的方式。核心是:入口 atomic.AddInt64(&counter, 1),出口 defer atomic.AddInt64(&counter, -1)。即使函数 panic,defer 仍会执行,计数不会失准。
- 计数器变量类型必须是
int64,否则atomic.LoadInt64会 panic - 别用
int或uint32—— 在 64 位系统上非对齐读写可能导致 crash - 读取当前值用
atomic.LoadInt64(&counter),不要直接读变量,否则有竞态 - 如果要暴露给 Prometheus,把
LoadInt64结果转成float64塞进Gauge.Set()即可
生产环境别只盯着数字,要看变化模式
数值本身没意义。runtime.NumGoroutine() 稳定在 3000 不一定异常;从 500 涨到 3000 且不回落才危险。关键是看趋势和上下文:
- HTTP 请求量突增时,goroutine 数短暂冲高是正常的;持续不降才是泄漏信号
- 用滑动窗口统计最近 30 秒最大值,比单点阈值告警更抗毛刺
- 如果配合 Prometheus,建议同时采集
go_goroutines和go_gc_duration_seconds,GC 频次升高 + goroutine 持续上涨,基本可锁定泄漏 - 最常被忽略的是第三方库自带的 goroutine:某些 metrics collector、logger、retry loop 如果没提供
Close()接口,它们会在后台永久存活
本文共计971个文字,预计阅读时间需要4分钟。
它返回的是当前已启动、尚未退出的所有+
常见错误是把它当成“用户创建的 goroutine 数”。实际上它包含 runtime 内部协程,比如:netpoll 的监听 goroutine、timer 管理协程、GC 辅助协程等,通常稳定在 2–5 个。别一看到值是 7 就怀疑自己漏了 defer wg.Done()。
- 在 HTTP handler 中暴露监控端点时,直接
return fmt.Sprintf("%d", runtime.NumGoroutine())即可 - 不要在每个请求里都调用并打日志——日志 IO 成本远高于
NumGoroutine()本身 - 该值不是流式指标,无法告诉你 goroutine 在哪卡住,只适合做数量趋势观察
用 /debug/pprof/goroutine?debug=2 查看真实堆栈
runtime.NumGoroutine() 返回一个整数,而 /debug/pprof/goroutine?debug=2 返回的是全部 goroutine 的完整调用栈快照,两者语义不同。pprof 默认(?debug=1)会过滤掉大量 idle 的 runtime 协程;加 ?debug=2 才能看到更全的视图,但依然可能不包含刚创建还没被调度的 goroutine。
典型误判场景:你看到 NumGoroutine() 是 1200,pprof 显示只有 800 行堆栈——差值大概率是 netpoll worker 或 GC mark assist 协程,它们处于 runtime.gopark 状态,在 pprof 中被默认归为 idle 并隐藏。
- 访问
http://localhost:6060/debug/pprof/goroutine?debug=2后,搜索关键词如select、chan receive、semacquire、net.(*pollDesc).wait,快速定位阻塞点 - 注意区分状态:
running可能是 CPU 密集型 bug;IO wait更可能是连接/句柄未释放导致 goroutine 挂起 - pprof 不是实时流,两次采样之间有窗口,别指望靠它抓到毫秒级瞬态 goroutine
统计特定业务 goroutine 要自己加原子计数
runtime.NumGoroutine() 统计的是全局总数,无法区分 processOrder 和 sendNotification 各自起了几个。要精确追踪某类逻辑的并发规模,必须在应用层埋点。
用 sync/atomic 是最轻量、无锁、panic-safe 的方式。核心是:入口 atomic.AddInt64(&counter, 1),出口 defer atomic.AddInt64(&counter, -1)。即使函数 panic,defer 仍会执行,计数不会失准。
- 计数器变量类型必须是
int64,否则atomic.LoadInt64会 panic - 别用
int或uint32—— 在 64 位系统上非对齐读写可能导致 crash - 读取当前值用
atomic.LoadInt64(&counter),不要直接读变量,否则有竞态 - 如果要暴露给 Prometheus,把
LoadInt64结果转成float64塞进Gauge.Set()即可
生产环境别只盯着数字,要看变化模式
数值本身没意义。runtime.NumGoroutine() 稳定在 3000 不一定异常;从 500 涨到 3000 且不回落才危险。关键是看趋势和上下文:
- HTTP 请求量突增时,goroutine 数短暂冲高是正常的;持续不降才是泄漏信号
- 用滑动窗口统计最近 30 秒最大值,比单点阈值告警更抗毛刺
- 如果配合 Prometheus,建议同时采集
go_goroutines和go_gc_duration_seconds,GC 频次升高 + goroutine 持续上涨,基本可锁定泄漏 - 最常被忽略的是第三方库自带的 goroutine:某些 metrics collector、logger、retry loop 如果没提供
Close()接口,它们会在后台永久存活

