C++项目推荐:eBPF+调度器性能分析框架

来源:程序员老廖的个人空间

想象一个场景:你写了一个程序,运行起来很慢,但你不知道慢在哪里。是 CPU 算得慢?还是等磁盘 IO?还是被锁卡住了?

传统的方法是加 printf 打时间戳,或者用 perf 采样。但这些方法要么侵入代码,要么输出晦涩难懂。

本项目提供了一个完整的解决方案:

你的程序 ──→ TaskScheduler 调度执行 ──→ eBPF 内核级追踪 ──→ 可读报告 + 火焰图

整个过程零代码侵入——你不需要修改被分析的程序,调度器会自动在内核层面"透视"你的程序行为。

本教程将带你理解:

  1. 原理层:调度器如何管理进程?eBPF 如何在内核中采集数据?
  2. 实践层:如何运行实验、解读报告、定位瓶颈?
  3. 设计层:为什么 workload 要这样设计?编译选项为什么重要?

全景图:一条命令的完整旅程

下面用一条真实命令,追踪它从你敲下回车到最终生成报告的每一步,标注对应的源文件和代码行号。

你输入的命令:

sudo ./build/scheduler \
    --cmd "./build/workload_io --rounds 3000" \
    --enable-ebpf --ebpf-script-dir ./bpf --timeout 60

这条命令触发的完整调用链如下(从上到下是时间顺序):

逐步详解:

阶段一:命令行解析与任务提交

阶段二:任务调度与进程创建

阶段三:eBPF 附加与 workload 启动

这是最关键的阶段——workload_io 就是在这里被真正执行的。

核心要点: workload_io 是通过 execvp 被加载执行的,不是直接调用函数。 execvp 会将当前子进程的整个内存空间替换为 workload_io 的二进制代码,然后从 workload_io 的 main() 入口开始运行。

三进程协作时间线——下图展示 scheduler 父进程、子进程、bpftrace 三者在时间维度上的状态:

时间轴 ────────────────────────────────────────────────────────────────────────►
scheduler 父进程:
┌──────────────────────────────────────────────────────────────────────────────┐
│ fork ───→ waitpid(WUNTRACED) ───→ start_profiling ───→ 轮询等待               │
│            等子进程暂停             fork bpftrace       bpftrace就绪           │
│                                                             │                │
│                                      reaper_loop:           │                 │
│                                     waitpid(WNOHANG) 轮询   │                 │
│                                     ────→ 检测到子进程退出   │                 │
│                                     ────→ stop_profiling    │                 │
│                                     ────→ 符号解析+火焰图    │                 │
└───────────────────────────────────────────────────────────────────────────────┘
子进程 → workload:
┌───────────────────────────────────────────────────────────────────────────────┐
│ fork ──→ raise(SIGSTOP) ──→ [冻结] ──→ SIGCONT ──→ execvp                     │
│              暂停自己                   被唤醒     加载workload                 │
│                                                       │                       │
│                                                  workload_io 运行中:           │
│                                                  do_work 循环 3000 轮          │
│                                                  compute_hash                 │
│                                                  write_data                   │
│                                                  sync_to_disk                 │
│                                                   ───────→ 正常退出            │
└───────────────────────────────────────────────────────────────────────────────┘
bpftrace 进程:
┌────────────────────────────────────────────────────────────────────────────────┐
│ fork ──→ 加载 BPF 程序 ──→ 附加探针 ──→ BEGIN 输出                               │
│           编译 profile_task.bt         "eBPF 启动"                              │
│                                            │                                   │
│                           实时采集 workload 数据:                               │
│                           profile:hz:99 CPU 栈采样                              │
│                           sys_enter/exit 系统调用跟踪                            │
│                           ───────→ 目标退出                                     │
│                           ───────→ 收到 SIGINT                                  │
│                           ───────→ END 块: 生成报告                              │
└─────────────────────────────────────────────────────────────────────────────────┘

关键同步点:

  • 同步点 A:子进程 raise(SIGSTOP) → 父进程 waitpid(WUNTRACED) 返回
  • 同步点 B:bpftrace BEGIN 输出 → 父进程轮询发现文件非空 → kill(SIGCONT) 唤醒子进程
  • 同步点 C:子进程退出 → bpftrace 检测到目标消失 → reaper waitpid 回收

