跳转到主要内容
  • Daily Report
  • eBPF
  • Profiling
  • Observability
  • Linux

异步系统里,eBPF Profiler 还要追踪什么?

异步运行时让工作跨越线程、队列与应用资源。本文分析 eBPF profiler 如何重建因果路径,并在不全量追踪的前提下控制开销与采样偏差。

假设一个请求在普通 CPU profile 里几乎不显眼。它先解析请求,提交一次 io_uring 读取,然后 yield;稍后同一个逻辑任务在另一条 worker thread 上恢复,又触发一个 kernel work item,访问应用自己管理的 cache,最后才发回响应。整个请求没有长时间连续占用任何一条线程。

CPU flamegraph 能告诉我们哪些函数运行过,off-CPU profile 能告诉我们线程在哪里等待,全量 trace 也许能记录其中很多转换。但这些工具不会天然知道,分散在几条线程、几个队列和多个资源上的事件其实属于同一个逻辑请求。

这就是异步 profiling 的核心问题:thread identity 已经不足以充当 causal identity。 Linux 其实已经暴露了不少很有价值的 handoff 证据。io_uring 会把应用提供的 user_data 从 submission 带回 completion;workqueue tracepoint 在排队和开始执行时都暴露同一个 work_struct *;Tokio 一类 async runtime 有自己的 task ID 和 tracing span;buffer pool、query cache 这类 application-defined resource 则可能只有应用代码自己知道它们的语义身份。

这些证据分别可以看到,但它们不会自动拼成一个统一的因果图。

所以真正值得研究的问题不是泛泛地“用 eBPF 做一个 async profiler”,而是建立一个带类型的 causal-edge contract:什么 identity 能连接两个执行上下文,这个 identity 有效多久,这条关联来自直接观测还是重建,以及某条 edge 没看到时 profiler 应该怎样表示不确定性。一个可部署的系统应该优先保住便宜但决定因果结构的 handoff edge,再在明确预算下采样昂贵的 stack 和上下文,而不是把不完整的 trace 渲染成看似完整的故事。

本文继续 eBPF Runtime, Extensibility, and Composition 系列,也承接 Eunomia 已有的 on-CPU + off-CPU wall-clock profiler 教程。wall-clock profiling 的下一步,不只是把等待时间补进 flamegraph,而是在逻辑工作跨执行上下文之后还能保留因果关系。

CPU 与 off-CPU profile 不是请求历史

持续 eBPF profiling 已经是成熟而实用的方向。Parca 的 eBPF profiler 可以自动发现进程并生成 pprof profile;Grafana 的 OpenTelemetry eBPF profiler 可以在 Linux host 上系统级采样 CPU stack,再通过 OpenTelemetry profiling pipeline 发送出去。这类方案的价值很明确:无需为每个应用接 SDK,就能低成本长期获得执行热点。

它们自然的聚合单位仍接近 process、thread、stack、cgroup 或 service label。对传统 profiler 来说这完全合理,但异步请求的逻辑身份未必等于这些单位。

考虑一个简化执行过程:

thread 17: request A -> submit async read -> yield
thread 23: request B -> CPU work -> yield
thread 31: completion of A -> resume task A -> queue work

CPU profiler 会把执行栈采在 thread 17、23、31 上。off-CPU profiler 可以计算一条线程何时阻塞、何时被唤醒。但 request A 并不是一条线程。它先变成一个 async I/O request,之后又进入另一个执行上下文。

这也不是 Rust 特有的问题。event loop、fiber、green thread、coroutine scheduler、work-stealing runtime、用户态 RPC runtime 和 callback 密集的 C/C++ 程序都可能让逻辑工作脱离最初的 kernel thread。

Tokio 的文档直接展示了这个边界。Tokio task 有一个 opaque ID,用来唯一标识当前运行中的 task;runtime 可以把大量 task 调度到 worker threads 上。Tokio 的 tracing 文档也解释了为什么异步系统里普通 thread-oriented log 很难读:多个 task 被复用在同一线程上,不同逻辑流的事件会互相穿插。runtime 能暴露 task 语义,但最丰富的应用因果上下文仍然需要上层 instrumentation。

