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

Cilium 深度拆解:eBPF 原生的云原生网络

目录

上一篇我们拆解了 Calico – BGP 路由 + iptables/eBPF 的混合数据面。Calico 的 eBPF 模式是后来加的,骨子里仍然是”路由协议 + 策略引擎”的思路。Cilium 走了一条完全不同的路:它从第一天起就把 eBPF 作为唯一的数据面,不需要 iptables,不需要 BIRD,不需要 kube-proxy,甚至不需要 sidecar 就能做 Service Mesh。

如果你管理的集群有几千个 Service、几万条 NetworkPolicy,iptables 的 O(n) 匹配和全量 iptables-restore 已经成为实实在在的性能瓶颈。Cilium 的回答是:把所有数据面逻辑搬到 eBPF 程序和 BPF Map 里,用 O(1) 哈希查找替代线性匹配,用原子 Map 更新替代全量刷新,用数字 Identity 替代 IP 地址做安全策略。

这篇文章是 Cilium 数据面基础 的深度续篇。那篇文章解释了”为什么不要 iptables”和 eBPF Map 的基本结构;本文会深入到 Cilium 内部,拆解包在数据面中的完整路径、Identity 安全模型的设计哲学、kube-proxy 替代的具体实现、Service Mesh 的无 sidecar 架构、Hubble 可观测性、以及 ClusterMesh 多集群连接。

如果你还没读过 eBPF 在网络栈中的挂载点,建议先看 eBPF 网络编程,那篇文章覆盖了 XDP、TC、cgroup 等 hook 点的基础知识。

本文基于 Cilium v1.16.x,Linux 6.x 内核。部分内部实现可能随版本演进而调整。 实验环境:Ubuntu 22.04, kernel 6.5, kind 集群 + Cilium Helm 安装。


一、设计哲学:eBPF-first 与 Identity-driven

为什么选择 eBPF 作为唯一数据面

传统的 Kubernetes 网络方案 – 无论是 kube-proxy 的 iptables/IPVS,还是 Calico 的 Felix – 都依赖内核的 netfilter 子系统。netfilter 的问题在前面的文章里反复讨论过:规则匹配是 O(n) 的,更新需要全局锁,conntrack 表在高并发下有严重的锁竞争。

Cilium 的选择是完全绕过 netfilter。eBPF 程序直接挂在 TC、XDP、cgroup 等 hook 点上,在包进入 netfilter 之前就完成了所有的 Service 负载均衡、NAT、策略执行和连接追踪。内核的 iptables 规则表可以是空的 – Cilium 根本不往里面写任何东西。

这不是”用 eBPF 优化 iptables”,而是用 eBPF 替代整个 iptables 子系统

Identity-driven:不看 IP,看身份

传统的 NetworkPolicy 实现(包括 Calico 的 iptables 模式)把策略翻译成 IP 地址匹配规则:

# 传统方式:策略 -> IP 规则
允许 app=frontend 访问 app=backend
  -> 查找所有 app=frontend 的 Pod IP
  -> 生成 iptables 规则:-s 10.244.1.5 -j ACCEPT
  -> 生成 iptables 规则:-s 10.244.2.12 -j ACCEPT
  -> ...每个 Pod 一条规则

问题是:Pod IP 是临时的。Pod 重启、滚动更新、HPA 扩缩容,IP 都会变。每次变化都要重新计算受影响的策略,更新 iptables 规则。在大规模集群里,这个”标签 -> IP -> 规则”的翻译链条会变得极其昂贵。

Cilium 引入了 Identity 的概念:# Cilium 方式:策略 -> Identity 规则 允许 app=frontend 访问 app=backend -> app=frontend 的 Identity = 12345 -> app=backend 的 Identity = 67890 -> BPF Policy Map: {src=12345, dst=67890} -> ALLOW

Identity 是一个数字编号,由 Pod 的安全相关标签(security-relevant labels)的哈希值决定。所有标签完全相同的 Pod 共享同一个 Identity。Pod 重启后 IP 变了,但标签不变,Identity 不变,策略规则不需要更新。

这就是 Cilium 的两个核心设计决策:eBPF-first 的数据面 + Identity-driven 的安全模型。下面逐一深入。


二、数据面架构:BPF 程序的分层分工

Cilium 的 eBPF 程序不是一个巨大的单体程序,而是按 hook 点分层,每一层有明确的职责。理解这个分层是理解 Cilium 性能的关键。

BPF 程序在各 hook 点的分工

