)
一、pprof 是什么:先建立正确的心理模型pprof 不是一个工具,而是一条流水线:┌─────────── 采集端(在你的进程里)───────────┐ ┌─── 分析端(在你的终端里)───┐ │ │ │ │ │ runtime 采样/记账 ──► pprof proto 格式 │ ───► │ go tool pprof │ │ (mprof.go/cpuprof.go) (runtime/pprof) │ 文件 │ top / list / 火焰图 │ │ │ 或HTTP│ │ └──────────────────────────────────────────┘ └──────────────────────────┘采集端在 runtime 内部:CPU 剖析靠时钟中断(SIGPROF / Windows 专用线程),内存/阻塞/锁剖析靠在mallocgc、gopark、semrelease等关键路径上记账(这就是源码系列的mprof.go)。格式端由runtime/pprof包把 runtime 内部的原始数据编码成pprof protobuf 格式(.pb.gz)。分析端是go tool pprof:它本身是 Google pprof 的内置版本,负责符号化、聚合、火焰图渲染。记住这个分工,后面读源码时就不会迷路:你调的每个 API、访问的每个 URL,都精确对应流水线上的一段代码。图pprof 三段流水线采集端 → 格式端 → 分析端pprof 与 trace 的分工:pprofgo tool trace回答的问题哪里耗 CPU/内存/锁为什么慢(调度、GC、系统调用的时序)数据形态按调用栈聚合的统计值全量事件时间线开销低(采样)中等(1.21 重写后 1~2%)典型用法常态监控、定期抓取出问题后定点抓 5~30 秒二、四种启用方式方式 1:net/http/pprof 常驻 HTTP 端点(生产最常用)packagemainimport(lognet/http_net/http/pprof// ① 只为副作用:init() 里向 DefaultServeMux 注册 /debug/pprof/*)funcmain(){// ② 你的业务 http server 如果用了 DefaultServeMux,什么都不用加// ③ 没有业务 server 时,单独起一个,只绑本机回环!gofunc(){log.Println(http.ListenAndServe(localhost:6060,nil))}()// ... 业务代码select{}}1.24.0 源码里,init()注册了 5 个精确路径(net/http/pprof/pprof.go:95-105):路径处理函数作用/debug/pprof/IndexHTML 索引页;其他所有 profile 名也由它分发/debug/pprof/cmdlineCmdline进程启动命令行/debug/pprof/profileProfileCPU profile(默认 30 秒)/debug/pprof/symbolSymbolPC → 函数名映射(pprof 工具按需调用)/debug/pprof/traceTrace执行 trace(默认 1 秒)而heap、goroutine、block、mutex、allocs、threadcreate并没有单独注册——请求/debug/pprof/heap时由Index判断前缀后转给handler(name)动态查找(pprof.go:386-392)。安全要点(1.22 起 pprof 路径强制 GET;pprof.go:97-99根据httpmuxgo121决定是否带GET前缀注册):永远绑localhost,要远程抓取就用kubectl port-forward/ssh -L。生产环境要暴露时,自己起 mux 加认证中间件:mux:http.NewServeMux()mux.Handle(/debug/pprof/,basicAuth(pprof.Index))// pprof.Handler(name) 可精确到单个 profilemux.Handle(/debug/pprof/profile,basicAuth(pprof.Profile))mux.Handle(/debug/pprof/trace,basicAuth(pprof.Trace))方式 2:runtime/pprof 显式抓取(脚本/批处理程序)适合跑完就退出的程序——必须在退出前 Stop,否则丢最后一批数据(参见坑 #1):packagemainimport(osruntime/pprof)funcmain(){f,_:os.Create(cpu.pprof)pprof.StartCPUProfile(f)// 内部:runtime.SetCPUProfileRate(100)deferpprof.StopCPUProfile()// 内部:SetCPUProfileRate(0) 等读goroutine排干fm,_:os.Create(mem.pprof)runtime.GC()// 先 GC:让 heap profile 反映存活对象而非垃圾pprof.WriteHeapProfile(fm)// 等价 Lookup(heap).WriteTo(fm, 0)fm.Close()}方式 3:go test 旗标(最零成本)gotest-bench.-benchmem-cpuprofilecpu.pprof-memprofilemem.pprof-outputdir./prof gotest-runXXX-bench.-blockprofileblock.pprof-blockprofilerate1000# 每 1µs 阻塞采一次gotest-runXXX-bench.-mutexprofilemutex.pprof-mutexprofilefraction5# 1/5 的锁事件gotest-tracetrace.out-bench.-outputdir指定输出目录;-memprofilerate可调内存采样粒度(默认 512KB,见第五节)。方式 4:runtime 直接控制(基本不用,懂语义即可)runtime.SetCPUProfileRate(500)// 默认 100;0 开启,0 关闭;运行中改频率必须先关runtime.SetBlockProfileRate(1e6)// 平均每阻塞 1ms 采一次runtime.SetMutexProfileFraction(4)// 平均 1/4 的锁事件被采注意:runtime.CPUProfile()在 1.9 起已废弃并 panic(cpuprof.go:208-210),别在旧博客里抄到它。图pprof 四种启用方式对比三、八类 profile 逐个讲透浏览器打开http://localhost:6060/debug/pprof/能看到完整列表。逐个说明(描述与 1.24.0profileDescriptions一致):3.1 profile —— CPU 剖析go tool pprof http://localhost:6060/debug/pprof/profile?seconds30机制:默认100 Hz,每 10ms 中断一次,记录当前 goroutine 栈。30 秒 ≈ 3000 个样本。回答CPU 时间花在哪些函数。统计的是On-CPU 时间(不包括阻塞等待,但包括密集自旋)。seconds不传默认 30 秒(pprof.go:146-149)。CPU 剖析是所有线程共享的:多核并行时,每秒样本数 ≈ 100 × 忙碌线程数。3.2 heap —— 存活对象内存采样curl-oheap.pprofhttp://localhost:6060/debug/pprof/heap?gc1# 抓之前先强制一次 GCgo tool pprof-sample_indexinuse_space heap.pprof机制:不是扫描堆,而是在mallocgc里按MemProfileRate(默认每 512KB)采样记账,free 时销账。所以它反映谁分配的内存还活着。四个视图:inuse_space(默认,存活字节)/inuse_objects/alloc_space(累计分配字节)/alloc_objects。?gc1参数触发runtime.GC()(pprof.go:261-264),排查泄漏时强烈建议带上——先排掉垃圾,剩下的才值得看。图heap/allocs 的四个 sample_index 视图存活×空间/对象 × 累计找泄漏的杀手锏是 delta 模式(1.20):heap?seconds30两次抓取求差,1.24 支持 delta 的有 allocs/block/goroutine/heap/mutex/threadcreate 六种(profileSupportsDelta,pprof.go:353-360)。3.3 allocs —— 累计分配采样与 heap 同源,只是默认视图为alloc_space。GC 压力大、频繁分配导致 CPU 高时看它:热点通常在序列化、反射、字符串拼接。3.4 goroutine —— 所有 goroutine 栈go tool pprof http://localhost:6060/debug/pprof/goroutinecurlhttp://localhost:6060/debug/pprof/goroutine?debug1# 文本:每栈一条计数curlhttp://localhost:6060/debug/pprof/goroutine?debug2# 与panic相同的全量栈转储debug2输出等价于未恢复 panic 时的完整 goroutine dump,排查卡住的 goroutine 在等谁最直观。pprof 格式下Count列就是每条栈的 goroutine 数量——重复出现的栈 泄漏嫌疑。抓取时会短暂 stop-the-world,高频抓取有感知,别做秒级轮询。3.5 block —— 阻塞剖析(默认关!)runtime.SetBlockProfileRate(1e6)// 必须先开:平均每 1ms 阻塞采样一次记录goroutine 在同步原语上等了多久:channel 收发、sync.WaitGroup、select、sync.Mutex的Lock阻塞段。回答并发程序空转等在哪——吞吐上不去但 CPU 不高时,第一个看它。rate 语义是纳秒:阻塞越久的事件被采到的概率越大,最终统计是无偏加权的(源码 mprof.go 的saveblockevent,见第 06 篇)。rate1 表示全采;rate0 关闭。3.6 mutex —— 锁竞争剖析(默认关!)runtime.SetMutexProfileFraction(5)// 平均 1/5 的锁竞争事件被采记录的是持有锁者的栈(谁拿着锁害别人等),不是等锁者——想看谁在等用 block。默认只统计sync.Mutex/sync.RWMutex。图block 记「等锁者」、mutex 记「持锁者」——一个病两个视角3.7 threadcreate —— 线程创建栈记录每个 OS 线程创建时的栈。主要用于诊断goroutine 不多但线程暴涨(典型:cgo 调用、长时间阻塞的 syscall 未释放 P)。无法关闭,数据量极小。3.8 cmdline / symbol —— 辅助端点cmdline:输出启动参数(NUL 分隔),确认你抓的是不是你以为的那个进程。symbol:pprof 工具自动调用,抓取时如果二进制已删除符号(容器场景),pprof 会回源这里要符号——理解它有助于排查符号显示不了的问题。图八类 Profile 逐个讲透描述与 go1.24.0 一致四、go tool pprof 分析姿势4.1 三种启动形态# A. 交互式 REPL(本地文件)go tool pprof cpu.pprof# B. 直接打线上(不落盘,pprof 自动 fetch 符号化)go tool pprof http://localhost:6060/debug/pprof/heap go tool pprof-seconds30http://localhost:6060/debug/pprof/profile# 等价 ?seconds30# C. 网页版(强烈推荐:火焰图、调用图全在这)go tool pprof-http:8080 cpu.pprof容器里没有浏览器?-http127.0.0.1:8080kubectl port-forward。完全离线机器用 A,把.pprof文件和带符号的二进制一起拷回来,go tool pprof binary cpu.pprof显式给符号。4.2 REPL 高频命令(pprof) top10 # 按当前采样值排序前 10 (pprof) top10 -cum # 按累计值排序(含被调函数)—— 找入口大户 (pprof) list hotFunc # 逐行标注该函数源码(需要符号源码路径) (pprof) disasm hotFunc # 反汇编级标注 (pprof) peek hotFunc # 谁调用了它 / 它调用了谁 (pprof) traces # 打印原始采样栈(不想聚合时用) (pprof) web # 生成调用图(需 graphviz) (pprof) sample_index alloc_space # heap/allocs 切视图(inuse/alloc × space/objects) (pprof) set focusEncode # 只看匹配的函数(支持正则) (pprof) set ignoreruntime.* # 排除噪音-cum是新手最容易漏的开关:top默认按自身(flat)排序,只看它会错过main.mainloop 间接占了 80%这种结构性结论。4.3 火焰图怎么读-http:8080→ View → Flame Graph:宽度 采样占比(CPU 剖析下即 CPU 时间占比),高度 栈深。读法:从下往上找宽而平的塔尖——顶得越宽,这函数自身烧的 CPU 越多。两种翻转视角:Flame Graph(默认,自底向上调用栈)和Flame Graph (reversed)(自顶向下,按被调函数聚合,找谁在总量上最大极快)。点击任意框可下钻;Search高亮匹配函数。图火焰图读法宽度采样占比从下往上找宽而平的塔尖4.4 对比模式(排查泄漏/回归的第一利器)go tool pprof-http:8080-baseheap_t0.pprof heap_t1.pprof# 三点对比(中间值自动推断):go tool pprof-http:8080-baseheap_t0.pprof-diff_baseheap_t1.pprof heap_t2.pprof显示的是t1 − t0 的增量。排查内存泄漏:间隔 10 分钟抓两次,top看正增长栈;回归排查:优化前后各抓一次 CPU。图对比模式-base显示 t1−t0 的增量4.5 标签(labels)给采样打上业务维度,聚合时按维度拆分(1.24 中pprof.Do/pprof.Labels,runtime/pprof包):pprof.Do(ctx,pprof.Labels(tenant,acme,route,/pay),func(ctx context.Context){handlePayment(ctx)})(pprof) tags # 列出所有标签 (pprof) top -tagfocus acme # 只看某租户多租户服务里,这比全服务聚合有意义得多——也正好和你 OTel 文档里的按 route 强制采样思路一致。图pprof.Do Labels 给采样打业务维度五、采样率 API 与默认值API默认语义调用建议CPU 频率100 Hz每秒中断次数一般不动;短生命周期 bench 想要更多样本可到 500runtime.MemProfileRate512 KB平均每分配 512KB 记 1 笔想抓小对象热点可临时改 64KB(必须在最早期设置;运行中改只影响新 bucket)SetBlockProfileRate(ns)0(关)平均每阻塞 ns 记 1 笔,长事件概率高、带反向加权1e6(1ms)起步;全采会显著变慢SetMutexProfileFraction(n)0(关)平均 1/n 的竞争事件被记5~100 视竞争频率内存采样的直觉:采样字节数/512KB ≈ 该栈的样本数,所以 pprof 里的值是按比例还原的估计值,不是精确计数——这是它开销低的代价。六、实战手册:五类问题的标准排查动作6.1 CPU 高go tool pprof-http:8080-seconds30http://localhost:6060/debug/pprof/profile火焰图宽顶 →list下钻 → 三种典型:GC 占比大(转 allocs)、lock/atomic 自旋(转 mutex)、runtime.mallocgc高(转 allocs 减分配)。6.2 内存持续上涨(疑似泄漏)curl-oh0.pprofhttp://localhost:6060/debug/pprof/heap?gc1sleep600curl-oh1.pprofhttp://localhost:6060/debug/pprof/heap?gc1go tool pprof-http:8080-baseh0.pprof h1.pprof正增长的栈即泄漏点。注意区分:inuse 增长 真泄漏;alloc 增长但 inuse 平 只是流量大。6.3 goroutine 泄漏curlhttp://localhost:6060/debug/pprof/goroutine?debug2|less全局搜chan receive/select/sync.WaitGroup卡住的栈,数数量;回代码看谁没 close。1.24 里 goroutine dump 的每条记录头有minutes live字段——存活几十分钟的空闲 goroutine 就是尸体。6.4 吞吐低但 CPU 闲(并发瓶颈)先开 rate(程序需重启或早期调用):runtime.SetBlockProfileRate(1e6)、runtime.SetMutexProfileFraction(10),压测 5 分钟后:go tool pprof-http:8080 http://localhost:6060/debug/pprof/block go tool pprof-http:8080 http://localhost:6060/debug/pprof/mutexblock 显示等在哪,mutex 显示谁拿着锁——一个病,两个视角。6.5 定位偶发毛刺pprof 是聚合统计,天然看不到毛刺——这正是 trace 的主场:毛刺发生时段抓 5 秒 trace(第 02 篇),先看 GC 停顿/调度延迟,再回头用 pprof 精确定位函数。七、与 go test / benchmark 联动标准三件套:gotest-benchBenchmarkParse-benchmem-count5\-cpuprofilecpu.pprof-memprofilemem.pprof\-outputdir./prof ./pkg/...-count5benchstat做统计显著性对比;pprof 文件用任意一次即可(采样结论稳定)。bench 与 profile 同跑时,对比实验必须同参数重跑——开 profile 本身有 ~1-3% 开销,混跑数据不可比。-memprofilerate65536提高内存采样密度后再 bench,alloc 火焰图会细很多。自写剖析入口的模板(bench 里想要 pprof 精确控制时):funcBenchmarkX(b*testing.B){f,_:os.Create(x_cpu.pprof)pprof.StartCPUProfile(f)deferpprof.StopCPUProfile()b.ResetTimer()fori:0;ib.N;i{...}}八、常见坑 Top 10#现象原因 → 解法1文件末尾数据缺失/损坏没调StopCPUProfile就退出了 →defer,或 HTTP 端点等 30 秒自然结束2线上抓 profile 把服务打挂pprof 端口暴露公网被扫描 → 只绑 localhost port-forward3heap 全是 runtime.goregister?/符号乱容器里 strip 过的二进制 → 保留符号,或用symbol端点回源;pprof 指定-symbolizeremote4block/mutex 一直是空的忘了开 rate(默认关!) →SetBlockProfileRate/SetMutexProfileFraction5两台机器的 profile 没法对比不同二进制/不同 Go 版本符号不同 → 同构建产物抓取6goroutine 数几十万,pprof 打不开?debug2文本 grep -c先数,再分批看;根治是修泄漏7CPU profile 显示的占用总和≠100%×核数100Hz×忙碌线程才是分母;空闲核不产样本 → 看相对占比而非绝对值8list 显示不了源码源码路径变了/pprof 找不到 →-source_path或在原目录跑9delta profile 报参数错误该类型不支持 seconds(如 cpu) → 1.24 仅六种支持(profileSupportsDelta)10采样后性能明显下降rate 调太狠(全采 block) → 放大 rate;短窗口诊断用,别常开