所以一个 profiler 完全可能准确地描述 CPU 在哪里执行,却错误地回答“这是谁的成本”

Linux 已经提供了一些很强的 handoff key

异步因果并非完全不可见。几个关键 Linux 子系统已经有可以跨执行上下文关联事件的 identity。

io_uring 自带应用选择的 correlation value

Linux io_uring UAPI 在每个 SQE 中定义了 64-bit user_data,对应 CQE 会把同一个值带回来。应用通常就靠它判断“哪一个请求完成了”。

对 profiler 来说,这几乎是天然的 causal edge:

提交时的逻辑上下文
        |
        | io_uring user_data = X
        v
   async request X
        |
        | completion user_data = X
        v
完成处理时的上下文

当前 Linux kernel source 也在 io_uring 请求提交和完成等路径上提供 tracepoint。不过实际 request lifecycle 比 submit/complete 两点复杂得多:请求可能 inline complete,也可能通过 task work、io-wq worker、poll path、linked request 或 multishot completion 继续执行。因此仅靠 sched_switch 很难恢复一个 io_uring request 的真实路径。

2026 年的 uringscope preprint 把这个机会和局限讲得很清楚。它用 CO-RE eBPF 重建 per-request io_uring flow,并专门评估 fidelity 和 overhead 的取舍。同时它指出相关 kernel tracepoint surface 不是稳定 ABI,所以 portability 本身就是 profiler 设计的一部分。

因此 io_uring 给了我们一个很好的子系统内部 causal key,但它仍然不是系统范围的统一 identity。

Workqueue 在排队和执行阶段暴露同一个对象

Linux workqueue tracepoint 会在 queue、activate、execute-start、execute-end 等阶段暴露 struct work_struct *work。即使实际 callback 由另一条 kernel worker thread 执行,profiler 仍可以把 producer 与 worker 连接起来:

producer context
    |
    | queue_work(work=W)
    v
work instance W
    |
    | execute_start(work=W)
    v
worker execution

这个 pointer 很有用,因为两端确实在命名同一个 work object。但 raw pointer 不是永久 global ID。对象生命周期结束后地址可以重用,trace event 也可能丢失。一个运行几天的 profiler 不能把地址当成永不重复的身份。

这直接导出一个更一般的设计原则:每一种 correlation key 都必须带生命周期或 generation 语义。 pointer、file descriptor、runtime task ID、request token 或应用 handle,都只在一个明确作用域里安全。

Scheduler 仍然重要,但只是 edge 类型之一

scheduler event 当然不能丢。它们解释 kernel task 什么时候 runnable、什么时候真正运行、什么时候 block、wake、migrate 或争抢 CPU,off-CPU profiling 也依赖这些证据。

问题只是不能再把 kernel task 升格成所有因果关系的唯一单位。更合理的模型应该同时容纳:

  • task runnable -> task running;
  • syscall caller -> kernel operation;
  • submitting task -> io_uring request;
  • work producer -> workqueue item;
  • runtime parent task -> spawned task;
  • request -> application-defined resource event。

最终输出仍然可以按 thread 聚合,但整个因果链不应该被强制限制在一条 thread 内。

Runtime task 和应用资源越过了 kernel visibility 边界

纯 eBPF observation 有一个必须承认的边界:kernel 可以看到执行和 kernel object,但它不会自动理解每一个用户态 task 或资源的语义身份。

Tokio 就是很清楚的例子。它有 task ID,也能通过 tracing 生态暴露结构化 task 信息。这个 ID 之所以有意义,是因为 runtime 自己定义了 task。kernel scheduler 看到的是“某条 worker thread 正在 poll future”,并不会把这个 future 当成 kernel task。

application-defined resource 更明显。数据库 buffer pool page、内部 query cache entry、模型服务的 KV-cache block、编译器临时对象,都可能直接决定性能,却没有一个 kernel resource ID 能表达它们在应用中的真实角色。

