fgprof能同时抓io和cpu样本,因为它基于go运行时调度钩子而非信号采样,统一记录事件时间戳并按调用栈聚合,从而在火焰图中既显示runtime.futex(io阻塞)也显示crypto/sha256.block(cpu密集)。

fgprof 为什么能同时抓 IO 和 CPU 样本
因为 fgprof 不是靠操作系统信号(如 SIGPROF)做周期采样,而是用 Go 运行时自带的 runtime/pprof 底层机制,在 Goroutine 调度点、系统调用进出、GC 暂停等关键位置主动插入采样钩子。它把 CPU 时间和阻塞时间(比如 read、write、netpoll 等系统调用等待)统一记录为“事件时间戳”,再按调用栈聚合——所以一张火焰图里,你既能看到 runtime.futex(IO 阻塞),也能看到 crypto/sha256.block(CPU 密集)。
常见错误现象:用 pprof 默认的 CPU profile 抓不到 IO 等待;用 blocking profile 又看不到 CPU 消耗。两者叠加分析时对不上时间线,就是没用对采集方式。
-
fgprof必须在程序启动后尽早启用,越晚开启,越可能漏掉初始化阶段的 IO/CPU 混合热点 - 不能和
net/http/pprof的/debug/pprof/profile同时长期运行,会互相干扰调度钩子 - 默认采样间隔是 99Hz(约 10ms),高并发下开销可控;但若服务 QPS 超 10k,建议临时调低到
50Hz避免调度抖动
怎么加到现有 Go 服务里不改主逻辑
最轻量的方式是通过 init() 注册,或者在 main() 开头启动 goroutine。不需要侵入业务代码,也不依赖 HTTP 接口。
示例:
立即学习“go语言免费学习笔记(深入)”;
import _ "github.com/felixge/fgprof"
func main() {
// 启动 fgprof HTTP handler(可选,仅用于按需抓取)
go http.ListenAndServe("localhost:6060", nil)
// 或直接后台常驻采样(推荐调试期)
go func() {
log.Println("fgprof started")
fgprof.Start()
select {}
}()
// 你的原有服务逻辑...
}
注意:fgprof.Start() 是阻塞式启动,必须放在 goroutine 里;否则主流程卡住。如果用了 http.ListenAndServe,记得确保端口没被占用,且只暴露给内网——/debug/fgprof 接口无鉴权。
- 不要在测试环境用
go test -cpuprofile同时跑fgprof,会导致 runtime 调度器状态错乱 - 若服务已用
pprof.Register自定义了 profile,要确认没覆盖fgprof的注册名(它注册的是"fgprof") - 交叉编译(如
GOOS=linux GOARCH=arm64)时,确保目标平台支持getrusage和clock_gettime,否则部分 IO 时间统计为空
看火焰图时怎么区分 IO 等待和真实 CPU 占用
关键看帧标签和调用栈底部函数。fgprof 输出的火焰图中,纯 CPU 样本底部是 runtime.mstart 或 runtime.goexit;而 IO 等待样本底部一定是系统调用相关函数,比如 runtime.syscall、runtime.netpoll、internal/poll.runtime_pollWait,再往上才是你的业务函数(如 http.(*conn).serve)。
容易踩的坑:
- 看到
runtime.gopark就以为是锁竞争?不一定——它也出现在io.ReadFull等同步 IO 中,得看上一级是不是internal/poll.(*FD).Read - 同一行代码(比如
json.Unmarshal)在火焰图里分两块:上面是 CPU 解析,下面是底层bufio.Reader.Read等待磁盘或网络,别误判成两个独立问题 - 如果大量样本堆在
runtime.futex且调用栈里没有 net/http 或 database/sql,大概率是 mutex 争用,不是 IO——这时该切到mutexprofile 验证
导出数据后怎么快速定位混合瓶颈
别直接扔进 pprof GUI。先用命令行过滤关键维度:
curl -s "http://localhost:6060/debug/fgprof?seconds=30" > fgprof.pb go tool pprof -http=:8080 fgprof.pb
然后在 Web 界面左上角切换 “Sample” 类型:选 samples(总事件数)看整体分布;选 inuse_space 没意义(fgprof 不采内存);重点看 duration,它代表该栈实际消耗的时间(含阻塞)。
- 搜索框输入
Read或Write,快速定位所有 IO 相关栈;再配合cpu关键字筛出 CPU 密集路径 - 右键某个热点函数 → “Focus” → 再点 “Diff” 对比前后两次采集,能看出是 IO 延迟升高了,还是 CPU 计算变慢了
- 如果
net/http.(*conn).serve占比高,但子栈里crypto/tls.(*Conn).Read和io.copy各占一半,说明 TLS 握手 + 大包传输共同拖慢了请求——这时候优化方向就明确:要么换更轻量的 TLS 配置,要么加缓冲区
真正难的不是采集,是判断哪一层阻塞可优化、哪一层是下游依赖无法控制。比如 database/sql.(*DB).QueryRow 下的 net.Conn.Read,可能是 DB 响应慢,也可能是连接池空闲连接不足——得结合 pg_stat_activity 或 SHOW PROCESSLIST 交叉验证。










