为什么会积压

很多服务写了 worker pool,但上线后仍然:

  • 高峰时队列暴涨
  • 平均延迟还行,P99 很差
  • CPU 没打满却开始超时

这通常是“入队速率 > 出队速率”的背压问题。

基础模型

type Job struct {
    ID string
}

func startPool(ctx context.Context, n int, jobs <-chan Job) {
    for i := 0; i < n; i++ {
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return
                case job, ok := <-jobs:
                    if !ok {
                        return
                    }
                    handle(job)
                }
            }
        }()
    }
}

结构不复杂,难点在参数选择。

三个观测指标

  • 吞吐:每秒处理多少请求
  • 延迟:P50/P95/P99
  • 队列深度:channel 长度趋势

如果队列长期接近上限,说明处理能力不足或外部依赖抖动。

调优顺序

  1. 固定业务流量,先找单 worker 处理能力
  2. 逐步增加 worker,观察 P99 与 CPU 变化
  3. 达到拐点后停止扩容,避免锁竞争和上下文切换过量
  4. 设置拒绝策略,不让队列无限增长

一个实用策略

  • 快速失败:队列满时直接返回可重试错误
  • 分级队列:高优先级任务单独通道
  • 限时执行:每个任务绑定 context 超时

总结

worker pool 不是“越大越好”。并发数本质是资源预算,必须和外部依赖能力、延迟目标一起设计。