土法炼钢兴趣小组的算法知识备份

eBPF + 容器:Cilium 的数据面为什么不再需要 iptables

目录

你管理的 Kubernetes 集群有 5000 个 Service,每个 Service 后面平均挂 3 个 Endpoint。kube-proxy 忠实地把它们翻译成 iptables 规则——大约十万条。某天一个 Deployment 滚动更新,kube-proxy 调用 iptables-restore 全量刷新规则表,锁住 netfilter 整整 5 秒。这 5 秒里,所有新建连接全部卡住。

这不是假设场景,这是很多大规模集群的日常。

问题不在 kube-proxy 写得烂,而在 iptables 这个抽象本身就不是为这种规模设计的。Cilium 的回答很直接:把数据面从 iptables 规则链搬到 eBPF Map,用 O(1) 哈希查找替代 O(n) 逐条匹配,用原子 Map 更新替代全量 iptables-restore

如果你还没接触过 eBPF 的基础概念,建议先读 eBPF:Linux 内核的隐藏武器


一、kube-proxy 的 iptables 困境

它是怎么工作的

kube-proxy watch Kubernetes API,把每个 Service 翻译成 iptables 规则。一个 ClusterIP Service 的规则链大致长这样:

# 一个 Service(my-svc, 10.96.0.100:80)有 3 个 Endpoint
# kube-proxy 生成的 iptables 规则(简化)

# 入口:匹配目标为 Service VIP 的流量
-A KUBE-SERVICES -d 10.96.0.100/32 -p tcp --dport 80 \
    -j KUBE-SVC-XXXXXXXXXXXXXXXX

# Service 链:用 statistic 模块做概率负载均衡
-A KUBE-SVC-XXXXXXXXXXXXXXXX -m statistic --mode random \
    --probability 0.33333 -j KUBE-SEP-AAAA
-A KUBE-SVC-XXXXXXXXXXXXXXXX -m statistic --mode random \
    --probability 0.50000 -j KUBE-SEP-BBBB
-A KUBE-SVC-XXXXXXXXXXXXXXXX -j KUBE-SEP-CCCC

# Endpoint 链:做 DNAT,把 VIP 替换成真实 Pod IP
-A KUBE-SEP-AAAA -p tcp -j DNAT --to-destination 10.244.1.5:8080
-A KUBE-SEP-BBBB -p tcp -j DNAT --to-destination 10.244.2.12:8080
-A KUBE-SEP-CCCC -p tcp -j DNAT --to-destination 10.244.3.8:8080

看起来很直白。问题在于规模化之后发生的事情。

O(n) 匹配的性能灾难

iptables 的规则匹配是线性的。每个新连接的第一个包要从 KUBE-SERVICES 链头开始,逐条匹配,直到命中。5000 个 Service 意味着 5000 次比较——这还只是第一跳。加上 DNAT、SNAT、MASQUERADE 等规则,一个包可能要穿过上万条规则。

                       第一个包的路径
                       ┌──────────────┐
                       │ PREROUTING   │
                       │   ↓          │
                       │ KUBE-SERVICES│ ← 5000+ 条规则逐条匹配
                       │   ↓          │
                       │ KUBE-SVC-xxx │ ← 概率匹配(3 条)
                       │   ↓          │
                       │ KUBE-SEP-xxx │ ← DNAT
                       │   ↓          │
                       │ POSTROUTING  │ ← SNAT/MASQUERADE
                       └──────────────┘

后续包虽然走 conntrack 快速路径,但 nf_conntrack 本身也不便宜——它用的是全局哈希表加自旋锁,高并发下锁竞争严重。

iptables-restore 全量刷新

这才是真正的杀手。kube-proxy 不能单独插入或删除一条规则(iptables 命令虽然支持,但在并发场景下有 race condition)。它的策略是:

  1. 导出全部规则
  2. 在内存里修改
  3. iptables-restore 原子写回

写回期间,整个 netfilter 子系统加写锁。十万条规则的写回在普通节点上要 2-5 秒,这期间所有新连接阻塞。

# 测量 iptables-restore 的耗时
time iptables-save | wc -l
# 输出: 102,347 条规则

