编程 Cilium 深度拆解:当 eBPF 把 kube-proxy 的 iptables 规则「烧」进内核——从 XDP 甩包、TC 转发到 Hubble 全链路可观测与零 Sidecar 服务网格的全链路实战

2026-08-19 04:42:28

Cilium 深度拆解:当 eBPF 把 kube-proxy 的 iptables 规则「烧」进内核——从 XDP 甩包、TC 转发到 Hubble 全链路可观测与零 Sidecar 服务网格的全链路实战

摘要:kube-proxy 的 iptables 模式,在 Service 数量破千的那一刻就成了一道性能悬崖——每来一个包都要在一条几万行的规则链上做线性匹配。Cilium 的解法是把网络逻辑从「静态规则链」搬进「内核里可动态加载的程序」,用 eBPF 在 XDP/TC 层直接完成负载均衡、NAT、策略判定与可观测采样。本文从 kube-proxy 的困局讲起,拆解 eBPF 的运行时模型、Cilium 的身份体系与数据面架构,配一套可运行的 Go+eBPF 代码与 Helm/CNP 实战,最后给出生产级性能调优清单。


一、背景介绍:kube-proxy 的 iptables 困局

每个在 Kubernetes 生产集群里摸爬滚过的工程师,大概率都踩过同一个坑:集群规模一上来,网络延迟就开始「抽风」,而且抽得毫无规律。

根因往往藏在 kube-proxy 的 iptables 模式里。

1.1 它是怎么工作的

kube-proxy 监听 Kubernetes 的 Service 和 Endpoints 变化,把「访问 ClusterIP:Port 要转发到哪些 Pod」这条映射,翻译成一串又长又深的 iptables 规则。数据包进入节点后,内核的 netfilter 框架会沿着 PREROUTING → KUBE-SERVICES → KUBE-SVC-XXX → KUBE-SEP-YYY 这条链一路匹配下去,命中后做 DNAT,把目标地址改写成某个后端 Pod 的 IP。

问题出在匹配方式上:iptables 的规则是顺序匹配的,本质是一个链表。Service 有 N 个,规则条数就大致是 O(N) 量级;每个数据包都要从第一条规则开始,逐个比对直到命中。当 Service 数从 100 涨到 5000,规则总数可能冲到几万甚至十几万行。

1.2 三个具体的痛点

① 每包线性扫描(O(n) 转发开销)
规模一大,单次转发要在几万条规则里走一遍。虽然 iptables 有 ipset 可以缓解,但 kube-proxy 默认的实现并不会把全部维度都塞进 ipset,kube-proxy 自身维护的规则链仍然逃不开顺序匹配。结果是:Service 越多,单包转发 CPU 越高,且与「这个包实际要访问哪个 Service」完全无关——纯属为规模买单。

② 规则全量重建(更新抖动)
kube-proxy 更新规则时是「先建新链、再原子替换、再删旧链」。iptables-restore 一次把整张表刷进去。Service 频繁变更(滚动发布、HPA 扩缩)时,这个全量操作会占用大量 CPU,并且在新旧链切换的瞬间造成微秒到毫秒级的转发毛刺。节点越多、规则越多,抖动越明显。

③ conntrack 与 SNAT 爆炸
DNAT 之后需要 conntrack 表记录连接,否则回包找不到「我是谁」。Pod 跨节点互访时还要做 SNAT,规则条目进一步膨胀。conntrack 表满了会直接丢包(conntrack: table full),这是生产环境里极其经典却又极难第一时间定位的故障。

更本质的一点:iptables 是一个「匹配引擎」,不是「编程引擎」。它擅长表达「如果满足 A 就做 B」,但很难表达「基于一致性哈希选一个健康的后端」「在 socket 层就决定出口」「按身份而非 IP 做策略」。所有复杂逻辑都被迫塞进静态规则,于是规则爆炸只是表象,表达力天花板才是病根。

1.3 eBPF:把逻辑搬进内核

eBPF(extended Berkeley Packet Filter)允许我们在不修改内核源码、不加载内核模块的前提下,把一段经过严格验证的字节码注入 Linux 内核的任意执行路径(网络收发、系统调用、函数追踪等),让它像内核原生代码一样运行。

