news 2026/10/16 11:06:20

从源码硬啃:Cilium 创建 Pod 网络时 eBPF 程序到底怎么下发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从源码硬啃:Cilium 创建 Pod 网络时 eBPF 程序到底怎么下发

从源码硬啃: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里按固定顺序推进:

  1. 解析 CNI 配置并连接本机 agent 的 Unix socket,拉取DaemonConfigurationStatus;
  2. 打开 pinned netns(netns.OpenPinned(args.Netns)),先删掉残留的同名接口;
  3. 分配 IP:默认走 agent 的 IPAM(allocateIPsWithCiliumAgent调用 RESTIPAMAllocate),也可配置为 delegated plugin;
  4. 构造EndpointChangeRequest,其中SyncBuildEndpoint: true是关键——要求 agent 同步完成 endpoint 的数据面构建(代码注释直指 GH-4409);
  5. 创建 veth pair(或 netkit),配置 MTU/GRO/GSO,将 peer 一端移入 Pod netns;
  6. 在 netns 内配置 IP、路由、规则,读取 netns cookie;
  7. 调用 agent 的EndpointCreate,把"半成品" endpoint 交给 agent 完成 BPF 下发;
  8. 若失败,通过 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完成加载与挂载,顺序非常讲究:

  1. collLoader.LoadAndAssign把 ELF 中的程序与 map 装配到内核,PinPath: TCGlobalsPath()让 map 落到 bpffs,并传入MapRenames;
  2. 先把 policy 程序插入尾调用数组:obj.PolicyMap.Update(epID, obj.PolicyProg)与EgressPolicyMap.Update(...),注释明确警告"如果 agent 在此之后异常退出,endpoint 连接会部分损坏"——因为 policy 程序一旦可被bpf_host尾调用到,却还没有完整的程序链,就会产生未定义行为;
  3. 通过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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 11:05:42

开源AI视频生成实战:Pixelle-Video+VideoClaw构建自动出片流水线

最近一直在折腾开源AI视频生成这条生产线,从最初只拿Pixelle-Video出一段一段的片段素材,到后面把VideoClaw接进来做批量后期,整个"一句话批量出片"才算真正落地。说白了一个放在前面当编剧和导演,一个放在后面当后期车…

作者头像 李华
网站建设 2026/10/16 11:05:28

前端Leader的AI Agent工程化转型实战:TypeScript驱动的智能体开发范式

1. 项目概述:一个前端技术管理者的真实AI Agent转型切口 “在职前端Leader学习/转行 AI Agent -DAY71”——这个标题不是打卡式的情绪宣泄,也不是轻飘飘的跨界宣言,而是一个在真实业务压力、团队管理职责和职业发展焦虑三重夹击下&#xff0c…

作者头像 李华
网站建设 2026/10/16 11:02:10

d3d10warp.dll丢失不用慌:原因分析与官方免费修复指南

打开某个游戏,或者启动一个平时用得挺顺的设计软件,屏幕正中突然弹出一条错误提示:无法启动此程序,因为计算机中丢失 d3d10warp.dll。请尝试重新安装该程序来解决此问题。英文版系统还会显示The code execution cannot proceed be…

作者头像 李华
网站建设 2026/10/16 11:01:56

RAG检索链路全解析:从混合检索到重排的工程化落地指南

我经常在技术群里看到有人贴一段RAG代码:embedding模型、FAISS、top_k5,然后问“为什么答案还是不对”。这类项目单看每一环都很完整,向量库建了、相似度算了、上下文也塞进Prompt了,但效果就是不稳定,有时候答非所问&…

作者头像 李华
网站建设 2026/10/16 11:01:50

C++手写贪吃蛇:从游戏循环到碰撞检测的完整实现

1. 为什么选择用C手写一个贪吃蛇1.1 这个项目的定位与适合人群贪吃蛇这个游戏,几乎每个学编程的人都见过,但真正自己从零用C写一个能跑、手感还不错的版本,其实比想象中要讲究。它不是那种"复制粘贴就能交差"的作业题,而…

作者头像 李华
网站建设 2026/10/16 11:00:18

Tomcat线程池详解:从默认实现原理到配置调优与故障排查

某天中午,我一个做物流查询系统的项目组同学打来电话,说线上两台实例出现了零星请求超时,监控上看到Tomcat活跃线程不高,队列却越积越长。我登上去一看,活跃线程稳定在60多,maxThreads配的300,队…

作者头像 李华