Hook 点 BPF 程序类型 职责
XDP(物理网卡) BPF_PROG_TYPE_XDP NodePort/LoadBalancer 入站加速,DDoS 过滤,DSR 回包
TC ingress(eth0) BPF_PROG_TYPE_SCHED_CLS 外部流量的 CT 查找、Service DNAT、策略执行、FIB 路由
TC ingress(lxc*) BPF_PROG_TYPE_SCHED_CLS Pod 出站流量的 CT 查找、策略执行、SNAT、重定向
TC egress(lxc*) BPF_PROG_TYPE_SCHED_CLS Pod 入站流量的 CT 反向查找、策略执行、L7 重定向
cgroup/connect4 BPF_PROG_TYPE_CGROUP_SOCK_ADDR Socket 级 Service 地址转换(透明 DNAT)
cgroup/sendmsg4 BPF_PROG_TYPE_CGROUP_SOCK_ADDR UDP Service 地址转换
cgroup/recvmsg4 BPF_PROG_TYPE_CGROUP_SOCK_ADDR 反向 NAT(让应用看到原始 Service IP)
cgroup/getpeername4 BPF_PROG_TYPE_CGROUP_SOCK_ADDR getpeername() 返回 Service IP 而非 Pod IP
sockops BPF_PROG_TYPE_SOCK_OPS 建立 socket-level redirect 的 sockmap 条目
sk_msg BPF_PROG_TYPE_SK_MSG 同节点 Pod 间的 socket-to-socket 直接转发

这张表的关键信息是:Cilium 在六个不同的内核 hook 点挂载了 BPF 程序,形成了一张覆盖从网卡到 socket 的完整可编程网格。

包的完整路径:从外部到 Pod

下图展示了一个外部请求到达 NodePort,最终被转发到目标 Pod 的完整数据路径:

Cilium 数据面完整流程

让我们逐步拆解一个从外部进入 NodePort 的包:

第一站:XDP(物理网卡 eth0)

// 简化的 XDP NodePort 处理逻辑
SEC("xdp")
int xdp_entry(struct xdp_md *ctx) {
    // 解析 L3/L4 头部
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    struct iphdr *ip = (void *)(eth + 1);
    struct tcphdr *tcp = (void *)(ip + 1);

    // 查找 NodePort Service
    struct lb4_key key = {
        .address = ip->daddr,
        .dport = tcp->dest,
        .proto = ip->protocol,
    };
    struct lb4_service *svc = map_lookup_elem(&LB4_SERVICES_MAP, &key);
    if (svc) {
        // 选择后端(Maglev 一致性哈希)
        // 如果后端在本节点 -> XDP_PASS,交给 TC 层处理
        // 如果后端在远端且启用 DSR -> 改写 IP option,XDP_TX 直接发回网卡
        return handle_nodeport(ctx, svc);
    }
    return XDP_PASS;
}

XDP 层只处理 NodePort 和 LoadBalancer 的入站流量。对于普通的 Pod-to-Pod 流量,XDP 直接返回 XDP_PASS,交给下一层。

第二站:TC ingress(eth0)

// 简化的 TC ingress 主逻辑
SEC("tc")
int from_netdev(struct __sk_buff *skb) {
    // 1. CT (Connection Tracking) 查找
    struct ct_entry *ct = ct_lookup4(&CT_MAP, &tuple);

    if (ct) {
        // 已有连接:直接用 CT 表里的 NAT 信息
        return ct_act_ok(skb, ct);
    }

    // 2. 新连接:查找 Service Map
    struct lb4_service *svc = lb4_lookup_service(&tuple);
    if (svc) {
        // Service 负载均衡:选择后端
        struct lb4_backend *backend = lb4_select_backend(svc);
        // 执行 DNAT:把 Service VIP 改成 Pod IP
        lb4_xlate(skb, backend);
        // 创建 CT 条目
        ct_create4(&CT_MAP, &tuple, ct_entry);
    }

    // 3. Policy 查找(基于 Identity)
    int verdict = policy_lookup(src_identity, dst_identity, dport, proto);
    if (verdict == DROP) return TC_ACT_SHOT;

    // 4. FIB 查找:确定出接口
    struct bpf_fib_lookup fib = {};
    bpf_fib_lookup(skb, &fib, sizeof(fib), 0);

    // 5. 重定向到目标接口(lxc* 或 eth0)
    return bpf_redirect(fib.ifindex, 0);
}

TC ingress 是 Cilium 数据面的主力 hook 点。它完成了 CT 查找、Service DNAT、Policy 执行、FIB 路由四个关键操作,全部是 O(1) 的 BPF Map 查找。

**第三站:TC ingress/egress(lxc*,Pod 的 veth host 端)**

到达目标 Pod 的 veth 之前,还会经过一次 TC 程序,执行入站策略检查和 L7 策略重定向(如果有 L7 策略,会把包重定向到 Envoy proxy)。

关键对比:iptables vs Cilium 的 CT/NAT/Policy

