我使用 Golang 写的一个融合日志服务, 定时融合不同云厂商的日志, 该服务部署在 K8S 上.

在国庆期间日志量倍增, 该服务频繁被 Killed.

通过仪表盘, 发现该服务运行一次后, 内存不会释放, 怀疑是内存泄露导致的. 见下图.

956c856349c6b383db802c218deee349.png

最终, 我通过使用 pprof 解决了该问题.

1. 开启 pprof 服务

pprof 是 Golang 自带的性能分析工具. 可以 2 步 开启 pprof 服务.

// 1. 引入 net/http/pprof 包_ import "net/http/pprof"// 2. 开启 http 服务go http.ListenAndServe(":9999", nil)
2. 通过 pprof 的 Web 页面分析 goroutine

执行完步骤 1 后, 程序启动后, 打开 http://127.0.0.1:9999/debug/pprof 以页面的形式获取程序当前的运行情况.

如图:

08b61933fc239f416dac8f056e60a7ec.png
  • 使用 full goroutine stack dump 来排查是否有 goroutine 运行时间过长. 如下图, 如果有的 goroutine 运行的时间非常长, 就要查看对应的代码, 是否是代码 hang 住导致的.
29950d40bf76112cbc114f941431cfdd.png
  • 使用 goroutine 来排查是否创建了大量的 goroutine. 我的程序是定时任务, 正常情况下在 sleep 期间没有任务运行, goroutine 的数量应该非常少的. 可以使用这点进行排查. 如下图, 可以看到总的 goroutine 数量和每个 goroutine 实例的数量. 根据代码逻辑去推断 goroutine 实例的数量是否正常.
d6ff8d8722e772d7f88a232aa20ab0fc.png

我在 Web 页面上并未发现有价值的信息, 于是使用命令行分析.

3. 通过 pprof 的命令行分析 heap

命令行执行命令: go tool pprof -inuse_space http://127.0.0.1:9999/debug/pprof/heap

这个命令的作用是, 抓取当前程序已使用的 heap. 抓取后, 就可以进行类似于 gdb 的交互操作.

  • top 命令, 默认能列出当前程序中内存占用排名前 10 的函数. 如图. 当时进行到这一步的时候, 我就非常惊讶, 因为 time.NewTimer 居然占据了 6 个多 G 的内存.
ed5f826ba952521a4107e606d2189181.png
  • list , 展现函数内部的内存占用. 使用 list time.NewTimer 查看了该函数的内部, 真相大白了, 原来每次调用 NewTimer 都会创建一个 channel, 还会生成一个结构体 runtimeTimer, 应该就是这两个地方内存没有释放造成的内存泄露.
54c56f80654c1e4bbfb5fecabd1e0240.png
修改 for ... select ... time.After 造成的内存泄露

原来程序中存在如下代码:

for {select {case a := 

time.After 就是封装了一层的 NewTimer, time.After 的源码:

func After(d Duration) 

修复该错误, 只调用一次 NewTimer:

downloadTimeout := time.NewTimer(20 * time.Minute)defer downloadTimeout.Stop()for {select {case a :=