You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang Cron任务CPU占用持续升高问题排查求助

CPU占用持续升高的排查与资源优化方案

一、代码中潜在的问题点及对应解决方法

1. WaitGroup 管理不当导致goroutine堆积

  • 问题分析:
    在RunCronForDbNames循环中每次调用wg.Add(1),但完全依赖CompleteBookingsCron调用wg.Done(),一旦业务函数遗漏该调用,或在异步goroutine中未等待就执行Done(),会导致wg.Wait()永久阻塞。而cron任务每5分钟触发一次,前次任务未完成时后续任务会重复启动,最终引发大量goroutine堆积,CPU占用飙升。
  • 解决方法:
    调整worker逻辑,将wg.Done()放在worker的job处理流程中,确保每个job完成后都标记任务结束:
    func Worker(id int, jobs <-chan view.CronChannelData, wg *sync.WaitGroup, cronType string) {
        switch cronType {
        case config.CompleteBookingCron:
            for job := range jobs {
                defer wg.Done() // 确保每个job处理完都触发Done
                controller.CompleteBookingsCron(job)
            }
        }
    }
    
    同时确保RunCronForDbNames中每发送一个job到channel前,正确调用wg.Add(1)。

2. MongoDB客户端频繁创建销毁

  • 问题分析:
    每次cron任务执行时都新建MongoDB客户端,执行完成后断开连接。MongoDB客户端本身是线程安全的,支持连接池复用,频繁创建销毁会导致连接池资源反复分配回收,增加CPU开销,甚至残留未正确关闭的连接。
  • 解决方法:
    全局初始化一次MongoDB客户端,程序启动时创建,整个生命周期复用:
    var globalMongoClient *mongo.Client
    var globalMongoCtx context.Context
    
    func InitMongoClient(ip string) error {
        var err error
        globalMongoCtx = context.Background()
        globalMongoClient, err = mongo.Connect(globalMongoCtx, options.Client().ApplyURI(fmt.Sprintf("mongodb://%s", ip)))
        if err != nil {
            return err
        }
        return globalMongoClient.Ping(globalMongoCtx, readpref.Primary())
    }
    
    后续cron任务直接使用globalMongoClient即可,无需每次创建。

3. 不必要的gin.Context创建

  • 问题分析:
    cron任务属于后台任务,无需依赖为HTTP请求设计的gin.Context。当前代码强行创建该结构体并赋值,不仅增加无意义的内存开销,还可能因未初始化完整字段引发潜在资源泄漏。
  • 解决方法:
    直接传递dbName和role参数给业务函数,移除gin.Context的使用:
    // 修改CronChannelData结构体
    type CronChannelData struct {
        Database struct {
            Client        *mongo.Client
            Ctx           context.Context
            MainDatabase  string
        }
        StartTime int64
        DbName    string
        Role      string
    }
    
    // 在RunCronForDbNames循环中赋值
    cronChData.DbName = dbName
    cronChData.Role = ""
    cronDataChannel <- cronChData
    

4. CheckGlobalCronStatus参数错误

  • 问题分析:
    代码中调用db.CheckGlobalCronStatus(cronType, dbNames)时传递了整个商户数组,而非当前处理的单个dbName。若函数需针对单个商户检查状态,会导致每次循环都遍历2000个元素,累计大量重复计算,增加CPU负载。
  • 解决方法:
    修正参数为当前商户的dbName:
    isContinue := db.CheckGlobalCronStatus(cronType, dbName)
    

5. 异步通知的goroutine泄漏

  • 问题分析:
    CompleteBookingsCron中用goroutine发送邮件/短信,若未对这些goroutine做管理(无超时、未捕获panic、未等待完成),会导致goroutine持续堆积,占用CPU和内存。
  • 解决方法:
    用内部WaitGroup等待所有异步操作完成,设置超时并捕获panic:
    func sendNotifications() {
        var wg sync.WaitGroup
        // 发送邮件
        wg.Add(1)
        go func() {
            defer wg.Done()
            defer func() {
                if r := recover(); r != nil {
                    // 记录panic日志
                }
            }()
            ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
            defer cancel()
            // 执行邮件发送逻辑
        }()
        // 发送短信逻辑同理
        wg.Wait()
    }
    

二、排查工具推荐

使用Go官方pprof工具定位具体CPU占用点:

  1. 在程序中开启pprof监听:
    import _ "net/http/pprof"
    
    func main() {
        go func() {
            log.Println(http.ListenAndServe("0.0.0.0:6060", nil))
        }()
        // 其他初始化逻辑
    }
    
  2. 采样CPU数据:
    go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
    
    进入交互界面后输入top查看占用最高的函数,输入web生成火焰图直观分析。
  3. 查看goroutine数量变化:
    curl http://localhost:6060/debug/pprof/goroutine?debug=1
    

三、资源释放的正确实践

  • MongoDB连接:全局复用客户端,仅在程序退出时调用Disconnect;
  • goroutine:所有异步goroutine必须通过WaitGroup或context管理,确保能正常结束;
  • context:为数据库操作、异步任务设置合理超时时间,避免无限阻塞;
  • 内存:减少不必要的结构体拷贝,及时清理不再使用的大对象,降低GC压力。

内容的提问来源于stack exchange,提问作者Rohit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 19:05:13