time iptables-restore < /path/to/rules.txt
# 真实耗时: 3.7s  ← 这 3.7 秒所有新连接卡住

IPVS 模式:好一点但不够好

kube-proxy 也支持 IPVS 模式(--proxy-mode=ipvs),用 ipvs 的哈希表做负载均衡,匹配复杂度降到 O(1)。但 IPVS 有自己的问题:

维度 iptables 模式 IPVS 模式
规则匹配 O(n) 线性 O(1) 哈希
规则更新 全量刷新 + 全局锁 增量更新
SNAT/MASQUERADE iptables 处理 仍然依赖 iptables
NodePort 处理 iptables 仍然依赖 iptables
conntrack nf_conntrack nf_conntrack
NetworkPolicy 需要额外 iptables 规则 需要额外 iptables 规则

关键问题:IPVS 只解决了负载均衡这一环,SNAT、NodePort、NetworkPolicy 仍然要回到 iptables。你没有消灭复杂度,只是把它推到了另一个地方。


二、Cilium 架构概览

Cilium 的目标很明确:用 eBPF 程序完全替代 kube-proxy 和 iptables 在数据面的角色

三个核心组件

┌─────────────────────────────────────────────────────┐
│                  Kubernetes API Server               │
└────────────┬───────────────────────┬────────────────┘
             │                       │
     ┌───────▼───────┐     ┌────────▼────────┐
     │ Cilium Operator│     │  Cilium Agent   │ ← 每个节点一个
     │ (全局资源管理)│     │ (本地数据面)    │
     └───────────────┘     └────────┬────────┘
                                    │
                           ┌────────▼────────┐
                           │   eBPF 程序      │
                           │ tc / XDP / sock  │
                           │  挂载到网络设备    │
                           └─────────────────┘

eBPF 程序在数据面的位置

这是理解 Cilium 的关键。eBPF 程序挂载在三个位置:

  1. tc ingress / egress:挂在每个 Pod 的 veth pair 和 host 网络设备上,处理所有 L3/L4 流量
  2. XDP:挂在物理网卡上,处理 NodePort 和 LoadBalancer 的入站流量(最快路径)
  3. socket-level(cgroup/connect、cgroup/sendmsg 等):拦截 socket 系统调用,实现透明的 Service 地址转换
Cilium eBPF 数据路径 vs 传统 iptables

与 kube-proxy 的替代关系很清晰:启动 Cilium 时加上 --set kubeProxyReplacement=true,就可以完全移除 kube-proxy。

# 安装 Cilium 并替代 kube-proxy
helm install cilium cilium/cilium --version 1.16.0 \
    --namespace kube-system \
    --set kubeProxyReplacement=true \
    --set k8sServiceHost=${API_SERVER_IP} \
    --set k8sServicePort=${API_SERVER_PORT}

# 验证 kube-proxy 替代状态
cilium status | grep KubeProxyReplacement
# 输出: KubeProxyReplacement: True [eth0 (DR)]

# 确认没有 iptables 规则残留
iptables-save | grep KUBE | wc -l
# 输出: 0  ← 干净了

三、用 eBPF Map 替代 iptables 规则

这是 Cilium 性能优势的核心:把 iptables 的规则链替换成 eBPF Map 的哈希查找

Service → Endpoint 映射

Cilium 用 BPF_MAP_TYPE_HASH 存储 Service 到 Endpoint 的映射。数据结构大致如下:

// Service 的 Key:VIP + Port + Protocol
struct lb4_key {
    __be32 address;     // Service ClusterIP,如 10.96.0.100
    __be16 dport;       // Service Port,如 80
    __u8   proto;       // TCP = 6, UDP = 17
    __u8   scope;       // 内部/外部
    __u32  backend_slot; // 0 = Service 元信息,>0 = 具体后端
};

// Service 的 Value:后端数量 + 标志
struct lb4_service {
    union {
        __u32 backend_id;  // 当 backend_slot > 0 时,指向具体后端
        __u32 count;       // 当 backend_slot = 0 时,后端总数
    };
    __u16 rev_nat_index;   // 反向 NAT 索引
    __u16 flags;           // DSR、SessionAffinity 等标志
};

