真cpu突发飙升极少见,90%是goroutine泄漏、锁争用或gc抢占伪装;需用pprof.startcpuprofile/stopcpuprofile手动圈定真实计算段,避免?seconds=30采样稀释热点,火焰图中runtime.futex高时应查/block?debug=2而非优化锁。

真 CPU 突发飙升极少见,90% 是 goroutine 泄漏、锁争用或 GC 抢占伪装出来的——pprof 能定位,但必须避开采样和解读的典型陷阱。
怎么抓到不被 runtime 掩盖的真实 CPU 样本
直接用 go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 最省事,但线上服务波动大时容易采到“假高峰”:前 10 秒高负载,后 20 秒空闲,热点被稀释。更可靠的做法是手动控制边界:
- 在 HTTP handler 入口加
pprof.StartCPUProfile(f),出口加pprof.StopCPUProfile(),只捕获该次请求真实计算段 - 测试阶段优先用
go test -cpuprofile cpu.out .,完全排除干扰 - 别用
?seconds=5:周期性任务(比如每 10 秒跑一次的定时器)根本抓不到 - 验证是否成功:运行
go tool pprof -top cpu.pprof,第一行应显示类似Showing nodes accounting for 28.45s, 97.2% of 29.26s total,且能列出业务函数名
火焰图里全是 runtime.futex,该查什么而不是改锁
runtime.futex 占比高 ≠ 你写的 sync.RWMutex 实现有问题,而是调度器在疯狂切出/切入 goroutine。典型信号包括:
-
runtime.futex+runtime.mcall同时高:大量 goroutine 卡在chan receive或无界for循环的default分支空转 -
runtime.gopark高:查/debug/pprof/block?debug=2,看是不是 channel send/recv、time.Sleep或锁等待阻塞了成百上千个 goroutine -
net/http.readRequest高:瓶颈在 TLS 握手或 HTTP 解析,不是 handler 逻辑——先加超时、换 fasthttp 或调优 TLS 配置
此时 top10 没用,进交互后输 top -cum,看调用链顶端是谁触发了这些阻塞点。
看到 json.Marshal 或 time.Now 占满火焰图,别优化算法
如果 encoding/json.(*encodeState).marshal 或 time.Now 在火焰图里铺开成扁平宽条,说明它们被高频、无条件调用,不是函数本身慢,而是调用模式错了:
- 检查是否每次循环都
new一个 struct 或 map:反射开销 + 内存分配双高,考虑提前序列化缓存,或实现json.Marshaler接口绕过反射 -
time.Now()出现在日志打点里?换成zap.Sugar().Debugw("msg", "ts", time.Now())并加条件包裹,避免无意义调用 -
strings.Builder.Write或bytes.Buffer.Write排在-alloc_space top前三:拼接逻辑在持续 new 底层切片,改用预分配或复用sync.Pool
goroutine 数量暴涨却查不到泄漏点?debug=2 是开关
/debug/pprof/goroutine 默认只返回状态为 running 或明显阻塞(如 chan send)的 goroutine,大量“活着但卡死”的会被过滤掉。必须加参数:
- 用
curl "http://localhost:6060/debug/pprof/goroutine?debug=2"获取全量栈,搜索重复出现的函数名(比如 80% 的 goroutine 都停在myClient.Do) - 对比趋势:每秒执行一次,看数字是否持续上涨;若从 500 → 5000 → 50000,基本可定性为泄漏
- 泄漏高发场景:HTTP 客户端没设超时、数据库连接未 Close、WebSocket 连接未注销、定时器未 Stop
真正难的从来不是命令怎么敲,而是看到 runtime.gopark 占 40% 时,能不能立刻想到去查 /debug/pprof/block?debug=2,而不是盯着 handler 一行行翻代码。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











