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

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

cover_image

分析一个简单的goroutine资源池

原创

go
go

Go语言教程

2026年7月19日 13:20
陕西

在小说阅读器读本章

去阅读

线上导入任务卡住,接口没报错,CPU 也没打满,日志停在一半。

这种问题我一般不先翻业务代码,先看 goroutine。

一看就很扎眼:

goroutines: 18432
heap: 910MB
last log: submit bill task, file=202606.csv, row=43891

这种数量基本不用猜,八成是哪里把 goroutine 当免费线程用了。

Go 写并发太顺手,顺手到容易出事。很多人一看到批量处理,就这么写:

for _, row := range rows {
    go syncBill(row)
}

开发环境跑 100 行数据,没问题。线上一批 10 万行,直接把下游接口、数据库连接池、机器内存一起拖下水。

goroutine 便宜,不等于不要钱。

我更愿意把它当成“要管理的资源”。既然是资源,就得有上限,有队列,有退出,有兜底。最简单的办法,就是搞一个 goroutine 池。

别一上来就找复杂框架,很多场景一个 channel 就够了。

我一般会先写成这样:

package pool

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

type Job struct {
 ID      string
 Payload string
}

type WorkerPool struct {
 size int
 jobs chan Job
 wg   sync.WaitGroup
}

func NewWorkerPool(size int, queueSize int) *WorkerPool {
 if size <= 0 {
  size = 1
 }
 if queueSize <= 0 {
  queueSize = size * 2
 }

 return &WorkerPool{
  size: size,
  jobs: make(chan Job, queueSize),
 }
}

