Logo 李超的自留地
使用 pprof 进行性能分析

使用 pprof 进行性能分析

September 26, 2024
3 min read
Table of Contents

背景

本文总结一下之前排查问题的一些过程,当时我们团队做标签服务迁移时, 某天晚上 3台8c16g 的机器内存使用率持续升高、重启 1hour 左右就内存报警。

当时第一想法是用户活跃数激增导致服务出现问题,于是看了一下该服务的请求日志,发现和之前的访问量并没什么明显。

后来想到是不是因为代码中用了太多的 goroutine 没被释放导致的,在这过程中我邀请了另一位经验丰富的同事也帮忙一起排查,也正是因为这个机会看到了 pprof 的威力。从怀疑到证实还得是靠 pprof 啊

服务介绍

用户标签写服务(下文称 tags-writer) 用来给用户打实时标签,方便运营同事做活动时能够在实时情况下圈选一批符合条件的用户,该服务有多个不同的业务上游来源,通过 http 和 Kafka消费标签数据,并在该服务中聚合后写入到将 json 内容存储到阿里云的 hologores 数据库中。

pprof 介绍

本文在这里不赘述安装和基本使用的介绍,这方面网络上的资料太多了,可以见我文末的参考章节来了解更多知识。

当我们服务在线上突然遇到报警时,需要用性能分析工具来分析程序的性能,找到瓶颈并解决,Go 语言自带的 pprof 库就可以分析程序的运行情况,并且提供可视化的功能。它包含两个相关的库:

  • runtime/pprof 对于只跑一次的程序,例如每天只跑一次的离线预处理程序,调用 pprof 包提供的函数,手动开启性能数据采集。

  • net/http/pprof 对于在线服务,对于一个 HTTP Server,访问 pprof 提供的 HTTP 接口,获得性能数据。当然,实际上这里底层也是调用的 runtime/pprof 提供的函数,封装成接口对外提供网络访问。

访问服务的 pprof 页面:http://xxx/debug/pprof/

homepage

路径功能主要应用场景
debug/pprof/allocs内存分配情况的采样信息排查频繁内存分配、回收
debug/pprof/heap堆上内存使用情况的采样信息排查内存占用过高
debug/pprof/profileCPU 占用情况的采样信息排查 CPU 占用过高
debug/pprof/blocks阻塞操作情况的采样信息
debug/pprof/cmdline显示程序启动命令及参数
debug/pprof/goroutine当前所有协程的堆栈信息可查看当前协程数,确认有无协程泄漏
debug/pprof/mutex锁争用情况的采样信息
debug/pprof/threadcreate系统线程创建情况的采样信息
debug/pprof/trace程序运行跟踪信息

allocsheap 采样的信息一致,不过前者是从程序启动开始,所有对象的内存分配,而 heap 则是当前活跃对象的内存分配(The difference between the two is the way the pprof tool reads there at start time. Allocs profile will start pprof in a mode which displays the total number of bytes allocated since the program began (including garbage-collected bytes).)

排查内存占用过高

内存飙升有两种场景:栈内存占用过多和堆内存飙升,前者可以通过 /debug/pprof/ 页面查看 goroutine 数量是否异常排查,后者可通过 debug/pprof/heap 查看堆内占用内存总量,默认情况下,统计的是当前内存使用数(字节数或对象数量),即使用 --inuse_objects

go tool pprof --http=:8081 http://127.0.0.1:5053/debug/pprof/heap\?seconds\=10

使用 go tool pprof 采样获取堆内存占用情况,seconds 由我们自行指定采样时间,默认为 30s,通过制定 —http 表明我们在浏览器里进行交互,操作简单点。

inuse_space

类型含义
inuse_spaceamount of memory allocated and not released yet 当前在用的内存总量
inuse_objectsamount of objects allocated and not released yet当前在用的object总个数
alloc_spacetotal amount of memory allocated (regardless of released),从程序启动至今(不考虑内存释放)分配的内存总额
alloc_objectstotal amount of objects allocated (regardless of released)从程序启动至今(不考虑内存释放)分配的内存对象总个数

view

得到四列数据

列名含义
flat本函数的执行耗时
flat%flat 占 CPU 总时间的比例。程序总耗时 16.22s
sum%前面每一行的 flat 占比总和
cum累计量。指该函数加上该函数调用的函数总耗时
cum%cum 占 CPU 总时间的比例

其他类型,如 heap 的 flat, sum, cum 的意义和上面的类似,只不过计算的东西不同,一个是 CPU 耗时,一个是内存大小。

问题排查

发现是 goroutine 数量激增导致的,点开goroutine 信息发现 处于database/sql 这个堆栈的goroutine 数量为119224个、基本占据了goroutine 数量的大头。

排查发现 服务使用gorm连接 阿里云实时数仓Hologres(兼容postgres协议),发现大量慢sql 慢SQL原因是因为 hologres sql未走fixed plan

查看 Hologres 实际使用连接数并不高,配置的连接池上限20,故怀疑因大量慢查询导致20个连接持续使用中(不释放)其他请求只能等待连接释放后才能执行,导致处于等待状态的goroutine持续增多。进而导致内存飙升,通过请求日志的 trace 可见sql 执行长达26min。

原因总结

数据库连接数配置较低

sql未走fixed plan,导致平均请求拉长,平均sql耗时2-3秒

对应的连接句柄数持续堆积,goroutine++到11万,内存稳定持续增长,最后导致告警

修复方案

扩大连接数上限

修复sql逻辑,使其走fixed plan,服务器+hologres监控都趋于正常稳定

参考