OSDI 2026 的 gigiprofiler 正好说明了这个缺口。它研究 application-defined resource 导致的性能问题,因为普通 system-level metric 看不到这些资源的语义。它用 LLM semantic inference 找候选资源,再用 static analysis 验证,运行时记录 resource usage 并关联到 request。论文在 5 个应用的 15 个真实问题上完成诊断,还发现了 2 个之后被 MariaDB 开发者确认的新问题。

这对“zero-instrumentation eBPF 可以看到一切”是一个很强的反例。系统级 profiler 确实能在无需应用 SDK 的情况下看到大量 effect,但 logical task 和 application resource 的身份有时只存在于 kernel 之上。

因此更合理的方案应该是 hybrid,而不是追求纯粹

  1. eBPF 负责 kernel-visible handoff、scheduler、syscall、I/O、process boundary 和低层资源;
  2. runtime 能提供 task ID 时直接消费 runtime-native identity;
  3. application-defined resource 无法可靠推断时允许极小 adapter;
  4. 每条 edge 都保留来源,明确它是 direct observation、reconstruction 还是缺失。

这和前面的 用户态 eBPF runtime contract 是同一个设计思想:可移植性来自显式表达 backend 能力差异,而不是假装所有环境都暴露相同语义。

“全部 trace 下来”也不是显然正确的答案

既然 profiling 需要 causal edge,一个直接方案就是把所有相关 event 全量追踪,再离线 join。这是很强的 baseline,也应该进入实验,但它不一定适合 always-on production。

OSDI 2026 有两项工作从两个方向说明了 measurement 的难点。

StriaTrace 观察到 production LLM inference 的细粒度 tracing 可能开销过高。它选择追踪关键 synchronization point 和 critical path,并只在异常附近展开详细 tracing。论文报告相对替代方案降低 97.8% tracing overhead,并在生产开发流程中诊断了数百个、覆盖 19 类 root cause 的异常。

Blink 研究的是另一面:sampling 本身也可能“撒谎”。对于包含大量短函数、没有明显 heavy hitter 的 flat workload,perf 一类 sampling profiler 会因为 skid、shadow effect 和 function coverage 不完整产生系统性误差,而不只是 variance 变大。Blink 用轻量 instrumentation 换取精确覆盖,在其目标 workload 上报告约 1% overhead 和 99.999% accuracy。

两项结果放在一起,排除了一个过度简单的策略:

  • 全量 trace 可能太贵;
  • uniform sample 可能系统性偏;
  • 所以单纯“少采一点”不是充分的 overhead 方案。

异步 causal profiler 必须区分哪些 event 决定图结构,哪些 event 只是提供昂贵上下文。

一个 queue handoff 也许不频繁,却非常关键。丢掉它可能直接把一个 request 分裂成两个互不相关的 profile。相反,在很长的 CPU phase 里少掉一个 stack sample,可能对最终 aggregate 几乎没有影响。这两类 event 不应该共用同一种 sampling policy。

更合理的 profiling object 是稀疏 typed causal graph

profiler 不一定要保存完整 event log。它需要的是足够多的证据,恢复对成本归属有意义的 causal graph。

内部数据模型可以类似:

Node {
    type: thread | runtime_task | io_request | work_item | resource | request
    id: source-specific identity
    generation: lifetime discriminator
}
 
Edge {
    type: submit | complete | queue | wake | spawn | resume | acquire | release
    src: Node
    dst: Node
    time: timestamp
    source: kernel | runtime | application | inferred
    confidence: observed | reconstructed | unknown
}
 
Sample {
    node: Node
    stack: optional stack context
    weight: measured or statistical weight
    inclusion_probability: optional sampling probability
}

这里最重要的一点不是 schema 长什么样,而是缺失 edge 不能被静默猜出来。如果 completion 到达时 submission record 已经被 eviction,系统应该保留 orphaned completion 或 unknown parent,而不是硬把它接到“最可能”的 request 上。

joined graph 最终也不必原样展示。它可以压缩成 pprof/flamegraph 兼容的 causal stack:

request A
  -> tokio task 91
    -> io_uring read X
      -> completion
        -> tokio task 91
          -> cache shard 4
            -> function foo

这样输出仍然是大家熟悉的 profile,但 ancestry 不再只有 call-stack ancestry,还包含跨 thread 的 causal handoff。

这才更接近“async profiler”应该解决的问题:把 profile weight 挂到重建出来的 causality tree/DAG 上,并同时暴露证据质量。

现有工作还薄弱在哪里

各子系统的 correlation identity 仍然彼此割裂

io_uringuser_data,workqueue 有 work_struct *,runtime 有 task ID,timer、futex、network、block I/O 和用户态 queue 又有各自的 identity,或者根本没有方便的统一 key。

缺的不是再发明一个 64-bit ID,而是一种表示法,能明确说“这个 ID 只在 ring R 内、直到 completion 前唯一”,或者“这个 pointer 只在这次 work lifetime 内有效”,并把这些 scoped identity 映射到 profiler 自己的 node namespace。

没有 lifetime 语义,pointer/ID reuse 会制造 false join;没有 source 语义,来自不同 subsystem 的两个 64-bit value 看上去一样,实际却完全不可比较。

最直接的实验是长时间高并发运行并主动制造对象复用。如果 profiler 在 request ID、pointer 和 runtime task ID 大量 reuse 后无法维持 causal precision,这个 identity model 就不够强。

Kernel 无法恢复所有 logical task 和 resource

纯 eBPF 可以看到 future 被哪条 worker thread poll,却不能天然知道 runtime 定义的 task identity;可以看到数据库周围的 syscall 与 memory behavior,却不懂某个 cache entry 或 buffer pool page 的应用语义。

缺少的是一个很小的 runtime/application adapter contract,并且在 adapter 不存在时必须安全退化到 unknown

研究价值不在于“adapter 能不能写”,答案当然是能。真正的问题是 adapter 能多小,同时让系统其余部分保留 zero/low-instrumentation 优势。如果每个 framework 都要大规模埋点,系统就退化回普通 distributed tracing。

Edge 与 context 没有统一的 measurement budget

传统 profiler 常决定 sample frequency,tracer 常决定 enable 哪些 event。async profiler 两个问题都要同时回答。

漏掉一个决定性 handoff edge,后面的所有 sample 可能全部失去 attribution;漏掉很多相似 CPU sample 中的一次,对 aggregate 影响却很小。因此 uniform sampling 会把 observability budget 花在错误的位置。

缺少的是一个明确的 budget allocator,优先保护 high-value causal edge,再单独采样 stack、argument、resource counter 等昂贵上下文。对于 probabilistic sample,还要保留 inclusion probability 或其他可解释 weighting 机制。

如果实验表明“deterministic edge + sampled context”在相同 overhead 下并不能比 full tracing、uniform sampling 或普通 profile 得到更准确的 attribution,这套额外机制就应该被否定。

还缺一个真正测 causal attribution 的 ground-truth benchmark

一个 flamegraph 看起来很合理,不代表它的 ancestry 是真的。这在 profiling 里尤其危险,因为视觉合理性很容易被误当成 correctness。

缺少的是一个 benchmark,能够知道跨多种 async mechanism 的真实 parent-child 关系,并主动制造 work stealing、completion reorder、multishot I/O、object reuse、event loss、nested task 和 application-resource contention 等困难情况。

核心指标不能只有 runtime overhead,还应该包括 causal-edge precision/recall、wall time/resource cost attribution error、orphan rate、false-join rate,以及 event loss 下的退化曲线。

如果普通 OpenTelemetry/runtime tracing 在目标 workload 上以相似 deployment cost 就能获得同等 attribution accuracy,那么 eBPF causal profiler 应该接受这个结果,而不是因为底层数据来自 eBPF 就宣称新颖性。

具有学术价值和生产价值的方向

为 eBPF profiler 建立 typed causal-edge substrate

