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

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

文章导航

分类入口
linux
标签入口
#cilium#ebpf#kubernetes#iptables#kube-proxy#service-mesh#networking#xdp#container

目录

你管理的 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 子系统的官方文档

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-01-22 · linux

【eBPF 系列】eBPF:Linux 内核的隐藏武器

eBPF 让你在内核里插代码而不用写内核模块。听起来很美,但验证器的限制、Map 的性能陷阱、BTF 的兼容性噩梦,这些他们不会在教程里告诉你。


By .