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 补上了这层运行时视角。排查时最有用的做法,是把 runnable、waiting、running、GOMAXPROCS、线程总量和创建总量放在同一组快照里看。
runnable高更像“准备运行但还没轮到”,waiting高更像“在等资源”,threads/total反映运行时拥有的线程;它们都是近似或不同时间属性的信号,不能用任意一个数字直接判定故障。
runnable、waiting、running是 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。

用 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() 列表暴露到监控系统。

把瞬时状态和累计计数放进同一张判断表
单次快照只适合回答“现在大概发生什么”。真正排查要看相邻两次采样。下面这张表是一个起点,阈值不要照抄到所有服务,先看自己的基线和采样间隔。
| 观察组合 | 更接近的解释 | 下一项检查 |
|---|---|---|
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 或同步等待;需要结合持续趋势、调用栈、取消路径和资源指标确认。
-
143 收藏
-
497 收藏
-
402 收藏
-
201 收藏
-
481 收藏
-
331 收藏
-
240 收藏
-
279 收藏
-
101 收藏
-
338 收藏
-
185 收藏
-
301 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习