操作 iptables 路径 Cilium eBPF 路径
Service DNAT PREROUTING -> KUBE-SERVICES 链(O(n) 匹配) TC ingress -> LB4_SERVICES_MAP 哈希查找(O(1))
连接追踪 nf_conntrack(全局哈希表 + 自旋锁) per-CPU CT Map(无锁)
策略执行 FORWARD 链 iptables 规则(O(n)) POLICY_MAP 哈希查找(O(1))
SNAT POSTROUTING -> MASQUERADE(O(n)) TC egress -> SNAT Map(O(1))
规则更新 iptables-restore 全量刷新 + 全局锁 原子 Map 更新,无锁

Socket-level redirect:绕过整个 TCP/IP 栈

Cilium 最激进的优化是 socket-level redirect。当同一节点上的两个 Pod 通信时,包不需要走完整的 TCP/IP 协议栈(发送端:socket -> TCP -> IP -> TC egress -> veth -> TC ingress -> IP -> TCP -> socket)。

Cilium 用 sockopssk_msg 两个 BPF 程序实现了 socket-to-socket 的直接转发:

// sockops 程序:在 TCP 连接建立时记录 sockmap 条目
SEC("sockops")
int bpf_sockops(struct bpf_sock_ops *skops) {
    switch (skops->op) {
    case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:  // connect() 成功
    case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: // accept() 成功
        // 把这个 socket 的五元组插入 sockmap
        sock_hash_update(skops, &SOCK_OPS_MAP, &key, BPF_NOEXIST);
        break;
    }
    return 1;
}

// sk_msg 程序:在 sendmsg 时直接把数据转发到对端 socket
SEC("sk_msg")
int bpf_skmsg(struct sk_msg_md *msg) {
    // 查找对端 socket
    struct sock_key key = {
        .sip = msg->remote_ip4,
        .dip = msg->local_ip4,
        .sport = msg->remote_port,
        .dport = bpf_ntohl(msg->local_port),
    };
    return bpf_msg_redirect_hash(msg, &SOCK_OPS_MAP, &key, BPF_F_INGRESS);
}

这条路径完全绕过了 TCP/IP 栈:数据从发送端的 socket 缓冲区直接复制到接收端的 socket 缓冲区,不经过 IP 层、不经过 TC hook、不经过 veth pair。对于同节点的 Pod-to-Pod 通信(在微服务架构中非常常见),延迟和 CPU 开销都有显著下降。

# 启用 socket-level redirect(Helm 安装时)
helm install cilium cilium/cilium \
  --set socketLB.enabled=true \
  --set bpf.socketLBHostnsOnly=false

注意:socket-level redirect 只对 TCP 生效(UDP 无连接,无法建立 sockmap 映射),且要求两个 Pod 在同一节点上。


三、Identity 模型:为什么用 Identity 而不是 IP

Endpoint -> Identity -> Policy 映射链

Cilium 的安全模型建立在三层抽象之上:

Identity 模型:Endpoint -> Identity -> Policy

第一层:Endpoint(CiliumEndpoint)

每个 Pod 在 Cilium 里对应一个 CiliumEndpoint(CEP)对象。CEP 记录了 Pod 的 IP 地址、所在节点、关联的 Identity 编号。

# 查看 CiliumEndpoint
$ kubectl get cep -n default
NAME        ENDPOINT ID   IDENTITY ID   INGRESS POLICY   EGRESS POLICY
frontend-1  1234          52781         Enabled          Enabled
frontend-2  1235          52781         Enabled          Enabled
backend-1   1236          39402         Enabled          Enabled

注意 frontend-1frontend-2 的 Identity ID 相同 – 因为它们的安全标签一样。

第二层:Identity(数字身份)

Identity 是 Cilium 安全模型的核心。它的生成规则:

  1. Cilium Agent 从 Pod 的标签中提取安全相关标签(security-relevant labels)。默认情况下,所有 k8s: 前缀的标签都是安全相关的,但 pod-template-hash 等自动生成的标签会被排除。
  2. 把这组标签排序后计算一个确定性哈希
  3. 用这个哈希到 kvstore 里查找或分配一个全局唯一的数字 Identity
Pod A: {app=frontend, env=prod, pod-template-hash=abc123}
  -> 安全标签: {app=frontend, env=prod}
  -> 哈希: sha256("app=frontend,env=prod") = 0x7f3a...
  -> Identity: 52781

Pod B: {app=frontend, env=prod, pod-template-hash=def456}
  -> 安全标签: {app=frontend, env=prod}  (相同!)
  -> 哈希: 相同
  -> Identity: 52781  (相同!)

这个设计的关键优势:

第三层:Policy(基于 Identity 的策略)

CiliumNetworkPolicyfromEndpointstoEndpoints 最终都被编译成 Identity 匹配规则,写入 BPF Policy Map:

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