阶段四:运行中 eBPF 数据采集

阶段五:收割与报告生成

报告后处理流水线

stop_profiling 内部将 bpftrace 的原始输出经过 6 步流水线处理,最终变成人类可读的报告。每一步对应源码中 的一个函数:

数据变化示例——以一段栈采样为例

第 1 步 原始数据: @cpu_stack_samples[\n 0x401505\n 0x4015d7\n]: 42
第 2 步 符号解析: cpu_stack_samples[\n heavy_compute\n do_work\n]: 42
第 3 步 清除 @ 前缀: [\n heavy_compute\n do_work\n]: 42
第 6 步 函数排名: >>> CPU 热点函数排名:
60.0% heavy_compute (42 samples)
第 7 步 火焰图 folded: do_work;heavy_compute 42
→ perl flamegraph.pl → .svg 文件

workload_cpu 的执行路径有何不同?

workload_cpu 的执行路径与 workload_io 完全相同——区别只在 execvp 的参数不同

# IO workload:
execvp("./build/workload_io", ["./build/workload_io", "--rounds", "3000"])

# CPU workload:
execvp("./build/workload_cpu", ["./build/workload_cpu", "--threads", "4", "--iterations",
"200000000"])

对调度器而言,它只是执行了 --cmd 参数里的命令字符串,不关心具体执行的是哪个程序。这就是设计的关键——调度器是通用的。

两种 workload 被 eBPF 追踪时的行为差异:

项目源码领取:*********************************************

第一章:项目架构与核心原理

1.1 整体架构

TaskScheduler 是一个 C++20 单机任务调度器,核心功能是接收用户提交的命令、以独立进程执行、并通过 eBPF 实时分析性能瓶颈。

关键设计决策:

1.2 调度器工作原理

调度器内部有三个核心线程协同工作:

dispatcher 线程是"发令枪":

  1. 从 pending 队列取出任务
  2. 向 ResourceManager 申请 CPU + 内存资源
  3. 创建 cgroup 并 fork 子进程
  4. 如果启用了 eBPF,子进程会先 raise(SIGSTOP) 暂停自己

reaper 线程是"收割者":

  1. 循环 waitpid(WNOHANG) 检查子进程状态
  2. 检测超时并分级发送 SIGTERM → SIGKILL
  3. 子进程退出后,收集 eBPF 报告、释放资源、清理 cgroup

psi 线程是"压力传感器":

  1. 定期读取 /sys/fs/cgroup/memory.pressure 和 cpu.pressure
  2. 如果系统压力过高,设置背压标志
  3. dispatcher 发现背压后暂停调度,等压力缓解

任务生命周期

1.3 cgroup v2 资源隔离

cgroup v2 是 Linux 内核提供的资源控制组机制。调度器利用它来限制每个任务可使用的资源:

/sys/fs/cgroup/scheduler/  # 调度器的 cgroup 根目录
├── job_1/              # 任务 1 的 cgroup
│ ├── cgroup.procs     # 写入 PID 即将进程加入此组
│ ├── memory.max       # 内存上限,如 "268435456"(256MB)
│ └── cpu.max          # CPU 配额,如 "100000 100000"(1 核)
├── job_2/
│ └── ...

工作流程:

cpu.max 格式解读: "$QUOTA $PERIOD" — 在每个 $PERIOD 微秒内,允许使用 $QUOTA 微秒的 CPU。例如 "200000 100000" 表示"100ms 周期内可用 200ms CPU"= 2 个核心。

1.4 PSI 背压机制

PSI(Pressure Stall Information)是 Linux 内核的资源压力指标。当系统 CPU 或内存紧张时,PSI 会报告压力百分比。

# /sys/fs/cgroup/scheduler/memory.pressure
some avg10=0.50 avg60=0.30 avg300=0.10 total=12345
full avg10=0.00 avg60=0.00 avg300=0.00 total=0

# /sys/fs/cgroup/scheduler/cpu.pressure
some avg10=25.30 avg60=15.00 avg300=8.00 total=98765

