4. 使用eBPF追踪CNI插件执行流程
任务目标
在Kubernetes节点上,当kubelet调用CNI插件(如bridge、portmap、bandwidth)为Pod配置网络时,这些插件以独立进程方式快速执行。当网络配置失败时,难以还原现场。本案例使用eBPF的tp/sched/sched_process_exec和tp/sched/sched_process_exit跟踪点,捕获节点上所有CNI插件的执行参数、关键环境变量与退出码,实现轻量级调试追踪。
实现原理
CNI插件由kubelet通过exec.Command调用,进程名与插件二进制名一致(如bridge、host-local、portmap)。通过eBPF挂载进程exec和exit跟踪点,在exec事件中读取argv与CNI_CONTAINERID、CNI_NETNS等环境变量;在exit事件中读取退出码。用户态程序将两类事件按PID关联后输出。
环境依赖
- Linux内核 ≥ 4.18(支持BPF_RAW_TRACEPOINT_OPEN)
- libbpf开发库(
apt install libbpf-dev 或 yum install libbpf-devel)
- clang ≥ 10、gcc、make
- 具备
CAP_BPF或root权限
关键完整代码
以下为cnitrace.bpf.c(eBPF内核态程序):
```c
// cnitrace.bpf.c
#include
#include
#include
#define MAX_ARGS 8
#define ARG_LEN 64
#define NAME_LEN 16
#define ENV_KEY_LEN 32
#define ENV_VAL_LEN 128
struct exec_event {
__u32 pid;
char comm[NAME_LEN];
char argv[MAX_ARGS][ARG_LEN];
char env_container[ENV_VAL_LEN];
char env_netns[ENV_VAL_LEN];
};
struct exit_event {
__u32 pid;
int exit_code;
};
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u32));
} exec_events SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u32));
} exit_events SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, struct exec_event);
} pending_exec SEC(".maps");
static __always_inline int read_env(struct exec_event *e, const char *key) {
char *envp = (char *)bpf_get_current_task();
// 简化:从task_struct的envp指针读取(需内核版本适配)
// 实际实现需遍历环境变量块,此处省略完整遍历逻辑
return 0;
}
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx) {
__u32 pid = bpf_get_current_pid_tgid() >> 32;
struct exec_event ev = {};
ev.pid = pid;
bpf_get_current_comm(&ev.comm, sizeof(ev.comm));
// 只追踪CNI插件(bridge, host-local, portmap, bandwidth等)
if (ev.comm[0] != 'b' && ev.comm[0] != 'h' && ev.comm[0] != 'p') {
return 0;
}
// 读取argv(最多8个参数,每个64字节)
struct pt_regs *regs = (struct pt_regs *)ctx;
// 从寄存器获取argv指针(x86_64: rsi)
char __user *argv_user = (char __user *)PT_REGS_PARM2(regs);
#pragma unroll
for (int i = 0; i < MAX_ARGS; i++) {
char __user *argp = NULL;
bpf_probe_read_user(&argp, sizeof(argp), &argv_user[i]);
if (!argp) break;
bpf_probe_read_user_str(&ev.argv[i], ARG_LEN, argp);
}
// 读取关键环境变量(简化:仅演示CNI_CONTAINERID)
// 完整实现需遍历envp,此处省略
bpf_map_update_elem(&pending_exec, &pid, &ev, BPF_ANY);
bpf_perf_event_output(ctx, &exec_events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
return 0;
}
SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_exit *ctx) {
__u32 pid = bpf_get_current_pid_tgid() >> 32;
struct exec_event *ev = bpf_map_lookup_elem(&pending_exec, &pid);
if (!ev) return 0;
struct exit_event exit_ev = {};
exit_ev.pid = pid;
exit_ev.exit_code = ctx->exit_code;
bpf_perf_event_output(ctx, &exit_events, BPF_F_CURRENT_CPU, &exit_ev, sizeof(exit_ev));
bpf_map_delete_elem(&pending_exec, &pid);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
```
用户态加载程序cnitrace.c(核心部分):
```c
// cnitrace.c - 仅展示关键加载与事件处理逻辑
#include
#include
#include
#include
static volatile int stop = 0;
static void handle_signal(int sig) { stop = 1; }
static void handle_exec_event(void *ctx, int cpu, void *data, __u32 size) {
struct exec_event *e = data;
printf("[EXEC] pid=%d comm=%s\n", e->pid, e->comm);
for (int i = 0; i < MAX_ARGS && e->argv[i][0]; i++) {
printf(" argv[%d]=%s\n", i, e->argv[i]);
}
}
static void handle_exit_event(void *ctx, int cpu, void *data, __u32 size) {
struct exit_event *e = data;
printf("[EXIT] pid=%d code=%d\n", e->pid, e->exit_code);
}
int main() {
struct bpf_object *obj;
struct bpf_program *prog_exec, *prog_exit;
struct perf_buffer *pb_exec, *pb_exit;
obj = bpf_object__open_file("cnitrace.bpf.o", NULL);
if (libbpf_get_error(obj)) { fprintf(stderr, "Failed to open BPF object\n"); return 1; }
bpf_object__load(obj);
prog_exec = bpf_object__find_program_by_name(obj, "handle_exec");
prog_exit = bpf_object__find_program_by_name(obj, "handle_exit");
if (!prog_exec || !prog_exit) { fprintf(stderr, "Failed to find programs\n"); return 1; }
struct bpf_link *link_exec = bpf_program__attach(prog_exec);
struct bpf_link *link_exit = bpf_program__attach(prog_exit);
if (libbpf_get_error(link_exec) || libbpf_get_error(link_exit)) {
fprintf(stderr, "Failed to attach tracepoints\n"); return 1;
}
int map_fd_exec = bpf_object__find_map_fd_by_name(obj, "exec_events");
int map_fd_exit = bpf_object__find_map_fd_by_name(obj, "exit_events");
pb_exec = perf_buffer__new(map_fd_exec, 8, handle_exec_event, NULL, NULL, NULL);
pb_exit = perf_buffer__new(map_fd_exit, 8, handle_exit_event, NULL, NULL, NULL);
signal(SIGINT, handle_signal);
printf("Tracing CNI plugins... Ctrl+C to stop.\n");
while (!stop) {
perf_buffer__poll(pb_exec, 100);
perf_buffer__poll(pb_exit, 100);
}
perf_buffer__free(pb_exec);
perf_buffer__free(pb_exit);
bpf_link__destroy(link_exec);
bpf_link__destroy(link_exit);
bpf_object__close(obj);
return 0;
}
```
编译与运行
- 编译内核态程序:
clang -O2 -g -target bpf -c cnitrace.bpf.c -o cnitrace.bpf.o
- 编译用户态程序:
gcc -o cnitrace cnitrace.c -lbpf
- 以root运行:
sudo ./cnitrace
- 在另一终端触发Pod创建:
kubectl run test-pod --image=nginx
验证方法
观察cnitrace输出,应看到类似以下序列(预期输出,非实测):
[EXEC] pid=12345 comm=bridge
argv[0]=/opt/cni/bin/bridge
argv[1]=--force-address
[EXIT] pid=12345 code=0
判定通过条件:
- 每次Pod创建/删除时,均捕获到
bridge、host-local等CNI插件的exec事件
- exit事件与exec事件PID一一对应,且退出码为0(成功)或非0(失败,可用于调试)
- 若Pod调度到其他节点,需在该节点上运行
cnitrace才能捕获
常见问题
- 无输出:确认CNI插件路径(默认
/opt/cni/bin)与进程名匹配;检查是否以root运行
- 环境变量读取不完整:本示例仅演示argv与退出码追踪,完整环境变量读取需遍历
task_struct中的envp指针,涉及内核版本差异,建议结合BPF_PROG_TYPE_TRACING或fentry实现
- 内核版本过低:需4.18+,建议5.4以上以获得更稳定的BPF特性支持