CPU飙高通常不是队列本身在“干活”,而是消费者卡在阻塞、重试、无界重入或低效序列化上;pprof需优先查/block和/goroutine,而非/profile,因真实瓶颈多在runtime.chansend、net/http.roundTrip等阻塞点,而非CPU计算热点。

直接结论:CPU飙高通常不是队列本身在“干活”,而是消费者卡在阻塞、重试、无界重入或低效序列化上;pprof 能快速锁定是 runtime.chansend、json.Marshal 还是 time.Sleep 在吃 CPU,而不是猜“是不是队列太忙”。
为什么 /debug/pprof/profile 采不到真实热点?
高并发队列场景下,profile 接口返回一片平(全是 runtime.mcall 或 runtime.park),说明 CPU 并没被业务逻辑占用,而是在等——等 channel 写入、等锁释放、等下游响应。这不是 CPU profile 的适用场景。
- 确认是否真有请求打到服务:检查 access log、
netstat -an | grep :6060是否有 ESTABLISHED 连接 - 确保监听地址不是
localhost:6060,而是0.0.0.0:6060,否则压测流量进不来 - Go 1.20+ 必须显式导入
import _ "net/http/pprof",否则所有/debug/pprof/*endpoint 默认禁用 - 如果只跑了几秒就去拉
/profile?seconds=30,结果必然空——要么等满 30 秒,要么立刻切到/goroutine?debug=2或/block
查阻塞:/debug/pprof/block 是第一入口
队列类服务 CPU 飙高的真正元凶,80% 出现在阻塞点:channel 满了没人读、select default 不做退避、DB 连接池耗尽、HTTP 客户端没设超时。这些不会让 CPU 持续跑满,但会让 goroutine 大量堆积在 park 状态,间接拖垮调度器。
- 访问
/debug/pprof/block?debug=1,看 top 函数里是否有高比例的runtime.chansend、runtime.semasleep或database/sql.(*DB).QueryRow - 若
runtime.chansend占比超 30%,说明生产者远快于消费者,且 channel 无缓冲或缓冲区太小;别急着加 buffer,先看消费者为何慢 - 若出现大量
net/http.(*persistConn).roundTrip,说明 HTTP 客户端没设Transport.ResponseHeaderTimeout或Timeout,一次卡顿传染整条链路
查 Goroutine 泄漏:/debug/pprof/goroutine?debug=2
队列服务最典型的泄漏模式:每来一个任务就 go func() { ... }(),但没配 context.WithTimeout 或没等 sync.WaitGroup;或者日志写 channel 不限速、不带 select default,导致 goroutine 堆积后持续调用 runtime.gopark。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 执行压测 5 分钟后,对比
runtime.NumGoroutine()初始值和当前值——增长超过 ±20% 就算可疑 -
/goroutine?debug=2输出中重点关注状态为chan receive、select、semacquire的堆栈,它们往往指向未关闭的 channel、死锁的 select 或无限重试逻辑 - 特别警惕
http.HandleFunc里裸写go func() { ... }():没有 context 控制生命周期,一旦下游慢,协程就永远挂起
查高频分配:/debug/pprof/heap?alloc_space
很多“CPU 飙高”其实是 GC 压力大导致的假象——每秒分配几 MB 的 []byte 或 map[string]interface{},触发频繁 STW,让 CPU 时间花在垃圾回收上,而非业务逻辑。
- 用
go tool pprof http://localhost:6060/debug/pprof/heap?alloc_space查累计分配字节数,top命令看谁在疯狂 new 对象 - 常见罪魁:无节制的
json.Unmarshal+json.Marshal、反复bytes.Buffer.Grow、每次消费都 new struct{} 而非复用对象池 - 注意区分
inuse_space(当前存活)和alloc_space(历史总分配):前者看内存泄漏,后者看 GC 压力源
真正难啃的点往往藏在「等待链」里:一个 goroutine 卡在 channel send,是因为另一个卡在 HTTP client roundTrip,而它又卡在 DB QueryRow,最终根因是某张表缺索引。pprof 不会自动帮你串起这条链,但它能让你在 3 分钟内确认哪一环最先堵住——别从 CPU 入口硬刚,先看 /block 和 /goroutine。


