调度器的背压策略:

任一指标超阈值 → 暂停调度新任务,直到压力缓解。

1.5 代码调用关系详解

本节用源码级别的调用图展示各模块之间的函数调用关系,帮助你在阅读源码时快速定位。

1.5.1 源文件之间的依赖关系

1.5.2 scheduler.cpp 内部函数调用关系

scheduler.cpp 是调度器的核心。以下是它内部的函数调用链:

1.5.3 launch_job 内部的 fork 分支详解

launch_job 是整个项目最关键的函数——它决定了 workload 如何被执行。

关键理解: fork() 之后,代码分成两条路径同时执行:

execvp 的效果是用 workload 的二进制代码完全替换子进程的内存。执行 execvp 之后,子进程已经不再是 scheduler 的代码了,它变成了 workload_io 或 workload_cpu 。

1.5.4 ebpf_profiler.cpp 内部函数调用关系

1.5.5 workload 内部函数调用关系

workload_io.cpp:

workload_cpu.cpp:

第二章:eBPF 分析原理

2.1 什么是 eBPF

eBPF(Extended Berkeley Packet Filter)是 Linux 内核中的一个可编程虚拟机。它允许你在不修改内核源码、不重启系统的前提下,将小段程序注入内核的特定位置执行。

类比理解:如果内核是一条高速公路,eBPF 就是在公路边设置的测速摄像头——不影响车辆通行,但能准确记录每辆车的速度。

传统性能分析工具的对比:

2.2 bpftrace 工具链

bpftrace 是 eBPF 的高级前端,提供类 awk 的脚本语言,大幅降低 eBPF 编程门槛。

本项目使用 bpftrace -p <PID> 模式——附加到一个正在运行的进程。整个工作流如下:

为什么要先 SIGSTOP 再 SIGCONT?

如果子进程直接运行,bpftrace 还没准备好,前几毫秒的事件就丢失了。通过 SIGSTOP/SIGCONT 协调,保证从第一条指令开始就被追踪。

2.3 探针类型与数据采集

profile_task.bt 脚本使用了以下探针类型:

2.4 On-CPU 采样原理

On-CPU 采样的核心思想:以固定频率打断 CPU,记录当前正在执行的函数。

举个例子:如果程序运行 10 秒,99Hz 采样就会产生约 990 个样本。假设:

  • 600 个样本的栈顶是 heavy_compute → 60% CPU 时间
  • 200 个样本的栈顶涉及 std::sort → 20% CPU 时间(来自 fast_init )
  • 190 个样本的栈顶涉及 sin/cos/log1p → 19% CPU 时间(来自 light_aggregate )

这就得出了函数级 CPU 热点排名。

为什么选 99Hz 而不是 100Hz?

避免与系统其他定时器(通常是 100Hz/250Hz)产生锁步效应(lock-step),导致总是采样到相同位置。99 是质数,能更均匀地分散采样点。

栈采样深度:

本项目的 BPF 脚本使用 ustack(16) 来采集用户态栈,最多可获取 16 层栈信息。这样可以确保捕获到完整的函数调用链,包括 main 函数内部调用的 func_a 、 func_b 、 func_c 等函数。

2.5 Off-CPU 分析原理

On-CPU 采样能找到"CPU 上在忙什么",但如果程序大部分时间在等待 IO,CPU 采样反而抓不到它——因为等待时进程不占 CPU。

Off-CPU 分析解决的问题是:程序不在 CPU 上的时间,到底在等什么?

本项目的 Off-CPU 分析策略:

  1. 在 sys_enter_fsync 时刻,记录用户态调用栈 + 进入时间
  2. 在 sys_exit_fsync 时刻,计算 阻塞时长 = 返回时间 - 进入时间
  3. 将阻塞时长按调用栈分组累加

为什么在 syscall 入口记录栈,而不是在内核态?

因为 libc 的 fsync() 实现可能不保留帧指针,导致从内核态回溯栈时断裂。在 syscall 入口记录用户态栈,能保证看到完整的应用层调用链。

2.6 符号解析流水线