对网络而言,这意味着:与其让内核去「匹配几万条规则」,不如直接在内核里跑一段「O(1) 查表 + 一致性哈希 + 策略判定」的小程序。Cilium 正是这件事最彻底的践行者——它用 eBPF 完全取代了 kube-proxy,并且在 1.16 时代已经把 CNI、网络策略、服务网格、可观测性全部统一到同一套 eBPF 数据面上。

1.4 三种数据面对比:iptables、OVS、eBPF

为了更直观地理解 eBPF 的差异化价值,把三种主流数据面放在一张表里对照:

维度iptables / NetfilterOVS(Open vSwitch)eBPF(Cilium)
编程模型静态规则链,顺序匹配流表(OpenFlow),中心下发内核内程序 + Map,事件驱动
转发复杂度O(n),随规则数线性增长O(1) 查流表,但用户态控制面重O(1) 查 Map,全程内核态
更新方式全量刷表,存在抖动增量流表,但 ovs-vswitchd 占用 CPU增量更新 Map,无规则重建
可观测性几乎为零sFlow / 流量镜像,开销大内核顺手采样,零额外成本
内核耦合度低(通用)中(需内核模块 / DPDK)高(强依赖内核版本)

一句话总结:iptables 胜在「到处都能跑」,OVS 胜在「功能全」,而 eBPF 胜在「又快又能编程,还能顺手看穿流量」。Cilium 恰好把 eBPF 的这三点优势,在 Kubernetes 网络这个最苛刻的场景里落了地。


二、核心概念

2.1 eBPF 运行时模型

理解 Cilium,先理解 eBPF 的几个基石:

  • Verifier(验证器):eBPF 程序加载进内核前,验证器会做一次「停机式」静态分析——确保没有越界内存访问、没有不可达的死循环、寄存器类型正确。这是 eBPF 安全性的根本:内核不会因为一个 eBPF 程序而崩。代价是编程受诸多限制(无原生循环以外的复杂控制流、栈大小受限、不能调用任意内核函数)。
  • Hook 点:eBPF 不是一个「东西」,而是一堆挂载点。网络场景里最关键的有三个:
    • XDP(eXpress Data Path):网卡驱动层、分配 sk_buff 之前,最早、最快。能做 DROP/PASS/REDIRECT/TX,适合抗 DDoS、早期负载均衡。
    • TC(Traffic Control):协议栈的 ingress/egress 收发处,能拿到完整的 sk_buff,做策略、NAT、转发的主力战场。
    • Socket 层(sockops / sk_msg):在应用 socket 上做拦截,实现「socket 级负载均衡」(绕过整个内核协议栈,性能最猛)。
  • BPF Map:用户态与内核态之间、以及 eBPF 程序之间的共享数据结构(hash、array、lru_hash、ringbuf、sockhash 等)。Cilium 的所有「状态」——Service 表、后端列表、策略、连接跟踪——都存在各类 Map 里,查找是 O(1)。
  • Helper 函数:内核提供给 eBPF 程序的安全 API(如 bpf_map_lookup_elembpf_redirectbpf_csum_diff),是 eBPF 程序与内核交互的唯一受控通道。

工具链上,现代 eBPF 开发主流是 clang/LLVM 把受限 C 编译成 BPF 字节码,再用 cilium/ebpf(Go)/ libbpf(C) 在用户态加载、挂载、读写 Map,用 bpftool 做运行时观测。

2.2 Cilium 的身份模型(Identity,不是 IP)

传统网络策略基于 IP/网段。但 Kubernetes 里 Pod IP 是 ephemeral 的——Pod 一重建,IP 就变,基于 IP 的策略瞬间失效,只能靠控制器不停重写。

Cilium 引入了一个更优雅的抽象:安全身份(Security Identity)

  • 每个 Pod 根据它的 label 组合,被赋予(或从 kvstore 中分配)一个 numeric identity(如 id=24867)。
  • 网络策略、连接跟踪、流日志都用这个 identity 来表达,而不是 IP。
  • 当 Pod 重建、IP 变了,只要 label 没变,identity 就不变,策略自然延续,无需任何重写。

这套「基于身份而非地址」的模型,是 Cilium 在大规模集群里依然能保持策略可维护性的关键。它把「谁可以访问谁」从不断变化的坐标,变成了稳定的语义标签。