// 后端信息
struct lb4_backend {
    __be32 address;     // Pod 真实 IP,如 10.244.1.5
    __be16 port;        // Pod 真实端口,如 8080
    __u8   proto;
    __u8   flags;
};

查找过程需要三次哈希查找:(1) Service Map 以 slot=0 查询,获取 backend 数量;(2) Service Map 以 slot=N 查询,获取 backend_id;(3) Backend Map 以 backend_id 查询,获取实际 endpoint 地址。

// 伪代码:Cilium 的 Service 查找逻辑
static __always_inline int lb4_lookup_service(struct __sk_buff *skb) {
    struct lb4_key key = {
        .address = ip4->daddr,    // 目标 IP(Service VIP)
        .dport   = tcp->dest,     // 目标端口
        .proto   = ip4->protocol,
    };

    // 第一次查找:获取 Service 元信息(backend_slot = 0)
    key.backend_slot = 0;
    struct lb4_service *svc = map_lookup_elem(&cilium_lb4_services, &key);
    if (!svc)
        return TC_ACT_OK;  // 不是 Service 流量,放行

    // 选择后端(简单取模,实际用 Maglev 一致性哈希)
    __u32 slot = bpf_get_prandom_u32() % svc->count + 1;

    // 第二次查找:获取具体后端
    key.backend_slot = slot;
    struct lb4_service *backend_slot = map_lookup_elem(&cilium_lb4_services, &key);

    // 第三次查找:获取后端 IP 和端口
    struct lb4_backend *backend = map_lookup_elem(&cilium_lb4_backends,
                                                   &backend_slot->backend_id);

    // 执行 DNAT:修改目标 IP 和端口
    // 从 10.96.0.100:80 → 10.244.1.5:8080
    skb_store_bytes(skb, /* ip dst offset */, &backend->address, 4, 0);
    skb_store_bytes(skb, /* tcp dst offset */, &backend->port, 2, 0);

    return TC_ACT_OK;
}

三次哈希查找,O(1) 完成。不管你有 5 个 Service 还是 50000 个,查找时间不变。

Conntrack:用 eBPF Map 替代 nf_conntrack

传统 Linux 用 nf_conntrack 做连接跟踪,它有几个问题:

Cilium 用自己的 eBPF conntrack Map:

// Cilium 的连接跟踪条目
struct ct_entry {
    __u64 rx_packets;        // 接收包数
    __u64 rx_bytes;          // 接收字节
    __u64 tx_packets;        // 发送包数
    __u64 tx_bytes;          // 发送字节
    __u32 lifetime;          // 过期时间
    __u16 rx_closing  : 1;   // 收到 FIN
    __u16 tx_closing  : 1;   // 发送了 FIN
    __u16 nat46       : 1;   // NAT46 转换
    __u16 lb_loopback : 1;   // loopback hairpin
    __u16 seen_non_syn: 1;   // 看到非 SYN 包
    __u16 node_port   : 1;   // NodePort 流量
    __u32 rev_nat_index;     // 反向 NAT 索引
    __be32 backend_id;       // 后端 ID(用于 session affinity)
};

优势很明显:

维度 nf_conntrack Cilium eBPF CT
数据结构 全局哈希表 Per-CPU 哈希 Map
锁粒度 桶级自旋锁 Per-CPU,无锁
表满行为 丢包 可配置 LRU 淘汰
与 NAT 耦合 深度耦合 独立 Map
可观测性 conntrack -L bpftool map dump

Map 热更新:零中断

当一个新 Endpoint 上线时:

# 传统 kube-proxy:
# 1. 收到 Endpoint 变更事件
# 2. 重新生成全部 iptables 规则
# 3. iptables-restore 全量写入 ← 锁住 netfilter

# Cilium:
# 1. 收到 Endpoint 变更事件
# 2. 计算新的 Map 条目
# 3. bpf_map_update_elem() 原子更新单条 ← 零锁、零中断

一个 bpf_map_update_elem 调用耗时在微秒级,不影响任何正在处理的连接。


四、Service Mesh without Sidecar

Sidecar 模型的代价