Cilium Agent 把这条策略编译成:

Policy Map 条目:
  Key:   {src_identity=52781, dst_port=8080, proto=TCP}
  Value: {verdict=ALLOW}

在数据面上,TC BPF 程序收到一个包时,从包的源 IP 查出 Identity(通过 IPCACHE Map),然后用 {src_identity, dst_port, proto} 到 Policy Map 里做一次 O(1) 哈希查找。

Identity 分配:kvstore 与 CRD 两种模式

Identity 是全局唯一的 – 集群里所有节点上标签相同的 Pod 必须使用同一个 Identity。这意味着 Identity 分配需要一个全局协调机制。

kvstore 模式(etcd)

早期的 Cilium 使用独立的 etcd 集群。分配流程:Agent 计算标签哈希,在 etcd 中尝试原子 PUT IF NOT EXISTS;如果 key 已存在则复用。

CRD 模式(Kubernetes API,默认)

从 Cilium 1.12 开始,CRD 模式成为默认选项:

$ kubectl get ciliumidentities
IDENTITY   NAMESPACE   LABELS
52781      default     k8s:app=frontend,k8s:env=prod
39402      default     k8s:app=backend,k8s:env=prod

CRD 模式用 Kubernetes API 的乐观锁(resourceVersion)替代 etcd 的原子操作,不需要维护额外的 etcd 集群。

特殊 Identity

Cilium 预留了一些特殊的 Identity 编号:

Identity 含义
1 host – 节点本身发出的流量
2 world – 集群外部的流量
3 unmanaged – 不由 Cilium 管理的 Pod
4 health – Cilium 健康检查探针
5 init – Pod 刚创建、Identity 尚未分配
6 remote-node – 其他节点的流量
7 kube-apiserver – API server 流量

这些保留 Identity 确保了即使在 Identity 分配完成之前,Cilium 也能对流量做出合理的策略决策。

IPCACHE:IP 到 Identity 的快速映射

数据面需要从包的源 IP 查出对应的 Identity。Cilium 维护了一个全局的 IPCACHE BPF Map:

// IPCACHE Map 结构
struct ipcache_key {
    __u32 lpm_key;     // LPM trie 前缀长度
    __u8  cluster_id;
    __u8  pad[3];
    __be32 ip4;
};

struct remote_endpoint_info {
    __u32 sec_identity; // 安全 Identity
    __u32 tunnel_ep;    // 隧道端点 IP(跨节点时使用)
    __u16 node_id;
    __u8  key;          // 加密 key 索引
};

每当 Pod 创建或 IP 变化时,Cilium Agent 更新 IPCACHE Map。BPF 程序只需一次 Map 查找就能从 IP 得到 Identity。


四、Cilium 作为 kube-proxy 替代

为什么要替代 kube-proxy

kube-proxy 的问题在 Cilium 数据面基础Service 与 kube-proxy 两篇文章里详细讨论过。总结为三个核心问题:

  1. iptables O(n) 匹配:5000 Service = 5000 次线性比较
  2. iptables-restore 全量刷新 + 全局锁:十万条规则刷新要几秒,期间新连接全部阻塞
  3. conntrack 锁竞争nf_conntrack 的全局哈希表在高并发下成为瓶颈

Cilium 的 kube-proxy 替代方案把这三个问题全部解决:Service 查找是 O(1) Map 查找,更新是原子 Map 操作,conntrack 是 per-CPU 的 BPF Map。

# 启用 kube-proxy 替代(Helm 安装时)
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}

# 验证状态
$ cilium status | grep KubeProxyReplacement
KubeProxyReplacement:   True   [eth0 (DR, XDP)]

# 确认 iptables 干净
$ iptables-save | grep -c KUBE
0

Maglev 一致性哈希

kube-proxy 的 iptables 模式用 statistic --mode random --probability 做负载均衡 – 本质上是概率随机。IPVS 模式支持多种调度算法,但仍然不支持一致性哈希。

Cilium 实现了 Google 的 Maglev 一致性哈希算法。Maglev 的核心特性:

  1. 连接亲和性:同一个 {src_ip, src_port, dst_ip, dst_port, proto} 五元组总是被分到同一个后端
  2. 最小重分配:后端增减时,只有 ~1/N 的连接需要重映射(N 是后端数量)
  3. 均匀分布:哈希表的填充率保证了后端之间的负载均衡
# 启用 Maglev(Helm 安装时)
helm install cilium cilium/cilium \
  --set loadBalancer.algorithm=maglev \
  --set maglev.tableSize=65521  # 默认值,质数

