4. Pod级指标采集:eBPF如何无侵入采集Pod的CPU、内存、网络、磁盘I/O指标
在Kubernetes环境中,传统的Pod指标采集依赖cAdvisor、Metrics Server或Prometheus exporter,这些方式存在两大痛点:一是需要修改应用代码或注入sidecar(有侵入性);二是采样粒度粗、延迟高,无法捕捉微秒级的突发行为。eBPF(extended Berkeley Packet Filter)的出现彻底改变了这一局面——它以内核沙箱程序的形式运行,无需修改Pod、无需重启应用,即可从内核态直接采集Pod级别的CPU、内存、网络和磁盘I/O指标。
4.1 eBPF无侵入采集的核心原理
eBPF通过挂载到内核的tracepoint、kprobe、kretprobe以及perf_event等钩子,在事件发生的瞬间捕获上下文数据。对于Pod级指标,关键路径是利用cgroup ID作为关联键——每个Pod在cgroup v2中拥有唯一的ID,eBPF程序在采集CPU调度、内存分配、网络收发、块I/O等事件时,同时记录当前进程的cgroup ID,从而将内核事件精确归因到Pod。
4.2 四大类指标的eBPF采集路径
| 指标类别 | 内核钩子 | 采集原理 | 关键eBPF Map |
|---|---|---|---|
| CPU | tracepoint/sched/sched_switch tracepoint/sched/sched_process_exit |
记录每个进程的on-CPU时间片,按cgroup ID累加 | cpu_time_map (key: cgroup_id, value: total_ns) |
| 内存 | tracepoint/cgroup/cgroup_attach_task kprobe/mm_account |
捕获mm_account事件中的内存增量,关联cgroup ID | mem_usage_map (key: cgroup_id, value: current_bytes) |
| 网络 | kprobe/tcp_sendmsg kprobe/tcp_recvmsg TC (traffic control) ingress/egress |
在TCP协议栈入口/出口捕获字节数,附加skb的cgroup ID | net_bytes_map (key: cgroup_id, value: {rx_bytes, tx_bytes}) |
| 磁盘I/O | tracepoint/block/block_rq_issue tracepoint/block/block_rq_complete |
捕获块设备请求的扇区数和延迟,通过进程的cgroup ID归因 | io_stats_map (key: cgroup_id, value: {read_bytes, write_bytes, io_latency_ns}) |
4.3 eBPF采集架构与数据流
下图展示了eBPF从内核事件到Prometheus指标的完整数据流,重点突出“无侵入”和“Pod级归因”两个核心设计。
4.4 关键代码片段:CPU时间采集
以下是一个简化的eBPF C程序片段,展示如何通过sched_switch tracepoint采集Pod级CPU时间。实际生产代码需要处理更多边界条件(如CPU热插拔、cgroup迁移),但核心逻辑一致。
// cpu_time_collector.bpf.c
struct cpu_time_key {
__u64 cgroup_id;
};
struct cpu_time_val {
__u64 total_ns;
__u64 last_updated;
};
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 10240);
__type(key, struct cpu_time_key);
__type(value, struct cpu_time_val);
} cpu_time_map SEC(".maps");
SEC("tracepoint/sched/sched_switch")
int sched_switch_tp(struct trace_event_raw_sched_switch *ctx) {
// 获取当前进程的cgroup ID(cgroup v2)
__u64 cgroup_id = bpf_get_current_cgroup_id();
if (cgroup_id == 0) return 0;
struct cpu_time_key key = { .cgroup_id = cgroup_id };
struct cpu_time_val *val, new_val = {};
val = bpf_map_lookup_elem(&cpu_time_map, &key);
if (val) {
// 累加时间片:当前时间 - 上次切换时间
__u64 now = bpf_ktime_get_ns();
__u64 delta = now - val->last_updated;
val->total_ns += delta;
val->last_updated = now;
} else {
// 首次记录
new_val.total_ns = 0;
new_val.last_updated = bpf_ktime_get_ns();
bpf_map_update_elem(&cpu_time_map, &key, &new_val, BPF_ANY);
}
return 0;
}
bpf_get_current_cgroup_id()获取cgroup ID,该函数在cgroup v1下可能返回0或错误值。生产环境务必确认集群已启用cgroup v2(检查/sys/fs/cgroup文件系统类型)。如果必须兼容cgroup v1,需要改用bpf_get_current_task()遍历task_struct中的cgroup指针,性能开销会显著增加。
4.5 网络与磁盘I/O的特殊处理
网络和磁盘I/O的Pod级归因比CPU/内存更复杂,因为内核网络栈和块设备层不直接携带cgroup上下文。以下是两种典型场景的eBPF处理策略:
- 网络指标(TCP方向): 在
kprobe/tcp_sendmsg和kprobe/tcp_recvmsg中,通过bpf_get_current_cgroup_id()获取当前进程的cgroup ID。但注意:如果Pod使用hostNetwork或通过kube-proxy转发,数据包可能经过内核的netfilter路径,此时进程上下文可能丢失。Cilium的解决方案是在TC钩子中通过skb的__sk_buff->cgroup_id字段直接获取(需要内核5.10+)。 - 磁盘I/O指标: 在
tracepoint/block/block_rq_issue中,通过bpf_get_current_task()获取当前进程的task_struct,再读取task_struct->cgroups->dfl_cgrp->id。对于内核发起的内核线程I/O(如writeback),当前进程可能是kworker,此时cgroup ID指向根cgroup而非Pod。生产环境建议结合blk-cgroup的io.stat接口做二次校准。
4.6 指标精度与性能权衡
| 指标类型 | 典型精度 | eBPF采集开销 | 传统方式开销对比 |
|---|---|---|---|
| CPU使用率 | 微秒级(1μs) | 每调度事件约200ns | cAdvisor 1s采样,精度低 |
| 内存使用量 | 页级(4KB) | 每内存事件约150ns | cgroup memory.stat轮询,开销高 |
| 网络吞吐量 | 字节级 | 每数据包约300ns | iptables/conntrack统计,延迟高 |
| 磁盘I/O | 扇区级(512B) | 每I/O请求约500ns | iostat轮询,无法归因到Pod |
4.7 与Cilium的集成方式
Cilium本身已经内置了eBPF指标采集能力,通过cilium-agent的--bpf-map-dynamic-size-ratio和--enable-metrics参数控制。对于Pod级容量规划,推荐使用Cilium的cilium metrics命令或直接读取其Prometheus端点:
# 查看特定Pod的CPU时间(来自eBPF map)
cilium metrics list -o json | jq '.[] | select(.name=="cilium_bpf_cpu_time_ns")'
# Prometheus查询示例:按Pod聚合CPU使用率
sum(rate(cilium_bpf_cpu_time_ns_total{namespace="production"}[5m])) by (pod)
如果Cilium未启用全部指标,也可以独立部署ebpf_exporter(开源项目)配合自定义eBPF程序,实现更细粒度的采集。两种方式均遵循“内核采集 → per-CPU map → 用户态聚合 → Prometheus”的架构,确保对Pod零侵入。