runtime.NumGoroutine() 是安全暴露协程数量的唯一轻量方式,返回包括用户与 runtime 内部协程在内的瞬时总数,应通过 Prometheus 标准路径(如 /metrics)暴露为 go_goroutines 指标,避免在 init() 或 panic/defer 中调用,高频采样(如每秒一次)可行,但需结合趋势与关联指标(如 go_threads、process_open_fds)判断是否泄漏。

怎么用 runtime.NumGoroutine() 安全暴露 Goroutine 数量
runtime.NumGoroutine() 返回的是当前所有存活 goroutine 的瞬时快照,包括 net/http 监听协程、pprof handler、GC 辅助协程等——它不是“你启动的业务协程数”,而是整个 runtime 的协程总数。直接裸调没问题,但关键在怎么用才有意义。
常见错误是把它当“并发请求数”监控:看到数值飙高就告警,结果发现只是 kubelet 每 5 秒调一次 /healthz,每个请求都 spawn 一个新 goroutine 处理,而 handler 里又没及时 return 或超时控制,导致堆积。
- 暴露方式必须走 Prometheus 标准路径(如
/metrics),用promhttp.Handler(),别手写 JSON 或文本格式——否则会被 Prometheus 拒绝 - 指标名建议用
go_goroutines(官方 client_golang 默认名),避免自定义命名导致 Grafana 面板不兼容 - 不要在
init()或 panic defer 中调用——前者可能返回 0,后者栈已损坏,数值失真 - 高频采集(如每秒一次)完全可行,该函数无锁、开销极低
为什么不能只看 runtime.NumGoroutine() 就判断泄漏
这个值本身无法区分 goroutine 生命周期阶段:刚退出但尚未被 runtime 清理的协程仍会计入;阻塞在 channel recv、syscall 或 time.Sleep 的也全算在内。所以单点数值没有意义,必须看趋势 + 关联指标。
典型误判场景:服务启动后 NumGoroutine() 稳定在 12~15,某天突然跳到 80+ 并缓慢爬升。你以为是泄漏,结果查发现是 WebSocket 连接数从 5 升到 70,每个连接对应一个处理 goroutine——这是正常行为,不是 bug。
- 必须搭配
go_threads(OS 线程数)、process_open_fds(文件描述符)一起看:如果 goroutine 增多但线程数/文件描述符没涨,大概率是空闲或阻塞态,未必危险 - 用
net/http/pprof的/debug/pprof/goroutine?debug=2抓堆栈,看哪些 goroutine 卡在select、chan send或数据库PingContext上 - 健康检查端点(如
/healthz)禁止埋点统计,否则会污染http_request_total指标
如何在 Iris 中给后台任务加可观测的 Goroutine 状态
Iris 本身不提供后台任务管理,所有长期运行的 goroutine 都得你手动控制生命周期和状态上报。重点不是“启动多少个”,而是“是否还在健康运行”。
比如一个定时同步用户数据的任务,不能只靠 go syncUser(ctx) 启动就完事——它挂了没人知道,也不会自动重启。
- 每个后台任务启动前,用
sync.Once注册一次指标:task_running{task="sync_user"} 1,退出时设为 0 - 用原子布尔值(
atomic.Bool)标记任务状态,在/readyz端点里检查:if !syncUserRunning.Load() { ctx.StatusCode(iris.StatusServiceUnavailable) } - 任务内部必须接收
context.Context,并在select中监听ctx.Done(),不能用for {}死循环 - 别在任务里直接调
ctx.JSON()或写日志文件——Iris 的Context不可跨 goroutine 使用,会 data race
怎么避免健康检查自己变成 Goroutine 泄漏源
/health 和 /readyz 被轮询频率极高(Kubernetes 默认每 10 秒一次),如果每次请求都新建 goroutine 去 ping DB 或 Redis,且没设超时或没回收,很快就会积压成千上万协程。
最典型的错误写法:go db.PingContext(ctx.Request().Context()) 启动协程但不等结果、不关 channel、不设 context 超时。
- 所有外部依赖调用必须带
context.WithTimeout,例如ctx, cancel := context.WithTimeout(ctx.Request().Context(), 2*time.Second) - 慢速依赖(如第三方 HTTP API)建议用异步刷新 + 缓存模式:单独 goroutine 每 30 秒探测一次,健康端点只读缓存值
- 禁止在健康检查里做任何写操作(发消息、写日志文件、更新 metric counter),只读探测
- 区分语义:
/livez只确认进程存活(如能响应 HTTP、未 panic),/readyz才查 DB/Redis 等依赖
真正难的不是采集数字,而是理解每个 goroutine 的来龙去脉——它为什么存在?卡在哪?有没有人负责清理?监控面板上的曲线只是表象,背后得有清晰的生命周期契约。











