
🧠 记忆锚点:并发必须有界,队列满要背压;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
}生产级参数配置:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| workers | API RPM / 60 | 例如 RPM=600,workers=10 |
| queueSize | workers × 10 | 缓冲队列,防止瞬间流量 |
| 超时 | 30s(生成)/ 5s(排队) | 分别控制 LLM 调用和入队等待 |
| 重试 | 指数退避,最多 3 次 | 429/503 才重试,4xx 不重试 |
三大踩坑点:
goroutine 泄漏
- 问题:
ctx已取消,但 goroutine 还在等 LLM 响应 - 解法:每次调用都传入
ctx,LLM SDK 感知取消
- 问题:
队列满时的背压策略
- 错误做法:直接 block,调用方卡住
- 正确做法:
select + default立即返回 503,上游触发限流
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%。"