WEBKT

Go语言Goroutine泄漏现场:从一次线上事故说起

12 0 0 0

Go语言Goroutine泄漏现场:从一次线上事故说起

最近线上服务出现了一次严重的性能问题,CPU占用率持续飙升至100%,最终导致服务瘫痪。经过一番排查,最终发现罪魁祸首竟是——Goroutine泄漏!

这次事故让我深刻体会到,在Go语言并发编程中,Goroutine泄漏是一个非常棘手的问题。它不像普通的内存泄漏那样容易被发现,往往会在潜移默化中蚕食系统的性能,直到最终爆发。

事故回放

我们的服务是一个处理用户请求的微服务,使用了Go语言编写,并大量使用了Goroutine进行并发处理。在高并发情况下,服务运行一段时间后,CPU占用率会逐渐升高,最终导致服务崩溃。

我们首先怀疑是数据库连接池出现了问题,因为数据库连接池的资源未及时释放,可能会导致连接耗尽。但是仔细检查后发现,数据库连接池并没有问题。

接下来,我们开始使用Go自带的runtime包的监控工具,检查Goroutine的数量。结果发现,Goroutine的数量在持续增长,而且增长速度非常快!这最终指向了Goroutine泄漏。

泄漏原因分析

经过仔细排查代码,我们发现泄漏的原因在于一个HTTP请求处理函数中,没有正确地关闭Goroutine。

该函数启动了一个Goroutine来处理耗时操作,例如网络请求或数据库查询。但是,如果该函数执行过程中出现错误,或者请求被取消,这个Goroutine并不会被关闭,导致其一直运行,占用系统资源。

func handleRequest(w http.ResponseWriter, r *http.Request) {
    go func() {
        // 处理耗时操作
        defer func() {
            // 错误处理,但缺少Goroutine关闭逻辑
            if r := recover(); r != nil {
                log.Printf("Error: %v", r)
            }
        }()
        // ...耗时操作...
    }()
}

这段代码中,defer语句只处理了panic,并没有处理其他类型的错误,也没有显式地关闭Goroutine。

解决方法

解决这个问题的方法很简单,就是在handleReques函数中,使用context包来管理Goroutine的生命周期。

func handleRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    go func(ctx context.Context) {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("Error: %v", r)
            }
        }()
        select {
        case <-ctx.Done():
            log.Println("Goroutine cancelled")
            return
        case <-time.After(5 * time.Second): // 设定超时时间
            log.Println("Goroutine timeout")
            return
        case result := <-someLongRunningFunc(ctx): // 耗时操作
            // 处理结果
        }
    }(ctx)
}

通过使用context.Done()通道,我们可以优雅地关闭Goroutine。如果context被取消,select语句会立即返回,关闭Goroutine。

同时,我们还设置了一个超时时间,防止Goroutine无限期地运行。

总结

这次Goroutine泄漏事故,让我们深刻认识到,在Go语言并发编程中,一定要注意Goroutine的生命周期管理。要养成良好的编程习惯,避免出现Goroutine泄漏,确保应用程序的稳定性和性能。

建议大家在编写Go程序时,尽可能使用context包来管理Goroutine的生命周期,并设置合理的超时时间,避免出现类似的线上事故。

此外,定期监控Goroutine的数量,也是预防Goroutine泄漏的重要手段。

老码农 GoGoroutine内存泄漏并发编程线上事故

评论点评