必须手动在业务关键路径用runtime/pprof.StartCPUProfile精准包裹采样,而非依赖默认30秒HTTP盲采;因长连接程序热点低频高耗时,盲采易错过真实瓶颈,且并发调用StartCPUProfile会报“profile already in use”,需用sync.Once或信号机制串行控制。

pprof 采集不到长连接程序的 CPU profile?
默认情况下,net/http/pprof 的 /debug/pprof/profile 端点只采集 30 秒(可通过 seconds 参数调整),但长连接程序(如 WebSocket、gRPC server、TCP 连接池常驻服务)的 CPU 热点往往藏在低频但高耗时的处理逻辑里——比如单次消息解析、加密解密、协议编解码。30 秒盲采很可能错过真实热点,返回的 profile 是“平均稀释”后的假象。
真正有效的做法是:在业务关键路径(例如消息 handler 入口)手动触发 profile 采样,而非依赖定时 HTTP 拉取。
- 用
runtime/pprof.StartCPUProfile+StopCPUProfile在怀疑的函数前后精准包裹,例如:func handleMsg(conn net.Conn) { f, _ := os.Create("/tmp/handleMsg.pprof") defer f.Close() pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() // 实际业务逻辑:json.Unmarshal、crypto/aes.Decrypt、proto.Unmarshal... } - 避免在高频 goroutine(如每秒数千次的 ping/pong)里调用,否则 I/O 和 profile 开销会污染结果
- 采样期间不要重启服务或 reload 配置,否则
.pprof文件可能损坏
分析时发现 top 函数全是 runtime 或 syscall?
这通常不是 profile 本身有问题,而是 Go 程序在等待系统资源——比如长连接程序卡在 read 系统调用上(syscall.Syscall)、GC 停顿(runtime.gcStart)、或 goroutine 调度竞争(runtime.mcall)。这些函数占高,恰恰说明 CPU 并没真忙,瓶颈在 I/O 或调度层。
关键判断依据是看 pprof -top 输出中是否包含你自己的包路径(如 myapp.(*Conn).handleLoop)。如果没有,说明 profile 捕获时段内你的业务代码根本没执行,或者被阻塞住了。
- 先运行
go tool pprof -http=:8080 your_binary /tmp/handleMsg.pprof,打开 Web UI 查看「Flame Graph」,聚焦非runtime/和syscall/的叶子节点 - 若火焰图中大量 goroutine 停在
net.(*conn).Read或internal/poll.runtime_pollWait,说明是网络 I/O 等待,不是 CPU 瓶颈,该查连接复用、buffer 大小或对端响应延迟 - 用
go tool pprof -symbolize=none可跳过符号解析,快速确认原始地址是否属于你的代码段
为什么 web handler 里调 pprof.StartCPUProfile 报错 “profile already in use”?
runtime/pprof 不支持并发 CPU profile;同一时刻只能有一个活跃的 CPU profile。长连接程序常有多路连接并行处理,多个 goroutine 同时调用 StartCPUProfile 就会触发这个错误。
安全做法是加全局互斥 + 限频,而不是靠运气错开时间:
- 用
sync.Once或atomic.CompareAndSwapUint32控制仅允许一次采样启动,例如在服务启动时就开启,持续 5 分钟后自动停 - 更实用的是改用信号触发:在
main中注册os.Interrupt或自定义 signal(如SIGUSR1),收到信号再启停 profile,避免竞态 - 绝对不要在 HTTP handler 里直接调
StartCPUProfile,除非你明确做了串行化(比如 channel + 单 goroutine 转发)
pprof 显示某个函数耗时高,但源码里它只是个简单 map 查找?
Go 的 pprof 默认采样的是“CPU 时间”,但 map 查找本身极快;如果它出现在 top 列表,大概率是因为该函数被高频调用(比如每秒数万次),或者它触发了隐式操作——最常见的是 map 扩容(runtime.mapassign)或 GC 扫描(runtime.scanobject)。
这时要结合「flat」和「cum」两列看:flat 是函数自身执行时间,cum 是包含子调用的累计时间。如果 cum 远高于 flat,说明热点在其调用链下游;如果两者接近,才说明是它自己慢。
- 检查该 map 是否在写多读少场景下频繁扩容:用
make(map[T]V, expected_size)预分配容量 - 确认 key 类型是否实现了高效
Hash()和Equal()(比如用string比用结构体做 key 更稳妥) - 用
go tool pprof -alloc_space对比内存分配 profile,看是否该函数引发大量临时对象(进而推高 GC 压力)


















