Go 1.26 即将引入一个关于运行时监控的重磅特性——Goroutine 调度指标。
这个特性看似不起眼,但对生产环境的可观测性来说至关重要,值得开发者关注和升级。
背景
说起 Go 的 runtime/metrics 包,相信做过性能调优的同学都不陌生。这个包从 Go 1.16 开始提供各种运行时的统计数据,例如内存分配、GC 情况等。
然而,一个令人无奈的事实是,这么多年过去了,关于 Goroutine 调度器的 metrics 指标却一直是空白的。

一些痛点
我们来看看实际开发中可能遇到的痛点。
假设线上服务突然变慢,打开 Grafana 监控面板却发现:
- CPU 使用率正常。
- 内存占用也无问题。
- GC 次数和耗时均在合理范围内。
问题究竟出在哪里?
很可能是 Goroutine 调度层面出了问题,从而引发性能异常。但过去你无法直接观测到以下关键信息:
- 程序到底创建了多少个 Goroutine?
- 有多少 Goroutine 在排队等待执行?
- 有没有 Goroutine 卡在系统调用或 cgo 调用中出不来?
- 运行时创建的线程数是否异常增长?
“新” 提案
这种无法观测的状态相当尴尬。实际上,早在 2016 年,就有开发者在 GitHub issue #15490 中提出了这个需求。

提案的描述很直接:“MemStats 提供了一种监控内存分配和垃圾回收的方式,我们也需要一个类似的工具来监控调度器。”
简单来说,当时的诉求就是需要能观测到:
- 程序启动以来总共创建了多少 Goroutine。
- 当前存活的 Goroutine 数量。
- 运行时启动了多少线程。
- Goroutine 从就绪状态到实际运行的延迟情况。
这个提案一提就是近十年,如今 Go 核心团队终于决定在 Go 1.26 版本中将其落地实现。
新的 Goroutine 调度指标
那么,Go 1.26 具体新增了哪些调度相关的指标呢?
在 runtime/metrics 包中,新增了以下 6 个关于 Goroutine 和线程的核心指标:
/sched/goroutines-created:goroutines:从程序启动到现在总共创建的 Goroutine 数量。
/sched/goroutines/not-in-go:goroutines:当前处于系统调用或 cgo 调用中的 Goroutine 数量(近似值)。
/sched/goroutines/runnable:goroutines:当前已就绪但尚未被调度执行的 Goroutine 数量(近似值)。
/sched/goroutines/running:goroutines:当前正在执行的 Goroutine 数量(近似值)。
/sched/goroutines/waiting:goroutines:当前正在等待某个资源(如 I/O、channel、锁)的 Goroutine 数量(近似值)。
/sched/threads/total:threads:当前 Go runtime 拥有的活跃线程总数。
这里有几点需要特别注意:
- 近似值说明:按状态统计的 Goroutine 数量(
not-in-go, runnable, running, waiting)都被标记为“近似值”。这是因为 Goroutine 的状态切换极其频繁,runtime 不可能为了获取一个绝对精确的瞬时值而施加全局锁,那会严重损害性能。对于监控和趋势分析而言,这些近似值已经足够有效。
- 总和不一定等于总数:上述分类状态的数量相加,不一定等于当前存活的 Goroutine 总数(即
/sched/goroutines:goroutines 指标的值)。这同样是由于采样时机和并发性导致。
- 数据类型:所有新增指标均使用
uint64 类型的计数器。
使用示例
当 Go 1.26 发布后,我们可以这样使用这些新指标。假设我们有一个简单的程序,启动了若干 Goroutine 来模拟不同的工作任务:
package main
import (
"fmt"
"runtime/metrics"
"time"
)
func main() {
// 启动一些 Goroutine 模拟真实场景
go work()
// 等待 100ms 让 Goroutine 运行一段时间
time.Sleep(100 * time.Millisecond)
// 打印 Goroutine 相关指标
fmt.Println("Goroutine 调度指标:")
printMetric("/sched/goroutines-created:goroutines", "累计创建")
printMetric("/sched/goroutines:goroutines", "当前存活")
printMetric("/sched/goroutines/not-in-go:goroutines", "系统调用/CGO")
printMetric("/sched/goroutines/runnable:goroutines", "等待执行")
printMetric("/sched/goroutines/running:goroutines", "正在执行")
printMetric("/sched/goroutines/waiting:goroutines", "等待资源")
// 打印线程相关指标
fmt.Println("\n线程指标:")
printMetric("/sched/gomaxprocs:threads", "最大 P 数量")
printMetric("/sched/threads/total:threads", "当前线程数")
}
func printMetric(name string, descr string) {
sample := []metrics.Sample{{Name: name}}
metrics.Read(sample)
// 注意:此处为演示简化了错误处理
// 生产代码中应检查 sample[0].Value.Kind()
fmt.Printf(" %s: %v\n", descr, sample[0].Value.Uint64())
}
func work() {
// 此处省略具体的工作逻辑,例如发起网络请求、执行计算任务等
}
运行上述程序,输出可能类似于:
Goroutine 调度指标:
累计创建: 52
当前存活: 12
系统调用/CGO: 0
等待执行: 0
正在执行: 4
等待资源: 8
线程指标:
最大 P 数量: 8
当前线程数: 4
从输出可以清晰地看到:程序累计创建了 52 个 Goroutine,当前存活 12 个。其中,4 个正在执行,8 个在等待资源(可能是在等待 channel、锁或 I/O 操作),没有 Goroutine 卡在系统调用中,也没有 Goroutine 在就绪队列中排队。
读取这些新指标的方式与使用现有的 runtime/metrics 包完全一致,都是通过 metrics.Read 函数来获取,因此在集成到现有监控系统中时基本没有额外的学习成本。
总结
这个从 2016 年提出的提案,社区呼声一直很高。Go 核心团队成员 Michael Knyszek 最终在 2024 年底推动了这个特性的实现,相关的代码变更已提交。

尽管新增的 API 非常简单,但其对生产环境可观测性的意义却非常重大。有了这些指标,我们终于能够直接洞察 Go 程序内部 Goroutine 的调度状态,而不再需要基于间接现象进行猜测和排查。
等 Go 1.26 正式发布后,建议开发者可以第一时间将这些新指标集成到监控系统中,相信它们对于定位和解决生产环境下的性能瓶颈、资源竞争等问题会有不小的帮助。如果你想了解更多关于 Go 调度模型或其他底层原理,欢迎到 云栈社区 的 Go 技术板块进行深入探讨和交流。