# 查看 Maglev 状态
$ cilium service list
ID   Frontend          Service Type   Backend
1    10.96.0.1:443     ClusterIP      1 => 192.168.1.10:6443 (active)
2    10.96.0.10:53     ClusterIP      1 => 10.244.0.5:53 (active)
                                      2 => 10.244.0.6:53 (active)
3    0.0.0.0:30080     NodePort       1 => 10.244.1.5:8080 (active)
                                      2 => 10.244.2.12:8080 (active)
                                      3 => 10.244.3.8:8080 (active)

Maglev 表是预计算的:Cilium Agent 在 Service 后端变化时重新计算 Maglev 查找表,写入 BPF Map。数据面的 BPF 程序只需要用五元组哈希值做一次数组索引就能得到后端编号。

DSR 模式(Direct Server Return)

默认情况下,Service 的回包路径是:Backend Pod -> 入口节点 -> Client。入口节点需要做 reverse SNAT,把源 IP 从 Pod IP 改回 Service IP。这意味着回包必须经过入口节点,增加了延迟和带宽消耗。

DSR 模式让回包直接从 Backend Pod 所在节点发回 Client,绕过入口节点:

正常模式(SNAT):
  Client -> Node A (DNAT) -> Node B (Backend Pod)
  Node B -> Node A (reverse SNAT) -> Client

DSR 模式:
  Client -> Node A (DNAT, 把原始客户端信息编码到包里) -> Node B (Backend Pod)
  Node B -> Client(直接回包,源 IP = Service IP)
# 启用 DSR
helm install cilium cilium/cilium \
  --set loadBalancer.mode=dsr \
  --set loadBalancer.dsrDispatch=opt  # 用 IP Option 传递原始信息
  # 或 --set loadBalancer.dsrDispatch=ipip  # 用 IP-in-IP 封装

DSR 的优势是回包延迟更低、入口节点 CPU 开销更小、后端 Pod 能看到客户端真实源 IP。限制是需要底层网络允许源 IP 为 Service VIP 的包通过(一些云平台的反欺骗规则会阻止)。关于 DSR 的更深入讨论,可以参考 DSR 与回包路径优化


五、Service Mesh without Sidecar

Sidecar 模式的代价

传统的 Service Mesh(如 Istio)在每个 Pod 里注入一个 sidecar proxy(通常是 Envoy)。这意味着:

Cilium 的 per-node Envoy 模型

Cilium 的 Service Mesh 实现不使用 sidecar。它在每个节点上运行一个 Envoy 实例(嵌入在 Cilium Agent 进程中),用 BPF 程序在需要 L7 处理时把流量透明重定向到这个节点级 Envoy。

传统 sidecar 模式:
  Pod A -> iptables REDIRECT -> Envoy sidecar A
    -> Envoy sidecar B -> iptables REDIRECT -> Pod B

Cilium per-node 模式:
  Pod A -> BPF redirect -> Node Envoy -> BPF redirect -> Pod B

BPF L7 重定向的实现

当一个 CiliumNetworkPolicy 包含 L7 规则(如 HTTP 路径匹配)时,Cilium 不会在 Policy Map 里返回 ALLOW/DENY,而是返回一个重定向指令,让 BPF 程序把包转发到节点上的 Envoy proxy socket:

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

这条策略里的 rules.http 部分不能在 BPF 层执行(BPF 看不到 HTTP 语义),所以 Cilium 会:

  1. 在 Policy Map 里标记这条规则需要 L7 代理
  2. BPF 程序检测到 L7 标志后,把包重定向到节点 Envoy 的监听端口
  3. Envoy 解析 HTTP 头,执行 L7 策略
  4. 如果允许,Envoy 把包转发给目标 Pod

优势对比

维度 Sidecar 模式 Cilium per-node 模式
Proxy 实例数 每 Pod 一个 每节点一个
内存开销 ~100 MiB x Pod 数 ~200 MiB x 节点数
纯 L3/L4 策略 仍经过 Envoy BPF 直接执行,不经过 Envoy
L7 策略 Envoy 处理 BPF 重定向到节点 Envoy
升级 需要重启所有 Pod 只需重启 DaemonSet

六、Hubble:eBPF 驱动的网络可观测性

从 BPF 事件到可观测性

Cilium 的每个 BPF 程序在关键决策点(CT 查找、Policy 判定、NAT 转换、丢包)都会往 perf ring bufferBPF ring buffer 写入事件。这些事件是结构化的,包含了完整的上下文信息:源/目标 IP、Identity、端口、协议、策略判定结果、丢包原因。

Hubble 是 Cilium 内置的可观测性组件,它消费这些 BPF 事件并提供三个接口:

  1. Hubble CLI:命令行实时查看流量
  2. Hubble Relay:集群级别的流量聚合服务
  3. Hubble UI:基于 Web 的可视化界面