2.3 组件地图

  • cilium-agent:以 DaemonSet 跑在每个节点上,是真正干活的。负责编译/加载 eBPF 程序、维护本地 Map、响应策略变更、与 API server 同步。
  • cilium-operator:集群级单副本,负责全局性工作——身份分配协调、IPAM、kvstore 同步、节点初始化。
  • Hubble:Cilium 的可观测层。eBPF 在内核里顺手把每条流的元数据写进 ringbuf,Hubble Relay 把各节点的流日志聚合并暴露成 Prometheus 指标与 service map。
  • kvstore(etcd/Consul):默认存储 identity 到 label 的全局映射。新版本也支持纯 CRD 模式,不依赖外部 kvstore。

三、架构分析

3.1 一条数据包的完整旅程(Pod A → Service → Pod B)

开启 kube-proxy replacement 后,一次跨节点访问是这样流动的:

  1. Pod A 发出包。如果开启了 socket-LB,Cilium 在 Pod A 的 socket 层就已经把 ClusterIP 解析成了某个后端 Pod B 的真实 IP(甚至直接重定向,连 netstack 路由都省了)。这一步叫 socket 级负载均衡
  2. 包离开 Pod A 的 netns,进入宿主机 TC egress。eBPF 程序在这里做源地址处理(需要时做 SNAT)、打上 identity 标记。
  3. 包经由底层网络(vxlan/genev 隧道或原生路由)到达节点 B 的网卡。
  4. 节点 B 网卡触发 XDP(若用 native 模式)。这里可以做最早的过滤与 DSR 甩包:如果是到 Service 的包且走 DSR 模式,XDP 直接改写目标 MAC 并 XDP_TX 送回,根本不进协议栈。
  5. 若没在 XDP 层处理完,包进入 TC ingress。eBPF 程序做连接跟踪(CT)查找、身份解析、网络策略判定,然后决定放行/丢弃/转发。
  6. 包送达 Pod B。回程包在 Pod B 的 egress 处,依据 CT 表被「原路认领」,把源地址还原成 Service 的 ClusterIP,对调用方完全透明。

整个过程里,没有任何一条 iptables 规则参与。所有逻辑都是内核里跑的 eBPF 程序 + 几张 O(1) 查找的 Map。

3.2 kube-proxy replacement:Service 如何「烧」进内核

当 Kubernetes 创建一个 Service,Cilium 不再写 iptables,而是把信息写进 eBPF Map:

  • services Map:记录 ClusterIP、端口、后端集合的引用。
  • endpoints / backends Map:记录每个后端 Pod 的真实 IP、健康状态、权重。
  • maglev / backend-slots Map:实现 Maglev 一致性哈希——把「(源IP, 目的IP, 目的端口)」五元组哈希到一个稳定的后端槽位。

收包时,eBPF 程序用 O(1) 查表 + 一次 Maglev 哈希,直接得到后端,完全跳过规则链。这带来两个关键收益:

  • 转发延迟与 Service 数量脱钩:无论有 100 个还是 10000 个 Service,单包处理成本恒定。
  • 扩缩容连接稳定:Maglev 哈希的特性是「新增/减少一个后端,只有约 1/N 的连接需要重映射」,远优于普通哈希的全局抖动。

两种转发模式值得关注:

  • SNAT 模式:Cilium 做源地址转换,回包需要经过 Cilium 还原。兼容性好。
  • DSR(Direct Server Return,直接服务器返回):后端 Pod 回包时,源地址直接是 Service 的 ClusterIP,回程不经过 Cilium 的二次 NAT,client 真实 IP 被保留,且省了一次 NAT 开销。代价是需要在后端节点也加载对应的 XDP 程序来修正回包,部署稍复杂。高并发入口(Ingress/网关)场景几乎必开。

在 DSR 模式里还有一个工程细节值得单独点出:为了回程包能被正确「认领」,Cilium 会在后端节点也加载 XDP 程序,负责把回包的外层封装剥掉、把源地址还原成 Service 的 ClusterIP。换句话说,DSR 不是「免费午餐」,它的收益建立在收发两端都参与的前提上——这也是为什么 DSR 通常只在性能敏感的入口链路(Ingress、API 网关、四层负载均衡器)上开启,而普通的东西向流量用 SNAT 模式更省心、兼容性也更好。

