eBPF AI 推理服务全链路可观测性工程实战
AI 推理服务的性能瓶颈往往隐藏在系统底层——GPU kernel launch 延迟、CPU 调度争用、内存拷贝阻塞、网络抖动、存储 I/O 排队。传统的应用层监控只能告诉你"慢",却无法告诉你"为什么慢"。eBPF(Extended Berkeley Packet Filter)为我们提供了一种零侵入、全链路的观测手段,从用户态代码到内核态驱动,从 syscall 到 GPU CUPTI,让每一个微秒级的延迟都无处遁形。
本文将深入探讨如何构建一套基于 eBPF 的 AI 推理服务可观测性工具栈,覆盖从请求入口到 GPU 计算完成的完整链路。
一、为什么 AI 推理服务需要 eBPF 级可观测性
1.1 AI 推理服务的独特性
与传统 Web 服务不同,AI 推理服务具有以下特征:
- 异构计算:CPU 预处理 → GPU 计算 → CPU 后处理,涉及多种计算单元的协同
- 批处理动态性:Dynamic Batching 导致请求处理时间高度不确定
- 内存密集型:模型权重常驻显存,KV Cache 按需分配,内存压力巨大
- 长尾延迟敏感:P99 延迟比平均延迟重要得多,GPU 利用率波动直接影响 Tail Latency
- 调用链长:一个推理请求经过 API Gateway、Load Balancer、Preprocess、Scheduler、Inference Engine、Postprocess 等多个环节
1.2 传统监控的盲区
| 层级 | 传统监控工具 | 盲区 |
|---|---|---|
| 应用层 | Prometheus metrics, APM | 无法追溯到内核态原因 |
| Runtime | pprof, trace | 采样频率低,遗漏短时事件 |
| 内核层 | perf, ftrace | 开销大,生产环境开启困难 |
| GPU | DCGM, nvprof | 粗粒度,无法定位到具体 syscall |
eBPF 的核心优势在于:在内核中安全地执行沙盒程序,以极低开销(通常 < 1% CPU)收集内核态任意事件。
二、全链路可观测性架构设计
2.1 观测点分布
┌─────────────────────────────────────────────────────────┐
│ AI Inference Service │
├──────────┬──────────┬──────────┬────────────┬────────────┤
│ Network │ CPU │ Memory │ Storage │ GPU │
│ Layer │ Layer │ Layer │ Layer │ Layer │
├──────────┼──────────┼──────────┼────────────┼────────────┤
│ sock │ sched │ mmap │ block │ ioctl │
│ tcp │ syscall │ pagefault│ ext4/xfs │ drm │
│ udp │ irq │ brk │ writeback │ nvidia.ko │
│ quic │ softirq │ mlock │ fsync │ CUPTI │
└──────────┴──────────┴──────────┴────────────┴────────────┘
↕ eBPF probes (kprobe/uprobe/tracepoint) ↕
┌─────────────────────────────────────────────────────────┐
│ eBPF Maps (Per-CPU Ring Buffer) │
├─────────────────────────────────────────────────────────┤
│ Userspace Agent (Go/Rust) │
├─────────────────────────────────────────────────────────┤
│ Prometheus / Grafana / OTel │
└─────────────────────────────────────────────────────────┘
2.2 核心 eBPF 探针类型
以下是我们实际生产环境中部署的探针矩阵:
| 探针名称 | 挂载点 | 采集数据 | 用途 |
|---|---|---|---|
trace_sched_switch |
sched:sched_switch |
prev_pid, next_pid, delay | CPU 调度延迟分析 |
trace_nv_ioctl |
sys_ioctl (DRM) |
cmd, arg, duration | GPU kernel launch 追踪 |
trace_sys_enter |
raw_syscalls:sys_enter |
syscall_nr, args | 系统调用频率与延迟 |
trace_tcp_retransmit |
skb:kfree_skb |
ip, tcp_seq | 网络重传统计 |
trace_block_rq |
block:block_rq_complete |
dev, sector, duration | 块设备 I/O 延迟 |
trace_oom_kill |
oom:oom_kill_process |
pid, totalpages | OOM 事件捕获 |
三、GPU 计算路径追踪
3.1 CUDA Runtime 到 Driver 的全链路
一个 CUDA kernel 的执行路径如下:
Application Code (PyTorch/vLLM)
↓
CUDA Runtime Library (libcudart.so)
↓ cuLaunchKernel()
CUDA Driver (libcuda.so)
↓ cuMemcpyHtoD() / cuLaunchGridAsync()
NVIDIA Kernel Driver (nvidia.ko)
↓ ioctl(DRM_IOCTL_NVGPU)
GPU Hardware Execution
我们在关键节点放置 uprobe:
// eBPF 程序:追踪 cuLaunchKernel 调用
SEC("uprobe//usr/lib/x86_64-linux-gnu/libcudart.so.12:cuLaunchKernel")
int trace_cu_launch(struct pt_regs *ctx) {
u64 ts = bpf_ktime_get_ns();
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct launch_event evt = {};
evt.pid = pid;
evt.timestamp = ts;
bpf_probe_read_user_str(&evt.kernel_name, sizeof(evt.kernel_name),
(void *)PT_REGS_PARM1(ctx));
// 记录 per-thread launch 时间戳
bpf_map_update_elem(&launch_starts, &pid, &evt, BPF_ANY);
// 发送到 ring buffer
bpf_ringbuf_submit(&events, &evt, sizeof(evt), 0);
return 0;
}
SEC("uretprobe//usr/lib/x86_64-linux-gnu/libcudart.so.12:cuLaunchKernel")
int trace_cu_launch_ret(struct pt_regs *ctx) {
u64 ts = bpf_ktime_get_ns();
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct launch_event *start = bpf_map_lookup_elem(&launch_starts, &pid);
if (start) {
u64 duration = ts - start->timestamp;
// 记录 launch 延迟分布
u32 slot = bpf_log2l(duration / 1000); // microseconds
u64 *count = bpf_map_lookup_elem(&launch_hist, &slot);
if (count) __sync_fetch_and_add(count, 1);
}
return 0;
}
3.2 GPU Kernel Execution 时间分析
仅追踪 launch 不够,还需要测量 GPU 端实际执行时间。我们使用 perf_event 配合 CUDA 流回调来实现精确计时:
// 结合 CUPTI 和 eBPF 的 GPU 端追踪
static void CUPTIAPI cupti_callback(void *userdata,
CUpti_CallbackDomain domain,
CUpti_CallbackId cbid,
const void *cbInfo) {
if (domain == CUPTI_CB_DOMAIN_RUNTIME_API &&
cbid == CUPTI_RUNTIME_TRACE_CBID_cudaLaunchKernel) {
const cudaLaunchKernel_params *params =
(cudaLaunchKernel_params *)cbInfo;
// 将 GPU kernel 元数据关联到 eBPF 进程上下文
gpu_kernel_meta meta;
meta.correlation_id = params->correlationId;
meta.stream = (uint64_t)params->stream;
meta.start_ns = get_clock_ns();
bpf_map_update_elem(&gpu_kernels, &meta.correlation_id, &meta, BPF_ANY);
}
}
四、CPU 调度与推理延迟根因分析
4.1 调度延迟直方图
推理服务对 CPU 调度延迟极其敏感。一个 batch 准备阶段的调度排队可能导致数十毫秒的延迟抖动:
SEC("tp_btf/sched_switch")
int BPF_PROG(trace_sched_switch, bool preempt,
struct task_struct *prev,
struct task_struct *next) {
u32 prev_pid = prev->pid;
u32 next_pid = next->pid;
// 只关注推理服务进程
u64 cpu = bpf_get_smp_processor_id();
// 记录 prev 任务的 off-cpu 开始时间
if (is_target_pid(prev_pid)) {
u64 now = bpf_ktime_get_ns();
bpf_map_update_elem(&offcpu_start, &prev_pid, &now, BPF_ANY);
}
// 记录 next 任务的 on-cpu 唤醒时间
if (is_target_pid(next_pid)) {
u64 *start = bpf_map_lookup_elem(&offcpu_start, &next_pid);
if (start) {
u64 duration = bpf_ktime_get_ns() - *start;
// 分类统计
if (duration < 10000) { // < 10us
__sync_fetch_and_add(&hist_under_10us, 1);
} else if (duration < 100000) { // < 100us
__sync_fetch_and_add(&hist_100us, 1);
} else if (duration < 1000000) { // < 1ms
__sync_fetch_and_add(&hist_1ms, 1);
} else {
__sync_fetch_and_add(&hist_over_1ms, 1);
// 记录长时间 off-cpu 事件
struct offcpu_event evt = {
.pid = next_pid,
.duration_ns = duration,
.prev_state = prev->__state
};
bpf_get_current_comm(&evt.comm, sizeof(evt.comm));
bpf_ringbuf_submit(&ringbuf, &evt, sizeof(evt), 0);
}
bpf_map_delete_elem(&offcpu_start, &next_pid);
}
}
return 0;
}
4.2 syscall 延迟热力图
// 追踪推理服务中耗时最长的系统调用
SEC("tp/raw_syscalls/sys_enter")
int trace_sys_enter(struct trace_event_raw_sys_enter *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
if (!is_inference_pid(pid)) return 0;
u64 ts = bpf_ktime_get_ns();
u32 syscall_nr = ctx->id;
struct sys_enter_key key = { .pid = pid, .syscall = syscall_nr };
bpf_map_update_elem(&syscall_starts, &key, &ts, BPF_ANY);
return 0;
}
SEC("tp/raw_syscalls/sys_exit")
int trace_sys_exit(struct trace_event_raw_sys_exit *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
if (!is_inference_pid(pid)) return 0;
struct sys_enter_key key = { .pid = pid, .syscall = ctx->id };
u64 *start = bpf_map_lookup_elem(&syscall_starts, &key);
if (!start) return 0;
u64 duration = bpf_ktime_get_ns() - *start;
// 按 syscall 类型聚合延迟
u64 *total = bpf_map_lookup_elem(&syscall_latency, &ctx->id);
u64 *count = bpf_map_lookup_elem(&syscall_count, &ctx->id);
if (total) __sync_fetch_and_add(total, duration);
if (count) __sync_fetch_and_add(count, 1);
bpf_map_delete_elem(&syscall_starts, &key);
return 0;
}
五、内存分配与 Page Fault 追踪
5.1 内存分配热点检测
推理服务的内存分配模式具有明显的阶段性:模型加载阶段大量 mmap,推理阶段以 CUDA malloc 为主,批处理阶段频繁的小对象分配。
// 追踪 mmap 调用,识别大内存映射
SEC("kprobe/__x64_sys_mmap")
int trace_mmap(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
if (!is_target_pid(pid)) return 0;
size_t len = PT_REGS_PARM3(ctx); // length parameter
int flags = PT_REGS_PARM5(ctx); // flags parameter
// 只记录 > 100MB 的映射
if (len < 100 * 1024 * 1024) return 0;
struct mmap_event evt = {
.pid = pid,
.length = len,
.flags = flags,
.timestamp_ns = bpf_ktime_get_ns()
};
bpf_get_current_comm(&evt.comm, sizeof(evt.comm));
bpf_ringbuf_submit(&ringbuf, &evt, sizeof(evt), 0);
return 0;
}
// 追踪 page fault
SEC("tp/exceptions/page_fault_user")
int trace_page_fault(struct trace_event_raw_page_fault *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
if (!is_target_pid(pid)) return 0;
struct pf_event evt = {
.pid = pid,
.address = ctx->address,
.ts_ns = bpf_ktime_get_ns()
};
bpf_get_current_comm(&evt.comm, sizeof(evt.comm));
// 进行 page fault 地址直方图统计
u32 zone = get_address_zone(ctx->address);
u64 *count = bpf_map_lookup_elem(&pf_zone_hist, &zone);
if (count) __sync_fetch_and_add(count, 1);
return 0;
}
六、网络层追踪与请求级可观测性
6.1 gRPC 推理请求追踪
AI 推理服务通常使用 gRPC/HTTP2 接收请求。我们追踪从 TCP 包到应用层请求解析的全链路:
// 追踪 TCP 层的推理请求到达时间
SEC("sockops")
int trace_tcp_connection(struct bpf_sock_ops *skops) {
u32 family = skops->family;
if (family != AF_INET && family != AF_INET6) return 0;
u32 local_port = skops->local_port;
u32 dst_port = bpf_ntohl(skops->remote_port);
// 只关注推理服务端口 (默认 50051 gRPC)
if (local_port != 50051 && dst_port != 50051) return 0;
// 记录连接元数据
struct conn_info info = {
.src_ip = skops->local_ip4,
.dst_ip = skops->remote_ip4,
.src_port = local_port,
.dst_port = dst_port,
.bytes_received = skops->bytes_received,
.bytes_acked = skops->bytes_acked
};
u32 idx = (u32)bpf_get_smcc_cookie();
bpf_map_update_elem(&conn_map, &idx, &info, BPF_ANY);
return 0;
}
6.2 请求级端到端追踪
通过 uprobe 挂钩 gRPC 框架的序列化/反序列化函数,构建请求级追踪:
// 在 gRPC 的 Deserialize 函数入口处挂钩
SEC("uprobe//usr/local/lib/libgrpc++.so:_ZN4grpc8internal13CallOpRecvMsgD2Ev")
int trace_grpc_recv(struct pt_regs *ctx) {
u64 ts = bpf_ktime_get_ns();
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 从 gRPC 请求上下文中提取 request_id
// 这需要理解 gRPC internal memory layout
// 简化版:记录请求到达与处理完成时间差
struct request_span span = {
.pid = pid,
.ts_enter = ts,
.type = GRPC_REQUEST_START
};
bpf_ringbuf_submit(&ringbuf, &span, sizeof(span), 0);
return 0;
}
七、生产级部署方案
7.1 eBPF Agent 架构
一个完整的生产级 eBPF 代理需要处理以下挑战:
- 高吞吐事件处理:单节点 10万+ 推理请求/秒,每秒可能产生百万级 eBPF 事件
- 低开销目标:CPU 占用 < 1%,内存 < 100MB
- 内核版本兼容:不同内核版本的 tracepoint 名称不同
- 安全权限:需要 CAP_BPF 或 root 权限
推荐架构:
[Kernel Space]
↓ eBPF Programs (compiled to BPF bytecode)
↓ BPF Ring Buffers (per-CPU, lock-free)
[Userspace Agent - Rust/Go]
↓ Dedicated reader goroutine per Ring Buffer
↓ Event batching & aggregation
↓ OTLP export / Prometheus exposition
[Observability Backend]
↓ Tempo/Jaeger (Traces) + Prometheus (Metrics) + Loki (Logs)
7.2 关键代码片段
Rust 实现的 eBPF Agent 核心循环:
use aya::{Bpf, programs::{trace_point::TracePointLink, KProbe}};
use aya::maps::RingBuf;
use tokio::sync::mpsc;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 加载编译好的 eBPF 字节码
let mut bpf = Bpf::load_file("target/bpfel-unknown-none/release/inference-observe")?;
// 挂载 tracepoint
let program: &mut TracePoint = bpf.program_mut("trace_sched_switch").unwrap().try_into()?;
program.load()?;
program.attach("sched", "sched_switch")?;
// 挂载 kprobe 追踪 ioctl
let kprobe: &mut KProbe = bpf.program_mut("trace_nv_ioctl").unwrap().try_into()?;
kprobe.load()?;
kprobe.attach("drm_ioctl", 0)?;
// 访问 Ring Buffer
let mut ringbuf = bpf.map_mut("RINGBUF").unwrap();
let (tx, mut rx) = mpsc::channel(10000);
// 启动 Ring Buffer 读取循环
for cpu_id in online_cpus()? {
let tx = tx.clone();
let mut rb: RingBuf = ringbuf.next(&cpu_id).unwrap();
tokio::spawn(async move {
let mut poll = tokio::time::interval(Duration::from_millis(10));
loop {
poll.tick().await;
while let Some(event) = rb.next() {
let _ = tx.send(event.to_vec()).await;
}
}
});
}
// 处理事件流
while let Some(event) = rx.recv().await {
process_event(&event);
}
Ok(())
}
7.3 性能数据采集配置
实际部署中,我们通过 BPF Map 暴露预聚合的 Prometheus 指标:
// 预聚合 Prometheus 指标在 BPF 侧完成
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, MAX_SYSCALL_NR);
__type(key, u32);
__type(value, struct latency_bucket);
} syscall_latency SEC(".maps");
// 用户态定期读取并暴露
struct latency_bucket {
u64 count;
u64 total_ns;
u64 min_ns;
u64 max_ns;
u64 hist[8]; // 1us, 10us, 100us, 1ms, 10ms, 100ms, 1s, >1s
};
八、实战案例:定位推理尾延迟根因
8.1 问题背景
某 AI 推理服务 P99 延迟周期性从 50ms 跳变至 200-500ms,持续 3-5 秒后恢复。传统的应用层监控只看到延迟升高,无法确定根因。
8.2 eBPF 分析过程
-
调度延迟分析:开启
trace_sched_switch后,发现延迟跳变期间 GPU 等待进程经历大量 off-cpu 事件,平均等待时间从 2μs 跳变至 800μs。 -
Syscall 分析:
trace_sys_exit显示延迟期间ioctl(DRM_IOCTL_NVGPU)调用延迟从 5μs 跳变至 300μs+,主要发生在 GPU Query 查询时。 -
GPU 利用率关联:同步抓取 GPU 利用率发现,延迟升高时 GPU 利用率从 95% 骤降至 40%,说明 GPU 在空转等待 CPU 提交 task。
-
根因定位:进一步追踪发现是batch scheduler中一个
malloc触发了 jemalloc 的madvise(MADV_DONTNEED)释放大页内存,导致后续 GPU 内存分配(首次触碰)触发大量 page fault,进而拖慢cuMemcpy操作。
8.3 修复方案
// 修复:对 GPU 内存使用 mlock + 预分配池
cudaError_t preallocate_gpu_pool(size_t pool_size) {
void* base;
cudaError_t err = cudaMalloc(&base, pool_size);
if (err != cudaSuccess) return err;
// mlock 防止被 swap(虽然 GPU 内存不会被 swap,但主机端 pinned memory 会)
if (mlock(base, pool_size) != 0) {
madvise(base, pool_size, MADV_HUGEPAGE); // 大页减少 TLB miss
madvise(base, pool_size, MADV_WILLNEED); // 预加载页表
}
return cudaSuccess;
}
修复后,P99 延迟稳定在 55ms 以内,消除了周期性尾部抖动。
九、最佳实践与陷阱清单
9.1 推荐做法
- 使用 Ring Buffer perf_event 替代 BPF Map polling:减少用户态与内核态的同步开销
- Per-CPU Map 替代共享 Map:避免跨 CPU 缓存同步
- 事件采样而非全量采集:对高频事件(如 syscall)采用每 N 次采样 1 次的策略
- 内核态预聚合:在 BPF 程序中直接计算直方图和百分位,只上传聚合结果
- 使用 BTF-enabled CO-RE:一次编译,跨内核版本运行
9.2 常见陷阱
- BPF 程序验证器限制:循环必须有界,栈空间严格限制(最大 512 字节),复杂逻辑必须放在用户态
- Map 内存巨大:每 CPU 的 Ring Buffer 默认 8-64 MB,多核系统下需要控制
- Probe 挂载点变更:不同环境下函数名可能 inline 或改名,需要 fallback 到 Tracepoint
- 与 kata/容器安全策略冲突:某些容器 runtime 限制 CAP_BPF,需要额外配置
9.3 开源工具链推荐
| 工具 | 用途 |
|---|---|
| eBPF Collectors (Linux kernel | 官方 eBPF 示例和库 |
| bcc/bpftrace | 快速原型和一次性追踪 |
| Aya (Rust) | 生产级 eBPF Agent 开发 |
| Tetragon (Cilium) | 安全 + 可观测性一体化 |
| Pixie | Kubernetes 场景即插即用 |
| Pyroscope Parca | 持续性能分析 |
十、总结
eBPF 为 AI 推理服务提供了一种前所未有的全链路可观测性能力。从 GPU kernel 的每一次 launch,到 CPU 调度的每一微秒排队,从内存的每一个 page fault,到网络的每一个 TCP 重传——eBPF 让每个环节都变得透明可追溯。
在 AI 推理服务日益复杂化的今天,仅有应用层监控已经远远不够。eBPF 级可观测性不再是"锦上添花",而是生产级推理服务的必备基础设施。随着 eBPF + AI 的融合加深(如 eBPF 辅助的 GPU 调度、eBPF 特征采集辅助 ML 推理优化),我们有理由相信这个领域将持续产生更多工程创新。
关键 Takeaways:
- eBPF 能以 <1% CPU 开销提供 deepest visibility,是生产级推理服务的理想观测方案
- 全链路可观测性需要覆盖 Network - CPU - Memory - Storage - GPU 五大层级
- 用户态预聚合 + Ring Buffer 是实现高性能事件采集的关键模式
- BTF CO-RE 一次编译多版本部署是生产环境的必要能力
- 调度延迟和 syscall 延迟是推理尾延迟的两大隐藏元凶

发表评论 取消回复