func (p *WorkerPool) Start(ctx context.Context) {
 for i := 0; i < p.size; i++ {
  workerID := i + 1
  p.wg.Add(1)

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

   for {
    select {
    case <-ctx.Done():
     log.Printf(\"worker exit, worker=%d, reason=%v\", workerID, ctx.Err())
     return

    case job, ok := <-p.jobs:
     if !ok {
      log.Printf(\"worker exit, worker=%d, reason=job_channel_closed\", workerID)
      return
     }

     p.handle(workerID, job)
    }
   }
  }()
 }
}

func (p *WorkerPool) Submit(ctx context.Context, job Job) error {
 select {
 case <-ctx.Done():
  return ctx.Err()

 case p.jobs <- job:
  return nil

 default:
  return fmt.Errorf(\"worker pool queue full, job=%s\", job.ID)
 }
}

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

func (p *WorkerPool) handle(workerID int, job Job) {
 start := time.Now()

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

 // 这里换成真实业务,比如同步订单、清洗文件、调用三方接口
 if err := pushToRemote(job); err != nil {
  log.Printf(\"job failed, worker=%d, job=%s, cost=%s, err=%v\",
   workerID, job.ID, time.Since(start), err)
  return
 }

 log.Printf(\"job done, worker=%d, job=%s, cost=%s\",
  workerID, job.ID, time.Since(start))
}

func pushToRemote(job Job) error {
 time.Sleep(80 * time.Millisecond)
 return nil
}

这个池子没什么花活。

size 控制同时跑多少个 goroutine。

jobs 是任务队列。

Start 只启动固定数量的 worker。

Submit 负责投递任务。

Stop 负责收口。

看着简单,但几个点不能乱动。

第一个点,Submit 里我用了 default。

select {
case p.jobs <- job:
 return nil
default:
 return fmt.Errorf(\"worker pool queue full, job=%s\", job.ID)
}

这个写法的意思是:队列满了就立刻返回错误,不在这里傻等。

这地方我比较保守。因为很多线上卡死,不是 worker 不干活,而是提交任务的 goroutine 全堵在写 channel 上,最后调用链一层一层堵住。

当然,不是所有业务都要立刻失败。

如果是离线导入,可以允许等一会儿:

func (p *WorkerPool) SubmitWait(ctx context.Context, job Job) error {
 timer := time.NewTimer(3 * time.Second)
 defer timer.Stop()

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

 case <-timer.C:
  return fmt.Errorf(\"submit timeout, job=%s\", job.ID)

 case <-ctx.Done():
  return ctx.Err()
 }
}

这里我宁愿给一个 3 秒超时,也不愿意无限等。

无限等待这种东西,在代码里看着温柔,在线上经常就是事故入口。

第二个点,worker 里面一定要 recover。

不是说业务代码就应该 panic,而是批量任务里只要有一条脏数据把 goroutine 打死,这个 worker 就少一个。少一个还不明显,少到最后池子还在,活没人干,日志也不一定好看。

所以这里要兜一下:

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

我不喜欢把 panic 吞得悄无声息。至少要把 workerID、jobID 打出来,不然排查的时候只能靠猜。

第三个点,关闭顺序别写反。

Stop 里先 close(p.jobs),再 Wait。

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

不能先等再关。你不关 channel,worker 就一直等任务,Wait 永远不回来。

也不要在多个地方 close 同一个 channel。这个错误很低级,但线上确实见过。一般我会把关闭动作只放在池子自己内部,外面别碰 jobs。

用的时候大概这样:

func RunImport(ctx context.Context, rows []string) error {
 p := NewWorkerPool(8, 64)
 p.Start(ctx)
 defer p.Stop()

 for i, row := range rows {
  job := Job{
   ID:      fmt.Sprintf(\"row-%d\", i+1),
   Payload: row,
  }

  if err := p.SubmitWait(ctx, job); err != nil {
   log.Printf(\"submit failed, job=%s, err=%v\", job.ID, err)
   return err
  }
 }

 return nil
}

这里的 8 不是拍脑袋越大越好。

如果任务主要是 CPU 计算,比如压缩、加密、图片处理,worker 数接近 CPU 核数就差不多了。

如果任务主要是 IO,比如调接口、写数据库、读文件,可以适当放大。但也要看下游扛不扛得住。你本地开 100 个 worker 很爽,数据库连接池只有 20 个,最后还是在连接池那里排队。

这也是我不太喜欢“无脑调大并发”的原因。

并发高,不等于吞吐高。很多时候只是把等待从 A 点挪到 B 点。

一个能用的 goroutine 池,至少要回答四个问题:

任务最多堆多少?

worker 最多跑多少?

提交失败怎么办?

服务退出时,剩下的任务怎么收?

这四个问题没想清楚,就别急着封装成公共库。

Go 的并发工具很锋利,go func() 一行就能起飞,也一行就能把系统打穿。简单资源池的价值不在于代码多高级,而是把“随便起 goroutine”这件事收住。

线上系统很多时候不怕慢一点,怕的是没边界。池子就是这个边界。


分析一个简单的goroutine资源池" 网络安全文章

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

针对Go无节制创建goroutine导致线上卡死的问题,本文指出需建立并发边界。作者基于channel实现轻量级资源池,强调三处关键设计:任务提交须快速失败或设超时防阻塞,worker内须recover防脏数据打挂,关闭时须先close再Wait。建议按任务特性配置worker数,通过池化约束并发以保障系统稳定。
FuckMyShitMountain:项目能跑不算完,先让AI下屎山挖一遍" 网络安全文章

FuckMyShitMountain:项目能跑不算完,先让AI下屎山挖一遍"

本文介绍了一个名为FuckMyShitMountain的代码审计工具,该工具为编程Agent提供skill以增强审计能力。它能自动分析项目结构、依赖、CI配置并生成包含严重程度、置信度、影响、修复建议和回归测试的详细报告。工具覆盖27个审计维度,包括安全、稳定性、性能、测试真实性、供应链及LLM成本等,并支持增量审计用于PRreview。作者强调其优势在于承认证据不足,标记未评估项,且支持Markdown、HTML、JSON输出以便集成到CI流程。同时指出AI审计依赖上下文理解和项目可运行性,不能替代人工
马斯克SpaceXAIGrokBuild开源" 网络安全文章

马斯克SpaceXAIGrokBuild开源"

xAI开源了由Grok4.5驱动的Rust终端编码代理工具GrokBuild。其具备计划模式、子代理并行、插件扩展及沙箱执行等特性,支持无头模式与CI/CD集成,可全流程在终端完成开发。该工具采用Apache2.0许可,适合复杂重构与自动化场景,但文末附有安全社区引流广告,整体阅读需注意甄别。
PentesterFlow——面向渗透测试人员和漏洞赏金猎人的AI自动化工作流工具" 网络安全文章

PentesterFlow——面向渗透测试人员和漏洞赏金猎人的AI自动化工作流工具"

PentesterFlow是一款开源的人机协同AI命令行工具,专为渗透测试和漏洞赏金猎人设计。它自动化从信息收集到报告生成的工作流,通过内置技能模块和基于证据的漏洞确认机制解决AI幻觉问题。其核心特色是分析人员审批机制,确保敏感命令执行前获得授权,并具备本地持续学习能力。该工具支持多种LLM后端和BurpSuite集成,适合对全自主代理持谨慎态度的安全团队,但仅限授权测试使用。
评论:0   参与:  0