连接状态靠 CT(conntrack)Map 维护:首包建连写入,后续包直接命中,策略和 NAT 决策在连接维度只算一次。这比 iptables 的 conntrack 更可控——Cilium 能精细限制 CT 表大小、按命名空间配额,避免「表满丢包」。

3.3 网络策略:从 IP 到身份,再到 L7

Kubernetes 原生 NetworkPolicy 基于 IP/selector,能力有限、实现依赖 iptables,且无法感知应用层。Cilium 的 CiliumNetworkPolicy(CNP) 是它的超集:

  • L3/L4:用 label selector 表达「只允许带 app=frontend 标签的端点访问 app=backend 的 8080 端口」。底层翻译成 eBPF 的 identity 匹配,零 iptables。
  • L7:通过集成的 Envoy,能做 HTTP 方法/路径级、Kafka topic 级、DNS 级的细粒度策略。注意 Cilium 的 L7 是每节点一个 Envoy(而非每个 Pod 一个 sidecar),资源开销天差地别。
  • DNS 感知:可以写「只允许访问 api.internal 这个域名解析出的 IP」,把 L3 策略与 DNS 解析结果联动,避免硬编码 IP。

3.4 Hubble:内核顺手「记一笔」的流日志

Hubble 的巧妙之处在于零额外采集成本:Cilium 的 eBPF 程序在处理每个包时,本来就要解析五元组、identity、 verdict(放行/丢弃),顺手把这些元数据写进一个 ringbuf。Hubble Relay 把各节点的流日志聚合,提供:

  • hubble observe:实时看集群里每一条流(谁→谁,什么协议,放行还是丢弃,丢弃原因)。
  • Service Map:可视化服务调用拓扑。
  • Prometheus 指标:吞吐、丢包率、策略命中、延迟分位。

对排障来说,这是降维打击——「为什么这个调用不通」从「翻遍 iptables + tcpdump 猜」变成「hubble observe --verdict DROPPED 一行看原因」。


四、代码实战

光讲架构不过瘾,下面这套代码能让你「亲手」把一段 eBPF 程序跑在网卡上。

4.1 最小可运行 XDP 程序(C + Go)

我们用 cilium/ebpf 写一个小程序:统计网卡收到的包数,并直接在内核里丢弃 ICMP。这正是 Cilium 数据面做「早期过滤」的缩影。

eBPF 程序(xdp_count.c)

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

// 一个最简单的数组 Map,用来从内核往用户态汇报计数
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, __u64);
} pkt_count SEC(".maps");

SEC("xdp")
int xdp_count(struct xdp_md *ctx) {
    // 每收一个包,计数 +1(原子自增,多 CPU 安全)
    __u32 key = 0;
    __u64 *count = bpf_map_lookup_elem(&pkt_count, &key);
    if (count) __sync_fetch_and_add(count, 1);

    void *data_end = (void *)(long)ctx->data_end;
    void *data     = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    // 只处理 IPv4
    if (eth->h_proto == __constant_htons(ETH_P_IP)) {
        struct iphdr *ip = (void *)(eth + 1);
        if ((void *)(ip + 1) > data_end)
            return XDP_PASS;
        // 内核态直接丢弃 ICMP,比在用户态 iptables 早得多
        if (ip->protocol == IPPROTO_ICMP) {
            return XDP_DROP;
        }
    }
    return XDP_PASS;
}

char __license[] SEC("license") = "GPL";

Go 加载器(main.go)

package main

import (
    "log"
    "net"
    "time"

    "github.com/cilium/ebpf"
    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
)

