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; } ```

编译与运行

  1. 编译内核态程序:clang -O2 -g -target bpf -c cnitrace.bpf.c -o cnitrace.bpf.o
  2. 编译用户态程序:gcc -o cnitrace cnitrace.c -lbpf
  3. 以root运行:sudo ./cnitrace
  4. 在另一终端触发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特性支持