从源码硬啃:Cilium 创建 Pod 网络时 eBPF 程序到底怎么下发
发布时间:2026/10/16 11:06:33 锦皓数字建站

从源码硬啃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 netnsnetns.OpenPinned(args.Netns)先删掉残留的同名接口分配 IP默认走 agent 的 IPAMallocateIPsWithCiliumAgent调用 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)ReloadDatapathpkg/datapath/loader/endpoint.go是下发 eBPF 程序的真正入口其设计有两点值得注意模板化编译优先在模板缓存中寻找同一配置的已编译对象避免每个 Pod 都重新clang编译。缓存键是base hash endpoint 配置 hashmap 重命名每个 endpoint 会把全局 map 重命名成带 endpoint ID 的私有 mapdefaultEndpointMapRenames将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 模板编译往往直接相关。加载与 attachtail 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其余走upsertTCXProgramattach 成功后再清理遗留的 legacy tc filtertcx 不可用link.ErrNotSupported才回退回退路径走legacy tcupsertTCProgram先replaceQdisc装载 clsact再以netlink.BpfFilter挂 filterpriority 默认 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 * veths 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),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。