func main() {
    // eBPF Map 默认会占用 memlock,先放开限制
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // 读取 clang 编译好的 BPF 对象文件
    spec, err := ebpf.LoadCollectionSpec("xdp_count.o")
    if err != nil {
        log.Fatal(err)
    }

    coll, err := ebpf.NewCollection(spec)
    if err != nil {
        log.Fatal(err)
    }
    defer coll.Close()

    iface, err := net.InterfaceByName("eth0")
    if err != nil {
        log.Fatal(err)
    }

    // 把 XDP 程序挂到网卡上(native 模式由内核自动选择)
    l, err := link.AttachXDP(link.XDPOptions{
        Interface: iface.Index,
        Program:   coll.Programs["xdp_count"],
    })
    if err != nil {
        log.Fatal(err)
    }
    defer l.Close()

    // 每秒从 Map 里读一次计数并打印
    var count uint64
    key := uint32(0)
    for {
        if err := coll.Maps["pkt_count"].Lookup(&key, &count); err != nil {
            log.Fatal(err)
        }
        log.Printf("已处理数据包: %d", count)
        time.Sleep(time.Second)
    }
}

编译与运行:

# 1. 用 clang 把 C 编成 BPF 字节码(需要内核头文件)
clang -O2 -target bpf -c xdp_count.c -o xdp_count.o

# 2. 编译并运行 Go 加载器(需要 root / CAP_BPF)
go build -o xdp-loader main.go
sudo ./xdp-loader

跑起来后,ping 这块网卡会被静默丢弃,而 hubble observe/ cilium status 这类 TCP 流量照常通过——这就是「内核里按程序决策」的最小样板。

4.2 部署 Cilium 并验证 kube-proxy replacement

用 Helm 安装,核心就是打开 kubeProxyReplacement

# values.yaml
kubeProxyReplacement: true          # 用 eBPF 取代 kube-proxy
k8sServiceHost: 10.96.0.1
k8sServicePort: 443
tunnelProtocol: vxlan               # 也可用 native-routing / geneve
socketLB:
  enabled: true                     # socket 级负载均衡(最猛优化)
hubble:
  enabled: true
  relay:
    enabled: true
  ui:
    enabled: true
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --version 1.16.x -n kube-system -f values.yaml

# 安装时记得:kubeadm 要加 --skip-phases=addon/kube-proxy,
# 或装完直接删掉 kube-proxy DaemonSet
kubectl delete ds kube-proxy -n kube-system

验证「iptables 真的空了」

# 在任意节点上,kube-proxy 时代的几万行规则,现在应该接近 0
sudo iptables-save | wc -l

# Cilium 状态里应显示 KubeProxyReplacement: True
cilium status --wait

# 看 Service 是如何被翻译成 eBPF 后端的
cilium service list
# ID   Frontend             Service Type   Backend
# 1    10.96.0.10:9153     ClusterIP      1 => 10.244.1.7:9153
#                                      ...

# 看策略是否生效
kubectl exec -n kube-system ds/cilium -- cilium-dbg policy get

4.3 L3/L4 + L7 网络策略实战

先来一个经典 L3/L4 规则:只允许 app=frontend 访问 app=backend 的 8080:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: frontend-to-backend
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: frontend
    toPorts:
    - ports:
      - port: "8080"
        protocol: TCP