传统 Service Mesh(如 Istio)给每个 Pod 注入一个 Envoy sidecar。架构很优雅,代价很实在:

                Pod
┌──────────────────────────────────┐
│  ┌──────────┐    ┌─────────────┐ │
│  │  App 容器 │───→│ Envoy Proxy │ │  ← 每个 Pod 一个
│  │          │←───│ (Sidecar)   │ │     ~50MB 内存
│  └──────────┘    └─────────────┘ │     ~2ms 额外延迟
└──────────────────────────────────┘

具体开销:

Cilium 的做法:内核层 L7 代理

Cilium Service Mesh 的核心思路是:能在内核做的事不要到用户态做

        Cilium Service Mesh (无 Sidecar)
┌──────────────┐                    ┌──────────────┐
│    Pod A     │                    │    Pod B     │
│  ┌────────┐  │                    │  ┌────────┐  │
│  │  App   │  │                    │  │  App   │  │
│  └───┬────┘  │                    │  └───▲────┘  │
└──────┼───────┘                    └──────┼───────┘
       │ socket                            │ socket
  ═════╪════════════════════════════════════╪═══════
       │        Linux Kernel               │
       │                                   │
       ▼                                   │
   eBPF: sockops/sk_msg                    │
   socket-level redirect ──────────────────┘
   (跳过整个 TCP/IP 栈!)

关键技术点:

1. Socket-level redirect

当 Pod A 连接 Pod B 的 Service VIP 时,Cilium 的 cgroup/connect4 eBPF 程序拦截 connect() 系统调用:

// cgroup/connect4 程序:拦截 connect() 调用
SEC("cgroup/connect4")
int sock4_connect(struct bpf_sock_addr *ctx) {
    // ctx->user_ip4 = Service VIP (10.96.0.100)
    // ctx->user_port = Service Port (80)

    // 查找 Service Map,选择后端
    struct lb4_backend *backend = lb4_lookup_and_select(ctx);
    if (!backend)
        return 1;  // 不是 Service 流量,放行

    // 直接修改 connect() 的目标地址
    // 应用进程感知到的是 Service VIP
    // 实际建连的是 Pod IP
    ctx->user_ip4  = backend->address;  // → 10.244.1.5
    ctx->user_port = backend->port;     // → 8080

    return 1;
}

下面用更详细的伪代码展示 cgroup/connect4 内部的完整决策流程——包括地址族检查、协议过滤、Service 查找和 sockaddr 重写:

// 伪代码:Cilium cgroup/connect4 的完整 socket redirect 流程
SEC("cgroup/connect4")
int sock4_connect_full(struct bpf_sock_addr *ctx)
{
    // ── 第一步:地址族检查 ──
    // cgroup/connect4 只处理 IPv4,IPv6 由 cgroup/connect6 处理
    if (ctx->family != AF_INET)
        return 1;  // 放行,不是 IPv4

    // ── 第二步:协议过滤 ──
    // 只处理 TCP 和 UDP(Service 负载均衡的对象)
    // ICMP、RAW socket 等直接放行
    __u8 protocol = ctx->protocol;
    if (protocol != IPPROTO_TCP && protocol != IPPROTO_UDP)
        return 1;

    // ── 第三步:构造 Service lookup key ──
    struct lb4_key svc_key = {};
    svc_key.address = ctx->user_ip4;           // 目标 IP(可能是 ClusterIP)
    svc_key.dport   = ctx->user_port;          // 目标端口
    svc_key.proto   = protocol;
    svc_key.scope   = LB_LOOKUP_SCOPE_INT;     // 集群内部
    svc_key.backend_slot = 0;                  // slot=0 查 Service 元信息

    // ── 第四步:查 Service Map ──
    struct lb4_service *svc = map_lookup_elem(&cilium_lb4_services, &svc_key);
    if (!svc) {
        // 目标地址不是 Service VIP → 直接放行
        // 普通的 Pod-to-Pod 通信走这条路径
        return 1;
    }

    // ── 第五步:选择后端 Pod ──
    // Cilium 支持多种负载均衡算法:
    //   - Random(默认)
    //   - Maglev 一致性哈希(推荐生产使用)
    //   - Session Affinity(基于 client IP 的会话保持)
    __u32 backend_count = svc->count;
    __u32 slot;

    if (svc->flags & SVC_FLAG_AFFINITY) {
        // Session Affinity:用 client IP 哈希选择固定后端
        slot = hash(ctx->user_ip4) % backend_count + 1;
    } else {
        // Random 或 Maglev
        slot = bpf_get_prandom_u32() % backend_count + 1;
    }

    // ── 第六步:查后端信息 ──
    svc_key.backend_slot = slot;
    struct lb4_service *be_slot = map_lookup_elem(&cilium_lb4_services, &svc_key);
    if (!be_slot)
        return 1;  // 防御性检查

    struct lb4_backend *backend = map_lookup_elem(&cilium_lb4_backends,
                                                   &be_slot->backend_id);
    if (!backend)
        return 1;

    // ── 第七步:重写 sockaddr ──
    // 这是核心:修改 connect() 系统调用的目标地址
    // 应用层看到的是 Service VIP (10.96.0.100:80)
    // 实际 TCP 连接的目标是 Pod IP (10.244.1.5:8080)
    ctx->user_ip4  = backend->address;
    ctx->user_port = backend->port;

    // ── 第八步:记录 NAT 信息(用于反向查找) ──
    // 当 Pod B 回复时,需要把源地址从 Pod IP 改回 Service VIP
    // 这样应用层感知不到后端切换
    struct ct_state ct = {
        .rev_nat_index = svc->rev_nat_index,
        .backend_id    = be_slot->backend_id,
    };
    ct_create4(&cilium_ct4_global, &ct, ctx);

    return 1;  // 1 = 允许 connect() 继续(用修改后的地址)
}