实时流量观测

# 安装 Hubble(Helm 安装 Cilium 时启用)
helm upgrade cilium cilium/cilium \
  --set hubble.enabled=true \
  --set hubble.relay.enabled=true \
  --set hubble.ui.enabled=true

# 使用 Hubble CLI 观测流量
$ hubble observe --namespace default
Jun 15 10:23:45.123  default/frontend-7b9d5 -> default/backend-4c8e2 http
  GET /api/v1/users HTTP/1.1  200  12ms  Identity: 52781 -> 39402  FORWARDED

Jun 15 10:23:45.456  default/frontend-7b9d5 -> default/database-1a3f7 tcp
  10.244.1.5:43210 -> 10.244.2.8:5432  Identity: 52781 -> 28193  DROPPED (policy)

Jun 15 10:23:46.789  world/0.0.0.0 -> default/ingress-nginx-6d8c1 tcp
  203.0.113.50:52100 -> 10.244.0.3:443  Identity: 2 -> 15847  FORWARDED

关键信息:Hubble 的输出包含了 Identity。你可以立即看到 52781 -> 39402 对应 frontend -> backend,不需要反查 Pod IP。Hubble 还支持按判定结果过滤(--verdict DROPPED)、按 Identity 过滤(--identity 52781)、按 HTTP 状态码过滤(--http-status 500)。

Hubble Metrics

Hubble 能把 BPF 事件聚合为 Prometheus metrics,直接对接 Grafana:

helm upgrade cilium cilium/cilium \
  --set hubble.metrics.enabled="{dns,drop,tcp,flow,icmp,httpV2:exemplars=true;labelsContext=source_namespace,destination_namespace}"

七、ClusterMesh:多集群连接

为什么需要多集群网络

单一 Kubernetes 集群有规模上限(官方建议 5000 节点),也有可用性需求(跨区域容灾)。ClusterMesh 让多个 Cilium 集群之间实现:

  1. Pod-to-Pod 跨集群直连
  2. 全局 Service 负载均衡
  3. 跨集群 NetworkPolicy

实现原理

ClusterMesh 的核心是 etcd 联邦。每个集群运行自己的 Cilium etcd(或使用 Kubernetes API),ClusterMesh 通过让每个集群的 Cilium Agent 额外连接到其他集群的 etcd 来实现状态同步:

Cluster A (etcd-A)                    Cluster B (etcd-B)
  Cilium Agent A1 ---watch---> etcd-A    Cilium Agent B1 ---watch---> etcd-B
                  ---watch---> etcd-B                    ---watch---> etcd-A

同步的内容包括:

跨集群 Service

# 在 Cluster A 创建一个全局 Service
apiVersion: v1
kind: Service
metadata:
  name: global-backend
  annotations:
    service.cilium.io/global: "true"    # 标记为全局 Service
    service.cilium.io/shared: "true"    # 共享到其他集群
spec:
  selector:
    app: backend
  ports:
  - port: 80

当 Cluster A 的 Pod 访问 global-backend 时,Cilium 会在 Service Map 里看到本集群和远端集群的所有后端,负载均衡覆盖全部后端:

# 在 Cluster A 查看全局 Service 的后端
$ cilium service list | grep global-backend
5    10.96.0.50:80     ClusterIP    1 => 10.244.1.5:8080 (active, cluster-a)
                                    2 => 10.244.2.12:8080 (active, cluster-a)
                                    3 => 10.1.1.5:8080 (active, cluster-b)
                                    4 => 10.1.2.8:8080 (active, cluster-b)

启用 ClusterMesh

# 在两个集群上分别启用 ClusterMesh
# Cluster A
cilium clustermesh enable --context kind-cluster-a
# Cluster B
cilium clustermesh enable --context kind-cluster-b

# 建立连接
cilium clustermesh connect \
  --context kind-cluster-a \
  --destination-context kind-cluster-b

# 验证状态
$ cilium clustermesh status
Cluster Connections:
  - cluster-b: 3/3 nodes ready, connected
  Identities synchronized: 247
  Services synchronized: 42

关于多集群网络的更多架构讨论,可以参考 多集群网络


八、实验:部署 Cilium 并实时观察包路径

下面我们用 kind 搭建一个测试集群,部署 Cilium,然后用 cilium monitor 和 Hubble 实时观察包的完整路径。

创建 kind 集群

# kind 配置:禁用默认 CNI 和 kube-proxy
cat <<EOF > kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  disableDefaultCNI: true   # 不安装默认 CNI
  kubeProxyMode: "none"     # 不安装 kube-proxy
nodes:
- role: control-plane
- role: worker
- role: worker
EOF

kind create cluster --name cilium-lab --config kind-config.yaml

安装 Cilium

