Golang中Cron导致CPU使用率逐日上升的问题如何解决

Golang中Cron导致CPU使用率逐日上升的问题如何解决 我正在运行一个用Golang编写的cron任务,并使用MongoDB(版本4.4)作为数据库。该cron任务每隔5分钟运行一次。我注意到,当我启动cron服务后,在一周内CPU使用率会从0%飙升至60%甚至更高,因此我不得不重启cron服务,以免影响后续的cron任务执行。这表明存在内存泄漏,资源没有得到正确释放。我无法确定具体原因。在整个cron任务中,我使用单一的数据库连接,假设一个cron任务服务于2000个商户。我使用带缓冲通道的工作池来运行cron任务。以下是我使用工作池运行cron任务的代码:

func RunCronForDbNames(cronType, subsType string) {
	cronStatus := GetCronStatusConditions(cronType)
	if cronStatus {
		dbNames, _ := controller.GetDbNamesForCron(subsType, cronType)
		if len(dbNames) == 0 {
			return
		}
		client, ctx := ConnectDb("ip")
		defer client.Disconnect(ctx)
		cronDataChannel := make(chan view.CronChannelData, len(dbNames))
		var wg sync.WaitGroup
		startTime := time.Now().Unix()
		var cronChData view.CronChannelData
		cronChData.Database.Client = client
		cronChData.Database.Ctx = ctx
		cronChData.StartTime = startTime
		for _, dbName := range dbNames {
			isContinue := db.CheckGlobalCronStatus(cronType, dbNames)
			if !isContinue {
				continue
			}
			wg.Add(1)
			contextKeys := make(map[string]interface{})
			contextKeys["db_name"] = dbName
			contextKeys["role"] = ""
			var c gin.Context
			c.Keys = make(map[string]any)
			c.Keys = contextKeys
			cronChData.C = c
			cronChData.Database.MainDatabase = dbName
			cronDataChannel <- cronChData
		}
		close(cronDataChannel)
		for w := 1; w <= 10; w++ {
			go Worker(w, cronDataChannel, &wg, cronType)
		}
		wg.Wait()
	}
	return
}

我的工作器一次运行10个:

func Worker(id int, jobs <-chan view.CronChannelData, wg *sync.WaitGroup, cronType string) {
    switch cronType {
    case config.CompleteBookingCron:
        for job := range jobs {
            controller.CompleteBookingsCron(job, wg)
        }
    }
    return
}

CompleteBookingsCron函数中,我使用wg.Done()来减少WaitGroup的计数,并将预订标记为已完成,然后根据客户设置发送邮件和短信。发送邮件和短信时使用了goroutine。

有人能帮我找出CPU使用率不断升高的原因吗?我应该遵循哪些做法来确保资源被正确释放,从而避免CPU使用率上升?


更多关于Golang中Cron导致CPU使用率逐日上升的问题如何解决的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中Cron导致CPU使用率逐日上升的问题如何解决的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


从代码分析来看,CPU使用率逐日上升可能有以下几个原因:

1. Goroutine泄漏

CompleteBookingsCron函数中使用goroutine发送邮件和短信,如果没有正确的同步机制,可能会导致goroutine堆积:

// 可能的问题代码示例
func CompleteBookingsCron(job view.CronChannelData, wg *sync.WaitGroup) {
    defer wg.Done()
    
    // 发送邮件 - 如果没有等待完成,goroutine会泄漏
    go sendEmail()
    
    // 发送短信 - 同上
    go sendSMS()
    
    // 主逻辑...
}

修复方案:使用WaitGroup等待所有goroutine完成:

func CompleteBookingsCron(job view.CronChannelData, wg *sync.WaitGroup) {
    defer wg.Done()
    
    var emailWg, smsWg sync.WaitGroup
    
    emailWg.Add(1)
    go func() {
        defer emailWg.Done()
        sendEmail()
    }()
    
    smsWg.Add(1)
    go func() {
        defer smsWg.Done()
        sendSMS()
    }()
    
    emailWg.Wait()
    smsWg.Wait()
    // 主逻辑...
}

2. MongoDB连接未正确关闭

虽然代码中有defer client.Disconnect(ctx),但在worker中可能有额外的连接创建:

// 检查CompleteBookingsCron中是否有新的数据库连接
func CompleteBookingsCron(job view.CronChannelData, wg *sync.WaitGroup) {
    defer wg.Done()
    
    // 确保使用传入的连接,而不是创建新连接
    collection := job.Database.Client.Database(job.Database.MainDatabase).Collection("bookings")
    
    // 如果有查询操作,确保游标关闭
    cursor, err := collection.Find(job.Database.Ctx, bson.M{})
    if err != nil {
        return
    }
    defer cursor.Close(job.Database.Ctx) // 必须关闭游标
    
    // 处理数据...
}

3. 上下文未正确传播和取消

缺少上下文取消机制,可能导致goroutine挂起:

func RunCronForDbNames(cronType, subsType string) {
    cronStatus := GetCronStatusConditions(cronType)
    if cronStatus {
        dbNames, _ := controller.GetDbNamesForCron(subsType, cronType)
        if len(dbNames) == 0 {
            return
        }
        
        // 创建可取消的上下文
        ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
        defer cancel()
        
        client, _ := ConnectDb("ip")
        defer client.Disconnect(ctx)
        
        cronDataChannel := make(chan view.CronChannelData, len(dbNames))
        var wg sync.WaitGroup
        
        // 将上下文传递给worker
        for w := 1; w <= 10; w++ {
            go Worker(ctx, w, cronDataChannel, &wg, cronType)
        }
        
        // ... 其余代码
    }
}

4. 资源密集型操作未限制速率

如果商户数量大,可能需要限制并发:

func RunCronForDbNames(cronType, subsType string) {
    // ... 前面的代码
    
    // 使用信号量限制并发
    sem := make(chan struct{}, 50) // 限制最多50个并发操作
    
    for w := 1; w <= 10; w++ {
        go func(workerID int) {
            for job := range cronDataChannel {
                sem <- struct{}{}
                controller.CompleteBookingsCron(job, wg, sem)
            }
        }(w)
    }
    
    // ... 其余代码
}

func CompleteBookingsCron(job view.CronChannelData, wg *sync.WaitGroup, sem chan struct{}) {
    defer wg.Done()
    defer func() { <-sem }()
    
    // ... 业务逻辑
}

5. 内存泄漏检测

添加pprof监控来识别泄漏:

import (
    _ "net/http/pprof"
    "net/http"
)

func main() {
    // 启动pprof服务器
    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()
    
    // 主程序逻辑...
}

运行后可以通过go tool pprof http://localhost:6060/debug/pprof/heap分析内存使用情况。

6. 定时器未正确清理

如果使用了time.Tickertime.Timer,确保正确清理:

func Worker(id int, jobs <-chan view.CronChannelData, wg *sync.WaitGroup, cronType string) {
    // 如果使用ticker,确保在函数退出时停止
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop() // 重要:防止goroutine泄漏
    
    for job := range jobs {
        select {
        case <-ticker.C:
            // 定期执行的操作
        default:
            controller.CompleteBookingsCron(job, wg)
        }
    }
}

关键点:

  1. 确保所有goroutine都有退出机制
  2. 数据库连接和游标必须正确关闭
  3. 使用context控制超时和取消
  4. 监控goroutine数量:runtime.NumGoroutine()
  5. 定期重启可能掩盖问题,需要找到根本原因
回到顶部