Gap. Linux 已经暴露多个异步 handoff identity,但每个 subsystem 的 identity 和 lifetime 语义不同,io_uring、workqueue、scheduler、runtime task 与应用资源之间没有共同 contract。

Mechanism. 建一个很薄的 edge normalization layer。每个 adapter 输出 scoped node identity 与 typed handoff edge。kernel adapter 可以把 (ring, user_data, generation) 映射成 io_request,把 (work pointer, queue generation) 映射成 work_item;runtime adapter 把 Tokio task ID 等 runtime-native handle 映射成 runtime_task。每条 edge 都记录 source、lifetime scope、timestamp,以及它是直接观测还是重建。

kernel side 只在有界 BPF map 中保存短生命周期 correlation state。长时间 join、generation management、eviction report 和 graph compression 放到 userspace,避免把 BPF map 变成无限增长的 trace database。

Delta. 和普通 trace 的区别不是再造一个 event schema,而是显式定义 cross-subsystem identity/lifetime,以及“unknown ancestry 必须保持 unknown”的规则。和普通 pprof 的区别则是 stack ancestry 可以跨 thread。

Artifact. 一个基于 libbpf 或 Aya 的 collector,先实现 scheduler/syscall、io_uring、workqueue 和一种 async runtime adapter,再提供 pprof-compatible exporter,把 causal frame 编码成 synthetic frame 或 label。kernel adapter 的最小复现 probe 可以继续放进 eunomia-bpf developer tutorial

Evaluation. 构建逐步增加 handoff 数量的请求 pipeline,测 edge precision/recall、pointer/ID reuse 下的 false join、每个 live causal node 的内存开销、event loss 后的行为和最终 attribution error。baseline 包括 perf/eBPF CPU profiling、wall-clock on/off-CPU profiling、runtime-native tracing 和 full tracing。

Academic value. 问题变成:一组很小的 typed、lifetime-aware edge,是否足以跨异构 async mechanism 重建有用因果关系。

Production value. operator 可以得到 request/operation-oriented profile,不需要所有组件统一接入某个 tracing SDK,同时还能看到证据哪里不完整。

Failure condition. 如果真实应用需要大量彼此无关的 runtime-specific edge type,最终没有紧凑共同 contract,那么应该回到 per-runtime tracing,而不是强行做统一 causal profiler。

在固定预算下分开保护 edge 与 context

Gap. always-on full tracing 可能太贵,uniform stack sampling 又可能漏掉短 phase、关键 handoff,甚至产生系统性 bias。

Mechanism. 把采集拆成两层。edge plane 优先捕获 queue、submit、completion、wake、spawn、resource acquire 等决定因果拓扑的事件;context plane 在固定预算下采样 stack、argument、resource counter 或 payload metadata。昂贵 context 可以按 causal node、phase 或 anomaly signal 自适应增加。

如果采用概率采样,就记录足够信息估计 inclusion probability,或者明确哪些 aggregate 仍然有效。当 buffer overflow 或 correlation-state eviction 导致 edge 丢失时,输出 coverage break,而不是跨缺口继续 join。

一个实用策略可以在 causal path 跨越更多 subsystem、累计异常 latency 或进入 rare error 时提高 context sampling,普通路径保持低成本。StriaTrace 的 key synchronization + abnormality-focused tracing 是很强的生产 baseline;Blink 则提醒我们 sampling policy 必须针对 systematic error 验证,而不是只看 overhead。

Delta. 核心新意是把 causal edge 和 context sample 当成损失代价不同的统计对象,而不是给所有事件一个统一 sample rate。

Artifact. 一个有固定 CPU/event budget、可配置 edge priority、event-loss telemetry 的 always-on profiler,query 输出同时给 cost estimate 与 causal coverage。

Evaluation. 从很小预算一直跑到接近 full trace,比较 attribution error、CPU overhead、event volume、tail-latency perturbation 和实际 diagnosis success。必须加入类似 Blink 的 flat workload,避免 profiler 只在 heavy-hitter workload 上显得准确。

