HOOOS

彻底抛弃 kube-proxy 后 Cilium 如何依靠 eBPF 驾驭 NodePort 与 ExternalIP 流量

0 9 云原生极客 CiliumeBPFKubernetes
Apple

在传统的 Kubernetes 集群中,服务发现和负载均衡主要依赖 kube-proxy。它通过维护大规模的 iptables 规则或 IPVS 虚拟服务器来实现流量转发。然而,随着集群规模的扩大,iptables 的 $O(N)$ 逐条匹配特性会导致延迟急剧上升,而 IPVS 虽然在查找性能上达到 $O(1)$,但在大规模规则刷新时依然存在内核锁竞争与延迟抖动。

Cilium 推出的 kube-proxy-replacement 模式彻底移除了 kube-proxy,将原本由内核网络栈及 netfilter 处理的 NodePort、ClusterIP、ExternalIP 和 LoadBalancer 流量,完全收拢到 eBPF(Extended Berkeley Packet Filter)程序中。

下面我们将深度拆解在没有 kube-proxy 的情况下,Cilium 在底层是如何利用 eBPF 接管并高效处理 NodePort 以及 ExternalIP 流量的。


核心基石:eBPF 挂载点与流量拦截

当外部客户端尝试访问 <NodeIP>:<NodePort><ExternalIP>:<Port> 时,数据包首先会到达主机的物理网卡(如 eth0)。Cilium 主要在以下两个关键层面对这些入站流量进行拦截和处理:

1. TC(Traffic Control)Ingress 钩子

这是 Cilium 默认且最通用的挂载方式。Cilium 将名为 cil_from_netdev 的 eBPF 程序挂载到物理网卡的 tc(Traffic Control)ingress 钩子上。

  • 执行时机:数据包刚通过网卡驱动,并已被分配了内核数据结构 sk_buff,但尚未进入内核协议栈的常规 L3/L4 处理逻辑。
  • 优势:由于直接在 ingress 处拦截,流量无需经过复杂的 netfilter/iptables 链路,极大地缩短了内核处理路径。

2. XDP(eXpress Data Path)

如果集群开启了高吞吐、低延迟的 XDP 加速(load-balancer-mode=xdp),Cilium 会将 eBPF 程序直接挂载到网卡驱动的常规接收路径(RX Ring Buffer)中。

  • 执行时机:在内核为数据包分配 sk_buff 之前。
  • 优势:在极早期阶段(甚至可以直接在网卡硬件/驱动层)对数据包进行修改或丢弃,几乎实现了物理极限级的包处理性能,非常适合应对高并发的 NodePort 流量和 DDoS 攻击防御。

BPF Map:内核态的高性能数据字典

在 eBPF 程序中,Cilium 无法直接调用用户态的 API,所有的路由查找、服务映射和连接状态都必须存在于内核态的高性能数据结构——BPF Map 中。处理 NodePort 和 ExternalIP 流量时,最核心的 BPF Map 包括:

  1. cilium_lb4_services_v2:存储 Service 的元数据。Key 是虚拟 IP、端口和协议(如 NodeIP:NodePortExternalIP:Port),Value 是一个包含 Backend 数量、Service ID 及负载均衡策略的结构体。
  2. cilium_lb4_backends:存储具体的后端 Pod 列表。Key 是 Backend ID,Value 是后端 Pod 的实际 IP 和 Port。
  3. cilium_ct_any4(Connection Tracking Map):Cilium 自建的连接跟踪表,用于记录 TCP/UDP 连接状态,确保同一连接的后续数据包能被路由到相同的后端,并用于反向 NAT(Rev-NAT)。
  4. cilium_lb4_reverse_nat:倒查表。用于在后端 Pod 回包时,将源 IP/Port(Pod IP)还原为客户端看到的 NodeIP:NodePortExternalIP:Port

NodePort 流量的底层处理全生命周期

假设外部客户端(IP 为 ClientIP)向 Node 1 发送了一个请求,目标地址为 NodeIP_1:NodePort。我们来看看 eBPF 是如何一步步处理这个数据包的:

第一阶段:入站拦截与服务检索

  1. 数据包 [Src: ClientIP:ClientPort -> Dst: NodeIP_1:NodePort] 到达 Node 1 的物理网卡。
  2. 挂载在网卡 Ingress 处的 eBPF 程序(如 cil_from_netdev)被触发,提取出 IP 首部和 TCP 首部。
  3. eBPF 程序以 [NodeIP_1, NodePort, TCP] 作为 Key,查询 cilium_lb4_services_v2 Map。
  4. 如果命中,说明这是一个合法的 NodePort 服务。Map 会返回该 Service 的配置,包含一个 ServiceID

