eBPF/Cilium 如何接管 Kubernetes Service 数据路径
从 ClusterIP 到后端 Pod,拆开 Cilium 的控制面、BPF map、socket LB 与 tc/XDP 数据路径。
在 Kubernetes 里创建一个 Service 后,会得到一个 ClusterIP。很多人第一次排查网络时,会去节点上找这个 IP 绑在哪块网卡上,结果什么也找不到。
这很正常。ClusterIP 通常是一个虚拟地址,它代表一组会变化的后端,而不是某台机器真实持有的地址。客户端访问 ClusterIP 时,节点上的数据路径会依据 Cilium agent 从 EndpointSlice 同步到 BPF map 的状态选择后端,再把发往虚拟地址的连接转到真实 Pod。
传统环境常由 kube-proxy 把 Service 和后端变化转成节点上的转发表,具体实现可能使用 iptables、nftables 等模式。Cilium 开启 eBPF kube-proxy replacement 后,可以由 Cilium 负责 ClusterIP、NodePort、LoadBalancer、externalIPs 以及部分 hostPort 的 Service 转发。
“接管”说的是这段 L4 Service 数据路径。Kubernetes Service、EndpointSlice 和 API Server 仍然存在,Cilium 也仍要监听这些对象。Ingress、Gateway、L7 路由和 service mesh 可能继续依赖 Envoy 等用户态代理,Cilium 只是可以把代理放在节点级别并用 eBPF 做重定向,不能简单概括成“所有网络逻辑都进了内核”。
控制面把 Service 变成 BPF 状态
假设有一个 Service:
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 80
targetPort: 8080
控制器会根据 selector 维护对应的 EndpointSlice。Cilium agent 监听 Service 和 EndpointSlice 的变化,再把前端地址、端口和后端地址写入相应的 BPF map;流量经过时,数据路径还会使用连接跟踪、反向 NAT 等 map 保存必要状态。可以把 map 理解成内核与用户态共享的键值存储:agent 负责同步控制面信息,挂在网络钩子上的 BPF 程序负责查询并更新流量状态。
这是一条很重要的边界。eBPF 没有取代 Kubernetes 控制面,也不会自己猜出后端。后端 Pod Ready、扩缩容或终止时,状态仍然要经过 Kubernetes API,再由 Cilium 同步到节点。
理解这些 map 时,可以先抓住三类关系。前端表回答“这个 Service IP 和端口对应哪个服务”;后端表保存可选的 Pod IP 和端口;连接跟踪、反向 NAT 等状态则保证同一条连接的后续数据包和回包能得到一致处理。具体 map 名称与字段会随版本和功能变化,不必先背命令。排障时更重要的是核对:控制面声明的后端数量,是否与故障节点实际加载的后端数量一致。
当 Service 不通时,排查也应分两层:
- Kubernetes 层看到的 Service、EndpointSlice 是否正确;
- Cilium 是否把相同的信息编程到了本机数据路径。
只看 kubectl get svc 不够,只看 BPF map 也不够。
一次连接可能很早就被改写
Cilium 的 Service 转发并不只有一个挂载点。具体走哪条路径,取决于版本、内核能力、安装参数、流量方向和工作负载类型。
对常见的 TCP 连接,socket-level load balancer 可以挂在 cgroup 的 socket 钩子上。当进程调用 connect() 连接 ClusterIP 时,BPF 程序就查询 Service map 并选择后端。应用以为自己连接的是虚拟 IP,内核里的 socket 实际已经指向后端 Pod。把服务选择提前到 socket 层,可以减少逐包路径重复查询 Service 转发表的工作;具体回程和 NAT 仍取决于所用数据路径。
简化后的路径如下:
应用 connect(Service IP)
→ cgroup socket BPF
→ 查询 Service / backend map
→ 选择后端 Pod IP
→ 建立连接
socket LB 并不适合所有场景。有些 sidecar 需要先看到原始 ClusterIP;KubeVirt、Kata Containers、gVisor 一类环境的网络 namespace 和 socket 路径也可能不同。Cilium 可以让 Pod namespace 绕过 socket 改写,回退到 veth 上的 tc BPF 程序做逐包负载均衡。
在 tc 路径里,包已经存在。BPF 程序在虚拟网卡或主机网卡的 ingress/egress 钩子上读取五元组,查询 map,执行后端选择和必要的地址转换,再交给路由系统。后端在另一节点时,后续仍要经过 Cilium 配置的隧道或原生路由。换成 eBPF 并不会让跨节点网络凭空消失。
XDP 是更早的接收路径,可用于某些 NodePort 或 LoadBalancer 的转发加速。它是否启用、网卡驱动是否支持、流量是否符合使用条件,都需要单独验证。不能看到 Cilium 就默认所有包都走 XDP,也不能笼统地说 Cilium 一定绕过 Netfilter。共存模式、回退路径、SNAT 配置和外部组件都可能继续使用 Netfilter。
Cilium 替换 kube-proxy 后改变了什么
最明显的变化不是 YAML,而是运维观察点。
过去排查 Service,工程师习惯查 kube-proxy 日志、iptables/nftables 规则和 conntrack。换成 Cilium 后,Service 状态主要落在 BPF map,流量事件可以通过 Hubble 或 Cilium 的监控命令观察。原来的 Linux 网络知识依旧有用,但还要理解 cgroup、tc/XDP 钩子、BPF map,以及 agent 如何从 Kubernetes 同步状态。
数据路径更可编程,也更容易把身份、策略和可观测信息放进同一套系统。它并不自动保证更快。性能取决于内核、网卡、流量模型、map 大小、负载均衡算法和配置,应该用自己的请求分布做压测。
故障形态也会变化。例如:
- Service 与 EndpointSlice 正常,但某些节点的 BPF 状态没有及时同步;
- Pod 间直连正常,访问 ClusterIP 失败,问题落在 Service 转换层;
- 普通 Pod 正常,带 sidecar 或特殊运行时的工作负载异常;
- 内部流量正常,NodePort 或 LoadBalancer 异常,问题落在外部流量策略、SNAT/DSR 或设备选择;
- 升级后新连接正常,旧连接中断,原因是新旧 NAT/连接状态彼此并不共享。
这些现象比“eBPF 比 iptables 快”更值得记住。
迁移时别直接拔掉 kube-proxy
Cilium 官方文档明确提醒,在已有连接的集群里切换 kube-proxy 与 eBPF 替代方案,连接可能中断。两套数据路径各自维护 NAT 和连接状态,不会自动交接。先删除 kube-proxy、再慢慢调 Cilium,也会让 Service 流量出现空窗。
更稳妥的做法是先确认四件事。
其一,核对当前 Kubernetes、Cilium、Linux 内核和容器运行时的兼容矩阵,尤其是 cgroup v2、socket LB 及特殊运行时要求。
其二,建立测试清单。ClusterIP、NodePort、LoadBalancer、外部源地址、externalTrafficPolicy、internalTrafficPolicy、双栈、hostNetwork、DNS、sidecar 和网络策略都应覆盖。
其三,保存迁移前的流量基线,至少包括连接错误率、延迟、丢包、conntrack 压力和 Service 后端分布。没有基线,迁移后很难区分真实回归与观察方式变化。
其四,准备可执行的回退方案。回退不是把 Helm 参数改回去这么简单,还要考虑现有连接、节点逐步切换顺序和控制面 API 的可达性。
一条实用的排查链路
遇到 “Service 不通” 时,可以按下面的顺序缩小范围:
Service selector
→ EndpointSlice 中是否有 Ready 后端
→ Cilium agent 是否健康并连得上 API Server
→ 本节点 Service / backend BPF 状态是否存在
→ 流量在 socket、tc 或 XDP 的哪一层被处理
→ 后端路由与策略是否允许
→ 回包是否按预期返回
kubectl get endpointslice 用来确认控制面事实;Cilium CLI 和 agent 内的 BPF 查看命令用来核对节点状态;Hubble 适合观察流量被转发、拒绝或丢弃的位置。具体命令会随版本调整,生产操作应以所用版本的官方文档为准。
测试时也不要只在单个节点上访问一次。至少要覆盖同节点后端、跨节点后端、后端扩缩容以及节点重启,才能看出状态同步和回程路径有没有问题。
理解 Cilium 的关键,不是背下一组 Helm 参数,而是把链路分清楚:Kubernetes 负责声明 Service 和后端,Cilium agent 把声明变成 BPF 状态,内核钩子在不同位置处理连接和数据包,底层路由再把包送到目标。哪一层的事实不一致,就去查那一层。