登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go 1.26 runtime/metrics 调度指标怎么选:runnable、waiting 与线程数的诊断边界

来源:17golang原创

时间:2026-09-04 01:32:46 405浏览 收藏

服务偶发变慢时,只看 runtime.NumGoroutine() 往往不够:它告诉你活着多少个 goroutine,却不能说明它们是在排队、等待 I/O,还是正在占用执行额度。Go 1.26 给 runtime/metrics 补上了这层运行时视角。排查时最有用的做法,是把 runnablewaitingrunningGOMAXPROCS、线程总量和创建总量放在同一组快照里看。

runnable 高更像“准备运行但还没轮到”,waiting 高更像“在等资源”,threads/total 反映运行时拥有的线程;它们都是近似或不同时间属性的信号,不能用任意一个数字直接判定故障。

要点速览
  • runnablewaitingrunning 是 goroutine 当前状态的近似计数。
  • /sched/gomaxprocs:threads 是同时执行用户级 Go 代码的额度,/sched/threads/total:threads 是运行时拥有的线程数。
  • /sched/goroutines-created:goroutines 是进程启动以来的累计创建量,适合看创建速度,不适合当作当前存量。

先把 Go 1.26 的调度指标分成三组

先记住三个维度。第一组是状态:/sched/goroutines/runnable:goroutines 表示已经准备执行但尚未执行的 goroutine,/sched/goroutines/waiting:goroutines 表示等待 I/O 或同步原语的 goroutine,/sched/goroutines/running:goroutines 表示正在执行的 goroutine。它们都是 approximate count,而且官方明确提醒它们不保证相加后等于全部 goroutine。

第二组是执行资源:/sched/gomaxprocs:threads 表示同时执行用户级 Go 代码的额度;/sched/threads/total:threads 则是当前由 Go runtime 拥有的存活线程数。两者名字都带 threads,但一个是并行执行上限,一个是线程存量,不能混读。

第三组是历史计数:/sched/goroutines-created:goroutines 只从进程启动开始累计创建量。它持续增长本身不等于泄漏,必须结合采样间隔计算增长速度,再对照 live goroutines。

Go 1.26 runtime metrics 中采样入口、goroutine 状态、执行额度与累计创建量的静态关系
图1:查看采样入口与三类调度指标的边界,区分当前状态、执行额度和进程启动以来的累计创建量。

用 runtime/metrics.Read 读取一组可诊断样本

runtime/metrics.Read 接收一组预先填好名称的 Sample。生产代码不要每次拼接指标名,也不要把不存在的指标当成零值;固定名称后检查 Value.Kind(),升级 Go 版本时更容易发现清单变化。

package main

import (
    "fmt"
    "runtime/metrics"
)

func main() {
    samples := []metrics.Sample{
        {Name: "/sched/goroutines/runnable:goroutines"},
        {Name: "/sched/goroutines/waiting:goroutines"},
        {Name: "/sched/goroutines/running:goroutines"},
        {Name: "/sched/gomaxprocs:threads"},
        {Name: "/sched/threads/total:threads"},
        {Name: "/sched/goroutines-created:goroutines"},
    }
    metrics.Read(samples)
    for _, sample := range samples {
        if sample.Value.Kind() != metrics.KindUint64 {
            fmt.Printf("%s: kind=%v\n", sample.Name, sample.Value.Kind())
            continue
        }
        fmt.Printf("%s = %d\n", sample.Name, sample.Value.Uint64())
    }
}

验证点很简单:输出名应与代码中的六个名称逐一对应,值类型应为 uint64。这段采样本身没有业务侵入,也不需要把整个 metrics.All() 列表暴露到监控系统。

Go runtime metrics Read 采样后将 runnable、waiting、running 与线程资源放入诊断判断边界
图2:对照当前状态、执行资源与累计计数三个分组,判断下一步是看排队、资源等待,还是 goroutine 创建速度。

把瞬时状态和累计计数放进同一张判断表

单次快照只适合回答“现在大概发生什么”。真正排查要看相邻两次采样。下面这张表是一个起点,阈值不要照抄到所有服务,先看自己的基线和采样间隔。

观察组合更接近的解释下一项检查
runnable 持续上升,running 接近 GOMAXPROCS可运行任务在排队,执行额度可能吃满看 CPU、调度延迟和热点调用
waiting 高,runnable大量 goroutine 在等 I/O 或同步资源看阻塞/互斥锁/网络调用的 pprof
threads/total 上升,GOMAXPROCS 不变线程存量变化,不等于并行额度变化结合 cgo、系统调用和线程画像判断
goroutines-created 增长很快,live goroutines 也长期抬高创建速度与回收速度可能失衡检查 goroutine 生命周期、取消和退出路径

这里有一个常见误区:waiting 高不一定是问题,连接池空闲等待、定时器和消费者阻塞都可能是正常状态;反过来,runnable 短时尖峰也可能只是流量突发。只有趋势与业务延迟、CPU 或资源耗尽同时出现,才值得升级为事故线索。

用连续采样确认趋势再决定排查方向

建议以 5 秒或 10 秒为间隔保存快照,至少保留一段稳定时段作为基线。每条记录同时写入进程启动时间、采样时间、服务实例和 Go 版本;不要只上报一个聚合后的 goroutine 数。对于 goroutines-created,用相邻样本差值除以时间间隔得到创建速率,重启后要重新标记计数归零。

runnable 与端到端延迟同步上扬,优先继续看调度延迟和 CPU;当 waiting 与数据库连接等待、锁等待一起抬升,优先看对应资源;当 live goroutines 上升而创建速率长期偏高,才把生命周期检查提到前面。若指标彼此矛盾,先保留“现象”结论,转到 pprof 或 execution trace 获取调用栈和阻塞原因。

常见问题

runtime/metrics 能替代 runtime.NumGoroutine 吗?

不能。live goroutines 是总量视角,调度指标是状态和资源视角,两者应该一起采集。

threads/total 为什么可能大于 GOMAXPROCS?

因为 GOMAXPROCS 是同时执行用户级 Go 代码的额度,而 threads/total 是运行时拥有的线程存量,系统调用、cgo 等场景会让两者承担不同职责。

只看到 waiting 很高,能直接判定 goroutine 泄漏吗?

不能。waiting 可能来自正常 I/O 或同步等待;需要结合持续趋势、调用栈、取消路径和资源指标确认。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>