为什么这比 iptables DNAT 更快? iptables DNAT 在 PREROUTING 链工作——这时包已经进入了内核协议栈,经过了 sk_buff 分配、IP 层解析等。而 cgroup/connect4socket 系统调用层工作——connect() 还没发出第一个 SYN 包,地址就已经被改好了。没有 sk_buff、没有 netfilter、没有 conntrack lookup,因为”连接”还不存在。

这比 iptables DNAT 更早介入——在 socket 层就完成了地址转换,根本不需要经过 netfilter

2. 同节点 Pod 通信跳过 TCP/IP 栈

如果 Pod A 和 Pod B 在同一节点,Cilium 用 sockops + sk_msg 程序实现 socket 之间的直接转发:

// sockops 程序:在 TCP 事件时更新 socket map
SEC("sockops")
int bpf_sockops(struct bpf_sock_ops *skops) {
    // 连接建立时,把 socket 记录到 Map
    if (skops->op == BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB ||
        skops->op == BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB) {
        // 把 (src_ip, src_port, dst_ip, dst_port) → socket
        // 写入 SOCKMAP
        sock_hash_update(skops, &cilium_sock_map, &key, BPF_NOEXIST);
    }
    return 0;
}

// sk_msg 程序:拦截 sendmsg,直接转发到对端 socket
SEC("sk_msg")
int bpf_sk_msg_verdict(struct sk_msg_md *msg) {
    // 查找对端 socket
    // 如果找到,直接把数据从 A 的发送缓冲区拷贝到 B 的接收缓冲区
    // 完全跳过 TCP/IP 栈、netfilter、tc 等所有中间层
    return msg_redirect_hash(msg, &cilium_sock_map, &peer_key, BPF_F_INGRESS);
}

数据从 Pod A 的 socket 直接到 Pod B 的 socket,跳过了整个 TCP/IP 栈。没有 sk_buff 分配,没有协议栈遍历,没有 netfilter hook。

3. mTLS 在 Node 级别做

传统方案每个 Pod 的 sidecar 独立做 TLS 握手。Cilium 把 mTLS 下沉到节点级别的 Envoy 代理(每节点一个,而非每 Pod 一个),或者用 WireGuard 做节点间加密:

# 使用 WireGuard 做节点间透明加密
helm install cilium cilium/cilium \
    --set encryption.enabled=true \
    --set encryption.type=wireguard

# 验证加密状态
cilium encrypt status
# 输出:
# Encryption: Wireguard
# Keys in use: 1
# Interfaces: cilium_wg0

