Go里一个简单的goroutine资源池,到底解决了什么问题"

admin 2026-10-17 09:53:03 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

Go 里一个简单的 goroutine 资源池,到底解决了什么问题

原创

go
go

Go语言教程

2026年7月22日 13:21
陕西

在小说阅读器读本章

去阅读

接口刚开始跑得挺稳,流量一上来,goroutine 数量直接从几百涨到几万。

日志里没有明显报错,CPU 也没打满,但数据库连接池开始超时:

sql: database is closed
context deadline exceeded
db pool wait_count=18432
goroutines=37641

这种问题我一般不会先去怀疑 SQL。goroutine 都堆成这样了,再快的 SQL 也架不住几万个任务同时往数据库连接池里挤。

代码通常长这样:

for _, orderID := range orderIDs {
 go syncOrder(orderID)
}

写起来很舒服,每个任务扔一个 goroutine。数据量几十条时没问题,换成几万条订单,这段代码就开始不讲武德了。

goroutine 确实比操作系统线程轻,但“轻”不等于没有成本。它要占栈空间,要被调度,任务里还会继续占数据库连接、HTTP 连接、文件句柄。真正把服务拖垮的,很多时候不是 goroutine 自己,而是它后面挂着的那些资源。

这时候需要的不是更快地创建 goroutine,而是给并发踩一脚刹车。

我平时写这种简单任务池,不会一上来搞复杂的动态扩缩容。先把三件事做对:

固定 worker 数量,任务统一进入队列,程序退出时能把 worker 收干净。

代码可以压到下面这个程度:

package taskpool

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

var ErrPoolBusy = errors.New(\"goroutine pool is busy\")

type Job func(context.Context)

type Pool struct {
 jobs   chan Job
 wg     sync.WaitGroup
 cancel context.CancelFunc
}

func New(parent context.Context, workers, queueSize int) *Pool {
 ctx, cancel := context.WithCancel(parent)

 p := &Pool{
  jobs:   make(chan Job, queueSize),
  cancel: cancel,
 }

 for workerID := 0; workerID < workers; workerID++ {
  p.wg.Add(1)
  go p.runWorker(ctx, workerID)
 }

 return p
}

func (p *Pool) runWorker(ctx context.Context, workerID int) {
 defer p.wg.Done()

 for {
  select {
  case <-ctx.Done():
   return

  case job, ok := <-p.jobs:
   if !ok {
    return
   }

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

    job(ctx)
   }()
  }
 }
}

func (p *Pool) Submit(job Job) error {
 if job == nil {
  return errors.New(\"job is nil\")
 }

 select {
 case p.jobs <- job:
  return nil
 default:
  return ErrPoolBusy
 }
}

func (p *Pool) Close() {
 p.cancel()
 p.wg.Wait()
}

这个池子没有多少花活。创建时启动固定数量的 worker,每个 worker 都阻塞在 jobs 通道上。任务来了就取一个执行,没有任务就等着,不会继续创建新的 goroutine。

这里我特意把 Submit 写成了非阻塞提交。

select {
case p.jobs <- job:
 return nil
default:
 return ErrPoolBusy
}

队列满了直接返回错误,不让调用方无限卡住。这个取舍不一定适合所有业务,但在线接口里,我更愿意明确拒绝,也不愿意把请求悄悄堆在内存里。

无限队列看起来不丢任务,实际上只是把故障往后拖。任务积压十几分钟后,数据可能早就过期了,内存倒是先吃满。

接到订单同步业务里,大概这样用:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

pool := taskpool.New(ctx, 12, 200)
defer pool.Close()

for _, id := range pendingOrderIDs {
 orderID := id

 err := pool.Submit(func(ctx context.Context) {
  if err := syncOrder(ctx, orderID); err != nil {
   log.Printf(\"sync order failed, id=%d err=%v\", orderID, err)
  }
 })

 if errors.Is(err, taskpool.ErrPoolBusy) {
  log.Printf(\"drop sync task, id=%d reason=pool_busy\", orderID)
 }
}

orderID := id 这一行别嫌多余。循环变量捕获问题虽然在新版 Go 中已经有所改善,但线上项目未必都统一版本,而且这种关键任务我不喜欢依赖别人记得版本差异。多写一行,代码意图也更清楚。

worker 数量也不能拍脑袋。

如果任务主要做计算,可以从 CPU 核数附近开始。如果任务大部分时间在等数据库或者远程接口,可以适当增加。但我更关心下游能扛多少。

数据库连接池最大只有 20 个连接,worker 开 200 个没有意义。最后还是 180 个 goroutine 堵在取连接上,监控里看着并发很高,实际全在排队。

任务池上线后,至少要盯两个值:

log.Printf(
 \"pool queue=%d capacity=%d goroutines=%d\",
 len(poolQueue),
 cap(poolQueue),
 runtime.NumGoroutine(),
)

实际项目里我会把队列长度、拒绝次数、任务耗时和执行失败数打到监控。只看 goroutine 数量不够,队列长期接近满载,说明 worker 太少、任务太慢,或者上游提交速度已经超过系统处理能力。

还有个容易被忽略的地方:任务里的 panic 必须兜住。

某个任务 panic 如果没人恢复,整个进程可能直接退出。资源池本来是想保护服务,结果一个脏数据把服务带走,那就有点难看了。所以 worker 执行任务时,至少做一层 recover,同时把任务标识和堆栈打出来。

这个简单资源池解决的并不是“复用 goroutine”这么表面的事情。

它真正限制的是系统同一时刻允许执行多少任务,并且把过载变成可观察、可处理的结果。队列满了就拒绝,任务慢了能从监控看到,服务关闭时 worker 也能退出。

至于动态扩容、任务优先级、超时回收,可以后面再加。一个连并发上限和退出流程都没处理好的池子,功能做得再全,我也不太敢往线上放。


Go里一个简单的goroutine资源池,到底解决了什么问题" 网络安全文章

Go里一个简单的goroutine资源池,到底解决了什么问题"

本文讨论了Go语言中goroutine数量激增导致数据库连接池超时的问题,并提出了一个简单的固定worker数量任务池解决方案。该方案通过固定worker数量、任务队列和非阻塞提交来限制并发,避免过载。文章强调了监控队列长度、拒绝次数和任务耗时的重要性,并提供了panic恢复机制。核心在于将过载变为可观察、可处理的结果,而非单纯复用goroutine。
MySQL连接数爆满时,运维第一时间该做什么?" 网络安全文章

MySQL连接数爆满时,运维第一时间该做什么?"

MySQL连接数爆满时切忌盲目调大参数,应先查状态定位占用连接最多的IP与库。止血须先从网关切断问题流量,再按条件安全清理长时间空闲的连接。恢复后需排查应用连接池配置是否超限,以及代码是否遗漏行关闭或事务回滚,最后才考虑调整数据库参数。
分析一个简单的goroutine资源池" 网络安全文章

分析一个简单的goroutine资源池"

针对Go无节制创建goroutine导致线上卡死的问题,本文指出需建立并发边界。作者基于channel实现轻量级资源池,强调三处关键设计:任务提交须快速失败或设超时防阻塞,worker内须recover防脏数据打挂,关闭时须先close再Wait。建议按任务特性配置worker数,通过池化约束并发以保障系统稳定。
评论:0   参与:  0