直接起Goroutine易致OOM,因并发突增时Goroutine数量线性上涨、内存飙升、GC压力剧增;Worker Pool通过固定数量Worker从带缓冲通道取任务执行,实现并发可控。

为什么直接起 Goroutine 容易导致 OOM
Gin 默认每个请求都在独立 Goroutine 中执行,但业务逻辑若包含耗时操作(比如调用外部 HTTP 服务、数据库慢查询、同步计算),会阻塞该 Goroutine。当并发突增时,Goroutine 数量线性上涨,内存占用飙升,runtime.GC 压力剧增,最终触发 out of memory 错误。
用 Worker Pool 控制并发上限的实操方式
核心是把“处理任务”从请求 Goroutine 中剥离,改由固定数量的 Worker 协程从通道取任务执行:
- 定义一个带缓冲的
chan *Task作为任务队列,容量建议设为预期峰值 QPS × 平均处理耗时(单位秒) - 启动固定数量(如
runtime.NumCPU()或 8~16)的 Worker,循环select从通道取任务 - 在 Gin Handler 中只做入队操作:
taskCh ,立刻返回 - 注意必须用
c.Copy(),原始*gin.Context不可跨 Goroutine 使用
示例关键片段:
var taskCh = make(chan *Task, 1000)
<p>type Task struct {
Ctx *gin.Context
Data interface{}
}</p><p>func startWorkers(n int) {
for i := 0; i </p>
sync.Pool 和 Worker Pool 的配合要点
Worker Pool 解决的是 Goroutine 数量失控,sync.Pool 解决的是单次任务中临时对象分配过多。二者常组合使用:
- 在 Worker 内部,用
bufferPool.Get().(*bytes.Buffer)复用序列化缓冲区 - 避免在 Worker 中 new 大量结构体;把高频复用对象(如
Response、sql.Rows封装体)放进池子 - 归还必须在 Worker 函数末尾执行:
defer bufferPool.Put(buf),不能放在异步 goroutine 里 - 池子本身无锁,但
New函数若含初始化开销(如json.NewEncoder),需确保轻量
容易被忽略的阻塞点:通道写入与超时控制
任务通道满时,taskCh 会永久阻塞 Handler,导致整个 Gin 请求协程卡住。必须加保护:
- 用带超时的
select入队:select { case taskCh - 或用带缓冲的通道 +
len(taskCh)检查水位,提前拒绝 - Worker 内部的耗时操作必须有上下文超时:
ctx, cancel := context.WithTimeout(task.Ctx.Request.Context(), 5*time.Second) - 别忘了在 Worker 中调用
cancel(),否则可能泄漏 context
真正压测时,瓶颈往往不在 CPU,而在未设限的通道写入和缺失的 context 超时——这两处不处理,协程池就只是把 OOM 延后几秒而已。











