• 记一次Golang中一次内存泄漏的问题排查


    背景:

    事情的起因是这样的。我的一个Golang服务通过docker部署在了线上环境中,但是该服务经常出现内存爆满导致容器自动被kill而退出的现象。因为是线上环境,排查问题会耗时很长,因此先在线上服务的容器上加了个–restart=always的参数,这样可以让容器退出后自动重启,继续进行任务。
    但在排查内存爆满问题时,线上环境有严格的网络限制,较难用golang的pprof工具进行内存分析,因此将其转移到本地环境中进行事故模拟。一次内存泄漏的排查便从此开始展开。

    排查过程:

    1. 本地模拟docker环境进行排查

    在本地运行时,我首先模拟了docker环境,通过docker stats 命令实时观测其内存变化,分别测试了发送http请求成功和发送http请求失败的两种情况,发现在http请求失败时内存不会增加,但http请求成功时,内存会逐渐增加,初步推断可能是http请求时出现的问题。

    2. 代码中加入runtime.GC(),在监测内存超过300MB时,自动进行GC清理

    根据第一步中的结果,初步推断可能是在http请求时生成了大量对象,但GC清理速度较慢,导致内存溢出的问题出现,因此,我在代码中加入了一个goroutine,用于实时监测内存状态,发现堆内存占用超过300MB时,则进行runtime.GC()清理。
    加入runtime.GC()后,的确起到了一些效果,但效果不佳,通过docker stats命令观测其内存变化从 200MB 到 600 MB 之间不断变化,初步认为问题已经解决。
    但容器在运行了一段时间后,又出现了自动退出的情况,且当时的内存占用仅为600MB左右,并未超过设定阈值 1 GB,问题仍需排查。

    3. 从docker容器中拿出来,在本地运行

    由于docker容器自动退出时,很多runtime的堆栈日志例如fatal错误不会输出到控制台,并且在docker容器内将这种错误进行导出的配置也相当麻烦,因此我把服务从容器中拿出来,直接在本地运行,观测其发生的现象。
    在本地运行时,不观测不知道,一观测吓一跳!
    在高并发下,起初程序运行并没有问题,但是通过top命令观测其内存变化,竟然逐渐增大至高达1.5GB,并且runtime.GC()并不其任何作用,说明程序中有大量内存无法被GC清理,说明肯定有很多对象的引用一直存在。
    并且在本地运行时,还观测到,日志报错中出现了:
    An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full.
    的错误。
    该错误表明,大量的http请求连接仍然处于keep-alive状态,并没有被消灭。那么问题出在什么地方呢?
    我的源代码是这样的:(示例代码。已将源代码敏感信息删去)

    func httpSend(dto *Dto, payload *bytes.Reader) error {
    	response, err := http.Post(url, "application/json", payload)
    	if err != nil {
    		log.Printf("ERROR: %s\n", err)
    		return err
    	}
    	log.Printf("HTTP post success, response.StatusCode is:%d\n", response.StatusCode)
    	if response.StatusCode == http.StatusOK {
    		log.Println("HTTP post success, response code is HTTP.StatusOK")
    		return nil
    	} else {
    		return errors.New("send failed")
    	}
    }
    

    看了半天没错呀,难道是response没有关闭?
    我试着找了下response的方法,发现并没有Close()这个方法。
    我百思不得其解,到底是为什么呢?
    此时,不经意间看到的其他的代码令我恍然大悟:
    原来是response.Body没有关闭!!!
    response.Body是属于 io.ReadCloser 对象,该对象会占用文件描述符socket,当该对象不被主动关闭时,随着时间的推移,肯定会造成系统中socket数量不够用,进而导致内存泄漏,容器崩溃,甚至服务器卡死的情况!

    结语:

    至此,一次golang中内存泄漏的问题排查到此结束,原来是在http请求时,没有主动关闭返回体中的response.Body导致了socket文件被大量占用,进而导致内存泄漏的问题出现。
    根据此次问题排查,以后在每一次http请求时,不管response.Body的内容是否被使用,只要是使用了http请求,请求的返回体都需要主动关闭!!!否则就会造成严重的内存泄漏,甚至服务器卡死!

  • 相关阅读:
    从源码里的一个注释,我追溯到了12年前,有点意思。
    照着这本“书”,3年量产自动驾驶卡车
    ruoyi权限设置的坑
    Nodejs -- 在Express使用Session认证
    Android左滑删除,自定义左滑删除控件
    面试题收集
    B_QuRT_User_Guide(28)
    C++ - 类型转换 - static_cast - reinterpret_cast - const_cast - dynamic_cast
    数字秒表VHDL实验箱精度毫秒可回看,视频/代码
    会话与终端
  • 原文地址:https://blog.csdn.net/qq_42362240/article/details/127104424