从源码硬啃:Cilium 创建 Pod 网络时 eBPF 程序到底怎么下发
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
当 kubelet 为新建 Pod 调用 CNIADD之后,Cilium 到底在用户态与内核态之间做了哪些事?cil_from_container、cil_to_container这些程序是怎么被编译、加载并 attach 到 veth 上的?本文不再停留在"eBPF 驱动数据面"的概念层,而是顺着真实仓库源码逐层拆解这条调用链,从 CNI 插件到 agent 的 REST API,再到 BPF 模板编译缓存与 tc/tcx attach,最后反推常见问题的排障思路。
整条链路的起点:CNI 插件进程里的 Add
每个节点上的cilium-cni是一个独立二进制,入口通过skel.PluginMainFuncs注册Add/Del/Check/Status四个操作(plugins/cilium-cni/cmd/cmd.go)。kubelet 触发ADD后,核心逻辑在Cmd.Add里按固定顺序推进:
- 解析 CNI 配置并连接本机 agent 的 Unix socket,拉取
DaemonConfigurationStatus; - 打开 pinned netns(
netns.OpenPinned(args.Netns)),先删掉残留的同名接口; - 分配 IP:默认走 agent 的 IPAM(
allocateIPsWithCiliumAgent调用 RESTIPAMAllocate),也可配置为 delegated plugin; - 构造
EndpointChangeRequest,其中SyncBuildEndpoint: true是关键——要求 agent 同步完成 endpoint 的数据面构建(代码注释直指 GH-4409); - 创建 veth pair(或 netkit),配置 MTU/GRO/GSO,将 peer 一端移入 Pod netns;
- 在 netns 内配置 IP、路由、规则,读取 netns cookie;
- 调用 agent 的
EndpointCreate,把"半成品" endpoint 交给 agent 完成 BPF 下发; - 若失败,通过 defer 释放 IP、删除 link,保证无残留。
链路两端的接口形态也值得注意:configureIface在容器 netns 内用 netlink 加地址、加路由、加策略路由规则;host 侧由connector.NewLinkPair创建设备(见 pkg/datapath/connector/veth.go 的setupVethPair:随机生成两端 MAC、TxQLen: 1000、并关闭 host 侧 rp_filter)。veth 只是一个载体,真正的转发逻辑全部挂在 host 侧接口的 hook 上。
agent 收到请求后:endpoint 重建与数据面实现
EndpointCreate落地的核心是 endpoint 的重建(regeneration)。pkg/endpoint/creator/creator.go的NewEndpointFromChangeModel把EndpointChangeRequest转成内存中的Endpoint对象,随后触发一次同步重建。重建过程在 pkg/endpoint/bpf.go 的realizeBPFState达到高潮:
// Compile and install BPF programs for this endpoint templateHash, err := e.orchestrator.ReloadDatapath(datapathRegenCtxt.completionCtx, datapathRegenCtxt.epInfoCache, &stats.datapathRealization)ReloadDatapath(pkg/datapath/loader/endpoint.go)是"下发 eBPF 程序"的真正入口,其设计有两点值得注意:
- 模板化编译:优先在模板缓存中寻找同一配置的已编译对象,避免每个 Pod 都重新
clang编译。缓存键是"base hash + endpoint 配置 hash"; - map 重命名:每个 endpoint 会把全局 map 重命名成带 endpoint ID 的私有 map,
defaultEndpointMapRenames将cilium_calls、cilium_policy重命名为cilium_calls_{id}、cilium_policy_{id},实现 endpoint 间隔离。
再看模板缓存实现(pkg/datapath/loader/cache.go)。fetchOrCompile先按配置算出 hash,再用serialize(hash)对同一 hash 加锁,同一配置的并发请求会被串行化——第一个请求负责编译,后续请求直接拿到缓存的CollectionSpec.Copy()。编译时build会先生成 endpoint 专属头文件(bpf_lxc.h),再调用compileDatapath调起 clang(编译器名与产物路径在 pkg/datapath/loader/compile.go 定义:compiler = "clang"、endpointProg = "bpf_lxc.c"、产物bpf_lxc.o),同时记录BpfCompilation与BpfWaitForELF指标。
这一设计直接决定了实际开销:首个新配置的 Pod 要等一次完整 clang 编译(可达数秒),同配置的后续 Pod 只是加载与 attach。因此生产环境里"创建 Pod 慢"与"BPF 模板编译"往往直接相关。
加载与 attach:tail call 装配 + tcx/legacy tc 双路径
拿到编译好的CollectionSpec后,reloadEndpoint完成加载与挂载,顺序非常讲究:
collLoader.LoadAndAssign把 ELF 中的程序与 map 装配到内核,PinPath: TCGlobalsPath()让 map 落到 bpffs,并传入MapRenames;- 先把 policy 程序插入尾调用数组:
obj.PolicyMap.Update(epID, obj.PolicyProg)与EgressPolicyMap.Update(...),注释明确警告"如果 agent 在此之后异常退出,endpoint 连接会部分损坏"——因为 policy 程序一旦可被bpf_host尾调用到,却还没有完整的程序链,就会产生未定义行为; - 通过
attachSKBProgram把cil_from_container挂到 host 侧接口的ingress,把cil_to_container挂到egress。
attach 的具体实现(pkg/datapath/loader/tc.go)体现了 Cilium 对内核特性的渐进式利用:
- 若启用并支持tcx:对 netkit 设备走
upsertNetkitProgram,其余走upsertTCXProgram,attach 成功后再清理遗留的 legacy tc filter;tcx 不可用(link.ErrNotSupported)才回退; - 回退路径走legacy tc:
upsertTCProgram先replaceQdisc装载 clsact,再以netlink.BpfFilter挂 filter,priority 默认 1; - 反向降级时清理对方:legacy tc attach 成功后
detachGeneric(..., "tcx")摘除 tcx link。
也就是说,同一个函数在不同内核版本上最终落点是 tcx bpf_link 或 tc filter,但对外表现一致——这正是 Cilium"内核特性自动降级"的工程化体现。
数据面视角:cil_from_container 里发生了什么
挂上 hook 只是第一步,包处理逻辑在 bpf/bpf_lxc.c。cil_from_container是整个 endpoint 数据面的入口:
__section_entry int cil_from_container(struct __ctx_buff *ctx) { __be16 proto = 0; __u32 sec_label = SECLABEL; __s8 ext_err = 0; int ret; bool valid_ethertype = validate_ethertype(ctx, &proto); bpf_clear_meta(ctx); check_and_store_ip_trace_id(ctx); /* Workaround for GH-18311 where veth driver might have recorded * veth's RX queue mapping instead of leaving it at 0. ... */ ctx->queue_mapping = 0; edt_set_aggregate(ctx, LXC_ID); ... switch (proto) { case bpf_htons(ETH_P_IP): ret = tail_call_internal(ctx, CILIUM_CALL_IPV4_FROM_LXC, &ext_err); sec_label = SECLABEL_IPV4; break; ...几个细节值得展开:
- 入口程序极薄:只做 ethertype 校验、清 meta、记 trace、按协议分发,然后
tail_call_internal跳转到CILIUM_CALL_IPV4_FROM_LXC等尾调用槽位。真正的策略、NAT、路由逻辑都在被尾调用的程序里(cil_from_lxc等)。这样设计让入口程序极小、验证器压力小、且能通过更新尾调用数组热切换策略逻辑; - queue_mapping 清零:针对 GH-18311 的 veth RX 队列映射 bug 的 workaround,避免物理设备上所有流量只打到一个 TX 队列;
- policy 程序的插入顺序:
reloadEndpoint在 attach 入口之前先向 policy map 插入程序,保证了"策略先于流量可见"。
再看出方向:cil_to_container从 host 侧 egress hook 接手入向包,在 bpf/bpf_lxc.c 的pass_to_stack分支里有专门针对 per-endpoint route 模式的 L7 LB 回注逻辑(redirect_self把包 hairpin 回cil_to_container重新走 ingress 策略),这说明同一个程序在不同配置下通过宏开关(ENABLE_ROUTING、ENABLE_L7_LB等)编译出不同行为——这也是模板缓存按配置 hash 区分对象的根本原因。
从源码反推性能与排障思路
把链路串起来,可以得到几条直接指导生产实践的结论:
1. Pod 创建慢先查 BPF 编译指标。metrics.SpanStat里有BpfCompilation与BpfWaitForELF两个关键计时:前者是真实 clang 编译耗时,后者包括等待同配置其他 endpoint 编译的排队时间。若大量 Pod 同时以"新配置"创建,serialize(hash)会把它们串行化,BpfWaitForELF会显著抬升。缓解手段:避免无意义的配置漂移(如标签、DNS 规则导致 endpoint 配置 hash 变化),让同一节点的 Pod 复用同一模板。
2. 排障先分清"没下发"还是"没生效"。cilium-dbg bpf系列命令(Documentation/cmdref/cilium-dbg_bpf.md)可以直接检查 map 状态;而cilium-dbg endpoint get的 datapath 状态会展示 hash 与程序挂载情况。若 endpoint 显示 config hash 与模板缓存不一致,说明重建未完成或配置漂移;若 map 存在但流量不通,问题多半在尾调用数组的 policy 插入阶段——按reloadEndpoint的注释,agent 在该阶段异常退出会留下"半挂"状态。
3. 关注 attach 路径的版本差异。同一份代码,在支持 tcx 的内核上走 bpf_link(生命周期随 link 管理,删除时整目录清理),在旧内核上走 legacy tc filter。升级内核后 attach 路径自动切换,若出现"旧 filter 残留"或"tcx 与 tc 双挂"的迹象,可对照attachSKBProgram的降级/清理逻辑排查;Unload时也是先删 tcx link 再删 filter、最后移除 bpffs 下 per-endpoint 目录(pkg/datapath/loader/endpoint.go 的Unload)。
4. 数据面问题要在入口程序处定位。cil_from_container是每个包的第一站,send_trace_notify打出的TRACE_FROM_LXC事件配合 Hubble 可以还原"包从 Pod 出来是否进过策略"。若 trace 显示包在入口就被DROP_UNKNOWN_L3之类丢弃,问题往往在 ethertype/协议层;若能走到尾调用但无后续 trace,则要检查cilium_calls_{id}尾调用数组是否被正确填充。
小结
从cilium-cni的Add到loader.ReloadDatapath,再到cil_from_container的尾调用分发,Cilium 的"下发 eBPF 程序"远不止一次bpf(BPF_PROG_LOAD):它是一套围绕模板编译缓存、map 重命名、tail call 装配、attach 双路径降级构建的完整工程体系。理解这条链路的每一环,既是读懂 Cilium 数据面的钥匙,也是生产环境里定位"Pod 创建慢""流量不通""升级后行为变化"等问题的起点。
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考