性能对比

下面是 Cilium(eBPF kube-proxy 替代)、kube-proxy(iptables 模式)、kube-proxy(IPVS 模式)在不同 Service 规模下的性能对比。数据来源于 Cilium 官方 benchmark、Isovalent 的对比测试以及社区复现的公开结果。测试环境:8 核 / 32GB 节点,100 并发客户端。

Service 数量 方案 新建连接延迟 P50 新建连接延迟 P99 CPU 使用率 内存占用
100 Cilium (eBPF) 0.12 ms 0.35 ms 1.2% ~80 MB
kube-proxy (iptables) 0.15 ms 0.52 ms 1.5% ~40 MB
kube-proxy (IPVS) 0.13 ms 0.40 ms 1.3% ~50 MB
1,000 Cilium (eBPF) 0.12 ms 0.38 ms 1.3% ~120 MB
kube-proxy (iptables) 0.35 ms 1.8 ms 3.5% ~60 MB
kube-proxy (IPVS) 0.15 ms 0.50 ms 1.8% ~80 MB
10,000 Cilium (eBPF) 0.13 ms 0.42 ms 1.5% ~250 MB
kube-proxy (iptables) 2.8 ms 15 ms 12% ~180 MB
kube-proxy (IPVS) 0.18 ms 0.65 ms 3.2% ~200 MB
50,000 Cilium (eBPF) 0.14 ms 0.48 ms 1.8% ~600 MB
kube-proxy (iptables) 12 ms 85 ms 35%+ ~500 MB
kube-proxy (IPVS) 0.25 ms 1.2 ms 5.5% ~450 MB

几个关键观察:

  1. Cilium 的延迟几乎不随 Service 数量增长——这就是 O(1) 哈希查找 vs O(n) 线性匹配的本质差异。从 100 到 50,000 个 Service,P99 延迟只从 0.35ms 增长到 0.48ms。
  2. iptables 在 10K+ Service 时崩溃式恶化——P99 延迟从亚毫秒跳到两位数毫秒,CPU 占用飙升。这还没算 iptables-restore 全量刷新时的秒级卡顿。
  3. IPVS 在负载均衡方面接近 Cilium——IPVS 用的也是哈希表,查找本身是 O(1)。但 IPVS 的 SNAT/NodePort 仍然依赖 iptables,所以在完整路径上不如 Cilium 干净。
  4. Cilium 的内存占用更高——eBPF Map 需要预分配空间,加上 Cilium Agent 的内存。但这是用内存换延迟和 CPU 的合理 tradeoff。

注意:以上数字是新建连接(首包)的延迟。已建立连接的后续包都走 conntrack 快速路径,三种方案差距较小。但 conntrack 本身的实现也不同——Cilium 用 per-CPU eBPF Map(无锁),nf_conntrack 用全局哈希表+自旋锁——在高并发场景下这个差距同样显著。

还有一个经常被忽略的维度:规则更新延迟

操作 kube-proxy (iptables) kube-proxy (IPVS) Cilium (eBPF)
单个 Endpoint 变更 2-5 秒(全量 iptables-restore) ~50 ms(增量 ipvsadm) < 1 ms(单次 Map update)
1000 个 Endpoint 批量变更 5-15 秒 ~500 ms < 100 ms
变更期间对流量的影响 全局锁,新连接阻塞 无影响 无影响
指标 Istio Sidecar Cilium eBPF (无 Sidecar) 差异
额外内存/Pod 50-100 MB ~0 MB(节点级共享) 50-100x
单跳延迟增加 2-3 ms 0.1-0.3 ms ~10x
同节点 Pod 延迟 2-3 ms(过 loopback) < 50 μs(socket redirect) ~50x
最大吞吐 (RPS) ~15,000 ~45,000 ~3x
Pod 启动额外耗时 3-5 秒(Envoy init) < 100 ms(eBPF 加载) ~30x
TLS 握手 每 Pod 独立 节点级共享 / WireGuard 更少握手

数据来源:Cilium 官方 benchmark 和 Isovalent 的对比测试,实际数字因配置而异。

