微服务中直接用 go func() {}() 易引发 OOM 和延迟毛刺,根本原因是未控制协程数量、生命周期和 panic;需用带缓冲通道、固定 worker 数、recover 保护及显式关闭的协程池,并配合拒绝策略、上下文透传与指标监控。

微服务中直接用 go func() {}() 处理请求或后台任务,90% 的线上 OOM 和延迟毛刺都源于此——不是协程太重,而是没控住数量、没管住生命周期、没兜住 panic。
为什么微服务特别需要协程池
微服务天然具备高并发、多调用链、强依赖外部资源(DB/HTTP/Redis)的特点。一个未加限制的 go doSomething() 可能瞬间触发:
- 下游服务连接池被打爆(比如同时发起 5000 个 HTTP 请求,远超
http.Transport.MaxIdleConnsPerHost) - 数据库连接耗尽,报错
sql: connection refused或too many connections - goroutine 数飙升至 10k+,GC 频繁 STW,P99 延迟从 50ms 拉到 2s+
- panic 在匿名协程里丢失,日志里只看到
fatal error: all goroutines are asleep - deadlock却找不到源头
用 chan func() 实现最小可行协程池
不依赖第三方库,50 行内搞定带关闭、统计、错误捕获的池。核心是:固定 worker 数 + 缓冲通道 + 显式生命周期控制。
关键点:
-
tasks通道必须有缓冲,否则Submit会阻塞——尤其在 burst 流量下,缓冲大小建议设为 worker 数的 2–5 倍 - 每个 worker 必须用
recover()捕获 panic,否则一次崩溃就 kill 整个 goroutine -
Shutdown()要先 close channel,再wg.Wait(),顺序反了会导致死锁 - 别把 context 传进 task 闭包里——task 执行时 context 可能已 cancel,应在 Submit 前判断
type Pool struct {
tasks chan func()
quit chan struct{}
wg sync.WaitGroup
}
<p>func NewPool(size int) <em>Pool {
p := &Pool{
tasks: make(chan func(), size</em>3),
quit: make(chan struct{}),
}
for i := 0; i < size; i++ {
p.wg.Add(1)
go p.worker()
}
return p
}</p><p>func (p *Pool) worker() {
defer p.wg.Done()
for {
select {
case task := <-p.tasks:
defer func() {
if r := recover(); r != nil {
log.Printf("worker panic: %v", r)
}
}()
task()
case <-p.quit:
return
}
}
}</p><p>func (p *Pool) Submit(task func()) {
select {
case p.tasks <- task:
default:
log.Warn("task dropped: pool full")
}
}</p><p>func (p *Pool) Shutdown() {
close(p.tasks)
close(p.quit)
p.wg.Wait()
}微服务场景下的关键增强点
纯 chan func() 在微服务里不够用,必须补三块:
-
拒绝策略:当队列满时,
default分支不能只打日志,要配合熔断器(如gobreaker)降级,或返回http.StatusTooManyRequests -
上下文透传:任务函数签名改为
func(context.Context),Submit 时绑定 request-scoped context,避免 goroutine 泄漏 -
指标暴露:用
prometheus.GaugeVec暴露pool_queue_length和pool_active_workers,告警阈值设为队列长度 > 缓冲区 80%
注意:不要在 worker 内部用 context.WithTimeout 包一层——超时应由调用方控制,worker 只负责执行;否则 timeout 时间不一致,监控失真。
别踩 sync.Pool 这个坑
sync.Pool 不是协程池,它连“池”字都名不副实:
- 它只缓存对象,不管理 goroutine 生命周期——你往里 Put 一个
func(),Get 出来的可能是任意旧闭包,根本不知道谁在跑 - GC 会清理空闲超过 5 分钟的 Pool 对象,worker 突然消失,任务直接丢弃
- 没有队列,无法实现“提交 → 等待完成”,和微服务的 RPC 场景完全不匹配
真正要用协程池的地方,比如订单异步通知、日志批量落盘、定时补偿任务——这些都需要可预期的并发数、可追踪的执行结果、可中断的生命周期。而 sync.Pool 适合的是 []byte、bytes.Buffer 这类临时缓冲,别混用。
协程池最难的不是代码,是确定那个 magic number:worker 数。它既不能等于 CPU 核数(I/O 密集型会饿死),也不能拍脑袋设 100(可能压垮下游)。唯一靠谱的方式是:用真实流量压测,观察 DB 连接数、HTTP client idle conn、P99 延迟拐点,再倒推回来。

















