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客户端,程序启动时创建,整个生命周期复用:
后续cron任务直接使用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()) }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占用点:
- 在程序中开启pprof监听:
import _ "net/http/pprof" func main() { go func() { log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) }() // 其他初始化逻辑 } - 采样CPU数据:
进入交互界面后输入go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30top查看占用最高的函数,输入web生成火焰图直观分析。 - 查看goroutine数量变化:
curl http://localhost:6060/debug/pprof/goroutine?debug=1
三、资源释放的正确实践
- MongoDB连接:全局复用客户端,仅在程序退出时调用
Disconnect; - goroutine:所有异步goroutine必须通过WaitGroup或context管理,确保能正常结束;
- context:为数据库操作、异步任务设置合理超时时间,避免无限阻塞;
- 内存:减少不必要的结构体拷贝,及时清理不再使用的大对象,降低GC压力。
内容的提问来源于stack exchange,提问作者Rohit
相关产品推荐
相关产品推荐

