Go语言内存泄漏问题排查实战-记一次线上容器重启问题排查

原创 吴就业 163 0 2024-02-06

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://www.wujiuye.com/article/c891c95ad16b4917b83a81e349b90c1a

作者:吴就业
链接:https://www.wujiuye.com/article/c891c95ad16b4917b83a81e349b90c1a
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

有个go项目的容器近两天几乎每天都异常重启一次,且两个节点基本都是差不多的时间异常重启。看了监控指标,发现CPU平稳,而内存是缓慢涨上去后,进程被操作系统kill掉,导致pod重启。

go项目内存泄漏

从内存指标可以看出,不会是因为突然的请求量上升所导致,而是应该存在内存泄漏了。 另外,从网络带宽指标看,流量也确实波动并不大,趋于平稳。

网络带宽指标

从监控指标还可以看出,go的线程数是平稳趋势,可以排除goroutine导致的内存泄漏。

线程数平稳

这里我们需要借助pprof工具查看内存泄漏问题。pprof(Profiling in Go)是Go语言内置的一个性能分析工具。该工具可用于在运行时进行应用程序性能分析和剖析,帮助我们找出go进程的性能瓶颈和资源利用问题。例如:

排查思路:由于容器已经重启过,当前go进程内存消耗还是正常值,不过从监控指标已经看出,内存会缓慢的涨上去,因此我们是先查看当前时间的内存使用情况,记录下来,待一个小时以后,再看一次,对比看哪里的内存是上涨的,然后再追踪内存是从哪里分配的,最后再看代码,看看哪里占用了内存没有释放。

我们已经给该go项目启用pprof:

import _ "net/http/pprof"

func init() {
    go func() {
       http.ListenAndServe("0.0.0.0:7005", nil)
    }()
}

进入容器,执行命令“go tool pprof -inuse_space http://127.0.0.1:7005/debug/pprof/heap”。 输入top命令,查看内存占用的前10。

go tool pprof

目前看各项占用的内存都是在正常值范围,比较可疑的是“crypto/x509.parseCertificate”,还不急着分析泄漏问题。

不过这里还看出一个问题,就是我们执行操作系统的top命令,看到占用的内存(RES),也是监控指标显示的内存值,当前已经1.4g,而pprof的top命令显示的heap内存占用才626MB,相差很大。

top命令

类似Java的堆内存+堆外内存,以及堆内存实际使用与已占用操作系统的内存。go除了堆内存使用,还有栈、gc、go的一些底层数据结构等使用的内存也是计算在堆外的,而堆内存已申请和已使用也跟java类似, gc后会有空闲的堆内存,不会全部马上归还给操作系统。而pprof的top统计的是当前已使用,不包含空闲的堆内存,所以看到的差距很大。

进入容器里面执行curl http://127.0.0.1:7005/debug/pprof/heap?debug=1命令,可以查看go的内存占用情况。

go堆

字段含义说明:

从图中可以看出,go进程总共为heap申请了1.2G的内存,当前已使用785MB,空闲461MB。top命令显示的进程占用1.4g,go进程heap占用1.2G,还差两百M,就是Stack(栈内存使用)、MSpan+MCache+BucjHashSys(go底层内部结构体使用)、GCSys(GC使用)、OtherSys(其它内存使用)。所以内存使用是对得上的。

大概一个小时后,重新执行pprof的top命令,输出的前10堆内存使用如下图。

前10堆内存使用

(方便对比,上一次的)

前10堆内存使用

其中gitlab.lizhi.fm/middleware/lz_common_romefs/fio.(*ByteBufPool).Get占用64.32mb刚好是内存池的最大大小,这是我们自己实现的内存池,说明内存池没有泄漏。而bytes.makeSlice、crypto/x509.parseCertificate都往上涨了。 通过peek bytes.makeSlice,发现bytes.(*Buffer).Write占用64.33MB,bytes.(*Buffer).Grow占用110.48MB。

peek bytes.makeSlice

继续peek Grow,发现与tls有关,占用110.48MB。

peek Grow

继续peek Write,由于是模糊匹配,一共有三个结果,中间第二个才是我们需要的,发现也与tls有关,占用64.33mb内存。

peek Write

代码中,与tls有关的地方就是发送https请求从s3下载文件,所以检查下载文件调用链路上是否存在可疑的内存泄漏,发现如下疑点。

没有close响应的body导致内存泄漏

统计了访问日记,发现确实经常出现响应403。

所以问题就清晰了,由于403是有body的,没有close响应的body导致的内存泄漏。

修改后指标恢复正常。

修改后指标

#中间件

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

文件上传ceph首次下载耗时慢问题排查

据业务反馈,AI生成图片上传后,首次立即下载耗时可能需要几秒,且出现概率极大,很容易复现。如果是上传后,过个几秒后再下载,耗时则只需要几百毫秒。

中间件服务上线,那跟电影里的拆炸弹一样刺激

而针对这种大迭代的发版,根本原因还是要解决灰度粒度问题,支持流量粒度,支持全链路的灰度。

全球化的IM产品技术架构调研

主要调研学习Slack和WhatsApp这两个产品的全球化架构,单数据中心或是多数据中心,怎样做架构设计,能够解决异地多活和延迟问题。

Java内存GC故障问题排查实战-推送系统频繁GC告警问题排查

记录一次工作中实战的Java内存泄漏问题排查,Pod重启后无法查看现场,但通过gc日记可以确认存在内存泄露,并通过运行一段时间发现有个Java类的实例数量非常高。

cpu负载高故障排查实战-网关故障导致业务请求堆积

因为go标准库实现tls握手性能比较差,在一台8核的机器,只能到达2000这个量级,所以当到达某个临界点的时候,握手占用CPU过高,反过头来影响正常的业务请求,导致了业务请求处理变得十分慢。

Go写的文件上传中间件内存泄露问题排查

用go开发的一个文件上传中间件,由于依赖了ceph这个c库,早期通过pprof排查,怀疑内存泄露在c层,而项目依赖ceph,于是就怀疑是ceph的问题。但通过使用jemalloc排查后,并未发现ceph有什么异常。最后使用最笨的方法,定位到github.com/chai2010/webp库存在内存泄露bug。