bpftrace 输出的原始栈帧可能是地址(如 0x401505 ),用户看不懂。需要将地址翻译成函数名。

详细步骤:

  • 收集 /proc/<pid>/maps :记录进程的内存映射,知道每个地址范围属于哪个可执行文件/共享库
  • 分组地址:将地址按所属 EXE/SO 分组
  • 计算文件偏移: file_offset = virtual_addr - segment_start + segment_offset
  • 调用 addr2line: addr2line -f -s -e /path/to/binary 0x... 获取函数名
  • C++ 反修饰: c++filt _ZN5heavy7computeEyy → heavy_compute

为什么 workload 要用 -no-pie 编译?

PIE(Position-Independent Executable)会让可执行文件每次加载到随机地址。对于可执行文件本身的符号,非 PIE 模式下地址是固定的(如 0x401xxx ), addr2line 可以直接解析。PIE 模式则需要额外计算 ASLR 偏移。

第三章:工作负载设计哲学

3.1 为什么需要精心设计的 workload

一个好的性能分析教学 workload 需要满足:

  1. 清晰的函数调用层次:能在栈中看到调用关系
  2. 多个可辨识的热点:不是只有一个函数占 99%,而是多个函数有不同占比
  3. 可被 eBPF 观测:函数不能被编译器内联、栈帧不能丢失
  4. 运行时间适中:太短采样不够,太长等待太久

3.2 IO 阻塞型负载设计

workload_io.cpp 模拟一个典型的日志写入/数据持久化场景:

每个函数的设计意图:

关键代码片段与编译器对抗:

// __attribute__((noinline)) 防止编译器将函数内联到 do_work 中
// 如果被内联,eBPF 栈采样就看不到独立的函数名
__attribute__((noinline))
int sync_to_disk(int fd) {
    // volatile 防止编译器将 fsync 优化为尾调用
    // 尾调用会复用调用者的栈帧,导致栈回溯时看不到 sync_to_disk
    volatile int rc = fsync(fd);
    return rc;
}

3.3 CPU 密集型负载设计

workload_cpu.cpp 模拟一个数据处理管线:初始化 → 计算 → 聚合。

每个函数的设计意图:

多线程的好处:4 个线程会让 CPU 采样数据更丰富,可以观察到:

  • 每个线程的采样是否均匀分布(通过 各线程 CPU 采样分布 段)
  • 多线程对 std::sort 等函数的竞争

3.4 编译器对抗:防内联与帧指针

eBPF 栈采样依赖完整的调用栈。编译器优化会破坏这一点:

CMakeLists.txt 中的关键配置:

set(WORKLOAD_FLAGS -fno-omit-frame-pointer -g -O0 -no-pie)
target_compile_options(workload_io PRIVATE ${WORKLOAD_FLAGS})
target_link_options(workload_io PRIVATE -no-pie)

__attribute__((noinline)) 是函数级的防内联指令。即使在 -O2 下,标记了 noinline 的函数也不会被内联。

第四章:环境搭建与编译

4.1 系统依赖安装

操作系统要求:Linux 内核 5.x+,推荐 Ubuntu 22.04 / 24.04 LTS。

# 基础编译工具
sudo apt update
sudo apt install -y build-essential cmake g++

# eBPF 工具链
sudo apt install -y bpftrace linux-headers-$(uname -r)

# 符号解析工具
sudo apt install -y binutils # 提供 addr2line 和 c++filt

# 火焰图工具(Brendan Gregg 的 FlameGraph)
git clone https://github.com/brendangregg/FlameGraph.git /tmp/FlameGraph
sudo cp /tmp/FlameGraph/flamegraph.pl /usr/local/bin/
sudo chmod +x /usr/local/bin/flamegraph.pl

# 或者放到项目的 tools/ 目录
mkdir -p tools
cp /tmp/FlameGraph/flamegraph.pl tools/
chmod +x tools/flamegraph.pl

验证依赖是否就绪:

# 检查 bpftrace
bpftrace --version
# 应输出 bpftrace v0.17+

# 检查 BTF 支持(eBPF 类型格式)
ls /sys/kernel/btf/vmlinux
# 应存在该文件

