Go 服务排查:pprof 实战
Go 服务的排查工具链比 Java 简洁:标准库自带 pprof,挂上接口就能拿到 CPU、内存、协程、阻塞四类数据。但简洁不等于安全——net/http/pprof 默认没有任何鉴权,挂到公网入口等于把进程内部结构、调用参数甚至内存快照公开出去。
接入:独立端口
import (
"net/http"
_ "net/http/pprof"
)
go func() {
// 独立端口,只监听内网地址
http.ListenAndServe("127.0.0.1:6060", nil)
}()
| 做法 | 是否推荐 | 原因 |
|---|---|---|
业务端口上挂 /debug/pprof | 不推荐 | 与业务同入口,最容易暴露到公网 |
| 独立端口 + 内网地址 | 推荐 | 不经过网关,不对外暴露 |
| 独立端口 + 鉴权中间件 | 推荐 | 需要跨机器抓取时使用 |
抓取本身也有代价:profile 会让服务做 30 秒 CPU 采样,heap 会做内存统计,?seconds= 被设成很大的值时采样开销会持续存在。因此这个端口必须只在内网可达。
五类 profile 的用途
| profile | 回答的问题 | 采样方式 | 开销 |
|---|---|---|---|
profile(CPU) | CPU 时间花在哪些函数 | 默认 30 秒采样 | 中,建议 30 秒以内 |
heap | 内存被谁占用、谁在分配 | 采样,默认 512KB 一个样本 | 低 |
goroutine | 协程有多少、都卡在哪 | 全量栈快照 | 低,抓栈有极短停顿 |
block | 谁在阻塞等待(channel、锁、IO) | 需先设置阻塞采样率 | 中 |
mutex | 锁竞争有多严重 | 需先设置竞争采样率 | 高,生产慎用 |
block 与 mutex 默认关闭,因为采样会影响性能:
runtime.SetBlockProfileRate(1000) // 每 1000 次阻塞采样一次,生产建议 1000 以上
runtime.SetMutexProfileFraction(100) // 每 100 次锁竞争采样一次,窗口期才开
取舍观点:block 可以在生产以很低采样率长期开启;mutex 只在排查锁竞争的窗口期开几分钟,采完立即设回 0。
常用命令与判读
# CPU 热点:采样 30 秒
go tool pprof http://10.0.0.12:6060/debug/pprof/profile?seconds=30
# 内存:默认看当前存活对象的 inuse_space
go tool pprof http://10.0.0.12:6060/debug/pprof/heap
go tool pprof -alloc_space http://10.0.0.12:6060/debug/pprof/heap # 看累计分配,找分配热点
# 协程
go tool pprof http://10.0.0.12:6060/debug/pprof/goroutine
# 阻塞与锁竞争
go tool pprof http://10.0.0.12:6060/debug/pprof/block
go tool pprof http://10.0.0.12:6060/debug/pprof/mutex
go tool pprof -http=:6060 http://10.0.0.12:6060/debug/pprof/profile?seconds=30
进入交互界面后:
| 命令 | 作用 | 典型用法 |
|---|---|---|
top | 按开销排序的函数列表 | top20 看前 20 名 |
top -cum | 按累计时间排序 | 找到调用链上游而不是叶子函数 |
list <函数名> | 逐行显示耗时分布 | list OrderService.Get 定位到具体行 |
web | 生成调用图,需要 graphviz | 看调用关系与宽度 |
判读要点:
观测值(top 输出) | 判断 |
|---|---|
flat 高且 cum 接近 flat | 该函数自身耗 CPU:计算、序列化、加解密 |
cum 高但 flat 低 | 只是调用链路过,热点在子调用里 |
热点在 runtime.mallocgc / runtime.gcDrain | 分配过多导致 GC 吃 CPU,去优化对象分配 |
热点在 encoding/json | 序列化开销大,考虑换编解码库或减少字段 |
heap 的 inuse_space 集中在某业务结构 | 内存泄漏或缓存无上限 |
GODEBUG=gctrace=1:读 GC 输出
GODEBUG=gctrace=1 ./app
输出形如:
gc 12 @3.402s 1%: 0.021+2.1+0.014 ms clock, 0.17+0.42/1.9/0.031+0.11 ms cpu, 32->34->17 MB, 40 MB goal, 8 P
| 字段 | 含义 | 判读 |
|---|---|---|
gc 12 @3.402s | 第 12 次 GC,进程启动后 3.4 秒 | 频率=次数/时间,超过每秒 1 次就偏高 |
1% | GC 占用的 CPU 比例 | 超过 10% 说明 GC 是主要开销 |
0.021+2.1+0.014 ms clock | STW 扫描 + 并发标记 + STW 收尾 | 两端之和是真实停顿,中间段是并发不阻塞 |
32->34->17 MB | GC 前堆、GC 后堆、存活堆 | 存活堆接近 GC 前堆=对象大量存活,疑似泄漏 |
40 MB goal | 下次 GC 的目标堆大小 | 目标长期贴着小值说明 GOGC 太激进 |
8 P | 使用的 P 数量 | 与 CPU 配额对照,容器里常被限制 |
GODEBUG=gctrace=1 ./app
生产注意事项
| 事项 | 建议 |
|---|---|
| CPU 采样 | 控制在 30 秒内,避免多个请求同时打 |
| 内存快照 | heap?debug=1 只给统计,debug=2 给完整栈且文件大,先看 debug=1 |
| mutex profile | 只在窗口期开,采完立即设回 0 |
| 端口暴露 | 仅内网、独立端口、加鉴权或 IP 白名单 |
| 抓取留证 | 采样文件归档到故障目录,便于复现与前后对比 |
排查流程
1. curl 'http://127.0.0.1:6060/debug/pprof/goroutine?debug=1' 先看协程数与栈聚合
2. go tool pprof -http=:6060 'http://127.0.0.1:6060/debug/pprof/profile?seconds=30'
3. top -cum 找调用链上游 → list <函数> 定位到行
4. GODEBUG=gctrace=1 或 runtime/metrics 确认 GC 开销
5. 修复后压测对比,同样位置再抓一次 profile
巡检项
/debug/pprof是否只在内网可达,用一次外网探测验证。- goroutine 数与堆内存进监控,设单调上涨告警。
- CPU 使用率与 GC CPU 占比分开监控,用于区分业务吃 CPU 与 GC 吃 CPU。
- Go 版本、
GOGC、GOMAXPROCS纳入配置管理,容器内必须显式设置GOMAXPROCS。
小结:pprof 五类 profile 各管一件事——CPU 找热点、heap 找内存、goroutine 找泄漏、block 与 mutex 找等待;用 top -cum 再 list 从调用链落到具体代码行,配合 GODEBUG=gctrace=1 区分业务 CPU 与 GC CPU,同时把端口限制在内网并控制采样开销。