一次Go项目进程重启故障问题排查

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

从内存指标可以看出,不会是因为突然的请求量上升所导致,而是应该存在内存泄漏了。
另外,从网络带宽指标看,流量也确实波动并不大,趋于平稳。
一次Go项目进程重启故障问题排查_第2张图片

从监控指标还可以看出,go的线程数是平稳趋势,可以排除goroutine导致的内存泄漏。
一次Go项目进程重启故障问题排查_第3张图片

这里我们需要借助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项目进程重启故障问题排查_第4张图片

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

不过这里还看出一个问题,就是我们执行操作系统的top命令,看到占用的内存(RES),也是监控指标显示的内存值,当前已经1.4g,而pprof的top命令显示的heap内存占用才626MB,相差很大。
一次Go项目进程重启故障问题排查_第5张图片

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

进入容器里面执行curl http://127.0.0.1:7005/debug/pprof/heap?debug=1命令,可以查看go的内存占用情况。
一次Go项目进程重启故障问题排查_第6张图片

字段含义说明:

  • Sys:进程总共从操作系统申请的字节数,包含运行时的heap、stack和其他内部数据结构的总和。
  • HeapSys:go进程heap申请的字节数。
  • HeapInuse:go进程当前使用的字节数。
  • HeapReleased:go进程已释放归还给操作系统的字节数。
  • HeapIdle:没有被使用的空闲字节数,包含HeapReleased,可以被再次申请,甚至作为栈内存使用。HeapIdle-HeapReleased=GC保留的。

从图中可以看出,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堆内存使用如下图。
一次Go项目进程重启故障问题排查_第7张图片

(方便对比,上一次的)
一次Go项目进程重启故障问题排查_第8张图片

其中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。
一次Go项目进程重启故障问题排查_第9张图片

继续peek Grow,发现与tls有关,占用110.48MB。
一次Go项目进程重启故障问题排查_第10张图片

继续peek Write,由于是模糊匹配,一共有三个结果,中间第二个才是我们需要的,发现也与tls有关,占用64.33mb内存。
一次Go项目进程重启故障问题排查_第11张图片

代码中,与tls有关的地方就是发送https请求从s3下载文件,所以检查下载文件调用链路上是否存在可疑的内存泄漏,发现如下疑点。
一次Go项目进程重启故障问题排查_第12张图片

统计了访问日记,发现确实经常出现响应403。
所以问题就清晰了,由于403是有body的,没有close响应的body导致的内存泄漏。
修改后指标恢复正常。
一次Go项目进程重启故障问题排查_第13张图片

你可能感兴趣的:(性能分析&故障排查,golang,开发语言,后端)