helm repo add cilium https://helm.cilium.io/ && helm repo update

helm install cilium cilium/cilium --version 1.16.0 \
  --namespace kube-system \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost=kind-control-plane \
  --set k8sServicePort=6443 \
  --set hubble.enabled=true \
  --set hubble.relay.enabled=true \
  --set hubble.ui.enabled=true \
  --set loadBalancer.algorithm=maglev \
  --set socketLB.enabled=true \
  --set bpf.masquerade=true

cilium status --wait

部署测试应用

# 部署 frontend 和 backend
kubectl create deployment frontend --image=nginx --replicas=2
kubectl create deployment backend --image=nginx --replicas=2
kubectl expose deployment backend --port=80 --target-port=80

# 添加标签
kubectl label deployment frontend app=frontend env=prod --overwrite
kubectl label deployment backend app=backend env=prod --overwrite

使用 cilium monitor 观察包路径

cilium monitor 是 Cilium Agent 提供的原始事件流,显示每个 BPF 程序的决策:

# 在一个 Cilium Agent Pod 里运行 monitor
$ kubectl exec -n kube-system ds/cilium -- cilium monitor --type trace

# 从 frontend Pod 发请求
$ kubectl exec -it deploy/frontend -- curl backend

# cilium monitor 输出:
-> endpoint 1236 flow 0x12345678 identity 52781->39402 state new ifindex lxcabc123
  Trace: (from-overlay) -> Endpoint 1236
  Conntrack: CT lookup: {tcp,10.244.1.5,10.244.2.12,43210,80} -> New
  NAT: No NAT required (direct Pod-to-Pod)
  Policy: L3/L4 ingress ALLOW (identity 52781, port 80/TCP)
  Delivery: -> lxcabc123

<- endpoint 1236 flow 0x12345679 identity 39402->52781 state reply
  Trace: (to-overlay) <- Endpoint 1236
  Conntrack: CT lookup: {tcp,10.244.2.12,10.244.1.5,80,43210} -> Established (reply)
  Delivery: -> eth0

每一行都对应一个 BPF 程序的决策点:CT 查找结果(New/Established/Reply)、NAT 操作、Policy 判定、最终投递。

使用 cilium monitor 观察丢包

# 创建一个只允许 monitoring 的策略(拒绝 frontend)
cat <<EOF | kubectl apply -f -
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: deny-frontend-to-backend
spec:
  endpointSelector:
    matchLabels:
      app: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: monitoring
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
EOF

# 观察丢包事件
$ kubectl exec -n kube-system ds/cilium -- cilium monitor --type drop
xx endpoint 1236 flow 0x1234567a identity 52781->39402 state new
  Drop: Policy denied  Source: 52781 (app=frontend)  Dest: 39402 (app=backend)  L4: TCP 43210 -> 80

查看 BPF Map 和 Endpoint

# 查看 Service Map
$ kubectl exec -n kube-system ds/cilium -- cilium bpf lb list
SERVICE ADDRESS      BACKEND ADDRESS
10.96.0.10:53        10.244.0.5:53 (1) [active]
                     10.244.0.6:53 (2) [active]
10.96.100.50:80      10.244.1.5:80 (1) [active]
                     10.244.2.12:80 (2) [active]

# 查看 CT Map
$ kubectl exec -n kube-system ds/cilium -- cilium bpf ct list global | head -5
TCP IN 10.244.1.5:43210 -> 10.244.2.12:80 expires=1200 rx_packets=5 tx_packets=5

# 查看 Policy Map
$ kubectl exec -n kube-system ds/cilium -- cilium bpf policy get 1236
POLICY   DIRECTION   IDENTITY   PORT/PROTO   PROXY
Allow    Ingress     52781      80/TCP       NONE

# 查看 Endpoint 列表
$ kubectl exec -n kube-system ds/cilium -- cilium endpoint list
ENDPOINT   POLICY (ingress)   POLICY (egress)   IDENTITY   LABELS (source:key[=value])
1234       Enabled            Enabled           52781      k8s:app=frontend k8s:env=prod
1235       Enabled            Enabled           52781      k8s:app=frontend k8s:env=prod
1236       Enabled            Enabled           39402      k8s:app=backend k8s:env=prod

Hubble UI 可以通过 kubectl port-forward -n kube-system svc/hubble-ui 12000:80 访问,提供实时的 Service Map 拓扑和流量状态可视化。


九、控制面:Cilium Agent 的工作流

前面拆解了数据面的 BPF 程序;这里简要梳理控制面。

Pod 创建的完整流程

