前言

我在 blued 工作期间,负责过一个跟谷歌推送相关的子项目,原先是 node 写的,然后用 golang 重构,这个项目可以算是我第一个有一定并发量的程序,前前后后也碰到过不少问题。

过度的并发问题
内存泄漏

grafana-before

图上是内存泄漏时内存增长的情况

添加 pprof 模块

在这之前我还没使用过这个工具,看网上介绍又是这个函数又是那个函数的,处理好像挺麻烦的,实际上现在最新版本的 go tool 分析工具已经很人性化了,使用其实比我想象的方便。

首先在入口文件里添加代码引入 pprof 模块:

package main

import (
    "log"
    "net/http"
    "os"
    "runtime"

    _ "github.com/mkevac/debugcharts" // 可选,添加后可以查看几个实时图表数据
    _ "net/http/pprof" // 必须,引入 pprof 模块

    "git.blued.cn/wangriyu/go-fcm/service"
)

func main() {
    if os.Getenv("debugPProf") == "true" {
        go func() {
            // terminal: $ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
            // web:
            // 1、http://localhost:8081/ui
            // 2、http://localhost:6060/debug/charts
            // 3、http://localhost:6060/debug/pprof
            log.Println(http.ListenAndServe("0.0.0.0:6060", nil))
        }()
    }

    service.Start()
    select {}
}

然后在 pm2 的启动文件里添加一个环节变量 debugPProf,方便使用时开启:

{
    "name": "FCM",
    "script": "main",
    "error_file": "/data/logs/pm2/FCM/error.log",
    "out_file": "/data/logs/pm2/FCM/out.log",
    "log_date_format": "YYYY-MM-DD HH:mm:ss Z",
    "instances": 1,
    "exec_mode": "fork",
    "merge_logs": true,
    "max_memory_restart": "1024M",
    "env": {
        "fcm_env": "dev",
        "debugPProf": false
    }
}

使用 pprof

使用时将 debugPProf 设为 true,启动程序,通过上面设置的端口即可访问 pprof 的数据

打开 http://$HOSTIP:6060/debug/pprof/ : 这个页面包含总览信息,我不做介绍了,网上都有,我主要讲讲内存分析这块的

点击 heap 进入 http://$HOSTIP:6060/debug/pprof/heap?debug=1 界面,这里有内存分配信息,然后拉到最下面(mac 可以使用快捷键 CMD+↓ 跳到底部)

web-heap

heapSys 是占用的物理内存大小,也就是我们直观上看到进程占用的大小;
heapIdle 是当中空闲的内存大小;heapInuse 才是此时程序真正使用的内存;nextGC 是下次触发 GC 的内存阀值

如果你添加了 “github.com/mkevac/debugcharts”,可以打开 http://$HOSTIP:6060/debug/charts

debugChart

分析工具

go tool pprof http://$HOSTIP:6060/debug/pprof/heaptop

go tool pprof 可以带上参数 -inuse_space (分析应用程序的常驻内存占用情况) 或者 -alloc_space (分析应用程序的内存临时分配情况)

-http=:8081
$ go tool pprof -http=:8081 http://$HOSTIP:6060/debug/pprof/heap

如果 Could not execute dot,可能需要安装 graphviz 这个工具

之后打开 http://$HOSTIP:8081/ui 即可,里面包含 dot 格式的图、火焰图、top 列表、source 列表等

dot

flamegraph

peek

top

排查内存泄漏的过程

第一遍跑了一段时间程序后查看结果发现是 amqp 库的一个函数 recvContent 占用内存最高

source

我仔细看了下源码,发现这个函数使用用于解析消息体的,但是为什么这里泄漏内存不清楚,然后去 github issue 上搜了一下,发现有不少类似的情况,比如 https://github.com/streadway/amqp/issues/272

然后出现问题的共同点是我们用了 autoAck 机制,rabbitmq server 会自动做 ack 然后有多少消息都推给 cient,我那个项目因为要求处理尽可能快、消息不能延迟太多,有部分消息丢失是允许的,所以一开始选择的是 autoAck,发现这个问题后我改成了 manualAck;

改完这个后第二天一看发现还是有内存泄漏,但这回出问题不是 recvContent 函数,而是 notifyClose 函数,因为在代码中实现了一层重连机制,用到这个函数用于监听连接是否断开

dot-2

最后我把 notifyClose 去掉用其他方法判断连接断开,这样才解决了泄漏问题

grafana-after

虽然解决了问题,但是根本原因还是在源代码里,当时因为时间原因就没深究下去了…