Cloud Hypervisor 启动追踪(Tracing)基础设施实战指南:从trace_scoped!埋点到 SVG 可视化
【免费下载链接】cloud-hypervisorA Virtual Machine Monitor for modern Cloud workloads. Features include CPU, memory and device hotplug, support for running Windows and Linux guests, device offload with vhost-user and a minimal compact footprint. Written in Rust with a strong focus on security.项目地址: https://gitcode.com/GitHub_Trending/cl/cloud-hypervisor
导读
Cloud Hypervisor 内置了一套轻量级追踪(tracing)基础设施,专门用于观察虚拟机**初始启动(initial VM setup)**过程中的耗时分布与调用层次。本文将完整介绍如何通过tracing编译特性开启追踪、解读输出的 JSON trace 文件、使用官方脚本生成 SVG 时间线图,并结合tracercrate 与vmm中的真实埋点源码,讲清trace_scoped!、tracer::start()/tracer::end()、trace_point!等 API 的底层实现原理,让你能够为代码库添加自定义埋点并输出可读性强的启动性能画像。
Cloud Hypervisor 的追踪设施定位
Cloud Hypervisor 的追踪基础设施刻意保持"基础(basic)"的定位:它不追求像 Tracing/OpenTelemetry 那样完备的分布式追踪能力,而是聚焦于一个非常具体的目标——VM 启动流程的性能观察。通过极少的 API 面(两个宏、两个函数)即可在启动代码路径上打点,最终产出一份 JSON 报告和一张 SVG 时间线图。
整个设施由工作区成员 cratetracer提供,其核心代码量非常小:
- tracer/src/lib.rs:按编译特性门控的模块分发入口;
- tracer/src/tracer.rs:开启
tracing特性后的完整实现; - tracer/src/tracer_noop.rs:关闭特性后的空实现(编译期剔除)。
快速上手:开启追踪并生成 trace 文件
编译:通过 Cargo feature 开关追踪
追踪功能以 Cargo featuretracing形式提供。构建 Cloud Hypervisor 时带上该特性即可:
cargo build --features "tracing"关键在于:不带该特性编译时,追踪代码会被完整编译掉(compiled out),不会产生任何运行时开销。这一机制由tracercrate 的模块级条件编译实现——tracer/src/lib.rs 中的逻辑是:
#[cfg(not(feature = "tracing"))] mod tracer_noop; #[cfg(not(feature = "tracing"))] pub use tracer_noop::*; #[cfg(feature = "tracing")] mod tracer; #[cfg(feature = "tracing")] pub use tracer::*;feature 的传导链在三个Cargo.toml中逐级串联:
- cloud-hypervisor/Cargo.toml:
tracing = ["tracer/tracing", "vmm/tracing"]; - vmm/Cargo.toml:
tracing = ["tracer/tracing"]; - tracer/Cargo.toml:
tracing = ["dep:libc", "dep:log", "dep:serde", "dep:serde_json"]。
也就是说,只有在顶层显式开启tracing时,libc、log、serde、serde_json这些仅用于追踪的依赖才会被引入;否则启用的是 tracer_noop.rs 中的全空实现(trace_scoped!/trace_point!为空宏,start()/end()为空函数)。这是典型的"零成本抽象"设计:不开启特性,追踪路径上几乎不存在任何指令。
运行:trace 文件落在哪里
按常规方式运行编译出的 Cloud Hypervisor 即可,无需额外命令行参数。追踪结束后,trace 会以 JSON 格式写入当前工作目录,文件名为:
cloud-hypervisor-<pid>.trace其中<pid>是 Cloud Hypervisor 进程自身的 PID。该命名逻辑来自 tracer/src/tracer.rs 的Tracer::end():它通过libc::getpid()取得进程号并拼接文件名,随后用serde_json::to_writer_pretty写出格式化 JSON,并打出一条 WARN 级别的日志提示输出路径:
let path = format!("cloud-hypervisor-{}.trace", unsafe { libc::getpid() }); // ... warn!("Trace output: {path}");这个文件是标准 JSON,你可以直接用任意工具(jq、Python、VS Code)自行检查分析。
trace 文件的数据结构
一份 trace 文件的顶层包含两个字段:
| 字段 | 类型 | 含义 |
|---|---|---|
duration | Duration(secs+nanos) | 从tracer::start()到tracer::end()的总耗时 |
events | Map<线程名, Vec<TraceEvent>> | 按线程名分组的追踪事件列表 |
其中单个TraceEvent(见 tracer/src/tracer.rs)包含四个字段:
timestamp:事件开始时间(相对tracer::start()的时刻),类型为Duration;event:事件名称(&'static str,即trace_scoped!/trace_point!传入的字符串字面量);end_timestamp:事件结束时间。作用域块(scope block)事件有值,瞬时 trace point 为None;depth:事件在该线程内的嵌套深度(由thread_depths维护的调用栈深度)。
events之所以按线程分组,是因为add_event以thread::current().name()作为键存储事件(见 tracer.rs)。这意味着未命名的线程会落入空字符串键下——这一点在自定义线程埋点时值得留意。
可视化:用 ch-trace-visualiser.py 生成 SVG 时间线
JSON 文件虽然信息完整,但直接阅读启动流程的耗时分布并不直观。仓库在 scripts/ch-trace-visualiser.py 提供了官方可视化脚本,可将 trace 文件转换成 SVG 时间线图:
scripts/ch-trace-visualiser.py cloud-hypervisor-39466.trace output.svg其中第一个参数是输入的 trace 文件,第二个参数是输出的 SVG 路径。
脚本的工作原理(从 ch-trace-visualiser.py 源码可以确认):
- 解析 JSON 中的
duration,将纳秒时间按比例映射到 1000×200 的 SVG 画布横向坐标; - 每个线程占据一行,行内按
timestamp排序事件; - 每个作用域块渲染为一个带随机颜色的矩形,宽度与事件耗时成正比,矩形上标注事件名与耗时毫秒数(如
vm_boot (123ms)); - 通过事件的
depth字段控制纵向偏移(每层 18px),从而直观呈现嵌套关系; - 矩形文字使用
clipPath裁剪,超长事件名不会溢出。
可以看到,可视化完全依赖TraceEvent的timestamp、end_timestamp、depth三个字段,这也是设计TraceEvent时"为可视化服务"的直接体现。
现有埋点全景:启动流程中的 trace_scoped!
追踪设施已预先在 VM 启动路径上埋好了一批作用域块,覆盖了从设备创建、内存管理、ACPI 表生成到引导装载的各个环节。以下是从vmm源码中检索到的全部trace_scoped!调用点:
| 埋点位置 | 文件与行号 | 事件名 | 追踪内容 |
|---|---|---|---|
| 启动总入口 | vmm/src/lib.rs | vm_boot | 整个 boot 流程顶层作用域 |
| 设备创建 | vmm/src/device_manager.rs | DeviceManager::new | 设备管理器初始化 |
| 设备创建 | vmm/src/device_manager.rs | create_devices | 创建设备 |
| 内存管理 | vmm/src/memory_manager.rs | MemoryManager::new | 内存管理器初始化 |
| VM 构造 | vmm/src/vm.rs | Vm::new_from_memory_manager | 从内存管理器创建 VM |
| VM 构造 | vmm/src/vm.rs | Vm::new | VM 对象构造 |
| 引导装载 | vmm/src/vm.rs | load_payload | 装载 guest 负载(内核/固件) |
| 系统配置 | vmm/src/vm.rs | configure_system | 系统寄存器与内存配置 |
| ACPI 表 | vmm/src/acpi.rs | create_dsdt_table | DSDT 表生成 |
| ACPI 表 | vmm/src/acpi.rs | create_facp_table | FADT 表生成 |
| ACPI 表 | vmm/src/acpi.rs | create_acpi_tables | ACPI 表整体生成 |
| vCPU 创建 | vmm/src/cpu.rs | create_boot_vcpus | 创建启动 vCPU |
| 入口点 | vmm/src/vm.rs | entry_point | 设置 guest 入口 |
| 启动 | vmm/src/vm.rs | Vm::boot | VM 启动 |
这些埋点大多互相嵌套(例如vm_boot内嵌套Vm::new、Vm::boot,Vm::new内嵌套MemoryManager::new、DeviceManager::new),配合depth字段即可在 SVG 中呈现出一棵清晰的启动调用树。
tracer::start()与tracer::end()已经就位,它们包住了整个 boot 流程。见 vmm/src/lib.rs 的vm_boot():
fn vm_boot(&mut self) -> result::Result<(), VmError> { match &self.vm { VmOwnership::Owned(_) => Err(VmError::VmAlreadyCreated), VmOwnership::Migration { .. } => Err(VmError::VmMigrating), VmOwnership::None => { tracer::start(); info!("Booting VM"); // ... let r = (|| { trace_scoped!("vm_boot"); // ... 创建 Vm、调用 vm.boot() ... })(); tracer::end(); // ... } } }即:进入VmOwnership::None分支、开始真正启动时调用tracer::start(),整个 boot 逻辑(含所有嵌套的trace_scoped!)执行完毕后调用tracer::end()落盘。文档明确指出,这两个函数的位置是可以移动的——如果需要把追踪聚焦到代码库中某个更窄的部分(例如只想测load_payload或某张 ACPI 表的生成),可以把start()/end()挪到相应位置重新编译即可。
底层原理:tracer crate 是如何工作的
全局单例与线程深度记账
tracer.rs 使用一个static mut TRACER: OnceCell<Tracer>作为全局追踪器,Tracer结构持有三块状态:
events: Arc<Mutex<HashMap<String, Vec<TraceEvent>>>>:按线程名分组的事件表;thread_depths: HashMap<String, Arc<AtomicU64>>:每个线程当前的作用域嵌套深度;start: Instant:追踪起点,用于计算所有事件的时间戳。
start()在其它线程启动之前完成TRACER的初始化(OnceCell::set),end()在其它线程结束后输出,这个时序约束在源码注释中有明确说明。
trace_scoped!:RAII 作用域块
trace_scoped!宏(tracer.rs)展开为:
macro_rules! trace_scoped { ($event:expr) => { let _trace_scoped = $crate::TraceBlock::new($event); }; }它在当前作用域声明一个TraceBlock。TraceBlock::new()会先把当前线程深度 +1 并记录Instant::now()起始时刻;当作用域结束时(变量析构),Drop实现会构造一个带有timestamp和end_timestamp的TraceEvent写入事件表,并把线程深度 -1。因此:
- 开发者只需提供一个有意义的事件名,块的开始、结束、耗时、嵌套深度全部自动完成;
- 由于是 RAII,即使作用域内 panic 或提前
return,Drop依然会执行,不会漏记结束时间。
这也是文档所说"除了提供有用的事件名,开发者无需做任何其它事"的底层含义。
trace_point!:瞬时埋点
trace_point!宏(tracer.rs)调用trace_point_log,构造end_timestamp: None的瞬时事件。它记录一个"时刻"而非一个"区间"。
文档特别指出了它的两个现状:
- 当前代码库中没有使用——搜索整个仓库,
trace_point!/trace_point_log只存在于tracercrate 自身定义中,vmm内没有任何调用点; - 可视化脚本没有处理它——由于瞬时事件没有
end_timestamp,无法在 SVG 中渲染成带宽度的块,ch-trace-visualiser.py 的add_traced_block依赖end_timestamp计算矩形宽度,因此这类事件不会被绘制。
所以如果你要用trace_point!做瞬时打点,需要自行解析 JSON 中的这类事件(end_timestamp为null),或者扩展可视化脚本。这也是"基础基础设施"定位的体现——它留下的是扩展空间,而非完备的现成方案。
为代码库添加自定义埋点
要在你的 Cloud Hypervisor 定制版本中做更细粒度的追踪,步骤非常简单:
第一步:确认构建时携带tracing特性:
cargo build --features "tracing"第二步:在需要测量耗时的函数入口(或代码块开头)插入作用域埋点:
fn my_expensive_operation(&mut self) { trace_scoped!("my_expensive_operation"); // ... 原有逻辑 ... }事件名是&'static str,推荐使用与函数/操作语义一致的描述性名称,例如仓库现有埋点风格的create_devices、load_payload、configure_system。
第三步:如需缩小追踪范围,把tracer::start()/tracer::end()从 vmm/src/lib.rs 与 vmm/src/lib.rs 移到目标代码路径的前后。
第四步:运行后检查当前目录生成的cloud-hypervisor-<pid>.trace,并用可视化脚本输出 SVG:
scripts/ch-trace-visualiser.py cloud-hypervisor-<pid>.trace output.svg局限性与注意事项
基于源码事实,使用这套追踪设施时有几点需要了解:
- 追踪窗口默认仅覆盖启动:
start()/end()包住的是vm_boot(),运行时(runtime)阶段的设备 I/O 不在默认追踪范围内,需要自行移动起止点; - 按线程分组:事件以线程名聚合,匿名线程会归入空字符串分组,命名线程时使用有辨识度的名字更利于解读;
trace_point!未被可视化:瞬时事件不会出现在 SVG 中,且仓库内尚无使用案例;- 输出文件覆盖风险:每次
end()都直接File::create同名文件(cloud-hypervisor-<pid>.trace),同一 PID 不会复用,但重复启动同名进程时会按各自 PID 区分文件; - 深度记账的不对称风险:
decrease_thread_depth在没有对应深度记录时会panic!("Unmatched decrease for thread: {thread_name}"),正常 RAII 配对不会触发,但任何绕过TraceBlock的手工操作都可能破坏深度平衡。
总结
Cloud Hypervisor 的追踪设施是一套"小而美"的启动性能观测方案:一条 Cargo feature 开关、两个宏、两个函数,即可在零负担的前提下获得按线程分组、带嵌套深度的启动耗时 JSON,并经官方脚本一键渲染为 SVG 时间线。通过本文的源码级拆解,你已经掌握了trace_scoped!的 RAII 语义、TraceEvent的数据结构、feature 的条件编译传导链,以及仓库中全部 15 处现有埋点的分布——无论是理解vm_boot期间各阶段(设备创建、内存管理、ACPI 表生成、负载装载、vCPU 创建)的时间占比,还是为特定热点添加自定义埋点,都可以立即动手实践。
相关参考文件:
- 官方追踪文档
- tracer crate 完整实现
- tracer crate 空实现
- tracer crate 特性定义
- 可视化脚本
- vmm 中 start/end 与 vm_boot 埋点
- vmm 中各 trace_scoped! 埋点
【免费下载链接】cloud-hypervisorA Virtual Machine Monitor for modern Cloud workloads. Features include CPU, memory and device hotplug, support for running Windows and Linux guests, device offload with vhost-user and a minimal compact footprint. Written in Rust with a strong focus on security.项目地址: https://gitcode.com/GitHub_Trending/cl/cloud-hypervisor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考