土法炼钢兴趣小组的算法知识备份

【可观测性工程】持续性能分析(Profiling):pprof、Pyroscope、Parca、async-profiler、JFR

文章导航

分类入口
architectureobservability
标签入口
#profiling#pprof#pyroscope#parca#async-profiler#jfr#ebpf#continuous-profiling#flamegraph#opentelemetry-profiles

目录

在 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 的种类与意义

Profiling 并不是单一维度的动作,而是一族针对不同资源、不同时间域的观测方法。理解它们的区别是用好任何工具的前提。

1.1 On-CPU Profiling:CPU 时间去哪了

On-CPU Profiling 回答的是:「在某段时间窗口内,进程占用 CPU 的每一个样本落在哪一个调用栈上?」它的统计单位通常是「CPU 时间」而不是「墙钟时间」(wall-clock time),因此线程在 I/O、锁、睡眠等非 CPU 阶段是不计入的。

On-CPU 是最常用的 Profiling 形态,因为:

典型工具:Linux perf record、Go pprof 的 /debug/pprof/profile、async-profiler 的 -e cpu、py-spy、Parca eBPF Agent。

1.2 Heap / Allocation Profiling:内存分配与驻留

Heap Profiling 关心两个问题:

Go 的 /debug/pprof/heap 会同时报告 alloc_spacealloc_objectsinuse_spaceinuse_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 采样的是:当线程获取锁失败陷入等待时,该事件对应的调用栈。

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.pybpftrace。async-profiler 在 -e wall 模式下也可以把线程非 CPU 时间纳入火焰图。

1.6 Wall-clock Profiling

Wall-clock 采样对每个线程都按固定频率采样,无论它当时是否在 CPU 上运行。得到的火焰图里,sleepepoll_waitfutex_wait 会成为显著贡献者,这反而是排查端到端延迟的利器——它让你看到「一次请求的 30ms 到底在哪儿等了」。

py-spy、async-profiler -e wall、Pyroscope 的 wall 模式都支持这种采样。代价是:对睡得多的服务,热点会被 idle 栈淹没,需要额外过滤。

1.7 Network / Syscall Profiling

严格来说这属于 Tracing 或 eBPF 可观测性范畴,但很多团队会把它并入 Profiling 讨论,因为它回答同一类「调用栈级归因」问题。例如 BCC 的 tcptoptcplife 按 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 时间占比。

优点:

缺点:

2.2 插桩式 Profiler

插桩在函数入口/出口注入计时代码,精确统计每次调用的耗时与次数。Python 的 cProfile、Java 字节码增强、C++ -pg 编译出的 gprof 都属于这一类。

优点:

缺点:

一个经验是:做本地压测、基准测试、算法分析用插桩;做线上诊断、持续剖析用采样。

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 内核提供的高精度性能监控子系统,核心能力:

最常见的使用方式:

# 以 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 个可编程计数器 + 若干固定计数器。典型事件:

对高性能服务(如交易、数据库内核、网关),单纯的 on-CPU 火焰图不足以揭示瓶颈——很多时候 CPU 被「cache miss 导致的 stall」消耗,而不是真的在执行热点函数。此时需要基于 PMU 事件(如 cache-misses)采样生成「缓存火焰图」。

2.6 火焰图与冰柱图

火焰图(flame graph) 是 Brendan Gregg 提出的调用栈可视化方式:

冰柱图(icicle graph) 是火焰图的上下翻转版,根在顶部、叶子在底部,Grafana Pyroscope 默认就是冰柱风格。对阅读「调用栈从谁发起」更直观。

还有两类常见衍生图:

三、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
}

这种结构很有设计感:

大多数现代 profiling 后端(Pyroscope、Parca、Polar Signals、字节内部 CPP)都原生支持 pprof 格式,这让跨语言互操作成为可能。

3.2 内置 profile 类型

Go 运行时内置多种 profile,通过 runtime/pprofnet/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()
}

注意两点:

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 火焰图有几个实战要点:

  1. 先看宽度分布。如果前三宽柱子相加只有 20%,说明 CPU 消耗非常分散,单靠优化热点收益有限,要么改算法要么减请求量;
  2. 识别运行时栈runtime.mallocgcruntime.scanobjectruntime.gcBgMarkWorker 一整片宽柱子 = GC 压力大,通常要降低分配;runtime.findrunnableruntime.schedule 占比高 = 调度争用或 GOMAXPROCS 配置问题;syscall.Syscall 占比高 = 系统调用多(网络、文件);
  3. 识别反射与序列化reflect.Value.*encoding/json.* 在中高并发服务中是常客,经常能通过替换 json 为 sonic/easyjson、避免反射获得数量级提升;
  4. 识别锁栈sync.(*Mutex).Locksync.(*RWMutex).RLock 叶子出现高占比,应该结合 mutex profile 进一步分析;
  5. 警惕 sleep / selectgo。CPU profile 里看到 runtime.selectgo 占比高并不一定说明有问题——selectgo 本身会被采样进入,需结合业务判断。