第二阶段:负载均衡与 DNAT(目标地址转换)

  1. eBPF 程序根据 ServiceID 和负载均衡算法(如 Maglev 或 Random),在 cilium_lb4_backends Map 中检索对应的后端 Pod 列表。
  2. 假设算法选中了位于 Node 2 上的 Pod A(IP 为 PodIP_A,端口为 ContainerPort)。
  3. DNAT 转换:eBPF 程序直接修改数据包的 IP/TCP 头部:
    • 将 Destination IP 修改为 PodIP_A
    • 将 Destination Port 修改为 ContainerPort
  4. 校验和更新:利用 eBPF 辅助函数(如 bpf_l3_csum_replacebpf_l4_csum_replace)在内核态增量更新 IP 和 TCP 的 Checksum,避免数据包因校验和错误被丢弃。
  5. 记录连接跟踪:在 cilium_ct_any4 中新建一条五元组状态记录,同时在 cilium_lb4_reverse_nat 中关联该连接,以便回包时进行反向 DNAT。

第三阶段:跨节点路由与转发

根据目标 Pod 是否在当前节点,Cilium 的转发策略分为两种:

情况 A:Pod 就在本地节点(Node 1)

eBPF 程序直接调用 bpf_redirect,绕过整个主机的 L3 协议栈,将数据包直接塞入连接该 Pod 的虚拟网卡对(lxcxxxx)的 Ingress 端。Pod A 随即收到流量。

情况 B:Pod 在其他节点(Node 2)

流量需要跨节点传输。这取决于 Cilium 的网络模式:

  • 隧道模式(VXLAN / Geneve):Cilium 会将修改后的数据包封装在隧道协议内,外层 IP 为 [Src: NodeIP_1 -> Dst: NodeIP_2]。数据包通过物理网络发送到 Node 2,Node 2 解包后放入本地 Pod A。
  • 直接路由模式(Native Routing):不进行封包。eBPF 程序直接查询主机的路由表,将数据包交给网卡,依靠物理网络交换机/路由器将包投递给 Node 2。

注意:SNAT 的取舍(externalTrafficPolicy

  • 如果服务的 externalTrafficPolicy 设置为 Cluster(默认),且选中的 Pod 在其他节点,为了避免回包时出现“异步路由”导致客户端拒绝连接,Cilium 会在转发给 Node 2 之前进行 SNAT(源地址转换),将源 IP 替换为 Node 1 的 IP。
  • 如果设置为 Local,Cilium 的 eBPF 程序在第一步查询时,就只会选择当前节点(Node 1)上的 Pod。如果当前节点没有该服务的 Pod,则直接丢弃数据包。

ExternalIP 的处理机制

ExternalIP 是 Kubernetes 允许用户为 Service 指定的任意外部 IP。它的底层处理逻辑与 NodePort 几乎完全一致:

  1. 网络管理员需要确保外部流量能够通过路由送达集群中的节点(通常配合 BGP 宣告或静态路由)。
  2. 当带有 Dst: ExternalIP:Port 的数据包到达节点时,eBPF 程序同样通过物理网卡 Ingress 处的 cil_from_netdev 捕获。
  3. cilium_lb4_services_v2 Map 中,Key 变为了 [ExternalIP, Port, Protocol]
  4. 后续的 DNAT、连接跟踪、Pod 负载均衡以及回包机制,与上述 NodePort 流程完全共享一套底层的 eBPF 代码逻辑。这种高度的抽象和复用,使得 Cilium 在处理多种 Service 类型时保持了极高的执行效率。

极致优化:eBPF 的直接服务返回(DSR)

在传统的 kube-proxy 架构中,NodePort 的跨节点流量必须经历“双向 NAT”:
Client -> Node 1 (DNAT + SNAT) -> Node 2 -> Pod A -> Node 2 -> Node 1 (Rev-SNAT + Rev-DNAT) -> Client

Node 1 变成了流量的必经之脊,这不仅增加了延迟,还白白消耗了 Node 1 的带宽和 CPU。

Cilium 利用 eBPF 实现了 DSR(Direct Server Return,直接服务返回)

       Client
      /      ^
     /        \ (Direct Return with Node 1's IP)
    v          \
  Node 1 ----> Node 2 (Pod A)
 (Ingress)
  1. 保存原始服务信息:当客户端请求到达 Node 1 时,eBPF 程序不进行 SNAT。它将原始的 NodeIP_1NodePort 编码到 IPv4 选项(IP Options)或 IPv6 目的选项扩展头中,然后直接转发给 Node 2。
  2. 后端直接回包:Node 2 的 eBPF 程序在收到包后解析出隐藏在 IP 选项中的原始地址。当 Pod A 回复时,Node 2 上的 eBPF 程序直接将回包的源 IP 伪装成 NodeIP_1,并绕过 Node 1,直接发送给外部 Client

通过 DSR 模式,回包流量不需要再折返回入口节点,单向网络带宽开销减半,网络延迟和 CPU 消耗也得到了显著降低。


总结

Cilium 通过将网络策略、负载均衡和 NAT 转换等逻辑下沉到 Linux 内核最底层的 eBPF 挂载点(TC/XDP),消除了 kube-proxy 产生的多重用户态/内核态切换以及庞大的 netfilter 链表。

无论面对的是 NodePort 还是 ExternalIP 流量,Cilium 都在内核最早期阶段通过哈希查表完成了 $O(1)$ 复杂度的包地址转换与状态更新。这种设计不仅赋予了 Kubernetes 网络近乎裸机的转发性能,也为更大规模的微服务集群提供了坚实的数据面支撑。

点评评价

captcha
健康