关于 eBPF 与 io_uring 的结合如何进一步提升网络性能,可以参考 eBPF + io_uring:Linux 高性能网络栈的终极形态


五、Cilium 的 eBPF 程序拆解

Cilium 的 eBPF 代码库在 bpf/ 目录下,核心程序按功能分文件:

bpf_lxc.c —— 容器网络入口/出口

这是最核心的程序,挂载在每个 Pod 的 veth pair 的 tc hook 上:

// bpf_lxc.c 的核心入口点
SEC("tc")
int cil_from_container(struct __sk_buff *skb) {
    // 从容器发出的流量
    // 1. 提取 IP/TCP/UDP 头
    // 2. 查 NetworkPolicy(eBPF Map)
    // 3. 查 Service Map → 做 DNAT
    // 4. 确定下一跳(同节点 redirect / overlay encap / 直接路由)
    // 5. 写 conntrack 条目
    return handle_xgress(skb, SRC_CONTAINER);
}

SEC("tc")
int cil_to_container(struct __sk_buff *skb) {
    // 进入容器的流量
    // 1. 查 conntrack(确认是已建立连接的回复包)
    // 2. 查 NetworkPolicy
    // 3. 做反向 NAT(revDNAT)
    // 4. 投递到容器
    return handle_xgress(skb, DST_CONTAINER);
}

bpf_host.c —— Host 网络

处理进出 Host 网络命名空间的流量:

// bpf_host.c
SEC("tc")
int cil_to_host(struct __sk_buff *skb) {
    // 流量目标是 Host 自身
    // 处理 HostFirewall 策略
    // 处理发给 localhost 的 Service 流量
}

SEC("tc")
int cil_from_host(struct __sk_buff *skb) {
    // Host 发出的流量
    // 可能是 kubelet、kube-apiserver 等访问 Service
}

bpf_overlay.c —— VXLAN/Geneve Overlay

当 Cilium 工作在 overlay 模式时,处理封装/解封装:

// bpf_overlay.c
SEC("tc")
int cil_from_overlay(struct __sk_buff *skb) {
    // 从 VXLAN/Geneve 隧道收到的流量
    // 1. 解封装
    // 2. 查目标 Endpoint
    // 3. redirect 到目标 Pod 的 veth
}

SEC("tc")
int cil_to_overlay(struct __sk_buff *skb) {
    // 需要发送到远端节点的流量
    // 1. 查远端节点信息(IP、tunnel key)
    // 2. 封装成 VXLAN/Geneve
    // 3. 发出
}

核心数据结构全景

// Cilium eBPF Map 全景(简化)
// ──────────────────────────────────────────────────
// Map 名称                  Key                 Value
// ──────────────────────────────────────────────────
// cilium_lb4_services      lb4_key             lb4_service
//   → Service VIP → 后端列表
//
// cilium_lb4_backends      backend_id          lb4_backend
//   → 后端 ID → Pod IP:Port
//
// cilium_ct4_global        ct_tuple            ct_entry
//   → 连接五元组 → 连接跟踪状态
//
// cilium_ipcache           ip_addr             remote_ep_info
//   → IP → 所属 Endpoint 的身份信息
//
// cilium_policy             endpoint_id+port   policy_entry
//   → Endpoint + 端口 → 策略允许/拒绝
//
// cilium_endpoints         endpoint_key        endpoint_info
//   → Endpoint Key → 本地 Endpoint 信息(ifindex 等)

可以用 bpftool 实际查看这些 Map:

# 列出所有 Cilium 相关的 eBPF Map
bpftool map list | grep cilium

# 查看 Service Map 的内容
bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_lb4_services_v2

# 查看 conntrack 表
bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_ct4_global

# 查看一个 Endpoint 上挂载的 eBPF 程序
tc filter show dev lxc_health egress
# 输出:
# filter protocol all pref 1 bpf chain 0
# filter protocol all pref 1 bpf chain 0 handle 0x1
#   bpf_lxc.o:[cil_from_container] direct-action ...

六、局限和选型建议

Cilium 不是银弹。在决定用它之前,你需要清楚这些限制。

内核版本要求

