Goroutine太多导致OOM?是时候给你的Go程序加上协程池了"

admin 2026-10-20 12:11:34 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

Goroutine 太多导致 OOM?是时候给你的 Go 程序加上协程池了

原创

go
go

Go语言教程

2026年6月28日 13:29
陕西

在小说阅读器读本章

去阅读

线上机器被 OOMKilled,第一眼别急着怀疑 Go 的 GC。

我一般先看两个东西:内存曲线是不是一路往上拱,goroutine 数是不是也跟着涨。要是这俩一起涨,业务代码里八成有这种东西:

for _, orderID := range orderIDs {
 go func(id int64) {
  err := syncOrderToWarehouse(ctx, id)
  if err != nil {
   log.Printf(\"sync order failed, order_id=%d err=%v\", id, err)
  }
 }(orderID)
}

这段代码看着挺 Go。

来一个订单,起一个 goroutine;来一批订单,起一批 goroutine。开发环境跑 100 条没事,测试环境跑 1000 条也没事,到了线上导入几十万条订单,机器直接开始喘。

日志大概会长这样:

runtime: goroutine stack exceeds limit
k8s: container OOMKilled
goroutines=182394 heap=3.8GB next_gc=4.6GB

这时候再去调 GC 参数,基本属于绕远路。

goroutine 确实轻,但不是不要钱。每个 goroutine 都有栈,栈会扩容;阻塞在 HTTP、DB、Redis、MQ 上的 goroutine,也都要被 runtime 管着。数量一多,内存顶上去,调度也开始乱。更烦的是,下游接口本来就慢,你还一口气打过去几万并发,下游没扛住,你这边也跟着堆死。

我更愿意把这种逻辑改成协程池。不是为了显得高级,就是把“无限创建”改成“有上限排队”。

下面这段是我现场会写的版本,不追求框架感,够用、能停、能看队列压力。

package worker

import (
 \"context\"
 \"errors\"
 \"log\"
 \"sync\"
 \"time\"
)

type OrderJob struct {
 OrderID int64
 Trace  string
}

type Pool struct {
 jobs chan OrderJob
 wg   sync.WaitGroup
}

func NewPool(workerNum int, queueSize int, handle func(context.Context, OrderJob) error) *Pool {
 p := &Pool{
  jobs: make(chan OrderJob, queueSize),
 }

 for i := 0; i < workerNum; i++ {
  workerID := i + 1

  p.wg.Add(1)
  go func() {
   defer p.wg.Done()

   for job := range p.jobs {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    err := handle(ctx, job)
    cancel()

    if err != nil {
     log.Printf(
      \"order sync failed worker=%d order_id=%d trace=%s err=%v\",
      workerID, job.OrderID, job.Trace, err,
     )
    }
   }
  }()
 }

 return p
}

func (p *Pool) Submit(ctx context.Context, job OrderJob) error {
 select {
 case p.jobs <- job:
  return nil
 case <-ctx.Done():
  return errors.New(\"submit order job timeout\")
 }
}

func (p *Pool) Stop() {
 close(p.jobs)
 p.wg.Wait()
}

用的时候别一股脑塞。入口这里也要有超时,不然队列满了,请求线程全卡住,最后还是换个姿势把自己拖死。

func SyncOrders(ctx context.Context, ids []int64, trace string) {
 pool := NewPool(32, 2000, func(ctx context.Context, job OrderJob) error {
  return syncOrderToWarehouse(ctx, job.OrderID)
 })
 defer pool.Stop()

 for _, id := range ids {
  submitCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
  err := pool.Submit(submitCtx, OrderJob{
   OrderID: id,
   Trace:   trace,
  })
  cancel()

  if err != nil {
   log.Printf(\"drop order sync job order_id=%d trace=%s err=%v\", id, trace, err)
   continue
  }
 }
}

这地方我不太喜欢写成“有多少任务开多少 goroutine”。因为线上不是只跑你这一个功能。一个批量同步占满 CPU、内存、连接池,旁边的支付回调、库存扣减、订单查询全会受影响。

协程池真正挡住的是三件事。

第一,挡住 goroutine 数量。比如 workerNum 是 32,那同一时刻真正干活的就是 32 个,不会突然冒出几万个。

第二,挡住下游压力。DB 连接池就 50 个,你开 5000 个 goroutine 也没意义,最后都是排队抢连接。还不如在自己进程里老老实实排队。

第三,挡住内存峰值。任务先进 channel,队列满了就超时或者丢弃,至少系统还活着。系统活着,后面才有补偿、重试、人工处理的机会。

worker 数怎么配?

这个别背公式。CPU 密集型,别超过 CPU 核数太多;IO 密集型,可以放大一点,但要看下游连接池和接口耗时。比如同步订单要访问仓库接口,接口 p95 已经几百毫秒,那 worker 开太大,只会把对方打慢。

我一般还会加一条监控,至少把队列长度打出来:

func (p *Pool) QueueLen() int {
 return len(p.jobs)
}

然后日志里定时打一行:

log.Printf(\"order_sync_pool queue_len=%d\", pool.QueueLen())

别嫌土。真出问题的时候,这种日志比一堆漂亮封装管用。

协程池也不是银弹。

任务必须能接受排队,不能是用户请求里必须立刻完成的强一致动作。队列满了之后怎么处理,也要提前定:是返回繁忙,是落库补偿,还是直接丢弃低优先级任务。这个不定清楚,协程池只是把 OOM 改成了请求超时。

还有一点,panic 要兜住。线上 worker 直接 panic 掉,池子少一个工人,时间长了也会慢慢死。

defer func() {
 if r := recover(); r != nil {
  log.Printf(\"order sync worker panic worker=%d err=%v\", workerID, r)
 }
}()

我排这种问题,一般顺序很固定:先看 goroutine 数,再看堆内存,再看阻塞点,最后才看 GC。要是发现 goroutine 大量卡在 HTTP 请求、DB 查询、Redis 读写上,先别优化算法,先把并发上限收回来。

Go 给了我们很便宜的 goroutine,但便宜不是免费。

线上程序最怕的不是慢一点,是没有边界。协程池干的就是这件事:给并发画一条线,超过这条线就排队、超时、降级,别让它把整个进程拖进 OOM。


AppleSilicon本地LLM推理引擎横向评测" 网络安全文章

AppleSilicon本地LLM推理引擎横向评测"

本文对AppleSilicon平台4个主流本地LLM推理引擎进行横向评测,涵盖吞吐量、延迟、内存占用、OOM概率和维护活跃度五个维度。核心发现omlx0.4.1+dflash在长上下文场景系统吞吐量领先73%,dflash需作为omlx子能力使用,4-bit量化不可替代。建议短档用rapid-mlx,中长档优先选omlx+dflash配置pagedSSD。
使用AI白盒审计NET源码拿下SQL注入案例&部分感想" 网络安全文章

使用AI白盒审计NET源码拿下SQL注入案例&部分感想"

本文分享使用AI工具对.NET源码进行白盒审计并发现SQL注入漏洞的实战案例,作者通过DeepSeekV4Pro反编译dll文件后,利用特定提示词进行代码审计、鉴权分析和框架路由分析,成功发现符合CNVD中危及以上标准的漏洞,并提供了验证过程与HTTP请求数据包作为POC。文章还总结了AI在黑盒与白盒漏洞挖掘中的应用效率,强调AI需与人工边界控制结合,并附带了代码审计培训课程宣传内容。
评论:0   参与:  0