上一篇我们拆解了 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 的完整数据路径:
让我们逐步拆解一个从外部进入 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 用 sockops 和 sk_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 的安全模型建立在三层抽象之上:
第一层: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-1 和 frontend-2
的 Identity ID 相同 – 因为它们的安全标签一样。
第二层:Identity(数字身份)
Identity 是 Cilium 安全模型的核心。它的生成规则:
- Cilium Agent 从 Pod
的标签中提取安全相关标签(security-relevant
labels)。默认情况下,所有
k8s:前缀的标签都是安全相关的,但pod-template-hash等自动生成的标签会被排除。 - 把这组标签排序后计算一个确定性哈希。
- 用这个哈希到 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 (相同!)
这个设计的关键优势:
- Pod 扩缩容不影响策略:新 Pod 只要标签不变,Identity 不变,策略不需要更新
- Pod 重启不影响策略:IP 变了,但 Identity 不变
- 策略规则数量与 Pod 数量解耦:1000 个 frontend Pod 共享一个 Identity,策略 Map 里只有一条规则
第三层:Policy(基于 Identity 的策略)
CiliumNetworkPolicy 的
fromEndpoints 和 toEndpoints
最终都被编译成 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: TCPCilium 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=prodCRD 模式用 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 两篇文章里详细讨论过。总结为三个核心问题:
- iptables O(n) 匹配:5000 Service = 5000 次线性比较
- iptables-restore 全量刷新 + 全局锁:十万条规则刷新要几秒,期间新连接全部阻塞
- 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
0Maglev 一致性哈希
kube-proxy 的 iptables 模式用
statistic --mode random --probability
做负载均衡 – 本质上是概率随机。IPVS
模式支持多种调度算法,但仍然不支持一致性哈希。
Cilium 实现了 Google 的 Maglev 一致性哈希算法。Maglev 的核心特性:
- 连接亲和性:同一个 {src_ip, src_port, dst_ip, dst_port, proto} 五元组总是被分到同一个后端
- 最小重分配:后端增减时,只有 ~1/N 的连接需要重映射(N 是后端数量)
- 均匀分布:哈希表的填充率保证了后端之间的负载均衡
# 启用 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)。这意味着:
- 每个 Pod 多一个容器,额外消耗 50-100 MiB 内存和 0.1-0.5 CPU
- 每个请求要经过两次 iptables REDIRECT + 两次 Envoy 代理(发送端 sidecar + 接收端 sidecar)
- 1000 个 Pod = 1000 个 Envoy 实例,升级时需要滚动重启所有 Pod
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 会:
- 在 Policy Map 里标记这条规则需要 L7 代理
- BPF 程序检测到 L7 标志后,把包重定向到节点 Envoy 的监听端口
- Envoy 解析 HTTP 头,执行 L7 策略
- 如果允许,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 buffer 或 BPF ring buffer 写入事件。这些事件是结构化的,包含了完整的上下文信息:源/目标 IP、Identity、端口、协议、策略判定结果、丢包原因。
Hubble 是 Cilium 内置的可观测性组件,它消费这些 BPF 事件并提供三个接口:
- Hubble CLI:命令行实时查看流量
- Hubble Relay:集群级别的流量聚合服务
- 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 集群之间实现:
- Pod-to-Pod 跨集群直连
- 全局 Service 负载均衡
- 跨集群 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
同步的内容包括:
- IPCACHE:其他集群的 Pod IP -> Identity 映射
- Service:全局 Service 的后端列表
- Identity:跨集群的 Identity 分配(确保同一标签集在所有集群得到相同的 Identity)
跨集群 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=prodHubble 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 插件,给出不同场景下的推荐方案。