在传统的 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 包括:
cilium_lb4_services_v2:存储 Service 的元数据。Key 是虚拟 IP、端口和协议(如NodeIP:NodePort或ExternalIP:Port),Value 是一个包含 Backend 数量、Service ID 及负载均衡策略的结构体。cilium_lb4_backends:存储具体的后端 Pod 列表。Key 是 Backend ID,Value 是后端 Pod 的实际 IP 和 Port。cilium_ct_any4(Connection Tracking Map):Cilium 自建的连接跟踪表,用于记录 TCP/UDP 连接状态,确保同一连接的后续数据包能被路由到相同的后端,并用于反向 NAT(Rev-NAT)。cilium_lb4_reverse_nat:倒查表。用于在后端 Pod 回包时,将源 IP/Port(Pod IP)还原为客户端看到的NodeIP:NodePort或ExternalIP:Port。
NodePort 流量的底层处理全生命周期
假设外部客户端(IP 为 ClientIP)向 Node 1 发送了一个请求,目标地址为 NodeIP_1:NodePort。我们来看看 eBPF 是如何一步步处理这个数据包的:
第一阶段:入站拦截与服务检索
- 数据包
[Src: ClientIP:ClientPort -> Dst: NodeIP_1:NodePort]到达 Node 1 的物理网卡。 - 挂载在网卡 Ingress 处的 eBPF 程序(如
cil_from_netdev)被触发,提取出 IP 首部和 TCP 首部。 - eBPF 程序以
[NodeIP_1, NodePort, TCP]作为 Key,查询cilium_lb4_services_v2Map。 - 如果命中,说明这是一个合法的 NodePort 服务。Map 会返回该 Service 的配置,包含一个
ServiceID。
第二阶段:负载均衡与 DNAT(目标地址转换)
- eBPF 程序根据
ServiceID和负载均衡算法(如 Maglev 或 Random),在cilium_lb4_backendsMap 中检索对应的后端 Pod 列表。 - 假设算法选中了位于 Node 2 上的 Pod A(IP 为
PodIP_A,端口为ContainerPort)。 - DNAT 转换:eBPF 程序直接修改数据包的 IP/TCP 头部:
- 将 Destination IP 修改为
PodIP_A。 - 将 Destination Port 修改为
ContainerPort。
- 将 Destination IP 修改为
- 校验和更新:利用 eBPF 辅助函数(如
bpf_l3_csum_replace和bpf_l4_csum_replace)在内核态增量更新 IP 和 TCP 的 Checksum,避免数据包因校验和错误被丢弃。 - 记录连接跟踪:在
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 几乎完全一致:
- 网络管理员需要确保外部流量能够通过路由送达集群中的节点(通常配合 BGP 宣告或静态路由)。
- 当带有
Dst: ExternalIP:Port的数据包到达节点时,eBPF 程序同样通过物理网卡 Ingress 处的cil_from_netdev捕获。 - 在
cilium_lb4_services_v2Map 中,Key 变为了[ExternalIP, Port, Protocol]。 - 后续的 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)
- 保存原始服务信息:当客户端请求到达 Node 1 时,eBPF 程序不进行 SNAT。它将原始的
NodeIP_1和NodePort编码到 IPv4 选项(IP Options)或 IPv6 目的选项扩展头中,然后直接转发给 Node 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 网络近乎裸机的转发性能,也为更大规模的微服务集群提供了坚实的数据面支撑。