你管理的 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)。它的策略是:
- 导出全部规则
- 在内存里修改
- 用
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 │
│ 挂载到网络设备 │
└─────────────────┘
- Cilium Agent(DaemonSet):运行在每个节点,watch K8s API,编译并加载 eBPF 程序,维护 eBPF Map
- Cilium Operator(Deployment):处理集群级资源,比如 IPAM 分配、CiliumNode 同步
- CNI Plugin:容器创建时被 kubelet 调用,配置 veth pair,触发 Agent 加载 eBPF 程序
eBPF 程序在数据面的位置
这是理解 Cilium 的关键。eBPF 程序挂载在三个位置:
- tc ingress / egress:挂在每个 Pod 的 veth pair 和 host 网络设备上,处理所有 L3/L4 流量
- XDP:挂在物理网卡上,处理 NodePort 和 LoadBalancer 的入站流量(最快路径)
- socket-level(cgroup/connect、cgroup/sendmsg 等):拦截 socket 系统调用,实现透明的 Service 地址转换
与 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
做连接跟踪,它有几个问题:
- 全局哈希表 + 自旋锁,高并发下严重争用
- 表满了会丢包(
nf_conntrack: table full, dropping packet) - 和 iptables 耦合,每个包都要查
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 额外延迟
└──────────────────────────────────┘
具体开销:
- 内存:每个 Envoy 实例占 50-100 MB,1000 个 Pod = 50-100 GB 纯 sidecar 内存
- 延迟:每跳增加 2-3ms(入站 Envoy + 出站 Envoy = 两次用户态代理)
- CPU:TLS 握手、协议解析都在用户态,CPU 开销显著
- 启动时间:Envoy 初始化和 xDS 配置拉取需要数秒,影响 Pod 启动速度
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/connect4在 socket 系统调用层工作——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 |
几个关键观察:
- Cilium 的延迟几乎不随 Service 数量增长——这就是 O(1) 哈希查找 vs O(n) 线性匹配的本质差异。从 100 到 50,000 个 Service,P99 延迟只从 0.35ms 增长到 0.48ms。
- iptables 在 10K+ Service
时崩溃式恶化——P99 延迟从亚毫秒跳到两位数毫秒,CPU
占用飙升。这还没算
iptables-restore全量刷新时的秒级卡顿。 - IPVS 在负载均衡方面接近 Cilium——IPVS 用的也是哈希表,查找本身是 O(1)。但 IPVS 的 SNAT/NodePort 仍然依赖 iptables,所以在完整路径上不如 Cilium 干净。
- 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 Map 哈希查找替代 iptables 规则链遍历——O(1) vs O(n)
- 原子 Map 更新替代
iptables-restore全量刷新——微秒 vs 秒级 - socket-level redirect 替代 sidecar 代理——内核态直通 vs 用户态绕行
- Per-CPU conntrack Map 替代
nf_conntrack——无锁 vs 自旋锁
代价是更高的内核版本要求、更陡的学习曲线、更少的社区”人人都会”的工具链。但对于规模在增长的集群,这个代价是值得付的。
eBPF 正在重新定义 Linux 的数据面。Cilium 只是这场变革里最显眼的一个应用。更多关于 eBPF 的基础原理和实战案例,参见 eBPF:Linux 内核的隐藏武器。
参考
- Cilium 官方文档 —— 架构、安装、配置的权威来源
- Cilium 源码 bpf/ 目录 —— eBPF 程序的实际实现
- Thomas Graf (2020). How to Make Linux Microservice-Aware with Cilium and eBPF. —— Cilium 创始人的架构演讲
- Martynas Pumputis (2022). Kubernetes without kube-proxy. —— kube-proxy 替代的深度技术分析
- eBPF:Linux 内核的隐藏武器 —— eBPF 基础概念和验证器限制
- eBPF + io_uring:Linux 高性能网络栈的终极形态 —— eBPF 与 io_uring 的协作
- Linux Kernel BPF Documentation —— 内核 BPF 子系统的官方文档