3.6 Go 常见反模式

pprof 反复暴露的一些 Go 性能反模式:

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 用 GetStackTraceGetAllStackTraces 抓取栈,但这些 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>

几个关键事件:

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 的设计理念:

启动方式:

# 启动时直接打开
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=myrec

settings 常见值:

4.4 JMC 与 JFR 分析

Java Mission Control(JMC)是 Oracle 开源的 JFR 分析 GUI,2018 年独立成 Adoptium 项目。JMC 提供:

除了 JMC,开源生态还有:

4.5 Honest profiling

async-profiler 引入了 honest profiling 概念:只在明确能还原栈的位置上报样本,无法还原的样本单独标记为 [unknown],避免瞎归因。与之相比,JFR 的 ExecutionSample 有时仍会出现 no Java frame 的样本,但好在比例可控。

在排查「火焰图底部出现大量陌生函数」时,区分 honest 样本和 fallback 样本非常重要。

4.6 生产部署建议

典型做法:

  1. 所有 JVM 进程启动时开启 JFR default.jfc,常驻运行;
  2. 健康检查或告警触发时,jcmd 把 JFR 环形缓冲 dump 到磁盘;
  3. 出现性能问题时,额外用 async-profiler -e cpu-e alloc 做 60 秒高精度采样;
  4. 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.out

cProfile 精确统计每个函数的调用次数、总耗时、累计耗时。缺点:开销较大(几十倍不罕见),不适合生产,也不适合 CPU-bound 紧密循环(插桩开销本身比函数体还大)。

5.2 py-spy:零侵入采样 profiler

py-spy 是 Rust 写的外部采样 profiler,核心思路是:

用法:

# 安装
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 的优点:

缺点:在容器内诊断需要 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 服务诊断流程:

  1. 监控发现延迟上涨 → 判断是 CPU 还是 I/O 瓶颈;
  2. 进入容器 py-spy top -p 1 实时看函数热度;
  3. 确认是 CPU 热点后 py-spy record -d 60 -o hot.svg
  4. 打开火焰图,识别是业务代码还是第三方库(如 requestssqlalchemy);
  5. 若是内存增长,用 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/myapp

Parca、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:

配合时间序列索引(按 service、profile_type、labels 切片),一次查询等价于:

  1. 按 labels 找到候选 series;
  2. 按时间范围拉取 raw profile 块;
  3. 合并并解码为 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.jar

Python

import pyroscope

pyroscope.configure(
    application_name = "worker",
    server_address   = "http://pyroscope:4040",
    tags             = {"env": "prod"},
)

7.4 部署模式

Pyroscope 支持三种部署:

  1. monolithic:单进程,所有组件跑在一起,适合小规模或测试;
  2. microservices:Distributor / Ingester / Compactor / Querier 各自独立部署,生产推荐;
  3. 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: 168h

7.5 Grafana 集成

Grafana 10+ 内置 Pyroscope 数据源,支持:

八、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     │
                               └────────────────────┘

8.2 eBPF 采样原理

parca-agent 不依赖语言运行时,也不需要目标进程合作:

  1. 加载 perf_event BPF program,附着到 PERF_COUNT_SW_CPU_CLOCK,以 19Hz / 49Hz / 99Hz 触发;
  2. 在 BPF program 里用 bpf_get_stack 抓取用户态指令指针;
  3. 把 pid + stackid 写入 BPF map;
  4. 用户态 agent 周期性读取 map,用目标二进制的符号表与 DWARF 信息还原函数名、文件、行号;
  5. 聚合成 pprof 上报给 parca-server。

这种方式的优势:

挑战主要在栈回溯。

8.3 Frame Pointer vs DWARF 栈回溯

eBPF 内核栈采样有两种机制:

很多发行版为了性能早期默认 -fomit-frame-pointer,导致 eBPF profiler 栈断裂。社区推动下:

parca-agent 1.x 引入了自定义的「unwind table」:提前解析 .eh_frame,把回溯规则压进 BPF map,让 BPF 内直接用表驱动回溯,从而摆脱对 FP 的依赖。这也是它的技术亮点。

8.4 Java / Python 符号