# 检查 addr2line
addr2line --version
# 检查是否有 root 权限(eBPF 需要 root)
sudo whoami
# 应输出 root

4.2 项目编译

# 克隆项目
git clone <项目地址> TaskScheduler
cd TaskScheduler

# 创建构建目录
mkdir -p build && cd build

# 配置
cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo

# 编译(使用所有 CPU 核心)
make -j$(nproc)

编译成功后, build/ 目录下会生成三个可执行文件:

build/
├── scheduler # 调度器主程序
├── workload_io # IO 阻塞型测试程序
└── workload_cpu # CPU 密集型测试程序

4.3 验证安装

# 1. 测试调度器基本功能(不需要 root)
./build/scheduler --cmd "echo hello" --timeout 5
# 应看到 "hello" 输出和 job 完成日志

# 2. 单独运行 workload,确认它们能正常执行
./build/workload_io --rounds 10
# 应看到 elapsed_ms 输出

./build/workload_cpu --threads 2 --iterations 10000000
# 应看到 elapsed_ms 和 checksum 输出

# 3. 测试 eBPF 功能(需要 root)
sudo ./build/scheduler \
   --cmd "./build/workload_io --rounds 100" \
   --enable-ebpf \
   --ebpf-script-dir ./bpf \
   --timeout 30
# 应看到 eBPF 性能分析报告输出

如果第 3 步报 "not available",请检查:

  • 是否以 root 运行
  • bpftrace 是否安装
  • /sys/kernel/btf/vmlinux 是否存在

第五章:实验操作指南

5.1 实验一:IO 阻塞分析

目标:分析一个以 fsync 为瓶颈的 IO 密集程序,学会识别阻塞热点。

运行命令:

sudo ./build/scheduler \
   --cmd "./build/workload_io --rounds 3000" \
   --enable-ebpf \
   --ebpf-script-dir ./bpf \
   --timeout 60

参数说明:

预期运行时间:约 3-5 秒。

预期结果:

  • CPU 热点排名: __GI_fsync 占 ~82%, compute_hash 占约 ~15%, __libc_pwrite64 占 ~3%
  • Off-CPU 阻塞排名: __GI_fsync 占 100%(约 2.2 秒)
  • 生成 On-CPU 和 Off-CPU 火焰图 SVG 文件

输出文件位置:

taskscheduler_ebpf/
├── job_1_pid_<PID>.txt            # 完整的文本分析报告
├── job_1_pid_<PID>_oncpu.svg      # On-CPU 火焰图(红色主题)
├── job_1_pid_<PID>_offcpu.svg     # Off-CPU 火焰图(蓝色主题)
└── job_1_maps.txt                 # 进程内存映射备份(用于符号解析)

你可以用浏览器打开 SVG 文件查看交互式火焰图:

firefox taskscheduler_ebpf/job_1_pid_*_oncpu.svg

思考题:

1. 为什么 fsync 在 CPU 热点中占比也很高?

提示:fsync 虽然是 IO 操作,但 CPU 需要切换到内核态、构建 IO 请求、等待返回。这段时间如果恰好被 99Hz 采样命中,就会被计入。

2. compute_hash 的 30 轮哈希,如果改成 3 轮,它还能在 CPU 采样中看到吗?

提示:如果计算时间太短(远小于采样间隔 ~10ms),被采样命中的概率就极低。

5.2 实验二:CPU 热点分析

目标:分析一个单线程 CPU 密集程序,学会从采样数据中识别函数级热点。

运行命令:

sudo ./build/scheduler \
    --cmd "./build/workload_cpu" \
    --enable-ebpf \
    --ebpf-script-dir ./bpf \
    --timeout 60

参数说明:

预期运行时间:约 1-3 秒。

预期结果:

输出文件位置:与实验一相同,在 taskscheduler_ebpf/ 目录下。CPU workload 通常只生成 On-CPU 火焰图 (Off-CPU 火焰图可能为空或很小,因为没有阻塞 IO)。

思考题:

1. 为什么 func_c 的采样占比最高?

提示: func_c 包含了最多的计算量(35 个单位),因此被采样命中的概率最高。