kubelet 调用 CNI plugin
  -> Cilium CNI 创建 veth pair
  -> CNI 通知 Cilium Agent:新 Endpoint
  -> Agent 从 Pod labels 计算 Identity
  -> Agent 查询 kvstore/CRD 分配 Identity
  -> Agent 更新 BPF Map:
     - IPCACHE: Pod IP -> Identity
     - Endpoint Map: Endpoint ID -> {IP, Identity, ifindex}
     - Policy Map: 编译相关的 CiliumNetworkPolicy
  -> Agent 在 veth host 端加载 TC BPF 程序
  -> Agent 在 cgroup 挂载点加载 socket BPF 程序
  -> CNI 返回 Pod IP 给 kubelet

策略更新的增量机制

当一条 CiliumNetworkPolicy 被创建或修改时,Agent 只更新受影响的 Endpoint 的 Policy Map 条目。整个过程是增量的 – 不需要重启 BPF 程序,不需要 iptables-restore,不需要全局锁。策略变更只影响相关的 Endpoint,不会触发全局刷新。这是 Cilium 在大规模集群中性能的关键。


十、生产部署要点

内核版本要求

Cilium 的不同功能需要不同的内核版本:

功能 最低内核版本
基础 CNI + NetworkPolicy 4.19
kube-proxy 替代 5.4
Socket-level redirect 5.4
BPF-based Masquerade 5.10
Maglev 一致性哈希 5.7
Bandwidth Manager 5.1
Host Routing(完全绕过 iptables) 5.10
WireGuard 透明加密 5.6
BBR 拥塞控制 5.18

建议生产环境使用 5.10+ 内核以获得完整功能。

BPF Map 大小调优

Cilium 的 BPF Map 大小决定了可以追踪的连接数、Service 数量和策略条目数。默认值适合中小集群,大规模集群需要调整:

helm install cilium cilium/cilium \
  --set bpf.ctTcpMax=524288 \      # CT TCP 最大条目(默认 524288)
  --set bpf.ctAnyMax=262144 \      # CT 非 TCP 最大条目
  --set bpf.natMax=524288 \        # NAT Map 最大条目
  --set bpf.policyMapMax=16384 \   # 每个 Endpoint 的策略条目上限
  --set bpf.lbMapMax=65536         # Service Map 最大条目

监控指标

# Cilium Agent 暴露 Prometheus metrics,关键指标:
cilium_bpf_map_pressure{mapName="cilium_ct4_global"}  # Map 使用率,>0.9 需扩容
cilium_drop_count_total{reason="Policy denied"}
cilium_policy_regeneration_time_stats_seconds
cilium_endpoint_count
cilium_identity_count

cilium_bpf_map_pressure 接近 1.0 时,说明对应的 Map 快满了,需要调大 Map 大小并重启 Agent。

与 Calico 的选型对比

维度 Cilium Calico
数据面 纯 eBPF iptables / eBPF(可选)
路由协议 VXLAN / Geneve / native BGP / VXLAN / IP-in-IP
安全模型 Identity-based IP-based(iptables) / eBPF
kube-proxy 替代 内置,完整 eBPF 模式支持,较新
Service Mesh 内置 per-node Envoy 不内置
可观测性 Hubble(内置) 需外部工具
多集群 ClusterMesh Federation(较弱)
内核要求 5.10+ 推荐 4.x 即可
成熟度 快速发展中 非常成熟
BGP 支持 有限(BGP CP) 原生 BIRD

选型建议:如果你需要 BGP 与物理网络深度集成,Calico 更成熟。如果你需要 Service Mesh、高级可观测性、大规模 Service 负载均衡,Cilium 是更好的选择。更详细的对比见 CNI 选型指南


十一、总结

Cilium 的核心技术贡献可以归纳为三点:

第一,eBPF 替代 iptables 作为数据面。不是在 iptables 上面加一层优化,而是完全绕过 netfilter,用 BPF Map 的 O(1) 查找替代规则链的 O(n) 匹配。这解决了 kube-proxy 在大规模集群下的性能瓶颈。

第二,Identity 替代 IP 作为安全原语。Pod IP 是临时的,Identity 是稳定的。策略规则数量与 Pod 数量解耦,策略更新不再需要逐个 Pod 重算 IP 匹配规则。

第三,BPF hook 的分层分工。XDP 处理 NodePort 加速,TC 处理主数据路径,cgroup/socket 处理透明 Service 转换和同节点加速。每一层做最适合它的事,叠加出一个高性能、低延迟的数据面。

在此基础上,Cilium 还构建了 Hubble(eBPF 原生可观测性)、Service Mesh without sidecar(per-node Envoy)、ClusterMesh(多集群连接)等上层能力。这些不是独立的功能模块,而是 eBPF 数据面的自然延伸。

下一篇我们将从选型的角度,系统对比 Flannel、Calico、Cilium 三大 CNI 插件,给出不同场景下的推荐方案。


By .