在 Metrics、Logs、Traces 三大可观测性信号之外,Profiling(性能剖析)正在成为生产可观测性的第四支柱。指标告诉我们「系统慢了」,链路告诉我们「慢在哪个服务」,日志告诉我们「发生了什么错误」,而 Profiling 回答一个更本质的问题:「CPU 的每一个时钟周期、堆上的每一个字节,究竟花在了哪一行代码上?」
传统上 Profiling 只在本地调试或性能攻坚阶段使用,一次采样、一个火焰图,问题解决后数据也被丢弃。这种「事件驱动」的剖析方式在微服务时代逐渐失效:当问题只在生产流量下复现、当集群规模扩展到上万个 Pod、当性能回归需要跨版本对比时,我们需要一种成本可承受、始终在线的 Profiling 方案,也就是持续性能分析(Continuous Profiling)。
本文从 Profiling 的分类与原理讲起,深入 Go pprof、Java async-profiler 与 JFR(Java Flight Recorder)、Python py-spy、Rust pprof-rs 等语言工具,再到 Pyroscope、Parca 等持续剖析系统,最后覆盖 OpenTelemetry Profiles 规范与国内大厂的落地经验、工程坑点与选型建议。
一、Profiling 的种类与意义
Profiling 并不是单一维度的动作,而是一族针对不同资源、不同时间域的观测方法。理解它们的区别是用好任何工具的前提。
1.1 On-CPU Profiling:CPU 时间去哪了
On-CPU Profiling 回答的是:「在某段时间窗口内,进程占用 CPU 的每一个样本落在哪一个调用栈上?」它的统计单位通常是「CPU 时间」而不是「墙钟时间」(wall-clock time),因此线程在 I/O、锁、睡眠等非 CPU 阶段是不计入的。
On-CPU 是最常用的 Profiling 形态,因为:
- CPU 是微服务中最昂贵、最容易成为瓶颈的资源;
- 采样成本低,99Hz 甚至 49Hz 就能获得足够的统计精度;
- 结果直接指向「热点函数」(hot function),可操作性强。
典型工具:Linux perf record、Go pprof 的
/debug/pprof/profile、async-profiler 的
-e cpu、py-spy、Parca eBPF Agent。
1.2 Heap / Allocation Profiling:内存分配与驻留
Heap Profiling 关心两个问题:
- 分配速率(allocation rate):单位时间分配了多少字节、调用栈是什么?高分配率会导致 GC 压力。
- 堆内驻留(in-use):当前堆上存活对象按调用栈归因是什么?用于定位内存泄漏。
Go 的 /debug/pprof/heap 会同时报告
alloc_space、alloc_objects、inuse_space、inuse_objects
四个维度;Java 通过 async-profiler -e alloc 或
JFR 的 ObjectAllocationSample 事件;C/C++ 常用
jemalloc/tcmalloc 自带的 profiler 或
heaptrack;Python 使用
tracemalloc。
Heap Profiling 的采样通常按「字节步长」进行:每分配 N KB
字节触发一次采样,这样既控制了开销,又能对大对象加权。Go
默认 MemProfileRate=512KB,Java
-XX:ObjectAllocationSampleInterval=512K。
1.3 Goroutine / Thread Profiling
Go 特有的 Goroutine Profiling 会枚举运行时所有 goroutine 的状态与调用栈,是调查 goroutine 泄漏、死锁、调度异常的核心工具。它是一种全量快照而不是采样——成本与当前存活的 goroutine 数量成正比。
线上一个常见故障模式是:某条路径在超时场景下没有关闭
channel 或 context,导致 goroutine 持续堆积最终 OOM。通过
/debug/pprof/goroutine?debug=2 可以看到每个
goroutine
的等待位置与栈,goroutine X [select, 23 minutes]
这样的行往往直接暴露问题。
Java 类似的能力是 jstack 与 JFR 的
ThreadDump 事件;两者都倾向于全量快照。
1.4 Lock Contention Profiling:锁争用
多线程程序中「明明 CPU 不满、磁盘不满、网络不满,但吞吐上不去」的场景往往是锁争用。Lock Profiling 采样的是:当线程获取锁失败陷入等待时,该事件对应的调用栈。
- Go:
runtime.SetMutexProfileFraction(N)打开 mutex profile,/debug/pprof/mutex。 - Java:async-profiler
-e lock,或 JFR 的JavaMonitorEnter、JavaMonitorWait事件。 - 内核:
perf lock、bcc的klockstat。
1.5 Block / Off-CPU Profiling:等待去哪了
Block Profiling 记录的是线程在同步原语(channel、条件变量、I/O 等待)上的阻塞时间。与 Lock Profiling 的区别是:Lock 只针对互斥锁竞争,而 Block 包含任何导致线程挂起的事件。
Off-CPU Profiling 更泛化,原则上覆盖一切「不在 CPU 上执行」的时间:I/O、网络、锁、sleep、调度延迟。实现上通常依靠内核调度事件:
sched:sched_switch → 线程让出 CPU,记录让出时刻与调用栈
sched:sched_wakeup → 线程被唤醒,记录唤醒时刻
差值即为该次 off-CPU 时长
Brendan Gregg 在 Netflix 早期推广了 Off-CPU
分析,典型实现是 bcc/offcputime.py 或
bpftrace。async-profiler 在
-e wall 模式下也可以把线程非 CPU
时间纳入火焰图。
1.6 Wall-clock Profiling
Wall-clock
采样对每个线程都按固定频率采样,无论它当时是否在 CPU
上运行。得到的火焰图里,sleep、epoll_wait、futex_wait
会成为显著贡献者,这反而是排查端到端延迟的利器——它让你看到「一次请求的
30ms 到底在哪儿等了」。
py-spy、async-profiler -e wall、Pyroscope 的
wall 模式都支持这种采样。代价是:对睡得多的服务,热点会被
idle 栈淹没,需要额外过滤。
1.7 Network / Syscall Profiling
严格来说这属于 Tracing 或 eBPF
可观测性范畴,但很多团队会把它并入 Profiling
讨论,因为它回答同一类「调用栈级归因」问题。例如 BCC 的
tcptop、tcplife 按 PID +
调用栈聚合网络字节数,相当于一张「网络火焰图」。
1.8 对比表
| 类型 | 采样维度 | 核心问题 | Go 工具 | Java 工具 | Python 工具 | 内核/通用 |
|---|---|---|---|---|---|---|
| On-CPU | CPU 时间 | 谁在烧 CPU | pprof cpu | async-profiler cpu / JFR | py-spy / Austin | perf / Parca |
| Heap | 分配字节 | 谁在分配内存 | pprof heap/allocs | async-profiler alloc / JFR | tracemalloc | heaptrack |
| Goroutine/Thread | 快照 | 线程是否泄漏 | pprof goroutine | jstack / JFR | threading.enumerate() | gdb thread apply all bt |
| Lock | 阻塞时间 | 锁争用在哪 | pprof mutex | async-profiler lock / JFR | — | perf lock |
| Block/Off-CPU | 等待时间 | 请求在哪儿等 | pprof block | async-profiler wall | py-spy –idle | bcc offcputime |
| Wall-clock | 墙钟时间 | 线程整体时间去哪 | — | async-profiler wall | py-spy | — |
二、采样 vs 插桩两类 Profiler
所有 profiler 按实现可以分成两大流派:采样(sampling)与插桩(instrumentation)。选择哪一类决定了开销、精度与可部署性。
2.1 采样式 Profiler
采样的核心思想是:以远低于函数调用频率的速度,周期性打断程序,记录当前调用栈;统计意义上,某函数在样本中出现的比例近似等于它的 CPU 时间占比。
优点:
- 开销低且可控。99Hz 的 CPU 采样,每秒每核中断 99 次,相对百万级指令的执行量几乎可忽略,实测开销通常在 1%~3%;
- 与代码无关。无需修改源代码或重新编译;
- 易于持续在线。这是 Continuous Profiling 的基础。
缺点:
- 统计误差。出现次数少的函数可能被漏采;
- 需要良好的栈回溯。如果缺少 frame pointer 或 DWARF,回溯可能失败或错栈。
2.2 插桩式 Profiler
插桩在函数入口/出口注入计时代码,精确统计每次调用的耗时与次数。Python
的 cProfile、Java 字节码增强、C++
-pg 编译出的 gprof 都属于这一类。
优点:
- 精度高,能报告每个函数的调用次数、总耗时、平均耗时;
- 小函数也不会漏。
缺点:
- 开销大。对调用频次高的热点函数,插桩本身就可能让程序慢 10 倍;
- 对内联、尾调用不友好;
- 不适合生产环境持续运行。
一个经验是:做本地压测、基准测试、算法分析用插桩;做线上诊断、持续剖析用采样。
2.3 基于信号的采样:SIGPROF 机制
POSIX 提供 ITIMER_PROF 定时器,内核按「进程
CPU 时间」触发 SIGPROF
信号。信号处理函数在被中断的线程上下文中执行,可以就地捕获调用栈:
struct itimerval tv = {
.it_interval = {.tv_usec = 10000}, /* 100 Hz */
.it_value = {.tv_usec = 10000},
};
setitimer(ITIMER_PROF, &tv, NULL);
void sigprof_handler(int sig, siginfo_t *info, void *ucontext) {
/* 回溯当前调用栈并写入环形缓冲区 */
unwind_and_record(ucontext);
}Go 运行时、老版本 gperftools、Ruby stackprof 都使用类似方案。SIGPROF 的优点是与语言无关、跨平台;缺点是只在 CPU 上采样(不能抓 off-CPU)、且信号处理函数必须是 async-signal-safe,限制很多。
2.4 Linux perf_events 基础
perf_events 是 Linux
内核提供的高精度性能监控子系统,核心能力:
- 硬件事件采样:基于 PMU(Performance
Monitoring
Unit,性能监控单元)计数器,在计数器溢出时触发中断采样,支持
cycles、instructions、cache-misses、branch-misses等; - 软件事件采样:
cpu-clock、task-clock、page-faults、context-switches; - tracepoint 与 kprobe/uprobe:用于事件驱动采样;
- 用户态栈与内核态栈采样:
--call-graph dwarf/fp/lbr。
最常见的使用方式:
# 以 99Hz 采样 30 秒,采集用户栈
perf record -F 99 -p $(pgrep myapp) --call-graph=dwarf -g -- sleep 30
perf report --stdio # 文本报告
perf script > out.stacks # 折叠栈,供 FlameGraph 使用Parca、Pyroscope eBPF 模式、字节 Continuous Profiling
平台底层都是基于 perf_events 或更现代的
bpf_get_stackid/bpf_get_stack eBPF
辅助函数。
2.5 PMU 硬件计数器
PMU 是现代 CPU 内置的性能计数器单元,以 Intel 为例,每个核心通常有 4~8 个可编程计数器 + 若干固定计数器。典型事件:
cycles:参考时钟周期数;instructions:退役指令数,配合cycles得到 IPC(每周期指令数);cache-misses/LLC-load-misses:缓存缺失;branch-misses:分支预测失败;cpu/mem-loads/:内存加载事件。
对高性能服务(如交易、数据库内核、网关),单纯的 on-CPU
火焰图不足以揭示瓶颈——很多时候 CPU 被「cache miss 导致的
stall」消耗,而不是真的在执行热点函数。此时需要基于 PMU
事件(如
cache-misses)采样生成「缓存火焰图」。
2.6 火焰图与冰柱图
火焰图(flame graph) 是 Brendan Gregg 提出的调用栈可视化方式:
- X 轴:样本按字母序或频次排序,宽度等价于样本数(CPU 时间占比);
- Y 轴:调用栈深度,底部是入口函数,顶部是叶子;
- 每个矩形的宽度越宽,说明该函数占用越多;
- 平顶(plateau)通常是真正的热点。
冰柱图(icicle graph) 是火焰图的上下翻转版,根在顶部、叶子在底部,Grafana Pyroscope 默认就是冰柱风格。对阅读「调用栈从谁发起」更直观。
还有两类常见衍生图:
- 差分火焰图(differential flame graph):两次采样相减,红色表示增多、蓝色表示减少,用于回归分析;
- Sandwich 视图:以某个函数为中轴,上方显示其调用者、下方显示其被调用者,用于聚焦分析。
三、Go pprof 深入
Go 是把 Profiling 做进标准库、做进运行时最深的语言之一。理解 pprof 的工作方式,对于 Go 服务的线上诊断是必修课。
3.1 pprof protobuf 格式
Go 生成的 profile 文件是一个 gzip 压缩的 protobuf,schema
定义在
github.com/google/pprof/proto/profile.proto
中。核心结构:
message Profile {
repeated ValueType sample_type = 1; // 指标维度:samples/count、cpu/nanoseconds、alloc_objects 等
repeated Sample sample = 2;
repeated Mapping mapping = 3; // 动态链接库、可执行文件段映射
repeated Location location = 4; // 指令地址 → 函数 + 源码行
repeated Function function = 5;
repeated string string_table = 6;
int64 time_nanos = 9;
int64 duration_nanos = 10;
int64 period = 12;
...
}
message Sample {
repeated uint64 location_id = 1; // 栈:叶子 → 根
repeated int64 value = 2; // 与 sample_type 等长
repeated Label label = 3; // 维度标签,如 goroutine id
}这种结构很有设计感:
sample_type支持多维度,一份 heap profile 同时携带alloc_space、alloc_objects、inuse_space、inuse_objects;string_table共享字符串,体积小;location与function分离,指令地址可以对应多行(内联)。
大多数现代 profiling 后端(Pyroscope、Parca、Polar Signals、字节内部 CPP)都原生支持 pprof 格式,这让跨语言互操作成为可能。
3.2 内置 profile 类型
Go 运行时内置多种 profile,通过
runtime/pprof 或 net/http/pprof
暴露:
| 名称 | 路径 | 含义 | 开销 |
|---|---|---|---|
profile |
/debug/pprof/profile?seconds=30 |
30 秒 CPU 采样 | 低,持续期间 ~1% |
heap |
/debug/pprof/heap |
堆快照,默认 in-use | 低 |
allocs |
/debug/pprof/allocs |
自进程启动的全部分配 | 低 |
goroutine |
/debug/pprof/goroutine |
全量 goroutine 栈 | 与 goroutine 数正比 |
mutex |
/debug/pprof/mutex |
互斥锁等待 | 需开启 fraction |
block |
/debug/pprof/block |
阻塞事件(chan、select) | 需开启 rate |
threadcreate |
/debug/pprof/threadcreate |
OS 线程创建栈 | 低 |
trace |
/debug/pprof/trace?seconds=5 |
执行跟踪(不是采样) | 高,短时使用 |
注意 mutex 与 block 默认关闭,需要显式打开:
runtime.SetMutexProfileFraction(1) // 1/N 的竞争事件被采样,1 表示全采
runtime.SetBlockProfileRate(10000) // 每 10 微秒阻塞事件采样一次生产环境 mutex profile 的 fraction 建议设成 100 或 1000,以控制开销。
3.3 net/http/pprof 接入
最简单的接入方式:
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
// 建议绑定到本地或内网,避免暴露在外网
log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
}()
runMyServer()
}注意两点:
- 生产环境不要把 pprof 端口绑定到
0.0.0.0,否则任何人都可以 dump goroutine、heap,泄露业务路径。推荐绑定到127.0.0.1+ sidecar proxy,或走内部管控平台; /debug/pprof/路径默认注册到http.DefaultServeMux,如果你的业务端口共用 DefaultServeMux,相当于也暴露了 pprof——需要主动隔离。
3.4 go tool pprof 交互式命令
采下来一份 profile 后,通过 go tool pprof
打开:
go tool pprof -http=:8080 http://127.0.0.1:6060/debug/pprof/profile?seconds=30交互式常用命令:
(pprof) top # 按 flat 值排序前 10
(pprof) top -cum # 按 cum(累计)排序
(pprof) list <regexp> # 显示匹配函数源码级开销
(pprof) web # 生成 svg 调用图
(pprof) peek <regexp> # 显示函数的调用者/被调用者
(pprof) tree # 调用树
(pprof) disasm <regexp> # 反汇编级别热点
(pprof) traces # 每条样本的完整栈
对比两份 profile 的差分:
go tool pprof -http=:8080 -diff_base=old.pb.gz new.pb.gz差分视图对排查「上线后 CPU 涨了 10%」类问题极为有用。
3.5 火焰图阅读要点
读 Go CPU 火焰图有几个实战要点:
- 先看宽度分布。如果前三宽柱子相加只有 20%,说明 CPU 消耗非常分散,单靠优化热点收益有限,要么改算法要么减请求量;
- 识别运行时栈。
runtime.mallocgc、runtime.scanobject、runtime.gcBgMarkWorker一整片宽柱子 = GC 压力大,通常要降低分配;runtime.findrunnable、runtime.schedule占比高 = 调度争用或 GOMAXPROCS 配置问题;syscall.Syscall占比高 = 系统调用多(网络、文件); - 识别反射与序列化。
reflect.Value.*、encoding/json.*在中高并发服务中是常客,经常能通过替换 json 为 sonic/easyjson、避免反射获得数量级提升; - 识别锁栈。
sync.(*Mutex).Lock、sync.(*RWMutex).RLock叶子出现高占比,应该结合 mutex profile 进一步分析; - 警惕 sleep / selectgo。CPU profile
里看到
runtime.selectgo占比高并不一定说明有问题——selectgo 本身会被采样进入,需结合业务判断。
3.6 Go 常见反模式
pprof 反复暴露的一些 Go 性能反模式:
- 反复拼接字符串:
s += x在循环里重分配,应当用strings.Builder或预分配切片; - 小对象 boxing:
interface{}承载int会在堆上分配一个 8 字节对象,在高并发下 allocs 火焰图极宽; - defer 在热路径:Go 1.14 之前 defer 有显著开销,即使 1.14+ 开启开放编码 defer,叶子函数每秒百万次的循环里 defer 仍然不便宜;
time.After泄漏:select { case <-time.After(d): ... }在大循环中会创建大量未释放的 timer,应当用time.NewTimer并Stop();- 全局锁:
sync.Mutex保护的大状态对象变成瓶颈,考虑分片锁或sync.Map; - goroutine-per-request 无上限:OOM 前 goroutine profile 会看到几十万 goroutine 栈相似;
- map 作为大集合:
map[K]V每扩容一倍 rehash 一次,巨大 map 可以看到runtime.mapassign_*占比异常。
3.7 benchmark + pprof
go test -bench 天生支持 pprof:
go test -bench=BenchmarkMyFunc -benchmem \
-cpuprofile=cpu.pb.gz \
-memprofile=mem.pb.gz \
-mutexprofile=mutex.pb.gz \
-blockprofile=block.pb.gz \
./...
go tool pprof -http=:8080 cpu.pb.gz配合 benchstat 对比两个 commit:
go test -bench=. -count=10 ./... > old.txt
git checkout feature-branch
go test -bench=. -count=10 ./... > new.txt
benchstat old.txt new.txt在 CI 中强制性能回归守门(PR 引入 >5% 回归时阻塞合并)是大型 Go 项目常见实践。
3.8 自定义 profile label
Go 1.9+ 支持 pprof.Labels,在 CPU profile
中给样本打标签:
import "runtime/pprof"
func handle(ctx context.Context, req *Request) {
labels := pprof.Labels("endpoint", req.Path, "tenant", req.TenantID)
pprof.Do(ctx, labels, func(ctx context.Context) {
actualWork(ctx, req)
})
}这样火焰图可以按 endpoint / tenant 过滤或分组,等价于「请求级别归因」。Pyroscope 依赖这个机制实现按请求维度切片。
四、Java async-profiler 与 JFR
Java 曾经以 Profiling 困难著称——JVMTI 基于的 profiler(VisualVM、JProfiler 早期版本、hprof)都受困于 safepoint bias,导致结果严重失真。async-profiler 与 JFR 的出现改变了这一局面。
4.1 Safepoint bias 与 AsyncGetCallTrace
传统 JVMTI profiler 用 GetStackTrace 或
GetAllStackTraces 抓取栈,但这些 API 只能在
safepoint(安全点)执行。JVM 的 safepoint 是 JIT
编译器认可的「可以安全暂停全部线程」的代码位置,通常在方法入口、循环回边、非
counted loop 的循环等。
问题是:safepoint 的分布不均匀。如果一个热点循环被优化成 counted loop(JIT 认为它不会太长),中间就没有 safepoint,profiler 永远采不到循环内部。结果是火焰图把时间错误地归给循环外的函数。这就是 safepoint bias。
AsyncGetCallTrace(AGCT)是 Sun/Oracle 1.6 引入的私有 API,允许在信号处理函数里异步抓取栈,无需 safepoint。它遍历 JIT frame 元数据与解释器帧,拼出当前 Java 调用栈。虽然是私有 API,但 OpenJDK 沿用至今,并在 JEP 435 中推进成为公共 API。
async-profiler 就是基于 AGCT + perf_events 的采样 profiler,被誉为 Java 生态最准的 sampling profiler。
4.2 async-profiler 工作模式
# 下载发布包
wget https://github.com/async-profiler/async-profiler/releases/download/v3.0/async-profiler-3.0-linux-x64.tar.gz
tar xf async-profiler-3.0-linux-x64.tar.gz
cd async-profiler-3.0-linux-x64
# 采 30 秒 CPU,输出火焰图 HTML
./bin/asprof -d 30 -f flame.html -e cpu <PID>
# 分配采样:每 512KB 采一次,输出 JFR
./bin/asprof -d 60 -e alloc --alloc 512k -f alloc.jfr <PID>
# 锁争用
./bin/asprof -d 60 -e lock -f lock.html <PID>
# Wall-clock(包含 off-CPU)
./bin/asprof -d 30 -e wall -t -f wall.html <PID>
# 通过 PMU 采 cache-misses
./bin/asprof -d 30 -e cache-misses -f cachemiss.html <PID>几个关键事件:
cpu:优先perf_events,退化到itimer;itimer:基于ITIMER_PROF,容器中 perf 不可用时使用;alloc:基于 AllocationSamplingJVMTI + TLAB 慢路径;lock:MonitorContendedEnter 回调;wall:对所有线程周期采样;ctimer:CPU timer,精确 per-thread CPU 时间;- 任意 PMU
事件:
-e cycles、-e cache-misses等。
4.3 JFR:从 Oracle 到 OpenJDK
JFR(Java Flight Recorder)最早是 Oracle JRockit JVM 的付费商业特性,2014 年整合进 HotSpot,2018 年通过 JEP 328 在 OpenJDK 11 中开源。现在所有主流 JDK(OpenJDK、Temurin、Corretto、Zulu)都带 JFR。
JFR 的设计理念:
- 事件驱动。JVM 定义了数百种事件(GC、JIT、Safepoint、SocketRead、FileRead、ExceptionThrown、ObjectAllocationSample、JavaMonitorEnter 等),每个事件带时间戳、线程、栈、参数;
- 始终可用。默认 profile 开销 <1%,作为长期黑匣子运行;
- 环形缓冲。内存里保留最近 N
分钟,发生异常时 dump 成
.jfr文件; - 与 JVM 深度集成。能记录 JIT 编译事件、GC 阶段等 async-profiler 看不到的内部状态。
启动方式:
# 启动时直接打开
java -XX:StartFlightRecording=duration=60s,filename=myapp.jfr,settings=profile MyApp
# 运行时通过 jcmd
jcmd <PID> JFR.start name=myrec settings=profile duration=60s filename=myapp.jfr
jcmd <PID> JFR.dump name=myrec filename=myapp.jfr
jcmd <PID> JFR.stop name=myrecsettings 常见值:
default.jfc:开销极低(~0.5%),事件较稀疏;profile.jfc:开销 1~2%,事件密集,生产推荐;- 自定义 jfc 文件可以裁剪事件集合。
4.4 JMC 与 JFR 分析
Java Mission Control(JMC)是 Oracle 开源的 JFR 分析 GUI,2018 年独立成 Adoptium 项目。JMC 提供:
- 自动分析器(Automated Analysis):对一份 JFR 文件给出问题清单,如「发现 17% 的 CPU 花在 String.concat 上」「检测到 GC 周期过长」「发现疑似死锁」;
- Threads 视图:线程级墙钟时间堆叠,看出哪些线程在 park、哪些在 running;
- Method Profiling 视图:基于 ExecutionSample 的火焰图/树视图;
- Memory 视图:TLAB 分配、GC 活动、Old Gen 驻留对象。
除了 JMC,开源生态还有:
- JFR Flame Graph(github.com/chrishantha/jfr-flame-graph):把 JFR 的 ExecutionSample 转成 FlameGraph 折叠栈;
- Pyroscope JFR Ingester:直接上传 JFR 到 Pyroscope;
- Datadog Continuous Profiler:内部使用 JFR 作为采集格式。
4.5 Honest profiling
async-profiler 引入了 honest profiling
概念:只在明确能还原栈的位置上报样本,无法还原的样本单独标记为
[unknown],避免瞎归因。与之相比,JFR 的
ExecutionSample 有时仍会出现
no Java frame 的样本,但好在比例可控。
在排查「火焰图底部出现大量陌生函数」时,区分 honest 样本和 fallback 样本非常重要。
4.6 生产部署建议
典型做法:
- 所有 JVM 进程启动时开启 JFR
default.jfc,常驻运行; - 健康检查或告警触发时,jcmd 把 JFR 环形缓冲 dump 到磁盘;
- 出现性能问题时,额外用 async-profiler
-e cpu或-e alloc做 60 秒高精度采样; - JFR 文件与 async-profiler 输出上传到 Pyroscope / 自建 profiling 平台。
五、Python 性能分析
Python 的动态特性让 profiling 既简单又复杂:简单在于
sys.settrace / sys.setprofile
提供了即插即用的
hook;复杂在于解释器帧结构会影响性能,开销容易失控。
5.1 cProfile:确定性 profiler
cProfile 是 Python 标准库自带的插桩
profiler,基于 sys.setprofile:
import cProfile
import pstats
def main():
compute_something()
cProfile.run("main()", "profile.out")
stats = pstats.Stats("profile.out")
stats.sort_stats("cumulative").print_stats(20)或作为命令行:
python -m cProfile -o profile.out myscript.py
python -m pstats profile.outcProfile 精确统计每个函数的调用次数、总耗时、累计耗时。缺点:开销较大(几十倍不罕见),不适合生产,也不适合 CPU-bound 紧密循环(插桩开销本身比函数体还大)。
5.2 py-spy:零侵入采样 profiler
py-spy 是 Rust 写的外部采样 profiler,核心思路是:
- 用
ptrace/process_vm_readv从目标进程读取内存; - 解析 CPython 解释器的
PyInterpreterState→PyThreadState→PyFrameObject链; - 反查符号,重建调用栈;
- 以 100Hz 采样,输出 flamegraph 或 dump。
用法:
# 安装
pip install py-spy
# 查看目标进程实时函数热度(类 top)
py-spy top --pid <PID>
# 采 30 秒生成火焰图
py-spy record -o profile.svg --duration 30 --pid <PID>
# 抓一份 thread dump
py-spy dump --pid <PID>py-spy 的优点:
- 目标进程无需任何改动、无需导入模块;
- 对 CPython 3.7+ 支持良好,包括 subinterpreter 与 asyncio;
- 生产可用,开销通常 <1%;
- 支持
--native显示 C 扩展栈(需要 libunwind)。
缺点:在容器内诊断需要 CAP_SYS_PTRACE 或共享
PID namespace;macOS 上需要关闭部分安全保护。
5.3 Austin
Austin 是另一个 C 写的 Python 采样 profiler,设计更轻量,支持输出 Austin 自定义格式或 collapsed 栈。它与 VS Code 插件、Austin Web 结合做成了交互式分析器。
5.4 Pyinstrument
Pyinstrument 是嵌入式 wall-clock profiler,侧重请求级剖析:
from pyinstrument import Profiler
profiler = Profiler()
profiler.start()
do_work()
profiler.stop()
print(profiler.output_text(unicode=True, color=True))输出是树状报告,按墙钟时间排序,非常适合 Web 请求的逐请求剖析(作为中间件挂到 Flask/Django)。
5.5 内存剖析:tracemalloc 与 memray
标准库 tracemalloc
追踪每一次内存分配的调用栈:
import tracemalloc
tracemalloc.start(25)
snapshot1 = tracemalloc.take_snapshot()
do_work()
snapshot2 = tracemalloc.take_snapshot()
for stat in snapshot2.compare_to(snapshot1, "lineno")[:10]:
print(stat)Bloomberg 开源的 memray
更强大:精确记录每次
malloc/free,支持火焰图、时间序列图、实时报告,线上模式开销可控。
5.6 生产实战
一个常见的 Python 服务诊断流程:
- 监控发现延迟上涨 → 判断是 CPU 还是 I/O 瓶颈;
- 进入容器
py-spy top -p 1实时看函数热度; - 确认是 CPU 热点后
py-spy record -d 60 -o hot.svg; - 打开火焰图,识别是业务代码还是第三方库(如
requests、sqlalchemy); - 若是内存增长,用
memray run --live myapp.py复现本地,或生产采tracemalloc快照。
Python 的 GIL 让并发 profiling 多了一层考虑:wall-clock
模式下,等 GIL 的线程会显示为「在
PyEval_AcquireLock」,看起来像热点,其实是争用信号。
六、Rust pprof-rs 与 criterion
Rust 虽然不像 Go 内置运行时 profiler,但生态非常活跃。
6.1 pprof-rs
pprof-rs 是 TiKV 团队开源的 Rust CPU profiler,支持输出 pprof 格式与 flamegraph:
use pprof::ProfilerGuard;
fn main() {
let guard = pprof::ProfilerGuardBuilder::default()
.frequency(100)
.blocklist(&["libc", "libgcc", "pthread", "vdso"])
.build()
.unwrap();
run_server();
if let Ok(report) = guard.report().build() {
let file = std::fs::File::create("flamegraph.svg").unwrap();
report.flamegraph(file).unwrap();
}
}内部基于 SIGPROF + backtrace-rs,可选启用
frame-pointer feature 加速回溯(但要求编译时
-Cforce-frame-pointers=yes)。
6.2 criterion:benchmark 框架
criterion.rs 是 Rust 的 benchmark 框架,统计上严格(置信区间、回归检测):
use criterion::{criterion_group, criterion_main, Criterion};
fn bench_fib(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(20)));
}
criterion_group!(benches, bench_fib);
criterion_main!(benches);运行 cargo bench
会自动对比历史结果,给出「性能退化」判断。
6.3 cargo-flamegraph
cargo-flamegraph
封装 perf + FlameGraph,一条命令出火焰图:
cargo install flamegraph
cargo flamegraph --bin myapp -- --flag1 arg1编译建议:
[profile.release]
debug = true # 保留调试符号
lto = "thin"
codegen-units = 1否则火焰图里到处是 [unknown]。
6.4 与 perf 集成
Rust 二进制与 C 程序一样可以被 perf record
直接采样,前提是编译时带调试符号且关闭 frame pointer
omission:
RUSTFLAGS="-C force-frame-pointers=yes -C debuginfo=2" cargo build --release
perf record -F 99 -g --call-graph=dwarf ./target/release/myappParca、Pyroscope eBPF agent 都能直接对 Rust 二进制采样,只要符号齐全。
七、Pyroscope 架构
Pyroscope 最初由 Ryan Perry 等人在 2020 年开源,目标是「为 Profiling 做 Prometheus + Grafana 这件事」。2023 年 Grafana Labs 完成收购,Pyroscope 成为 Grafana Observability 栈的一部分。
7.1 架构总览
┌──────────┐ ┌──────────┐ ┌────────────┐
│ Go SDK │ │ Java SDK │ │ Agent/eBPF │
└────┬─────┘ └────┬─────┘ └─────┬──────┘
│ pprof/flamebearer/JFR │
▼ ▼
┌────────────────────────────────────────┐
│ Pyroscope Distributor │
└───────────────┬────────────────────────┘
│
┌───────┴────────┐
▼ ▼
┌───────────┐ ┌───────────────┐
│ Ingester │ │ Object Store │
│ (TSDB-ish)│◀──▶│ (S3/GCS/...) │
└────┬──────┘ └───────────────┘
│
▼
┌─────────────┐
│ Querier │
└──────┬──────┘
│
▼
┌─────────────────┐
│ Grafana Plugin │
└─────────────────┘
从 Pyroscope 1.x 起,其内部架构大量借鉴了 Grafana Loki / Mimir:分离 Distributor / Ingester / Compactor / Querier,并支持多租户、对象存储后端(S3、GCS、Azure Blob、本地文件系统)。
7.2 存储模型:折叠栈与 symdb
Pyroscope 的核心存储单元是折叠栈(collapsed
stack):一条调用栈被编码成
frame1;frame2;frame3 value
的字符串,然后做字典编码 + 前缀压缩。
更精细的是 v1.x 引入的 symdb:
- 每个 tenant 维护一个共享的 symbol database(类似 pprof string_table);
- 调用栈以「location id 序列」存储;
- 同一 frame 在全球只保存一次字符串。
配合时间序列索引(按 service、profile_type、labels 切片),一次查询等价于:
- 按 labels 找到候选 series;
- 按时间范围拉取 raw profile 块;
- 合并并解码为 flamegraph。
7.3 接入方式
Go 服务:
import "github.com/grafana/pyroscope-go"
func main() {
_, err := pyroscope.Start(pyroscope.Config{
ApplicationName: "order-service",
ServerAddress: "http://pyroscope:4040",
Logger: pyroscope.StandardLogger,
Tags: map[string]string{"env": "prod", "region": "cn-east-1"},
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileAllocSpace,
pyroscope.ProfileInuseObjects,
pyroscope.ProfileInuseSpace,
pyroscope.ProfileGoroutines,
pyroscope.ProfileMutexCount,
pyroscope.ProfileBlockCount,
},
})
if err != nil {
log.Fatal(err)
}
runApp()
}Java 服务:挂载 async-profiler 版本的 pyroscope agent:
java -javaagent:./pyroscope.jar \
-DPYROSCOPE_APPLICATION_NAME=order-service \
-DPYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040 \
-DPYROSCOPE_PROFILING_INTERVAL=10s \
-DPYROSCOPE_PROFILER_EVENT=itimer \
-DPYROSCOPE_LABELS=env=prod,region=cn-east-1 \
-jar app.jarPython:
import pyroscope
pyroscope.configure(
application_name = "worker",
server_address = "http://pyroscope:4040",
tags = {"env": "prod"},
)7.4 部署模式
Pyroscope 支持三种部署:
- monolithic:单进程,所有组件跑在一起,适合小规模或测试;
- microservices:Distributor / Ingester / Compactor / Querier 各自独立部署,生产推荐;
- scalable single
binary:单二进制多角色,通过
-target=<role>切换,运维简单。
建议生产使用 Helm Chart
grafana/pyroscope,开启对象存储后端,保留策略按租户隔离:
pyroscope:
structuredConfig:
storage:
backend: s3
s3:
bucket_name: pyroscope-prod
endpoint: s3.amazonaws.com
limits:
ingestion_rate_mb: 20
ingestion_burst_size_mb: 40
max_profile_size_bytes: 5_000_000
max_profile_stacktraces: 16000
retention_period: 168h7.5 Grafana 集成
Grafana 10+ 内置 Pyroscope 数据源,支持:
- Flame Graph 面板;
- 与 Tempo trace 的 trace-to-profile 跳转(通过 exemplar span label);
- Dashboard 变量、对比差分;
- Explore 页面的 Profile tab。
八、Parca 与 eBPF Profiling
Parca 由 Polar Signals 团队(Frederic Branczyk、Matthias Loibl 等)从 Red Hat OpenShift Monitoring 的经验中孵化,目标是「零代码侵入的多语言持续剖析」。
8.1 Parca 架构
┌─────────────────────┐ ┌────────────────────┐
│ parca-agent (eBPF) │──────▶│ parca-server │
│ (DaemonSet) │ pprof │ (TSDB + FrostDB) │
└─────────────────────┘ └─────────┬──────────┘
│
▼
┌────────────────────┐
│ Parca Web UI │
└────────────────────┘
- parca-agent:以 DaemonSet 部署到每个节点,通过 eBPF 对整机所有进程采样;
- parca-server:接收 pprof 上报,用 FrostDB(列式 TSDB for profiles)存储;
- UI:火焰图、冰柱图、Top、Sandwich、差分视图。
8.2 eBPF 采样原理
parca-agent 不依赖语言运行时,也不需要目标进程合作:
- 加载
perf_eventBPF program,附着到PERF_COUNT_SW_CPU_CLOCK,以 19Hz / 49Hz / 99Hz 触发; - 在 BPF program 里用
bpf_get_stack抓取用户态指令指针; - 把 pid + stackid 写入 BPF map;
- 用户态 agent 周期性读取 map,用目标二进制的符号表与 DWARF 信息还原函数名、文件、行号;
- 聚合成 pprof 上报给 parca-server。
这种方式的优势:
- 多语言:C/C++/Go/Rust/Java(AOT 或带符号 JIT)/Node/Ruby 等原生代码都能采样;
- 零代码改动:目标进程无需重启、无需引入 SDK;
- 全局视角:一次部署覆盖节点上所有进程。
挑战主要在栈回溯。
8.3 Frame Pointer vs DWARF 栈回溯
eBPF 内核栈采样有两种机制:
- Frame pointer(FP)回溯:假设
%rbp寄存器始终指向栈帧链,沿着%rbp遍历即可。要求目标二进制用-fno-omit-frame-pointer编译; - DWARF CFI 回溯:通过 DWARF
调试信息(
.eh_frame)描述的 Canonical Frame Address 规则逐帧还原。不依赖 FP,但解析开销大、.eh_frame通常需要拉到用户态计算。
很多发行版为了性能早期默认
-fomit-frame-pointer,导致 eBPF profiler
栈断裂。社区推动下:
- Fedora 38+ 默认开启 frame pointer;
- Ubuntu 24.04 默认开启 frame pointer;
- Golang 二进制自 1.7 起默认保留 frame pointer。
parca-agent 1.x 引入了自定义的「unwind table」:提前解析
.eh_frame,把回溯规则压进 BPF map,让 BPF
内直接用表驱动回溯,从而摆脱对 FP
的依赖。这也是它的技术亮点。
8.4 Java / Python 符号
对于 JIT 语言,parca-agent 会:
- 监听
perf-<pid>.map文件(async-profiler、V8、OpenJDK-XX:+PreserveFramePointer+-agentpath:libperfmap.so可生成); - 读取运行时符号信息重建 JIT 代码区间 → 符号映射;
- Python 支持通过特殊的 BPF uprobes 解析
PyFrameObject。
但相比 async-profiler / py-spy 的语言原生采样,eBPF 对 JIT 语言支持仍然粗糙;许多团队采用「eBPF 覆盖原生语言 + 各语言 SDK 覆盖自己」的混合部署。
8.5 parca-agent 部署
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: parca-agent
namespace: parca
spec:
selector:
matchLabels: { app: parca-agent }
template:
metadata:
labels: { app: parca-agent }
spec:
hostPID: true
containers:
- name: parca-agent
image: ghcr.io/parca-dev/parca-agent:v0.30.0
args:
- /bin/parca-agent
- --node=$(NODE_NAME)
- --remote-store-address=parca-server.parca:7070
- --remote-store-insecure
- --profiling-duration=10s
securityContext:
privileged: true
env:
- name: NODE_NAME
valueFrom: { fieldRef: { fieldPath: spec.nodeName } }
volumeMounts:
- { name: tmp, mountPath: /tmp }
- { name: run, mountPath: /run }
- { name: proc, mountPath: /host/proc, readOnly: true }
- { name: sys, mountPath: /sys/kernel/debug }
volumes:
- { name: tmp, emptyDir: {} }
- { name: run, hostPath: { path: /run } }
- { name: proc, hostPath: { path: /proc } }
- { name: sys, hostPath: { path: /sys/kernel/debug } }关键点:
hostPID: true才能看到主机所有进程;privileged: true或CAP_BPF + CAP_PERFMON + CAP_SYS_PTRACE;- 挂
/sys/kernel/debug用于加载 BPF program 与读取 kallsyms。
8.6 与 OpenTelemetry 集成
2024 年 Elastic 把其内部基于 eBPF 的 Universal Profiler 捐赠给 OpenTelemetry 社区,成为 OpenTelemetry eBPF Profiler。Parca / Polar Signals 也在持续对 OTel Profiles 信号做贡献。趋势很明显:eBPF profiling 正在被标准化。
九、持续性能分析(Continuous Profiling)
「持续」是持续性能分析的关键词,它要求 profiler 能常年挂在生产上,而不只是在事故发生时才启动。
9.1 为什么要持续
- 事故定位:事故窗口过去后,按需启动 profiler 已经晚了。持续 profiling 像黑匣子,能回看几小时、几天前的热点变化;
- 性能回归基线:每次发版后 CPU 占比变化、GC 分配变化都能直接看到;
- 容量规划:长期占比分布指导机器配比、核数选择;
- 成本归因:按 service/tenant/endpoint 标签切分,给出「这个 endpoint 消耗了多少 CPU 美元」级别的数据。
Google 在 2010 年就用 Google-Wide Profiling(GWP)做到了整数据中心级持续采样,年度 CPU 开销控制在 0.01% 以内。
9.2 采样合并与 TSDB 化
持续 profiling 与传统 profiling 的关键区别是:
- 传统:一份 profile 文件是一次采样,独立存在;
- 持续:profile 是一条时序,需要按时间范围合并。
合并算法的核心是「同结构栈的值相加」:
profile_A: A;B;C 100
A;B;D 50
profile_B: A;B;C 30
A;E 70
------ 合并 --------
merged: A;B;C 130
A;B;D 50
A;E 70
Pyroscope、Parca、pprof CLI 都实现了这套合并逻辑。难点在于大规模场景下:单 tenant 每秒钟几千份 profile、每份包含上万条栈,合并必须在列式存储上高速进行——这正是 FrostDB、Pyroscope symdb 的价值。
9.3 与 trace 的关联:exemplar
一条慢链路(trace)可以附带一份那段时间的 profile,即 trace-to-profile 关联:
- OpenTelemetry SDK 在 span 中写入
pyroscope.profile.id或profiling.context.span_id; - Pyroscope SDK 把当前 span id 写入 pprof label;
- Grafana 界面点开 Tempo trace,右上角「Profile」按钮查询对应 span 覆盖时间 + 服务的 profile。
这是 2024 年以来可观测性工具链的共识方向,为「慢请求定位到代码行」提供了最后一公里。
9.4 Grafana Tempo 与 Pyroscope 联动
# Grafana datasource 配置
apiVersion: 1
datasources:
- name: Tempo
type: tempo
jsonData:
tracesToProfiles:
datasourceUid: pyroscope-prod
tags:
- key: service.name
value: service_name
profileTypeId: process_cpu:cpu:nanoseconds:cpu:nanoseconds
customQuery: true
query: '{service_name="$${__tags.service_name}"}'配上 span 中的
profile.thread.id、profile.id
label,Tempo 就能跨数据源跳转到火焰图。
十、存储模型
持续 profiling 的存储是独立的技术挑战:写入吞吐比 metrics 高、查询聚合比 logs 重、schema 比 traces 复杂。
10.1 折叠栈格式
最早最简单的格式,Brendan Gregg 的 FlameGraph 工具链使用:
main;foo;bar 100
main;foo;baz 42
main;qux 37
每行「栈;栈;栈 值」。优点是人类可读、极易生成;缺点是冗长、无类型。
10.2 pprof profile.proto
Google 设计的二进制格式,也是 Go / Pyroscope / Parca 的事实标准。核心创新:
- string table 消重;
- location / function 分层,支持内联;
- sample_type 多维度;
- gzip 压缩。
一份 30 秒 CPU profile 通常 50~300KB;压缩前可达 MB 级。
10.3 profile 树与列式存储
Parca 的 FrostDB 把 pprof 按列式存储:
timestamp、duration、period等元数据列;label_<name>列;stack_trace_id列(指向 symdb);value列。
这样按 label 过滤 + 时间范围聚合时,IO 只扫相关列。
Pyroscope 引入 symdb 后走向类似方向:
- Meta Index:service → profile_types → head/block;
- Profile Block:每 5 分钟/1 小时切块,内含列式存储与 symdb;
- Compactor:后台合并小 block,减少查询扇出。
10.4 Delta vs Cumulative
一份 CPU profile 其实是「时间窗口内的增量」(delta),每 10 秒上报一份。但 heap/goroutine 更自然是「当前累计快照」(cumulative)。存储时要区分:
- delta:直接相加;
- cumulative:需要相邻两点相减再相加(差分求导)。
OpenTelemetry Metrics 早就有 delta vs cumulative 的讨论,Profiles 信号基本沿用。
10.5 压缩
几层压缩叠加:
- 串表消重(~3x);
- 差分编码(stack_id 列变化小,delta + varint);
- zstd / snappy 块级压缩(~2-4x)。
合计一份 30s CPU profile 落地后可能只有 10~30KB。即便如此,1000 个 pod、每 10 秒一份,一天也有 2.6 亿 profile,TB 级是常态,冷热分层与保留策略必不可少。
十一、OpenTelemetry Profiles(2024+)
长期以来 OpenTelemetry 只定义了 Metrics / Logs / Traces 三种信号,Profiles 缺位是社区共识的短板。
11.1 规范状态
- 2023 年 11 月:OpenTelemetry Profiling SIG 正式成立,Elastic、Grafana、Polar Signals、Splunk 等联合推动;
- 2024 年 3 月:Elastic 把其内部基于 eBPF 的 Universal Profiler 捐赠给 OpenTelemetry,成为 OpenTelemetry eBPF Profiler 的基础;
- 2024 年 9 月:OTel Profiles 数据模型 OTEP 212 合入;
- 2024 年 Q4:Profiles 信号进入 OTLP 1.3,状态为「development」;
- 2025 年起:SDK / Collector / Backend 陆续支持。
11.2 数据模型
OTel Profiles 的 OTLP schema 与 pprof 高度相似,增加了:
- Resource:与其他信号统一的 Resource
语义(
service.name、k8s.pod.uid等); - InstrumentationScope:谁产生的 profile;
- ProfileSample:extending pprof Sample,携带 TraceID / SpanID 关联字段;
- Attributes:沿用 OTel 统一属性模型。
核心 message(简化):
message Profile {
repeated ValueType sample_type = 1;
repeated Sample sample = 2;
repeated Mapping mapping_table = 3;
repeated Location location_table = 4;
repeated Function function_table = 5;
repeated KeyValue attribute_table = 6;
repeated AttributeUnit attribute_units = 7;
repeated Link link_table = 8; // 与 Trace 的关联
...
}Link 字段是 OTel Profiles 的独特创新:让
profile sample 能反向链接到 trace span,实现「火焰图某一竖条
→ 具体 trace」的跳转。
11.3 Collector 与 Exporter
OpenTelemetry Collector 1.x 引入
profilesreceiver 与
profilesexporter:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
exporters:
otlphttp/pyroscope:
endpoint: https://profiles.example.com/otlp
debug:
verbosity: detailed
service:
pipelines:
profiles:
receivers: [otlp]
exporters: [otlphttp/pyroscope, debug]这让生态迁移变得平滑:业务代码只接 OTel SDK,后端从 Pyroscope 切到 Parca 或自研系统,改 collector 配置即可。
11.4 采纳状态
截至 2026 年初:
- Grafana Pyroscope 已支持 OTLP Profiles 接入(同时保留私有协议);
- Parca 宣布兼容 OTLP Profiles;
- Datadog / Dynatrace 开始对齐;
- Java / Go / .NET SDK 进入 beta;
- Python / Rust SDK 在 alpha。
Profiles 信号预计在 2026~2027 年成为与 Traces 同等级别的一等公民。
十二、国内落地案例
12.1 字节跳动 CPP(Continuous Profiling Platform)
字节跳动公开分享过内部的持续剖析平台,其演进路线具有代表性:
- 早期:业务自行接入 pprof,按需启动
/debug/pprof/profile; - 中期:自研 Go agent,10 秒级定时采样上报到中心化后端(基于 pprof protobuf);
- 现阶段:agent 融合 eBPF(覆盖 C++/Rust 原生服务)+ 语言 SDK(Go、Java、Python),后端自研类 Pyroscope 存储,支持按 region / cluster / service / endpoint 多维分析;
- 特色:与 APM trace 深度联动,慢 trace 一键跳火焰图;多版本差分(灰度 vs 全量)自动出报告。
公开分享的效果数字:通过持续剖析发现的 Go 服务 CPU 优化项平均每季度节省万核级资源。
12.2 美团 Profiling 实践
美团技术博客数篇文章讨论过:
- Java 服务的 async-profiler 平台化:内部 Java 诊断平台让业务「一键剖析」,按 PID 远程注入 async-profiler agent,输出火焰图;
- Go 服务的在线诊断:网关、服务发现等基础组件出现性能抖动时,通过平台触发 pprof 采样,结合链路 trace 快速定位;
- 内存泄漏日志联动:heap profile + GC 日志联合分析,识别「新生代持续涨、Full GC 无法回收」的泄漏点。
12.3 Java 服务的 async-profiler
国内大型 Java 服务(支付、电商、搜索)基本形成共识:
- 启动时 JFR
default.jfc常驻; - 运维诊断平台集成 async-profiler;
- 线上事故触发
-e cpu、-e alloc、-e lock三份采样; - 采样结果通过 jfr-flame-graph 或内部工具出火焰图;
- 对比上一次稳定版本做差分分析。
12.4 Go 服务在线诊断
Go 社区常见做法:
- 所有服务强制启用 pprof(绑定到 unix socket 或内网地址);
- 运维平台一键「拉火焰图」:后台执行
go tool pprof到目标 pod,生成 svg; - 与 K8s 结合:事件驱动,如 CPU 告警时自动采 30s 后上传对象存储;
- 版本回归:CI 阶段对核心基准跑 benchmark + pprof,保存为基线。
十三、工程坑点
13.1 Safepoint bias 与 honest profiling
前文提过的 JVM safepoint bias 是经典坑。排查方法:
- 同时用 async-profiler 与传统 JVMTI profiler 抓样;
- 对比热点函数排名,如果差异巨大(top3 完全不同),说明 safepoint bias 严重;
- 信任 async-profiler 的结果。
13.2 符号表丢失
编译时 strip 或 UPX
压缩会剥离符号,火焰图全是
[unknown]。对策:
- C/C++/Rust:发布 build 保留
.debugsymbol,separate debuginfo 放对象存储; - Go:默认带符号,不要
-ldflags '-s -w'; - Java:JIT 符号通过
perf-<pid>.map暴露; - 容器:镜像里保留
/proc/self/exe的 buildid,让 agent 通过 debuginfod 回拉符号。
13.3 容器内 profiling 权限
容器默认权限非常严格,profiler 常用权限:
CAP_SYS_PTRACE:py-spy、gdb、async-profiler(attach 时);CAP_PERFMON+CAP_SYS_ADMIN:使用perf_events与 BPF;CAP_BPF(内核 5.8+):装载 BPF program;hostPID: true:跨容器看 PID;SYS_PTRACEsecComp 白名单。
K8s 最佳实践:
- 把 profiler 做成独立
DaemonSet,
hostPID: true+privileged: true; - 业务 Pod 不授予高权限;
- async-profiler 作为 sidecar 时,业务容器
shareProcessNamespace: true+ sidecarCAP_SYS_PTRACE。
另外,某些 PaaS 默认禁用 perf_event_paranoid
低等级,需要协调
sysctl kernel.perf_event_paranoid=1。
13.4 Frame pointer omission
默认编译选项 -fomit-frame-pointer 让 BPF
栈回溯失败。解决方案:
- 重编目标二进制加
-fno-omit-frame-pointer; - 使用支持 DWARF 回溯的 profiler(parca-agent、perf
--call-graph=dwarf); - 升级到默认保留 FP 的发行版。
注意 DWARF 回溯内存/CPU 开销是 FP 的 5~10 倍,高频采样时需权衡。
13.5 Cross-container 符号解析
同一节点上多个容器的可执行文件路径在主机命名空间下看是
/proc/<pid>/root/usr/bin/app。agent
读符号时要正确处理命名空间映射:
- 用
/proc/<pid>/rootprefix 访问容器内文件; - 用
/proc/<pid>/maps获得正确的可执行映射; - 对 Go 二进制读
.note.go.buildid做身份比对。
parca-agent、pyroscope-agent 都专门处理这部分逻辑,自研时容易忽略。
13.6 内存 profiling 开销
Java 在老版本 JVM 开启 -Xrunhprof 做内存
profile 会拖慢 100 倍。现代 async-profiler
-e alloc 基于 JVMTI
SampledObjectAlloc hook,每 TLAB slow path
才触发一次,开销 1~3%。尽管如此,采样间隔需合理:
--alloc 512k(默认):开销可接受;--alloc 64k:开销明显上升,仅用于短时诊断;--alloc 1:等同于全量插桩,仅用于本地调试。
Go 的 MemProfileRate 也是类似考量,默认
512KB,不建议调小到 KB 级。
13.7 Pyroscope 存储增长
一个 tenant 如果:
- 服务数 200;
- 每服务 50 个 pod;
- 每 pod 每 10 秒一份 CPU + heap profile;
每天 profile
数量:200 × 50 × 2 × 6 × 60 × 24 ≈ 3.5 亿。即便每份压缩后
20KB,裸数据也达 7TB/天。对策:
- 合理的保留策略(detailed 7 天 + compact 30 天);
- 抽样上报(pod 内 80% 抽一份);
- 按服务 tier 差异化采样频率(核心 10s、边缘 60s);
- 冷数据归档对象存储,查询时按需拉回。
13.8 Go MutexProfile 的误区
runtime.SetMutexProfileFraction(1)
让每次锁竞争都采样,在高竞争场景下开销很大。更实际的值是
100~1000。另外 mutex profile 只覆盖 sync.Mutex
/ sync.RWMutex,channel 争用走 block
profile,不要混淆。
13.9 py-spy 与 musl/alpine
alpine 镜像基于 musl libc,py-spy 有时栈回溯异常。对策:
- 使用 glibc 基础镜像(debian-slim);
- 或使用
py-spy最新版(已改善 musl 兼容); - 确认 Python
二进制带调试符号(
python3-dbg)。
13.10 采样频率与 Nyquist
按 Nyquist 定理,要观察 X Hz 的现象需要至少 2X Hz 采样。实际中 99Hz 足以覆盖绝大多数热点,但如果你要分析「每秒 50 次的微 GC pause」这种快速事件,99Hz 会严重欠采样。此时需要:
- 特殊事件驱动(GC pause
tracepoint、
bpftrace); - 或提高采样率到 997Hz,但开销上升到 10% 量级。
十四、选型建议与落地清单
14.1 选型决策表
| 场景 | 首选工具 | 备选 | 原因 |
|---|---|---|---|
| Go 服务本地调优 | go tool pprof + benchmark |
go trace | 内置、零成本、精度高 |
| Go 服务线上持续剖析 | Pyroscope Go SDK | Parca + eBPF | 支持 label/exemplar,与 trace 联动 |
| Java 服务事故诊断 | async-profiler | JFR + JMC | 准确无 safepoint bias,多事件支持 |
| Java 服务常驻黑匣子 | JFR default.jfc |
Pyroscope Java agent | 开销极低、JVM 内部事件丰富 |
| Python 服务在线诊断 | py-spy | Pyinstrument | 无侵入、生产可用 |
| C++/Rust 服务 | perf + FlameGraph | Parca eBPF | 成熟生态,符号齐全即可 |
| 整机多语言持续剖析 | Parca eBPF Agent | OTel eBPF Profiler | 不依赖 SDK、覆盖面广 |
| 差分回归分析 | pprof -diff_base |
Pyroscope 差分视图 | CI 流程可自动化 |
| 与 trace 关联 | Pyroscope + Tempo | Datadog / 字节 CPP | exemplar label 打通 |
14.2 采纳路线图
分四步走:
Step 1:按需 profiling(0~1 月)
- 所有 Go 服务接入
net/http/pprof,端口绑定本地; - 所有 Java 服务启动脚本备好 async-profiler;
- 所有 Python 服务容器镜像预装 py-spy;
- 运维脚本库里沉淀「拉 CPU 火焰图」「拉 heap」「拉 goroutine」等一键脚本。
Step 2:持续 profiling 试点(2~3 月)
- 选 1~2 个核心服务;
- 部署 Pyroscope single-binary + S3 存储;
- 接入 SDK,labels 含
service / env / region / version / endpoint; - Grafana 面板展示 Top、差分、按 label 切片。
Step 3:全量推广(4~6 月)
- 所有 Go / Java 服务接 Pyroscope SDK;
- 关键节点部署 Parca eBPF agent 作为安全网;
- JVM 常驻 JFR;
- 存储扩容,建立多租户隔离;
- 建立保留策略与成本告警。
Step 4:与可观测性打通(6 月+)
- Tempo ↔︎ Pyroscope trace-to-profile;
- 慢 endpoint 自动出火焰图报告;
- 发版前后自动差分;
- 接入 OpenTelemetry Profiles(规范成熟后);
- 故障回溯流程纳入 profile 证据。
14.3 最终检查清单
十五、参考资料
- Brendan Gregg. Flame Graphs. https://www.brendangregg.com/flamegraphs.html
- Brendan Gregg. The Flame Graph. ACM Queue 2016. https://queue.acm.org/detail.cfm?id=2927301
- Google. Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers. IEEE Micro 2010.
- Go team. Profiling Go Programs. https://go.dev/blog/pprof
- Google pprof. github.com/google/pprof
- Russ Cox. Profile-Guided Optimization in Go 1.21. https://go.dev/blog/pgo
- async-profiler. github.com/async-profiler/async-profiler
- JEP 328: Flight Recorder. https://openjdk.org/jeps/328
- JEP 349: JFR Event Streaming. https://openjdk.org/jeps/349
- JEP 435 (draft): Asynchronous Stack Trace VM API. https://openjdk.org/jeps/435
- Java Mission Control. https://adoptium.net/jmc/
- py-spy. github.com/benfred/py-spy
- Austin. github.com/P403n1x87/austin
- Pyinstrument. github.com/joerick/pyinstrument
- memray. github.com/bloomberg/memray
- pprof-rs. github.com/tikv/pprof-rs
- cargo-flamegraph. github.com/flamegraph-rs/flamegraph
- criterion.rs. github.com/bheisler/criterion.rs
- Grafana Pyroscope 文档. https://grafana.com/docs/pyroscope/
- Pyroscope 架构. https://grafana.com/docs/pyroscope/latest/reference-pyroscope-architecture/
- Parca. github.com/parca-dev/parca
- parca-agent. github.com/parca-dev/parca-agent
- Polar Signals Blog. https://www.polarsignals.com/blog
- Elastic Universal Profiling. https://www.elastic.co/observability/universal-profiling
- OpenTelemetry Profiling SIG. github.com/open-telemetry/community/blob/main/sigs/profiling.md
- OpenTelemetry Profiles Data Model (OTEP 212). github.com/open-telemetry/oteps/pull/212
- Linux perf. https://perf.wiki.kernel.org/
- Linux perf_event_open(2). man perf_event_open
- BCC offcputime. github.com/iovisor/bcc/blob/master/tools/offcputime.py
- Frame Pointers in Fedora 38. https://fedoraproject.org/wiki/Changes/fno-omit-frame-pointer
- Ubuntu 24.04 Frame Pointers. https://ubuntu.com/blog/ubuntu-performance-engineering-with-frame-pointers-by-default
- FrostDB. github.com/polarsignals/frostdb
- 字节跳动 Continuous Profiling 分享. InfoQ / ArchSummit 相关演讲
- 美团技术博客:Java 性能诊断实践. https://tech.meituan.com/
- Datadog Continuous Profiler. https://docs.datadoghq.com/profiler/
上一篇:OpenTelemetry 深入:SDK、Collector、语义约定与版本演进
下一篇:Events 与变更关联:CloudEvents、发布打点、K8s 事件
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
持续性能分析(Continuous Profiling):Parca、Pyroscope、Grafana Beyla
深入剖析持续性能分析(Continuous Profiling)的原理、架构与落地实践,覆盖 Parca、Pyroscope、Grafana Beyla 三大主流方案,包含 eBPF 采样、符号解析、火焰图、差异分析以及字节跳动、美团的生产案例与工程坑点。
可观测性工程
从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。
【可观测性工程】可观测性全景:Metrics、Logs、Traces、Profiles、Events 五大支柱
从控制论到云原生:拆解可观测性的五大信号支柱,对比监控与可观测性的本质区别,梳理开源/商业/SaaS 分类,以及国内互联网公司三大支柱落地现状与典型工程坑点。
【可观测性工程】eBPF 可观测性全景:bcc、bpftrace、libbpf 的工程路径
eBPF 如何实现零侵入、内核级、低开销的可观测性:从 kprobe/uprobe/tracepoint/fentry 钩子机制,到 bcc 工具集、bpftrace 脚本语言、libbpf+CO-RE 可移植编程,再到 Pixie、DeepFlow、Grafana Beyla 等商业化工具,结合内核版本兼容性与生产部署实战。