2. 火焰图中能看到完整的调用链吗?

提示:火焰图会将所有栈帧展开,你可以看到 main → func_a → func_d 的完整路径。

5.3 实验三:自定义程序分析

你可以分析任何自己的程序。只需要两个条件:

1. 编译时保留帧指针: -fno-omit-frame-pointer -g

2. 用调度器运行:

sudo ./build/scheduler \
    --cmd "/path/to/your/program args..." \
    --enable-ebpf \
    --ebpf-script-dir ./bpf \
    --timeout 300

小贴士:

  • 如果程序运行太快(< 1 秒),CPU 采样可能不够。增加工作量或循环次数。
  • 如果看不到函数名,检查是否用 -g 编译,或尝试加 -no-pie 。
  • 火焰图 SVG 在 taskscheduler_ebpf/ 目录下。

第六章:报告解读指南

eBPF 分析报告是整个项目的核心产出。本章逐段解读报告中的每一个部分。

6.1 报告整体结构

6.2 总览区解读

=====================================================
                   eBPF 性能分析报告
=====================================================
分析时长: 4961 ms
┌───────────────────────────────────────────────────┐
│                    总览                           │
├───────────────────────────────────────────────────┤
│ 系统调用总次数:         24880
│ 慢系统调用(>1ms):       438
│ 上下文切换:             5258
│ - 自愿切换:             5256
│ - 非自愿切换:           2
│ CPU 采样次数:           121
│ 线程创建次数:           0
└───────────────────────────────────────────────────┘

逐行解读:

诊断规则:

  • 自愿切换 >> 非自愿切换 → 程序以 IO 等待为主
  • 非自愿切换 >> 自愿切换 → CPU 密集,被抢占
  • CPU 采样次数 << 理论值 → 程序大部分时间不在 CPU 上(Off-CPU)

6.3 系统调用分布解读

┌───────────────────────────────────────────────────────────────┐
│                系统调用分布 (按 syscall nr)                    │
└───────────────────────────────────────────────────────────────┘
close: 1
write: 1
unlink: 1
exit_group: 1
fsync: 2764
pwrite64: 22112

后处理器已将系统调用号翻译为人类可读的名称。分析要点:

诊断技巧:用 syscall 次数反推代码行为。 pwrite64 / fsync ≈ 8 ,说明 write_data 函数每轮写 8 遍,与代码中 write_count=8 一致。

注意:实际采集到的 syscall 次数可能略少于理论值。因为 bpftrace 在 BEGIN 就绪和 SIGCONT 之间有微小时 间差,前几轮的 syscall 可能未被追踪到。

6.4 IO 分析区解读

┌──────────────────────────────────────────────────────────────┐
│                             IO 分析                          │
├──────────────────────────────────────────────────────────────┤
│ Write 调用次数:                22057
│ Write 总字节:                  90341632
│ fsync 调用次数:                2757
│ fsync 总耗时(us):              2181503
└──────────────────────────────────────────────────────────────┘

关键计算:

  • Write 总字节 = 90,341,632 bytes ≈ 86 MB
  • 每次 Write 大小 = 90341632 / 22057 ≈ 4096 bytes = BUF_SIZE,符合预期
  • fsync 总耗时 = 2,181,503 us ≈ 2.2 秒 — 全程 5 秒中 44% 在 fsync
  • fsync 平均耗时 = 2181503 / 2757 ≈ 791 us ≈ 0.8 ms
--- fsync 延迟分布 (us) ---
[128, 256) 174      |@@@@                                                 |
[256, 512) 53       |@                                                    |
[512, 1K) 2111      |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[1K, 2K) 378        |@@@@@@@@@                                            |
[2K, 4K) 37         |                                                     |
[4K, 8K) 2          |                                                     |
[8K, 16K) 1         |                                                     |
[16K, 32K) 1        |                                                     |

直方图读法:横向柱状图,左边是延迟区间,右边是次数和柱形。

解读:绝大多数 fsync(2111 次)延迟在 512us~1ms 之间。少部分(174 次)在 128~256us 很快完成。有极少量尾部延迟到 32ms,可能是磁盘写合并或操作系统调度抖动。

