直接用go启动协程会导致微服务OOM和下游超时,因goroutine失控且未管控资源;应使用ants.NewPool限制并发,配合wg和错误处理,并注意协程池生命周期管理。

为什么直接用 go 启动协程在微服务里会崩
微服务一压测就 OOM、pprof 显示 goroutine 数长期卡在 10k+、下游 DB 或 API 频繁报 connection refused 或 context.DeadlineExceeded——这些不是业务逻辑错,是并发失控的典型症状。每个 goroutine 占 2KB+ 栈内存,1 万并发就是 20MB+;更致命的是,下游资源(如 MySQL 连接池上限 100、HTTP 客户端默认 MaxIdleConnsPerHost=100)根本没被你代码感知,全靠运气扛。
用 ants.NewPool 控制 HTTP 并行调用最简写法
别在 handler 里写 go callA() + go callB(),改用池提交:
func handler(w http.ResponseWriter, r *http.Request) {
pool, _ := ants.NewPool(20) // 最多 20 个并发执行
defer pool.Release() // 测试必须加,长期运行可省略
<pre class='brush:php;toolbar:false;'>var wg sync.WaitGroup
results := make([]string, 2)
for i, fn := range []func() string{callA, callB} {
wg.Add(1)
idx := i
_ = pool.Submit(func() {
defer wg.Done()
results[idx] = fn() // 实际含 timeout 的 http.Get
})
}
wg.Wait()
json.NewEncoder(w).Encode(results)}
-
ants.NewPool(20)的 20 是最大并发数,不是预热数;空闲协程 60 秒后自动回收 -
pool.Submit()返回error,满载时返回ants.ErrPoolOverload,必须判断并降级(比如 fallback 到串行或返回缓存) - 任务函数内部要自己
recover(),否则 panic 会让 worker 消失,池容量悄悄缩水
自己手写协程池时三个必踩的坑
不用第三方库时,常见翻车点不是逻辑错,而是生命周期管理漏掉:
立即学习“go语言免费学习笔记(深入)”;
- 用
chan struct{}做信号量,但忘了在每个任务结束时归还,导致后续任务永久阻塞 - worker 用
for task := range jobs读任务,但没加if !ok { break }判断 channel 关闭状态,关 channel 后 worker 不退出,无法做 cleanup -
sync.WaitGroup忘记在pool.Submit前调用wg.Add(1),导致wg.Wait()提前返回,任务还没跑完就响应了
http.Transport 级并发控制比协程池更直接
如果目标只是限流某个外部 API,协程池是“绕路”:它控的是 Go 层的执行并发,而真正打穿下游的是 TCP 连接数。直接配 http.Transport 更准:
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
},
}这能确保最多 10 个空闲连接,配合 context.WithTimeout 控制单次请求,比在上层堆协程池更底层、更可靠。真要混用,记得协程池并发数 ≤ Transport 连接上限,否则照样被打拒。
真正容易被忽略的是:协程池只管“谁来跑”,不管“跑多久”;一个没设 timeout 的 HTTP 请求卡死,就会一直占着 slot,整个池慢慢被耗尽。


















