
上一篇沿着数据包回答了两个问题：Service 流量在哪里做 DNAT，源 IP 又为什么会被 SNAT。那还留着一个更底层的问题：**同样是把 Service 前端映射到后端 Pod，iptables、nftables、IPVS 和 eBPF 到底把映射存在哪里，又在哪个内核路径执行它？**

这不是四种语法的横向罗列。实现位置不同，会改变规则更新成本、可观测入口、负载均衡行为，甚至决定排障时应该先看 `iptables-save`、`nft list ruleset`，还是 BPF map。

本文使用五个独立的三节点 kind 集群，给同一份 Service 和工作负载换上不同数据面。所有输出都来自 2026-09-21 在 m6 上执行的实验：

| 集群 | Kubernetes | Service 数据面 | 实现者 |
| --- | --- | --- | --- |
| `svc-iptables` | v1.35.0 | iptables | kube-proxy |
| `svc-ipvs` | v1.35.0 | IPVS | kube-proxy |
| `svc-nftables` | v1.35.0 | nftables | kube-proxy |
| `svc-cilium` | v1.35.0 | eBPF | Cilium v1.19.8 |
| `svc-calico-bpf` | v1.35.0 | eBPF | Calico v3.31.7 |

这里没有做性能压测。十二次请求只能帮助识别选择行为和数据面状态，不能推出吞吐量或延迟排名。清单、采集脚本和输出已提交到 [k8sdev `6da31f8`](https://github.com/jimyag/k8sdev/tree/6da31f8221e048b204208c2d37b060fa3e8c0630/service-dataplane-comparison)。

## 先把控制面和数据面分开

无论使用哪一种实现，输入都大致相同：Service 给出稳定前端，EndpointSlice 给出当前后端。真正变化的是“谁监听这些对象”和“谁把前端改写成后端”。

```mermaid
flowchart LR
    API[API Server<br/>Service + EndpointSlice]
    KP[kube-proxy]
    CIL[Cilium agent]
    CAL[Calico Felix]
    IPT[iptables chains]
    NFT[nftables maps and chains]
    IPV[IPVS virtual services]
    CBPF[Cilium BPF maps]
    FBPF[Calico BPF NAT maps]

    API --> KP
    API --> CIL
    API --> CAL
    KP --> IPT
    KP --> NFT
    KP --> IPV
    CIL --> CBPF
    CAL --> FBPF
```

kube-proxy、Cilium agent 和 Calico Felix 都属于控制循环：监听对象变化，再把期望状态编译进节点。它们通常不在用户数据包的每次转发中充当一个进程级代理。数据包真正命中的是 Netfilter、IPVS 或挂在 socket/网络设备上的 BPF 程序。

这也解释了一个常见误区：安装了 Calico 或 Cilium，不自动等于它们已经接管 Service。普通 CNI 模式下，Pod 网络可能由 CNI 提供，Service 仍由 kube-proxy 实现。只有明确启用 kube-proxy replacement，并用运行时证据确认，才能把 Service 数据面归到 eBPF。

## 公共实验：只替换数据面

五个集群都使用两个 worker。每个 worker 上运行一个 Python HTTP 后端，客户端固定在 `worker2`。Service 配置如下：

```yaml
# 为两个后端 Pod 提供 ClusterIP 和固定 NodePort。
apiVersion: v1
kind: Service
metadata:
  name: echo-server
  namespace: service-dataplane
spec:
  # ClusterIP 和 NodePort 共用同一组后端，便于比较不同入口。
  type: NodePort
  selector:
    app.kubernetes.io/name: echo-server
  ports:
    # Service 80 端口转发到 Pod 的 8080 端口。
    - name: http
      port: 80
      targetPort: 8080
      nodePort: 30081
```

每次实验都检查四层证据：

1. 控制组件宣告的运行模式；
2. Service 和 EndpointSlice 的实际地址；
3. 新建连接命中了哪个后端、后端看到了什么源地址；
4. 对应内核数据结构里是否存在此前端和后端。

只看配置文件不够，因为配置可能没有被进程采用；只看请求成功也不够，因为另一个数据面可能仍在工作。

## iptables：把每个 Service 编译成链跳转

iptables 模式将 Service 和 Endpoint 编译成 `nat` 表中的链。一个 ClusterIP 请求通常经历：

```mermaid
flowchart LR
    P[Pod packet<br/>dst Service IP:80]
    S[KUBE-SERVICES]
    C[KUBE-SVC-*<br/>choose endpoint]
    E[KUBE-SEP-*<br/>DNAT]
    R[Pod IP:8080]

    P --> S --> C --> E --> R
```

`KUBE-SERVICES` 是总入口，`KUBE-SVC-*` 代表某个 Service，`KUBE-SEP-*` 代表某个 Endpoint。后端选择并不是应用层轮询；规则通过 `statistic --mode random --probability ...` 逐级决定跳到哪个 Endpoint 链，随后执行 DNAT。

上一篇实验中的 Service `10.96.123.219:80` 只有一个后端 `10.244.1.3:8080`，节点上产生了这组真实规则：

```text
-A KUBE-SVC-HMXYGTJUZB5GQ45C \
  -m comment --comment "service-path/source-ip:http -> 10.244.1.3:8080" \
  -j KUBE-SEP-MGYZ72N52QLOHZGY

-A KUBE-SEP-MGYZ72N52QLOHZGY -p tcp \
  -m comment --comment "service-path/source-ip:http" \
  -m tcp -j DNAT --to-destination 10.244.1.3:8080
```

只有一个后端时可以直接跳转；多个后端会形成带概率匹配的规则。连接第一次选定后，conntrack 记录 NAT 结果，后续数据包沿用同一映射。因此不能用一个长连接里的连续 HTTP 请求检验“是否轮询”，应当创建多条新连接。

iptables 模式的优点是成熟、普遍、排障资料多。代价是对象规模增长时会生成大量线性规则，规则同步和匹配结构都不如集合、map 直接。这里的“线性”描述的是规则组织方式，不等于每个包都会遍历集群全部 Service：跳转链、连接跟踪和具体入口会缩小实际路径。

实现入口可从 Kubernetes v1.35.0 的 [`pkg/proxy/iptables/proxier.go`](https://github.com/kubernetes/kubernetes/blob/v1.35.0/pkg/proxy/iptables/proxier.go) 对照：`syncProxyRules` 根据 Service 和 Endpoint 状态生成链，`writeServiceToEndpointRules` 写后端选择规则。

## nftables：语义相似，数据结构不同

nftables 模式仍由 kube-proxy 监听 Service 和 EndpointSlice，也仍依赖 Netfilter 完成 DNAT/SNAT。它把原先大量兼容 iptables 的链，改为 nftables 原生的 set、map、verdict map，并通过事务更新规则。

实验中的 Service 是 `10.96.66.43:80`，两个 Endpoint 是 `10.244.1.2:8080` 和 `10.244.2.2:8080`。节点上的规则包含：

```text
map service-ips {
  type ipv4_addr . inet_proto . inet_service : verdict
  elements = {
    10.96.66.43 . tcp . 80 : goto service-AKD4XF6G-service-dataplane/echo-server/tcp/http
  }
}

map service-nodeports {
  type inet_proto . inet_service : verdict
  elements = {
    tcp . 30081 : goto external-AKD4XF6G-service-dataplane/echo-server/tcp/http
  }
}
```

Service 前端通过拼接键 `地址 . 协议 . 端口` 直接映射到 verdict。进入具体 Service 链后，`numgen` 负责选择后端：

```text
chain service-AKD4XF6G-service-dataplane/echo-server/tcp/http {
  ip daddr 10.96.66.43 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
  numgen random mod 2 vmap {
    0 : goto endpoint-Q7LEI3H3-...__10.244.1.2/8080,
    1 : goto endpoint-JHFC3PET-...__10.244.2.2/8080
  }
}
```

Endpoint 链仍然执行 DNAT：

```text
chain endpoint-Q7LEI3H3-...__10.244.1.2/8080 {
  ip saddr 10.244.1.2 jump mark-for-masquerade
  meta l4proto tcp dnat to 10.244.1.2:8080
}
```

十二条新连接的命中结果是 6:6，但顺序不是严格交替：

```text
echo-server-jp7pk
echo-server-4q5nm
echo-server-jp7pk
echo-server-4q5nm
echo-server-jp7pk
echo-server-jp7pk
echo-server-jp7pk
echo-server-4q5nm
echo-server-4q5nm
echo-server-jp7pk
echo-server-4q5nm
echo-server-4q5nm
```

这与 `numgen random mod 2` 相符。样本恰好 6:6 不代表它承诺每 N 个连接绝对均匀。

[Kubernetes v1.35 文档](https://v1-35.docs.kubernetes.io/docs/reference/networking/virtual-ips/#nftables-proxy-mode)将 nftables 模式标为自 v1.33 起稳定，并指出它改善了大规模 Service 规则更新和处理性能。这里仍应区分“数据结构更适合扩展”和“你的工作负载一定更快”：最终收益还受内核版本、Service 数量、连接模型和观测工具影响。实现可对照 v1.35.0 的 [`pkg/proxy/nftables/proxier.go`](https://github.com/kubernetes/kubernetes/blob/v1.35.0/pkg/proxy/nftables/proxier.go)。

## IPVS：虚拟服务负责选择后端，但没有完全离开 iptables

IPVS 模式也由 kube-proxy 编程。它把 ClusterIP 建成 IPVS virtual service，把 Endpoint 建成 real server，并把 Service IP 绑定到虚拟接口 `kube-ipvs0`。

实验日志明确记录：

```text
"Using ipvs Proxier"
"The ipvs proxier is now deprecated and may be removed in a future release. Please use 'nftables' instead."
"IPVS scheduler not specified, use rr by default"
```

Service `10.96.169.181:80` 的 `/proc/net/ip_vs` 状态如下：

```text
TCP  0A60A9B5:0050 rr
  -> 0AF40202:1F90      Masq    1      0          0
  -> 0AF40102:1F90      Masq    1      0          0
```

十六进制解码后正好对应：

```text
0A60A9B5:0050 = 10.96.169.181:80
0AF40202:1F90 = 10.244.2.2:8080
0AF40102:1F90 = 10.244.1.2:8080
```

默认 `rr` 调度器让十二条新连接严格交替命中两个后端：

```text
echo-server-wdjwh
echo-server-4khtv
echo-server-wdjwh
echo-server-4khtv
```

后面八条保持相同交替顺序。这说明本次实验里选择逻辑来自 IPVS 的 round-robin，而不是 iptables 的随机概率规则。

但 IPVS 模式不是“删除所有 iptables”。实验节点仍有 `KUBE-SERVICES`、`KUBE-NODE-PORT` 和 ipset 匹配规则，用来处理捕获、标记、NodePort 和 masquerade 等环节；`kube-ipvs0` 上则绑定了 Service `/32` 地址：

```text
kube-ipvs0 DOWN 10.96.0.1/32 10.96.0.10/32 10.96.169.181/32
```

因此排查 IPVS 不能只看 `ipvsadm` 或 `/proc/net/ip_vs`，还要看 ipset、iptables 和 `kube-ipvs0`。Kubernetes v1.35 已将 IPVS proxier 标为 deprecated，官方建议迁往 nftables；新部署不应因为“IPVS 听起来更专业”就忽略这个生命周期信号。源码入口是 [`pkg/proxy/ipvs/proxier.go`](https://github.com/kubernetes/kubernetes/blob/v1.35.0/pkg/proxy/ipvs/proxier.go)。

## eBPF：把 Service 查表前移到 socket 或设备 hook

eBPF 不是一份统一的 Kubernetes Service 实现。Cilium 和 Calico 都能替换 kube-proxy，但它们的程序组织、map 布局、命令行工具和功能边界不同。共同点是：把前端/后端状态放入 BPF map，让挂载在内核 hook 上的程序查表并改写流量。

```mermaid
flowchart TB
    APP[Application opens connection]
    SOCK[cgroup socket hook]
    TC[TC or TCX device hook]
    MAP[BPF service and backend maps]
    POD[Backend Pod]

    APP --> SOCK
    SOCK -->|socket LB applies| MAP
    SOCK -->|packet continues| TC
    TC --> MAP
    MAP --> POD
```

图里画出两个可能入口，不表示每条连接一定重复做两次负载均衡。具体命中哪个 hook，取决于流量来源、协议、host network、配置和实现。

### Cilium：Service 表与 BPF LB map

`svc-cilium` 创建时禁用了默认 CNI 和 kube-proxy，再通过本地 v1.19.8 Helm chart 安装 Cilium，配置 `kubeProxyReplacement=true`。运行时状态是：

```text
Kubernetes:             Ok         1.35 (v1.35.0) [linux/amd64]
KubeProxyReplacement:   True
Attach Mode:            TCX
Device Mode:            veth
  Socket LB:            Enabled
  Mode:                 SNAT
```

同时查询 kube-proxy 得到：

```text
Error from server (NotFound): daemonsets.apps "kube-proxy" not found
```

状态明确启用了 replacement，集群中又没有 kube-proxy。因此这里的 Service 由 Cilium 接管，不是“Cilium CNI + kube-proxy”的组合。

实验 Service 是 `10.96.155.52:80`，Endpoint 是 `10.0.0.77:8080` 和 `10.0.1.216:8080`。`cilium-dbg service list` 显示：

```text
ID   Frontend               Service Type   Backend
6    0.0.0.0:30081/TCP      NodePort       1 => 10.0.0.77:8080/TCP (active)
                                           2 => 10.0.1.216:8080/TCP (active)
8    10.96.155.52:80/TCP    ClusterIP      1 => 10.0.0.77:8080/TCP (active)
                                           2 => 10.0.1.216:8080/TCP (active)
```

`cilium-dbg bpf lb list` 中又能找到相同映射：

```text
10.96.155.52:80/TCP (1)   10.0.0.77:8080/TCP (8) (1)
10.96.155.52:80/TCP (2)   10.0.1.216:8080/TCP (8) (2)
```

节点的 `iptables-save -t nat` 没有 `KUBE-SVC` 或 `KUBE-SEP`。客户端 `10.0.1.141` 发起请求，后端记录的是 `10.0.1.141:60184`，说明这条 Pod 到 ClusterIP 的路径保留了源 IP。

Cilium 官方的 [kube-proxy-free 文档](https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/) 说明了安装前提、API Server 地址和 replacement 验证方式；本次实验使用的 chart 对应 [`cilium/cilium v1.19.8`](https://github.com/cilium/cilium/tree/v1.19.8/install/kubernetes/cilium)。

### Calico：Felix 编程 BPF NAT map

`svc-calico-bpf` 先以普通 kube-proxy 引导，再让 Tigera Operator 管理替换过程。安装对象的关键字段是：

```yaml
# 让 Calico eBPF 接管 Linux 数据面和 kube-proxy 生命周期。
calicoNetwork:
  # 选择 BPF 数据面。
  linuxDataplane: BPF
  # 在替换早期维持到 Kubernetes API 的连通性。
  bpfNetworkBootstrap: Enabled
  # eBPF 就绪后由 Operator 停用 kube-proxy。
  kubeProxyManagement: Enabled
```

不能直接照抄仓库示例的 IPPool：示例使用 `192.168.0.0/16`，而本次 kind 集群的 Pod CIDR 是 `10.244.0.0/16`。第一次应用时，Operator 明确拒绝：

```text
IPPool 192.168.0.0/16 is not within the platform's configured pod network CIDR(s) [10.244.0.0/16]
```

将 IPPool 改为 `10.244.0.0/16` 后，运行时证据收敛为：

```text
calico              True        False         False      All objects available
ippools             True        False         False      All objects available
kubeproxy-monitor   True        False         False      All objects available
linuxDataplane=BPF bpfNetworkBootstrap=Enabled kubeProxyManagement=Enabled
bpfEnabled=true bpfConnectTimeLoadBalancing=TCP
```

kube-proxy DaemonSet 对象还在，但 Operator 给它增加了不会匹配节点的 selector，因此副本数为零：

```text
NAME         DESIRED   CURRENT   READY   NODE SELECTOR
kube-proxy   0         0         0       ...,operator.tigera.io/disable-kube-proxy=true
```

Calico 的结果和 Cilium 在表现上相似，但内部表和工具不同。Service `10.96.229.201:80` 在 `calico-node -bpf nat dump` 中是：

```text
10.96.229.201 port 80 proto 6 id 7 count 2 local 1
    7:0  10.244.71.194:8080
    7:1  10.244.159.66:8080
```

同一张表还包含节点 `172.30.0.16:30081` 到这两个后端的映射。客户端 `10.244.71.195` 发起 ClusterIP 请求，后端看到 `10.244.71.195:42264`；节点上同样没有 `KUBE-SVC/KUBE-SEP`。

安装字段可在 [`projectcalico/calico v3.31.7` 的 BPF 示例](https://github.com/projectcalico/calico/blob/v3.31.7/manifests/custom-resources-bpf.yaml) 中核对，工作原理和前置条件见 Calico 官方的 [Enable the eBPF data plane](https://docs.tigera.io/calico/latest/operations/ebpf/enabling-ebpf)。

## 四种实现到底差在哪里

| 维度 | iptables | nftables | IPVS | eBPF replacement |
| --- | --- | --- | --- | --- |
| 控制器 | kube-proxy | kube-proxy | kube-proxy | CNI agent / Felix |
| 主要状态 | Netfilter chains | sets、maps、chains | virtual service + real server | BPF maps |
| 主要执行位置 | Netfilter hooks | Netfilter hooks | IPVS hooks，辅以 Netfilter | socket、TC/TCX、XDP 等实现相关 hook |
| 本实验后端选择 | 概率链 | `numgen random` | `rr` | map + 实现自己的选择逻辑 |
| 首选观测入口 | `iptables-save` | `nft list table ip kube-proxy` | `/proc/net/ip_vs`、ipset、iptables | Cilium: `cilium-dbg service list`、`cilium-dbg bpf lb list`；Calico: `calico-node -bpf nat dump` |
| kube-proxy 是否存在 | 是 | 是 | 是 | replacement 模式下否或 0 副本 |
| Kubernetes v1.35 方向 | 成熟兼容 | 稳定、推荐关注 | 已 deprecated | 不属于 kube-proxy 内建模式 |

“eBPF 更快”不是足够的选型理由。它通常能更早处理流量、使用高效 map，并把网络策略、Service 和可观测性结合起来；与此同时，升级边界从 Kubernetes 本身扩展到了 CNI 版本、内核能力和特定 CLI。团队如果无法稳定获取 BPF 状态、理解 hook 和验证回退路径，理论收益可能换来更高的事故处置成本。

## 新建集群应该怎么选

```mermaid
flowchart TB
    A{是否准备采用<br/>Cilium 或 Calico eBPF?}
    A -->|是| B{内核、功能矩阵和<br/>运维工具是否已验证?}
    B -->|是| C[eBPF replacement]
    B -->|否| D[先保留 kube-proxy]
    A -->|否| E{Kubernetes 与内核<br/>是否满足 nftables 要求?}
    E -->|是| F[nftables]
    E -->|否| G[iptables]
    H[现有 IPVS 集群] --> I[制定并验证迁移到 nftables]
```

对新集群，我会按下面顺序判断：

- 已选 Cilium/Calico，且内核、NetworkPolicy、HostPort、NodePort、健康检查、可观测性和回退路径都经过验证：考虑 eBPF replacement。
- 仍使用 kube-proxy，版本和内核支持 nftables：优先验证 nftables，而不是新上已 deprecated 的 IPVS。
- 兼容性约束强，或现有自动化完全围绕 iptables：可以继续使用 iptables，但要实测大规模规则同步时间。
- 已运行 IPVS：不要只因 deprecated 立刻切换生产数据面；先做并行集群验证，比较源 IP、externalTrafficPolicy、健康检查、NodePort、防火墙和监控差异。

数据面切换不是改一行 `mode` 就结束。连接跟踪中的旧连接、节点滚动顺序、规则清理、监控告警和故障回退都应纳入迁移方案。

## 排障时先辨认实现，再查流量

同一个“ClusterIP 不通”，在不同数据面下应从不同入口开始。

### kube-proxy 三种模式

```bash
# 确认配置声明的模式。
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep '^mode:'

# 确认进程实际选择的 proxier。
kubectl -n kube-system logs daemonset/kube-proxy --tail=200 | \
  grep -E 'Using .* Proxier|deprecated'
```

然后分别检查：

```bash
# iptables
iptables-save -t nat | grep -E 'KUBE-SVC|KUBE-SEP'

# nftables
nft list table ip kube-proxy

# IPVS；没有 ipvsadm 时仍可读取内核状态。
cat /proc/net/ip_vs
ip -brief address show kube-ipvs0
```

### Cilium replacement

```bash
cilium-dbg status --verbose
cilium-dbg service list
cilium-dbg bpf lb list
```

### Calico eBPF

```bash
kubectl get tigerastatus
kubectl get felixconfiguration default -o yaml
calico-node -bpf nat dump
```

所有模式都还要回到共同输入：Service、EndpointSlice、Pod readiness、端口协议和连接跟踪。内核表里没有对应前端时，先查控制循环为什么没编程；表里有映射但请求仍失败，再查路由、策略、SNAT、MTU 和返回路径。

## 实验给出的结论

同一个 Kubernetes Service API，至少可以落成四类内核状态：iptables 链、nftables map、IPVS virtual service 或 BPF map。API 对象没有告诉你当前节点究竟使用哪一种；必须把配置、组件日志、内核状态和真实请求连起来验证。

本次实验还给出三个容易忽略的事实：

1. IPVS 负责后端调度，不代表节点上不再需要 iptables；
2. 安装 Cilium/Calico 不代表 Service 自动进入 eBPF，replacement 必须有运行时证据；
3. eBPF 也不是单一实现，Cilium 和 Calico 的启用方式、map 和诊断入口都不同。

下一篇会把视角从单个数据包和单个节点抬高到生产生命周期：DNS 如何返回 Service 名称、Endpoint 终止时连接如何排空，以及怎样用一套固定顺序定位“解析成功但连接失败”“偶发 502”“节点重启后 NodePort 异常”等问题。