6.5 CPU 热点函数排名解读

这是最有价值的分析段落,直接告诉你哪个函数消耗了最多 CPU 时间。

>>> CPU 热点函数排名(栈顶采样合并):
    81.8%   __GI_fsync (99 samples)
    14.9%   compute_hash (18 samples)
    3.3%    __libc_pwrite64 (4 samples)

解读方式:

为什么看到的是 __GI_fsync 而不是 sync_to_disk ?

因为 CPU 采样命中时,CPU 正在 glibc 的 fsync() 内部执行。 sync_to_disk 虽然是调用者,但采样记录的是当前最内层函数(栈顶)。火焰图中可以看到完整的调用链。

6.6 Off-CPU 阻塞函数排名解读

┌────────────────────────────────────────────────────────┐
│             Off-CPU 分析 (阻塞等待热点栈)                │
├────────────────────────────────────────────────────────┤
│ 阻塞切换次数:                  2746
│ 阻塞总耗时(us):                2176269
└────────────────────────────────────────────────────────┘

>>> Off-CPU 阻塞函数排名(耗时合并):
    100.0% __GI_fsync (2.18 s)

解读:

  • 阻塞切换次数 2746 ≈ fsync 调用 2757 次(几乎每次 fsync 都触发了阻塞切换)
  • 2.18 秒全部花在 fsync 等待磁盘上
  • 如果有其他阻塞函数(如 pthread_mutex_lock ),也会出现在排名中

CPU 型 workload 的 Off-CPU 情况:

对于 workload_cpu ,Off-CPU 数据通常为空或极少,因为纯 CPU 计算不产生阻塞。这本身就是有意义的信息 ——"程序不存在 IO 瓶颈"。

6.7 两种 workload 报告核心指标对比

下表将 IO workload 和 CPU workload 的报告关键指标并排对比,帮助你快速判断一个程序的瓶颈类型:

快速判断口诀:

  • 自愿切换多 + Off-CPU 有数据 + CPU 采样少 → IO 瓶颈
  • 非自愿切换多 + Off-CPU 为空 + CPU 采样满 → CPU 瓶颈

6.8 直方图读法

报告中多处出现 bpftrace 的 hist() 直方图,格式如下:

[区间下界, 区间上界) 次数 |@@@@@@@@@@@@@@@@@@@@@@@|

关键点:

  • 区间使用2 的幂次(对数刻度): [1K, 2K) 表示 1024~2047
  • 相对比例
  • 单位取决于上下文:syscall 延迟用 ns ,IO 延迟用 us

快速判断模式:

  • 柱形集中在低区间 → 正常,延迟低
  • 柱形分散到多个区间 → 延迟波动大,可能有尾部延迟问题
  • 高区间有孤立柱形 → 偶发高延迟(如 GC、页面换入、磁盘抖动)

第七章:火焰图解读

7.1 什么是火焰图

火焰图(Flame Graph)是 Brendan Gregg 发明的性能分析可视化方法。它将栈采样数据以堆叠柱形展示,像一簇火焰。

火焰图阅读三原则:

  1. 越宽的色块,占用时间越多(宽度 = 采样占比)
  2. 上层函数被下层函数调用(从下往上读 = 从调用者到被调用者)
  3. x 轴的顺序无意义(仅按字母排序,不代表时间先后)

7.2 On-CPU 火焰图解读

代码已经改成了一个更简单的on-cpu火焰图,实际函数以代码为准

On-CPU 火焰图(文件名: *_oncpu.svg )显示 CPU 上在执行什么。

IO Workload 的 On-CPU 火焰图示意:

┌───────────────────────────────────────────────────────┐
│                       main                            │ ← 底层
├───────────────────────────────────────────────────────┤
│                      do_work                          │
├────────────────────┬───────────────┬──────────────────┤
│ sync_to_disk       │ compute_hash  │   write_data     │
│ __GI_fsync         │               │ __libc_pwrite64  │
│ █████████          │ ██████        │ ██               │
│ 82%                │  15%          │   3%             │
└────────────────────┴───────────────┴──────────────────┘