Academic value. 这是一个清晰的 measurement 问题:固定 observability budget 应该怎样在“定义拓扑的事件”和“估计数值的 sample”之间分配。

Production value. 团队可以长期运行 profiler,并知道某次 diagnosis 的 causal evidence 是否完整,而不是得到一个非常精确但其实错误的 flamegraph。

Failure condition. 如果高事件率 workload 中 edge plane 本身就成为主要 overhead,或者 adaptive sampling 无法维持可解释权重,就需要更强的 source-side aggregation,或缩小支持 workload 范围。

建立 async causality ground-truth benchmark

Gap. profiler 可以在 overhead 和 profile shape 上互相比较,却不一定知道“这些工作到底属于哪个逻辑请求”。

Mechanism. 设计一个 evaluator-only ground-truth channel,让每个逻辑 operation 穿过一组可控 async mechanism:同步调用、io_uring、kernel workqueue、timer、futex handoff、Tokio 等 task runtime,再加一个有界 cache/buffer pool 作为 application-defined resource。ground-truth ID 只供 evaluator 使用,不能暴露给被测 profiler。

故障模式主动覆盖 work stealing、pool reuse、delayed/out-of-order completion、multishot request、trace record loss、buffer truncation、nested task spawn、cancellation 和 application-resource reuse。

Delta. benchmark 评分的是 causal attribution,不只是“该出现的 event 有没有出现”。这样可以在同一 workload 上比较 CPU sampler、off-CPU profiler、full tracing、runtime/OpenTelemetry instrumentation、uringscope 一类 subsystem reconstruction 与 hybrid causal profiler。

Artifact. 可复现 Linux benchmark suite、reference trace,以及 edge precision/recall、false join、orphan rate、wall-time attribution error、resource-cost error、overhead 和 tail perturbation 指标。

Evaluation. 跨 kernel version、CPU 数量、runtime worker 数量、queue depth、request fan-out 和人工 event-loss rate 测试。必须同时包含 thread-affine workload,让普通 profiler 在简单场景里应该赢,也包含高度异步 workload,验证 causal reconstruction 是否真的带来收益。

Academic value. 它把“更懂 async system”这种模糊 claim 变成可证伪 profiling 问题。

Production value. profiler 作者可以明确告诉用户哪些 async mechanism 与 event-loss regime 是被验证支持的,而不是只宣传“低开销”。

Failure condition. 如果 causal accuracy 和真实 diagnosis outcome 几乎没有相关性,那么 edge precision 不是正确目标,benchmark 应该转向 operator decision。

什么证据会改变这个结论?

最直接的反例是 thread-local workload。CPU-bound worker pool、同步 service,或者 request identity 始终不离开同一条 thread 的应用,可能用 CPU + off-CPU profile 就足够了。不能因为技术上能画 causal graph,就强迫所有 workload 使用它。

第二个反例是 full tracing 变得足够便宜。如果关键 synchronization/runtime event 能以接近零的持续开销、稳定 schema 和有界存储长期全量采集,那么复杂的 edge/context budget 就没有必要。uringscope 已经说明针对一个 subsystem 做 request reconstruction 可以很实用,未来 kernel/tracing infrastructure 也可能让更广泛的 full-fidelity capture 继续降价。

第三个反例是上层 instrumentation 已经很好。如果 runtime 和应用已经产生高质量 OpenTelemetry 或 native causal span,那么 eBPF 更适合做 system-effect validation 与低层 gap filling,而不是重建 primary request graph。

最后,这张图必须真的帮助 diagnosis。若 prototype 只是画出更复杂的 causal flamegraph,却没有降低 root-cause 时间、提高 attribution accuracy,或者发现普通 profile 看不到的问题,就应该否定它。

因此本文的结论有明确边界:eBPF 很适合成为异步 profiling 的 system-level edge substrate,但可信 profiler 必须同时处理 subsystem-local identity、runtime/application semantics、显式 lifetime,以及 edge 与 context 不同的 measurement budget。缺失的因果证据应该被表示为不确定,而不是被补成一个干净的 thread-shaped story。

参考资料