再叠一层 L7 HTTP 策略,只允许 GET /api/public/*,其他路径一律拒绝:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: l7-http-public
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: frontend
    toPorts:
    - ports:
      - port: "8080"
        protocol: TCP
      rules:
        http:
        - method: "GET"
          path: "/api/public/.*"

注意:L7 规则会自动触发 Cilium 把对应流量引流到本节点的 Envoy 做应用层解析。因为是「每节点一个 Envoy」而非「每 Pod 一个 sidecar」,一个 1000 Pod 的集群,Istio 式 sidecar 要跑 1000 个 Envoy 进程,Cilium 可能只跑几十个——内存与启动开销差出一两个数量级。

4.4 用 Hubble 排障「为什么不通」

# 实时看集群所有流
hubble observe --follow

# 只看被丢弃的流,以及丢弃原因
hubble observe --verdict DROPPED
# Aug 19 04:12:33.221  default/frontend-7d9:54231 -> default/backend-5c2:8080  policy-verdict:DROPPED  Traffic'backend' is not allowed

# 看 HTTP 层可见性(需开启 L7 解析)
hubble observe --protocol http
# GET /api/public/health 200
# POST /api/admin/delete 403   <- L7 策略拦掉的

这一行 policy-verdict:DROPPED 加原因,直接告诉你「是策略拦的、拦的是哪个 endpoint」,比 tcpdump 抓半天再反推高效太多。


五、性能优化

Cilium 性能强,但「开箱即用」和「压榨到极致」之间有大量旋钮。按收益从大到小排序:

5.1 开启 socket-LB(收益最大)

socketLB.enabled=true 时,Cilium 在应用的 socket 层就完成 Service→后端的解析与重定向,包根本不进内核协议栈的路由/Netfilter 路径。这意味着:省掉 conntrack 查表、省掉一次完整的协议栈收发。对东西向(Pod 互访)流量密集的服务网格类负载,这是单项最猛的优化,延迟可下降两位数百分比。

5.2 XDP native 模式 + DSR

  • XDP native(驱动层) 优于 generic(协议栈层):native 在网卡驱动里就处理,连 sk_buff 都不分配,抗 DDoS、做 LB 时吞吐翻几倍。前提是网卡驱动支持(主流的 ixgbe/i40e/mlx 都支持)。
  • DSR(Direct Server Return):入口流量在 XDP 层就改 MAC 甩到后端,回包从后端直出,保留 client 真实 IP 且省一次 NAT。Ingress / API 网关类入口强烈建议开。

5.3 Maglev 一致性哈希

默认开启。它保证「后端扩缩容时,只有约 1/N 的已有连接需要重新映射」,避免普通哈希导致的全局重映射抖动。在大频度 HPA 场景里,这直接关系到长连接的稳定性。

5.4 BIG TCP + Bandwidth Manager(EDT)

  • BIG TCP:把 GSO 段大小从 64KB 提到 256KB+,减少每段协议栈开销,单连接大吞吐场景(备份、大数据传输)收益明显。需内核 5.19+。
  • Bandwidth Manager(EDT,Earliest Departure Time):用 eBPF 做基于时间的最早 departure 限速,比 tc-HTB 更公平、更省 CPU,能精确限制某 Pod/某方向的带宽而不拖垮整节点。

5.5 CT 表与 Hubble 采样调优

  • 生产里给 bpf.ctMapEntries 留足空间(默认偏保守),并按 namespace 配 CT 配额,避免 conntrack 表满丢包——这是迁移到 Cilium 后最容易被忽略的坑。
  • Hubble 的 L7 解析、流日志有开销。生产可在高频路径上关掉 L7 解析、对 flow 日志做采样hubble.metrics.sample-rate),只保留 Prometheus 指标做长期监控。需要排障时再临时打开。

5.6 内核与可观测基线

  • 内核版本:Cilium 1.16 推荐 5.10+,想用 BIG TCP / 最新 socket-LB 特性建议 5.15/6.x。老内核(3.x/4.14)跑不了完整数据面,会回退到较弱模式。
  • 关键监控指标:cilium_forward_count_total(转发量)、cilium_drop_count_total(丢包)、cilium_policy_l7_*(L7 决策)、Hubble 的 drop 原因分布。把这些接进 Grafana,迁移前后做对照,才能证明「真的变快了」。

社区基准里,开启 kube-proxy replacement + socket-LB 的 Cilium,相比 iptables 模式的 kube-proxy,在大规模 Service 下通常能看到 p99 延迟下降、节点网络 CPU 占用下降,且延迟与 Service 数量彻底解耦——这正是「从规则链到程序」的红利。

5.7 迁移期的真实收益基线(社区基准参考)

在约 4000 个 Service、单节点 1000+ Pod 的压力模型下,社区与多家厂商的基准大致呈现这样的趋势:kube-proxy(iptables)模式在 Service 列表批量变更时,会出现数百毫秒级的转发抖动;而 Cilium kube-proxy replacement 模式下,这类抖动趋近于零。东西向小包(如 gRPC、HTTP 短连接)的 p99 延迟,在开启 socket-LB 后通常下降 20%~40%;节点网络相关 CPU 占用,在规模化场景下可下降 30% 以上。必须强调的是,这些数字强烈依赖内核版本、网卡驱动与业务流量形态,落地时务必用自己集群的 Hubble 指标做前后对照,而不是盲信任何公开基准

5.8 五个最常踩的坑

  1. CT 表满丢包:迁移后第一周最容易被忽略。bpf.ctMapEntries 默认偏保守,规模上来后建议按 namespace 配额调大。
  2. 内核太老被迫回退:4.14 以下内核跑不了完整数据面,会静默回退到弱模式,性能收益全无。
  3. L7 解析意外吃 CPU:高频路径没关 L7、没做采样,Hubble 反而成了瓶颈。
  4. XDP generic 模式:网卡驱动不支持 native 时,XDP 会退回 generic,性能差一个量级却不报错,需要用 cilium status 主动确认。
  5. 删了 kube-proxy 却没开 replacement:结果 Service 完全不通,这是迁移操作顺序最容易犯的低级错误。

六、总结展望

Cilium 的意义,远不止「一个更快的 CNI」。它代表了一种范式转变:内核数据面从「静态配置」走向「可编程」。过去十年我们习惯用 iptables/ovs 这类「配置型」引擎拼网络,未来十年,eBPF 程序会成为数据面的「一等公民」——你写的不再是规则,而是一段在内核里运行的逻辑。

从产品演进看,Cilium 的边界正在持续外扩:

  • CNI → 服务网格:用 eBPF 做 L3/L4/L7,零 sidecar 地取代 Istio 类方案的大部分能力,资源开销断崖式下降。
  • Gateway API / Ingress:把南北向流量也纳入同一张 eBPF 数据面,统一东西向与南北向。
  • 运行时安全:Cilium 姊妹项目 Tetragon 用 eBPF 做内核级运行时安全检测(异常系统调用、提权行为),把「网络」与「安全」在数据面层合流。

但也要清醒看待风险:

  • 内核强耦合:eBPF 能力高度依赖内核版本,老内核要么功能残缺要么直接跑不了。升级内核往往比升级 Cilium 本身更费劲。
  • 调试门槛高:eBPF 程序出问题不像用户态那样有完整堆栈,bpftool、perf、cilium-dbg 是必备技能,团队需要相应的内核功底。
  • Verifier 限制:编程受约束,复杂逻辑得拆到用户态或用 CO-RE 技巧,学习曲线陡。

落地建议(给想上车的你):不要一上来就全量替换,按下面的路线图分阶段推进,每一步都用 Hubble 与 Prometheus 指标做前后对照,让「更快、更稳」可被证明,而不是凭感觉:

  1. 阶段一·单机试点:在测试或边缘集群安装 Cilium,但先保留 kube-proxy,只验证 Pod 网络、NetworkPolicy 基本可用。这一步的目标是熟悉 cilium statuscilium-dbg 等排障工具。
  2. 阶段二·替换 kube-proxy:确认阶段一稳定后,执行 kubeadm--skip-phases=addon/kube-proxy 或直接删除 kube-proxy DaemonSet,打开 kubeProxyReplacement。用 cilium service list 验证 Service 转发无误,iptables-save | wc -l 应接近 0。
  3. 阶段三·打开可观测:启用 Hubble(relay + UI),用 hubble observe 跑通一次完整的「为什么不通」排障,把 Hubble 指标接入 Grafana,建立迁移前基线。
  4. 阶段四·谨慎上 L7 与 socket-LB:在受控的小范围命名空间里引入 L7 HTTP 策略与 socket-LB,观察 Envoy 与节点 CPU 变化;确认收益后再逐步放大。
  5. 阶段五·常态化:把 Cilium 版本升级、内核版本基线、Hubble 采样率纳入 SOP,避免「跑着跑着 CT 表满了」这类隐性问题复发。

这套节奏的核心思想是:先求稳,再求快,最后求细。绝大多数 Cilium 翻车事故,都不是因为 eBPF 不可靠,而是因为一上来就全量替换、又跳过了可观测基线。

eBPF 不会让 iptables 一夜消失,但它确实把云原生网络的「表达力天花板」抬高了一个量级。当你下次再被 kube-proxy 的规则风暴折磨时,记住:那几万行规则,本可以被烧进内核里一段几十行的程序里。


延伸阅读

  • Cilium 官方文档与 Interactive Labs:cilium.io
  • eBPF 官方入门与 bpf2go 工作流:cilium/ebpf(GitHub)
  • Hubble 流日志与 Service Map:Cilium Hubble 文档
  • 运行时安全延伸:Cilium Tetragon
  • 内核侧观测工具:bpftool / perf / cilium-dbg

推荐文章

程序员茄子在线接单