你能看到的信息:

  • sync_to_disk → __GI_fsync 占据了最宽的色块 → 主要 CPU 占用
  • compute_hash 有独立色块 → CPU 计算清晰可见
  • write_data → __libc_pwrite64 很窄 → 写入操作 CPU 开销小

CPU Workload 的 On-CPU 火焰图示意:

┌────────────────────────────────────────────────────────────────────────────┐
│                                   main                                     │
├────────────────────────────────────────────────────────────────────────────┤
│                              std::thread::invoke                           │
├────────────────────────────────────────────────────────────────────────────┤
│                                  do_work                                   │
├────────────────┬─────────────────────────────────┬─────────────────────────┤
│  fast_init     │       heavy_compute             │     light_aggregate     │
│  std::sort     │  ┌────────────────┬───────────┐ │ ┌──────┬──────┬──────┐  │
│  ██████        │  │   heavy_comp   │ do_sin    │ │ │ log  │ sqrt │ sin  │  │
│  20%           │  │   ████████     │ ████████  │ │ │████  │ ████ │ ████ │  │
│                │  │   27%          │ 25%       │ │ │  5%  │ 4%   │ 4%   │  │
└────────────────┴──┴────────────────┴───────────┴─┴─┴──────┴──────┴──────┴──┘

你能看到的信息:

  • heavy_compute 虽然在函数排名中只占 27%,但加上它调用的 do_sin (25%),整体占约 52%
  • fast_init → std::sort 路径清晰可见,约 20%
  • light_aggregate 下面分叉为 log1p 、 sqrt 、 sin 等数学库函数

火焰图的优势就在这里:报告中 heavy_compute 和 do_sin 是分开的两行,但火焰图中它们是上下堆叠的,一眼看出 heavy_compute 的真实开销 = 自身 + 调用链。

7.3 Off-CPU 火焰图解读

Off-CPU 火焰图(文件名: *_offcpu.svg ,蓝色主题)显示 程序在等什么。

IO Workload 的 Off-CPU 火焰图示意:

┌─────────────────────────────────────────────────────────────┐
│                             main                            │
├─────────────────────────────────────────────────────────────┤
│                            do_work                          │
├─────────────────────────────────────────────────────────────┤
│                           sync_to_disk                      │
├─────────────────────────────────────────────────────────────┤
│                           __GI_fsync                        │
│ ███████████████████████████████████████████████████████████ │
│                           100%                              │
└─────────────────────────────────────────────────────────────┘

解读:蓝色火焰图中只有一根"柱子",从 main → do_work → sync_to_disk → __GI_fsync ,说明所有阻塞时间 都花在同一条调用路径上。

如果有多种阻塞原因(比如既有 fsync 又有 mutex_lock),火焰图会分叉成多根"柱子"。

7.4 火焰图分析技巧

技巧一:从最宽的色块开始看

最宽的色块就是性能瓶颈所在。如果 __GI_fsync 占了 82%,优化方向就是减少 fsync 调用(如批量写入后再 sync)。

技巧二:关注"平顶"

如果火焰图上有一个"平顶"(没有子函数的宽色块),说明该函数本身消耗大量 CPU,而不是调用其他函数。这种函数通常是优化的直接目标。

┌─────────────────────────────────┐
│               heavy_compute     │ ← 平顶 = 函数本身在消耗 CPU
│ ███████████████████████████████ │
└─────────────────────────────────┘

技巧三:对比 On-CPU 和 Off-CPU 火焰图

技巧四:在浏览器中使用

SVG 火焰图是交互式的!用浏览器打开后:

  • 点击色块可以放大查看子调用
  • 悬停显示函数名和采样占比
  • 搜索功能可以高亮特定函数名
# 在浏览器中打开火焰图
firefox taskscheduler_ebpf/job_1_pid_12345_oncpu.svg

项目源码:*********************************************

#秋招##校招##c++##简历中的项目经历要怎么写##秋招白月光#
全部评论

相关推荐

宝藏沈幼楚:没啥好说的,好好玩,努力三年考生大学,高中想着大学学习压力小,玩的时候多,现在上大学,大一就开始焦虑了,没有必要,珍惜宝贵的大学时间
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务