对于 JIT 语言,parca-agent 会:

但相比 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 } }

关键点:

8.6 与 OpenTelemetry 集成

2024 年 Elastic 把其内部基于 eBPF 的 Universal Profiler 捐赠给 OpenTelemetry 社区,成为 OpenTelemetry eBPF Profiler。Parca / Polar Signals 也在持续对 OTel Profiles 信号做贡献。趋势很明显:eBPF profiling 正在被标准化。

九、持续性能分析(Continuous Profiling)

「持续」是持续性能分析的关键词,它要求 profiler 能常年挂在生产上,而不只是在事故发生时才启动。

9.1 为什么要持续

Google 在 2010 年就用 Google-Wide Profiling(GWP)做到了整数据中心级持续采样,年度 CPU 开销控制在 0.01% 以内。

9.2 采样合并与 TSDB 化

持续 profiling 与传统 profiling 的关键区别是:

合并算法的核心是「同结构栈的值相加」:

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 关联:

这是 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.idprofile.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 的事实标准。核心创新:

一份 30 秒 CPU profile 通常 50~300KB;压缩前可达 MB 级。

10.3 profile 树与列式存储

Parca 的 FrostDB 把 pprof 按列式存储:

这样按 label 过滤 + 时间范围聚合时,IO 只扫相关列。

Pyroscope 引入 symdb 后走向类似方向:

10.4 Delta vs Cumulative

一份 CPU profile 其实是「时间窗口内的增量」(delta),每 10 秒上报一份。但 heap/goroutine 更自然是「当前累计快照」(cumulative)。存储时要区分:

OpenTelemetry Metrics 早就有 delta vs cumulative 的讨论,Profiles 信号基本沿用。

10.5 压缩

几层压缩叠加:

合计一份 30s CPU profile 落地后可能只有 10~30KB。即便如此,1000 个 pod、每 10 秒一份,一天也有 2.6 亿 profile,TB 级是常态,冷热分层与保留策略必不可少。

十一、OpenTelemetry Profiles(2024+)

长期以来 OpenTelemetry 只定义了 Metrics / Logs / Traces 三种信号,Profiles 缺位是社区共识的短板。

11.1 规范状态

11.2 数据模型

OTel Profiles 的 OTLP schema 与 pprof 高度相似,增加了:

核心 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 引入 profilesreceiverprofilesexporter

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 年初:

Profiles 信号预计在 2026~2027 年成为与 Traces 同等级别的一等公民。

十二、国内落地案例

12.1 字节跳动 CPP(Continuous Profiling Platform)

字节跳动公开分享过内部的持续剖析平台,其演进路线具有代表性:

公开分享的效果数字:通过持续剖析发现的 Go 服务 CPU 优化项平均每季度节省万核级资源。

12.2 美团 Profiling 实践

美团技术博客数篇文章讨论过:

12.3 Java 服务的 async-profiler

国内大型 Java 服务(支付、电商、搜索)基本形成共识:

12.4 Go 服务在线诊断

Go 社区常见做法:

十三、工程坑点

13.1 Safepoint bias 与 honest profiling

前文提过的 JVM safepoint bias 是经典坑。排查方法:

  1. 同时用 async-profiler 与传统 JVMTI profiler 抓样;
  2. 对比热点函数排名,如果差异巨大(top3 完全不同),说明 safepoint bias 严重;
  3. 信任 async-profiler 的结果。

13.2 符号表丢失

编译时 strip 或 UPX 压缩会剥离符号,火焰图全是 [unknown]。对策:

13.3 容器内 profiling 权限

容器默认权限非常严格,profiler 常用权限:

K8s 最佳实践:

另外,某些 PaaS 默认禁用 perf_event_paranoid 低等级,需要协调 sysctl kernel.perf_event_paranoid=1

13.4 Frame pointer omission

默认编译选项 -fomit-frame-pointer 让 BPF 栈回溯失败。解决方案:

注意 DWARF 回溯内存/CPU 开销是 FP 的 5~10 倍,高频采样时需权衡。

13.5 Cross-container 符号解析

同一节点上多个容器的可执行文件路径在主机命名空间下看是 /proc/<pid>/root/usr/bin/app。agent 读符号时要正确处理命名空间映射:

parca-agent、pyroscope-agent 都专门处理这部分逻辑,自研时容易忽略。

13.6 内存 profiling 开销

Java 在老版本 JVM 开启 -Xrunhprof 做内存 profile 会拖慢 100 倍。现代 async-profiler -e alloc 基于 JVMTI SampledObjectAlloc hook,每 TLAB slow path 才触发一次,开销 1~3%。尽管如此,采样间隔需合理:

Go 的 MemProfileRate 也是类似考量,默认 512KB,不建议调小到 KB 级。

13.7 Pyroscope 存储增长

一个 tenant 如果:

每天 profile 数量:200 × 50 × 2 × 6 × 60 × 24 ≈ 3.5 亿。即便每份压缩后 20KB,裸数据也达 7TB/天。对策:

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 有时栈回溯异常。对策:

13.10 采样频率与 Nyquist

按 Nyquist 定理,要观察 X Hz 的现象需要至少 2X Hz 采样。实际中 99Hz 足以覆盖绝大多数热点,但如果你要分析「每秒 50 次的微 GC pause」这种快速事件,99Hz 会严重欠采样。此时需要:

十四、选型建议与落地清单

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 月)

Step 2:持续 profiling 试点(2~3 月)

Step 3:全量推广(4~6 月)

Step 4:与可观测性打通(6 月+)

14.3 最终检查清单

十五、参考资料

  1. Brendan Gregg. Flame Graphs. https://www.brendangregg.com/flamegraphs.html
  2. Brendan Gregg. The Flame Graph. ACM Queue 2016. https://queue.acm.org/detail.cfm?id=2927301
  3. Google. Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers. IEEE Micro 2010.
  4. Go team. Profiling Go Programs. https://go.dev/blog/pprof
  5. Google pprof. github.com/google/pprof
  6. Russ Cox. Profile-Guided Optimization in Go 1.21. https://go.dev/blog/pgo
  7. async-profiler. github.com/async-profiler/async-profiler
  8. JEP 328: Flight Recorder. https://openjdk.org/jeps/328
  9. JEP 349: JFR Event Streaming. https://openjdk.org/jeps/349
  10. JEP 435 (draft): Asynchronous Stack Trace VM API. https://openjdk.org/jeps/435
  11. Java Mission Control. https://adoptium.net/jmc/
  12. py-spy. github.com/benfred/py-spy
  13. Austin. github.com/P403n1x87/austin
  14. Pyinstrument. github.com/joerick/pyinstrument
  15. memray. github.com/bloomberg/memray
  16. pprof-rs. github.com/tikv/pprof-rs
  17. cargo-flamegraph. github.com/flamegraph-rs/flamegraph
  18. criterion.rs. github.com/bheisler/criterion.rs
  19. Grafana Pyroscope 文档. https://grafana.com/docs/pyroscope/
  20. Pyroscope 架构. https://grafana.com/docs/pyroscope/latest/reference-pyroscope-architecture/
  21. Parca. github.com/parca-dev/parca
  22. parca-agent. github.com/parca-dev/parca-agent
  23. Polar Signals Blog. https://www.polarsignals.com/blog
  24. Elastic Universal Profiling. https://www.elastic.co/observability/universal-profiling
  25. OpenTelemetry Profiling SIG. github.com/open-telemetry/community/blob/main/sigs/profiling.md
  26. OpenTelemetry Profiles Data Model (OTEP 212). github.com/open-telemetry/oteps/pull/212
  27. Linux perf. https://perf.wiki.kernel.org/
  28. Linux perf_event_open(2). man perf_event_open
  29. BCC offcputime. github.com/iovisor/bcc/blob/master/tools/offcputime.py
  30. Frame Pointers in Fedora 38. https://fedoraproject.org/wiki/Changes/fno-omit-frame-pointer
  31. Ubuntu 24.04 Frame Pointers. https://ubuntu.com/blog/ubuntu-performance-engineering-with-frame-pointers-by-default
  32. FrostDB. github.com/polarsignals/frostdb
  33. 字节跳动 Continuous Profiling 分享. InfoQ / ArchSummit 相关演讲
  34. 美团技术博客:Java 性能诊断实践. https://tech.meituan.com/
  35. Datadog Continuous Profiler. https://docs.datadoghq.com/profiler/

上一篇OpenTelemetry 深入:SDK、Collector、语义约定与版本演进

下一篇Events 与变更关联:CloudEvents、发布打点、K8s 事件

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / observability

持续性能分析(Continuous Profiling):Parca、Pyroscope、Grafana Beyla

深入剖析持续性能分析(Continuous Profiling)的原理、架构与落地实践,覆盖 Parca、Pyroscope、Grafana Beyla 三大主流方案,包含 eBPF 采样、符号解析、火焰图、差异分析以及字节跳动、美团的生产案例与工程坑点。

2026-04-22 · architecture / observability

可观测性工程

从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。


By .