🧩 场景还原:你是否遇到过这些“悬案”?
| 问题现象 | 传统排查方式 | 痛点 |
|---|---|---|
| 服务 CPU 5%,但响应延迟飙升 | go tool pprof -http :8080 → top → list | ❌ 需重启 + 手动采样,错过瞬时峰值 |
日志刷屏 context deadline exceeded | 猜“是不是数据库慢?”“是不是锁竞争?” | ❌ 盲人摸象,靠经验试错 |
| 压测时 Goroutine 数暴涨到 10w,但吞吐不升反降 | GODEBUG=gctrace=1 看 GC,net/http/pprof 抓 goroutine dump | ❌ dump 文件几十 MB,grep 到崩溃 |
💡 真相往往藏在细节里:
是 goroutine 全卡在sync.Mutex?
还是大量 syscall 阻塞(如 DNS 查询)?
或是 CPU 不够,导致 runnable 队列堆积?
—— Go 1.26 给了你“X 光机”。
🚀 新增 6 个 Goroutine Metrics:官方“调度状态体检表”
Go 1.26 在 runtime/metrics 中新增以下指标(单位均为 uint64 计数器):
| 指标名 | 说明 | 监控价值 |
|---|---|---|
/sched/goroutines-created:goroutines | 程序启动至今创建的 goroutine 总数 | 发现 goroutine 泄漏(持续增长但 live 不降) |
/sched/goroutines/not-in-go:goroutines | 正在 syscall / cgo 中的 goroutine 数(如 net.Dial, os.ReadFile) | 定位 I/O 瓶颈、DNS 慢查询、C 库阻塞 |
/sched/goroutines/runnable:goroutines | 就绪但未运行的 goroutine 数(等 CPU) | CPU 不足预警!> 0 持续几秒 = 饥饿 |
/sched/goroutines/running:goroutines | 当前正在 CPU 上执行的 goroutine 数 | 应 ≤ GOMAXPROCS,> 说明异常 |
/sched/goroutines/waiting:goroutines | 等待资源的 goroutine 数(锁、channel、timer) | 锁竞争、channel 积压的直接证据 |
/sched/threads/total:threads | Go 运行时当前持有的 OS 线程数 | 结合 GOMAXPROCS 看 syscalls 开销 |
🔔 注意:
- 这些是瞬时近似值(非原子快照),但足够用于趋势监控;
- 不保证
(not-in-go + runnable + running + waiting) == live goroutines(因采样时态差);- 旧指标
/sched/goroutines:goroutines(总存活数)自 Go 1.16 起已存在。
🧪 实战:5 行代码打印 Goroutine “体检报告”
// go.mod: go 1.26+
package main
import (
"fmt"
"runtime/metrics"
"time"
)
func main() {
// 启动 10 个 sleep goroutine 模拟 waiting
for i := 0; i < 10; i++ {
go func() { time.Sleep(10 * time.Second) }()
}
// 启动 2 个 busy loop 模拟 runnable(需多核)
go func() { for {} }()
go func() { for {} }()
time.Sleep(100 * time.Millisecond) // 等调度稳定
fmt.Println("=== Goroutine 状态快照 ===")
printGoroutineMetrics()
}
func printGoroutineMetrics() {
names := []string{
"/sched/goroutines:goroutines", // 总存活
"/sched/goroutines-created:goroutines", // 总创建
"/sched/goroutines/not-in-go:goroutines",
"/sched/goroutines/runnable:goroutines",
"/sched/goroutines/running:goroutines",
"/sched/goroutines/waiting:goroutines",
"/sched/threads/total:threads", // 线程数
}
samples := make([]metrics.Sample, len(names))
for i, name := range names {
samples[i] = metrics.Sample{Name: name}
}
metrics.Read(samples)
for i, s := range samples {
switch s.Value.Kind() {
case metrics.KindUint64:
fmt.Printf("%-40s : %d\n", names[i], s.Value.Uint64())
default:
fmt.Printf("%-40s : (unsupported)\n", names[i])
}
}
}
🔍 输出:
=== Goroutine 状态快照 ===
/sched/goroutines:goroutines : 15
/sched/goroutines-created:goroutines : 17
/sched/goroutines/not-in-go:goroutines : 0
/sched/goroutines/runnable:goroutines : 1 ← 1 个 goroutine 等 CPU!
/sched/goroutines/running:goroutines : 2 ← 2 个正在跑(= GOMAXPROCS)
/sched/goroutines/waiting:goroutines : 12 ← 10 sleep + 2 main? 等 channel
/sched/threads/total:threads : 6
✅ 解读:
runnable=1:说明 CPU 已饱和(2 核跑满,1 个排队)→ 需扩容或优化 CPU 密集逻辑waiting=12:10 个time.Sleep+ 2 个main等待子 goroutine(实际还有 pprof 后台 goroutine)not-in-go=0:无 syscall 阻塞 → 排除 I/O 瓶颈
⚠️ 若你在 Go < 1.26 运行,会报错
unknown metric—— 快升级!
🛠️ 5 大典型使用场景 + 排查 SOP
场景 1️⃣:延迟毛刺突增(Latency Spike)
- 现象:P99 延迟从 50ms → 800ms,但 CPU/内存正常
- 查:
/sched/goroutines/runnable是否 > 0 持续 1s+? - 对策:
- ✅ 是 → 增加 CPU 或优化 hot path(如减少锁粒度)
- ❌ 否 → 查
waiting+not-in-go
场景 2️⃣:Goroutine 泄漏(Leak)
- 现象:
/sched/goroutines(总存活)持续上升,不回落 - 查:
created - live = 已结束 goroutine是否持续增长?- 若
live↑ 但created不 ↑ → 泄漏(goroutine 卡在 waiting/not-in-go)
- 对策:
waiting高 → 检查 channel 无人读、sync.WaitGroup未 Donenot-in-go高 → 检查 DNS、DB 连接池、慢 syscall
场景 3️⃣:CPU 饥饿(Starvation)
- 现象:
GOMAXPROCS=4,但running常为 2–3,runnable波动 > 5 - 查:
runnable均值 /running均值 > 0.5? - 对策:
- ✅ 是 → 调高
GOMAXPROCS(或runtime.GOMAXPROCS(0)自动) - 用
pprof抓cpuprofile 看谁在抢 CPU
- ✅ 是 → 调高
场景 4️⃣:cgo / syscall 阻塞
- 现象:服务“假死”,
not-in-go持续高位(如 > 100) - 典型原因:
- DNS 查询慢(
net.LookupHost默认不超时) - SQLite cgo 驱动锁表
- 自定义 cgo 库无并发控制
- DNS 查询慢(
- 对策:
- 用
net.Resolver{PreferGo: true}避免 cgo DNS - 用纯 Go 驱动(如
modernc.org/sqlite) - 为 cgo 调用加超时:
ctx, cancel := context.WithTimeout(...)
- 用
场景 5️⃣:监控告警配置
- 推荐阈值(根据业务调整):
# 严重:runnable > GOMAXPROCS * 2 持续 10s rate(sched_goroutines_runnable[1m]) > 2 * sched_gomaxprocs_threads # 警告:not-in-go > total_goroutines * 0.3 sched_goroutines_not_in_go / sched_goroutines > 0.3 # 泄漏预警:created - live 增速 > 1k/s rate(sched_goroutines_created[5m]) - rate(sched_goroutines[5m]) > 1000
🌟 结语:从“猜问题”到“看数据”,只需升级 Go
Goroutine 是 Go 的灵魂,但也是最容易“失控”的部分。
Go 1.26 的这组指标,把调度器的黑盒变成了透明仪表盘——
- 当
runnable飘红,你知道该扩容; - 当
not-in-go暴涨,你秒懂是 DNS 在作祟; - 当
created - live持续增长,你提前拦截泄漏。
优秀的可观测性,不是等故障发生后去“破案”,而是让问题在发生前就“自首”。
