Skip to content
🔗 分享本题
查看我的学习进度 →

Go 有界任务队列、固定 Worker、全局限流、取消传播和优雅关闭图

🧠 记忆锚点:并发必须有界,队列满要背压;context 取消要一路传到上游,关闭时先停接收、再排空、等待并只关闭一次。

💡 答案要点

为什么 LLM 请求需要 Worker Pool?

问题:LLM API 有并发限制(如 OpenAI RPM/TPM),
     朴素 goroutine-per-request 会导致:
     - 瞬间打满 API 限额 → 429 Too Many Requests
     - 内存无边界增长 → OOM
     - 无法做背压控制 → 上游雪崩

Worker Pool 核心实现(Go):

展开 Go 代码示例(53 行)
go
type LLMWorkerPool struct {
    taskCh  chan Task       // 任务队列(有缓冲 channel)
    sem     chan struct{}   // 信号量控制并发数
    wg      sync.WaitGroup
}

type Task struct {
    Prompt string
    Result chan<- string
    Ctx    context.Context
}

func NewLLMWorkerPool(workers int, queueSize int) *LLMWorkerPool {
    pool := &LLMWorkerPool{
        taskCh: make(chan Task, queueSize),
        sem:    make(chan struct{}, workers),
    }
    pool.start(workers)
    return pool
}

func (p *LLMWorkerPool) start(workers int) {
    for i := 0; i < workers; i++ {
        p.wg.Add(1)
        go func() {
            defer p.wg.Done()
            for task := range p.taskCh {
                p.sem <- struct{}{}        // 获取令牌
                go func(t Task) {
                    defer func() { <-p.sem }() // 释放令牌
                    result, err := callLLM(t.Ctx, t.Prompt)
                    if err != nil {
                        t.Result <- ""
                        return
                    }
                    t.Result <- result
                }(task)
            }
        }()
    }
}

func (p *LLMWorkerPool) Submit(ctx context.Context, prompt string) <-chan string {
    resultCh := make(chan string, 1)
    select {
    case p.taskCh <- Task{Prompt: prompt, Result: resultCh, Ctx: ctx}:
        // 成功入队
    default:
        // 队列满了,直接返回错误
        resultCh <- "queue full, please retry"
    }
    return resultCh
}

生产级参数配置:

参数推荐值说明
workersAPI RPM / 60例如 RPM=600,workers=10
queueSizeworkers × 10缓冲队列,防止瞬间流量
超时30s(生成)/ 5s(排队)分别控制 LLM 调用和入队等待
重试指数退避,最多 3 次429/503 才重试,4xx 不重试

三大踩坑点:

  1. goroutine 泄漏

    • 问题:ctx 已取消,但 goroutine 还在等 LLM 响应
    • 解法:每次调用都传入 ctx,LLM SDK 感知取消
  2. 队列满时的背压策略

    • 错误做法:直接 block,调用方卡住
    • 正确做法:select + default 立即返回 503,上游触发限流
  3. TPM 超限(Token Per Minute)

    • 问题:RPM 没超,但 prompt 太长导致 TPM 超限
    • 解法:入队前估算 token 数,超限提前拒绝

面试话术:

"Go 处理高并发 LLM 请求用 Worker Pool,核心是三层控制:有缓冲 channel 做任务队列,信号量控制 Worker 并发数,context 做超时取消。生产上我踩过两个坑:一是 goroutine 泄漏,ctx 取消后 LLM 调用还没结束,要确保 SDK 支持 context;二是 TPM 超限比 RPM 更难控,prompt 长的请求需要在入队前就估算 token,超预算直接降级用小模型或拒绝。优化后 P99 延迟从不稳定降到 180ms 以内,429 错误率从 8% 降到 0.1%。"