go的block profile默认关闭,必须显式调用runtime.setblockprofilerate(1)并在main开头执行,同时导入_ "net/http/pprof"注册handler,否则/debug/pprof/block返回空或404。

block profile 默认不采集,必须显式开启
Go 的 block profile 默认关闭,访问 /debug/pprof/block 会返回空或 404,不是服务没跑起来,而是根本没启用采集。关键动作只有两个:runtime.SetBlockProfileRate(1) 和导入 _ "net/http/pprof"。
必须在 main() 函数最开头、任何 goroutine 启动前调用 runtime.SetBlockProfileRate(1)(生产环境建议设为 100 或 1000);否则早期阻塞事件直接漏掉。值为 1 表示每个阻塞事件都记录,单位是纳秒——但注意:它控制的是「采样精度」,不是「采样时长」。
常见错误:
- 把
SetBlockProfileRate放在 HTTP server 启动之后,结果 profile 里全是 runtime 底层调用,没业务函数 - 只导入
"net/http/pprof"而不是_ "net/http/pprof",导致 handler 没注册,/debug/pprof/路径 404 - 用了自定义
http.ServeMux(如 Gin、Echo),却没手动挂载 pprof handler,DefaultServeMux里压根没路由
阻塞点不在“谁卡住了”,而在“谁花了最多等待时间”
block profile 统计的不是当前有多少 goroutine 堵着,而是所有同步原语(锁、channel、网络 I/O)累计阻塞的**总时长**。一个函数在 top 里占比高,可能是因为它被反复调用并每次等 10ms,也可能是 100 个 goroutine 同时堵在它里面等 1ms。
真正要盯的是调用栈顶部的业务函数,而不是底部的 runtime.gopark 或 runtime.semasleep。例如:
- 看到
sync.(*Mutex).Lock上面紧跟着UpdateUserCache,问题就在UpdateUserCache里锁没及时释放 - 看到
net.(*pollDesc).wait或internal/poll.runtime_pollWait,大概率是没设超时的conn.Read()或http.Client.Do() - 看到
runtime.gopark+chan receive,且上面是handleOrder,说明handleOrder在等某个无缓冲 channel 或接收方已退出
别只看 flat 值——它只算该函数自身的阻塞时间;sum 才包含整个调用链,更反映真实影响。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
抓不到数据?先确认三件事:负载、时长、阻塞阈值
执行 wget -O block.prof 'http://localhost:6060/debug/pprof/block?seconds=30' 后发现文件几乎为空,或 go tool pprof 提示 sampled 0 out of X goroutines,通常不是工具问题,而是条件没满足:
- 程序当前没有真实阻塞压力:压测没打满、QPS 太低、请求路径根本没走同步逻辑
-
?seconds=30是 HTTP handler 的响应超时,不是 profile 采样窗口——实际是否采到,取决于SetBlockProfileRate设的阈值和当时有没有发生阻塞 - 默认阻塞采样阈值是 1ms,小于 1ms 的等待会被忽略;若怀疑短时阻塞是瓶颈,可临时设
runtime.SetBlockProfileRate(10000)(10 微秒),但线上禁用
如果 profile 里全是 runtime.notesleep 或 runtime.semasleep,没业务函数,说明阻塞发生在标准库底层(比如 http.Server 的 accept loop),这时得配合 /debug/pprof/goroutine?debug=2 看完整栈,或用 go tool trace 定位调度延迟。
Gin/Echo 等框架里 pprof handler 容易 404
框架没用 http.DefaultServeMux,_ "net/http/pprof" 的副作用就失效了,/debug/pprof/ 路由不会自动注册。
正确做法是手动挂载:
- Gin:
router.Any("/debug/pprof/*pprof", gin.WrapH(http.DefaultServeMux)) - Echo:
e.GET("/debug/pprof/*pprof", echo.WrapHandler(http.DefaultServeMux)) - go-zero:
Profile: { Enable: true }配置项即可,不用手写
别图省事把 pprof 绑到主业务端口——必须单独监听 127.0.0.1:6060 或内网地址,禁止外网访问。线上暴露 :6060 等同于把内存快照、CPU profile 全送给攻击者。
最后提醒一句:block profile 只捕获进入 park 状态的阻塞;死循环、空 select{}、无限重试不 sleep 的逻辑,它完全看不见——这种得切到 cpu profile 或 trace 查。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










