969 字
5 分钟
一次 p99 抖动,牵出 Go 调度器的偷任务

一个内部 RPC 服务,平均延迟 2ms,p99 每隔十几秒跳到 40ms 左右。CPU 使用率稳定在 40%,GC 暂停不到 1ms,看板上一片正常。最后把 runtime/trace 抓下来,问题出在调度器的工作窃取上。

模型先摆清楚#

Go 的程序由三类东西组成:G(goroutine)、M(内核线程)、P(处理器上下文)。G 只有在绑定到 P 之后才能运行,M 是真正执行代码的线程。每个 P 有一条本地运行队列,长度上限 256;放不下的 G 进全局队列。

当一个 P 的本地队列空了,它不会立刻休眠,而是先进入 spinning 状态,尝试从别的 P 的队列尾部偷一半任务。如果全局队列和所有本地队列都没有活,它才去 netpoll 里找可运行的 G,最后才让 M 睡眠。

这套设计的目的是让线程尽量不闲下来,代价是:G 会在 P 之间迁移,迁移意味着缓存失效,而窃取本身也要拿锁。

尾延迟是怎么被抬起来的#

把 trace 展开后能看到一条很规整的因果链:

  1. 每个请求会派生若干个小 goroutine,其中一个负责解析大 JSON,运行时间明显长于其他 goroutine。
  2. 长任务所在 P 的本地队列被它占住,队列里排着的短任务无法被执行。
  3. 空闲 P 来偷任务时,偷走的是本地队列尾部的一半。短任务被搬到另一个 P,原本在 L1/L2 里的数据全部失效。
  4. 短任务在新 P 上重新预热缓存,单个耗时从几微秒涨到几十微秒;大量短任务叠加,p99 就被抬起来。

一个容易忽略的细节是 sync.Pool。它按 P 分片,任务被偷走后,原来 P 上缓存的临时对象在新 P 上用不到,等于每次迁移都多了一次分配和一次 GC 压力。

怎么确认#

go tool trace 里看两条:Scheduler latency profile 和 Goroutine analysis。前者给出“G 从可运行到真正运行”的等待分布,后者列出运行时间最长的 goroutine。如果等待分布的长尾和慢请求的时间点对得上,基本可以确定是调度排队,而不是下游依赖。

再看 runtime/metrics 的 /sched/latencies:seconds,它把调度延迟做成了直方图,适合长期监控:

samples := []runtime.MetricSample{{Name: "/sched/latencies:seconds"}}
runtime.ReadMetrics(samples)
h := samples[0].Value.Float64Histogram()
for i, c := range h.Counts {
fmt.Printf("%.6f..%.6f: %d\n", h.Buckets[i], h.Buckets[i+1], c)
}

三个改动#

把长短任务分开#

解析大 JSON 的 goroutine 不再和请求处理共用同一个 P 的本地队列,而是丢进一个有界工作池。池的 worker 数量固定,队列长度有上限,满载时上游直接返回 503,而不是继续堆积。

type Pool struct {
jobs chan func()
wg sync.WaitGroup
}
func NewPool(workers, queue int) *Pool {
p := &Pool{jobs: make(chan func(), queue)}
for i := 0; i < workers; i++ {
p.wg.Add(1)
go func() {
defer p.wg.Done()
for job := range p.jobs {
job()
}
}()
}
return p
}
func (p *Pool) Submit(job func()) bool {
select {
case p.jobs <- job:
return true
default:
return false // 有界队列,满了就拒绝
}
}

缩小单次任务#

短任务如果只有几十微秒,窃取和迁移的成本占比就很高。把同一批小对象合并成一次处理,减少 goroutine 数量,往往比调 GOMAXPROCS 有效。

给缓存留位置#

sync.Pool 里的对象不要在跨 goroutine 传递后继续复用;按请求生命周期重新取,比让对象跟着任务迁移更便宜。

结论#

调度器的工作窃取是为了吞吐,不是为了尾延迟。当任务粒度不均、缓存局部性又重要时,窃取会把缓存打散。trace 里的调度等待直方图能直接定位这件事,修法通常是控制并发形状,而不是继续加机器。

一次 p99 抖动,牵出 Go 调度器的偷任务
/posts/go-work-stealing/
作者
fantasy-peak
发布于
2026-09-22
许可协议
CC BY-NC-SA 4.0