内核版本        Cilium 支持程度
─────────────────────────────────────
< 4.19         ✗ 不支持
4.19 - 5.3     基本功能(kube-proxy 替代)
5.4 - 5.9      大部分功能(Host Reachable Services 需要 5.7+)
≥ 5.10         全功能(推荐)
≥ 6.1          最佳性能(BIG TCP、bpf_loop 等)

很多企业还在跑 CentOS 7(kernel 3.10)或 RHEL 8(kernel 4.18)。这直接把 Cilium 排除在外。

调试困难度上升

iptables 虽然慢,但可观测性极好:

# iptables 调试:简单直接
iptables -L -n -v          # 看规则和计数器
iptables -t nat -L -n -v   # 看 NAT 规则
conntrack -L               # 看连接跟踪表

# Cilium eBPF 调试:学习曲线陡峭
bpftool prog list                   # 列出 eBPF 程序
bpftool map dump id <map_id>        # dump Map 内容(输出是二进制)
cilium bpf ct list global           # Cilium CLI 封装
cilium monitor                      # 实时查看 eBPF 事件
hubble observe                      # Hubble 可观测层(推荐)

iptables -L 人人会用,bpftool map dump 需要理解数据结构。Cilium 通过 Hubble 提供了更好的可视化,但底层调试仍然比 iptables 复杂。

与 NetworkPolicy 的兼容性

好消息:Cilium 完全支持 Kubernetes 原生 NetworkPolicy,还额外支持 CiliumNetworkPolicy(支持 L7 规则、FQDN 规则等)。

但有一个常见的坑:如果你从 Calico/Flannel 迁移过来,已有的 iptables-based NetworkPolicy 实现要小心验证。Cilium 的 NetworkPolicy 实现在语义上和其他 CNI 基本一致,但边缘场景可能有细微差异(比如对 except CIDR 的处理)。

选型决策树

你的集群有多少 Service?
├── < 500 → Calico / Flannel 够用,iptables 性能不是瓶颈
├── 500 - 2000 → 考虑 kube-proxy IPVS 模式
└── > 2000 → 强烈建议 Cilium
    │
    ├── 需要 Service Mesh?
    │   ├── 是 → Cilium + Hubble(省掉 Istio sidecar 的开销)
    │   └── 否 → Cilium 单独做 CNI + kube-proxy 替代
    │
    ├── 内核版本 >= 5.10?
    │   ├── 是 → 全功能 Cilium,推荐
    │   └── 否 → 升级内核或接受功能限制
    │
    └── 团队有 eBPF 调试能力?
        ├── 是 → 放心上
        └── 否 → 先培训,Hubble 能降低门槛但救不了所有场景

一句话总结:如果你的 iptables 规则已经超过万条且还在增长,Cilium 不是可选项,而是必选项


结语

kube-proxy + iptables 是 Kubernetes 网络的”默认答案”,它在小规模下工作得很好。但当 Service 数量过千、Pod 数量过万,iptables 的 O(n) 匹配和全量刷新就变成了真正的瓶颈。

Cilium 的解法不是修补 iptables,而是换掉整个数据面:

代价是更高的内核版本要求、更陡的学习曲线、更少的社区”人人都会”的工具链。但对于规模在增长的集群,这个代价是值得付的。

eBPF 正在重新定义 Linux 的数据面。Cilium 只是这场变革里最显眼的一个应用。更多关于 eBPF 的基础原理和实战案例,参见 eBPF:Linux 内核的隐藏武器


参考

  1. Cilium 官方文档 —— 架构、安装、配置的权威来源
  2. Cilium 源码 bpf/ 目录 —— eBPF 程序的实际实现
  3. Thomas Graf (2020). How to Make Linux Microservice-Aware with Cilium and eBPF. —— Cilium 创始人的架构演讲
  4. Martynas Pumputis (2022). Kubernetes without kube-proxy. —— kube-proxy 替代的深度技术分析
  5. eBPF:Linux 内核的隐藏武器 —— eBPF 基础概念和验证器限制
  6. eBPF + io_uring:Linux 高性能网络栈的终极形态 —— eBPF 与 io_uring 的协作
  7. Linux Kernel BPF Documentation —— 内核 BPF 子系统的官方文档

By .