Skip to content

CNAT:为 Kubernetes 而生的翻译器

这是全书最后一个插件,也是最不像"NAT"的一个。它的名字里那个 C 来自 Calico —— Kubernetes 生态里最主流的网络插件之一。

先说结论

CNAT 在 VPP 26.06 上无法通过 CLI 单独配出一条能用的负载均衡。 去程翻译工作正常(有 trace 佐证),但回程不做还原,连接建不起来。

原因不是有 bug 那么简单 —— CNAT 从设计上就不是给人手工配的, 它是给控制面(Calico 的 Felix agent)通过 API 驱动的。CLI 主要是调试接口。

这一篇讲清它是什么、能做什么、我实测到哪一步、以及为什么它长这样。 不假装能搭出一个可用的实验。

它想解决什么问题

Kubernetes 的 Service 本质上就是一个负载均衡器:

text
  ClusterIP 10.96.0.1:443
        ↓ 分流到
  Pod 10.244.1.5:6443
  Pod 10.244.2.7:6443
  Pod 10.244.3.9:6443

这件事的特殊之处在于规模和变化速度

text
  · 一个中等集群有几千个 Service,每个 Service 几个到几十个后端
  · Pod 随时被创建销毁,后端列表每秒都在变
  · 变更由控制面(kube-apiserver → Felix)推送,不是人手工敲
  · 要求变更**不打断已有连接**

传统方案是 iptablesipvs,两者在规则数上万时性能都会明显下降。 CNAT 是 VPP 给这个场景的答案:把 Service 的 VIP → Pod 的翻译做进 VPP 的转发图里。

命令族

bash
cnat translation [add|del] proto [TCP|UDP] [vip|real] <ip> <port> \
    [to <ip> <port>-><ip> <port>]
cnat client add <prefix> [fib <n>] [fwd-fib <n>] [return] [snat] [exclusive] [no-client]
cnat log [enable|disable] [ip <ip>]

set cnat feature <interface> [enable|disable]
set cnat snat-policy [none|if-pfx|k8s|dnat] [fib <id>]
set cnat snat-policy addr|if|prefix ...

show cnat client / session / snat-policy / timestamp / translation
test cnat maglev tests <n> backends <n> len <n>

几个名词一看就知道它的出身:

名词含义
vip / realService 的虚拟地址 / Pod 的真实地址
client需要被跟踪的源地址前缀(Pod 网段)
snat-policy k8s专门为 Kubernetes 准备的一套 SNAT 策略
maglevGoogle 的 Maglev 一致性哈希算法(见下文)

Maglev:为什么它值得一提

test cnat maglev 暴露了 CNAT 用的负载均衡算法。Maglev(Google 2016 年的论文) 解决的是一致性哈希在负载均衡里的一个具体痛点:

text
  普通哈希 hash(五元组) % 后端数
    ✗ 后端数变化 → 几乎所有连接重新分配 → 全部断开

  一致性哈希(如 Ketama)
    ✓ 后端变化只影响一小部分连接
    ✗ 负载分布不够均匀

  Maglev
    ✓ 后端变化只影响 1/N 的连接
    ✓ 分布非常均匀(用一张预计算的查找表)
    ✓ 查表 O(1),适合数据面

Maglev 的做法是预先算出一张固定大小的查找表(典型 65537 项), 每个后端按权重占据若干槽位。查表就是一次数组索引,极快。 后端变化时重算这张表,但算法保证大部分槽位的归属不变

这正是 K8s 场景需要的:Pod 频繁上下线,但不能因此打断无关连接。

对比第 2 章的 nat44 负载均衡

nat44 的负载均衡映射用的是普通五元组哈希, 而且改后端必须整条删掉重建,会打断所有已有连接。

CNAT 用 Maglev,理论上后端变化只影响一小部分连接。 这个差别在 Pod 每秒都在变的环境里是决定性的。

实测:去程能通,回程不还原

想自己复现的话

这一节没有配套的 apply.sh / verify.sh(因为跑不通), 下面是我实际敲的命令,可以照着走一遍看现象。

bash
./labctl up base && ./labctl reset base

N='./labctl vppctl base'
$N cnat translation add proto TCP vip 203.0.113.161 8080 \
    to 10.10.0.11 9000-\>10.10.0.12 9000
$N set cnat feature host-eth2 enable
$N set cnat feature host-eth1 enable

# 让 s1 能找到 VIP
ssh chenzhong@192.168.49.182 \
  'docker exec clab-natbase-s1 ip route replace 203.0.113.161/32 via 203.0.113.1
   docker exec clab-natbase-s1 nc -w 3 203.0.113.161 8080'

翻译建得起来:

text
[0] 203.0.113.161;8080 TCP lb:unknown fhc:0x9f(default)
10.10.0.11;9000->10.10.0.12;9000
  fib-entry:29
  [@0]: dpo-load-balance: [proto:ip4 index:31 buckets:1 ...]

连接却建不起来。抓 trace 看清了原因:

text
Packet 1(去程)
  → ip4-input
    TCP: 203.0.113.11 -> 203.0.113.161    TCP: 44027 -> 8080
  → cnat-input-ip4
  → ip4-lookup
    TCP: 203.0.113.11 -> 10.10.0.12       TCP: 44027 -> 9000   ← 改写成功
  → ip4-rewrite → host-eth1-output

Packet 2(回程)
  → ip4-input
    TCP: 10.10.0.12 -> 203.0.113.11       TCP: 9000 -> 44027
  → cnat-input-ip4                        ← 经过了 cnat 节点
  → ip4-lookup
    TCP: 10.10.0.12 -> 203.0.113.11       TCP: 9000 -> 44027   ← 地址没变!
  → cnat-output-ip4

去程的 DNAT 完全正确 —— VIP 203.0.113.161:8080 被改写成后端 10.10.0.12:9000

回程经过了 cnat 节点却没有改写。 客户端 s1 期待的是从 203.0.113.161:8080 回来的包,实际收到的是从 10.10.0.12:9000 来的包, TCP 栈判为陌生连接,直接丢弃。

试过但没解决的几条路

text
  set cnat snat-policy dnat            → "no snat policy for fib 0"
  set cnat snat-policy if-pfx          → 同样报错
  set cnat snat-policy if table ...    → 同样报错
  set cnat snat-policy addr <vip>      → 同样报错
  cnat client add 10.10.0.0/24 return  → client 建了,回程仍不还原
  cnat client add 10.10.0.0/24 snat    → 同样无效

snat-policy 这一族命令全部返回 no snat policy for fib 0, 看起来需要更前置的初始化 —— 而那个初始化很可能只存在于 API 路径上。

好消息:整个过程 VPP 没有崩溃,比 det44pnat 都文明。

多后端的语法也没跑通

to A portA->B portB 看起来是"一个路径"而不是"两个后端"。 重复 to 子句只有第一条生效:

bash
$N cnat translation add proto TCP vip 203.0.113.162 8080 \
    to 10.10.0.11 9000-\>10.10.0.11 9000 \
    to 10.10.0.12 9000-\>10.10.0.12 9000
text
[1] 203.0.113.162;8080 TCP lb:unknown fhc:0x9f(default)
10.10.0.11;9000->10.10.0.11;9000        ← 只有第一条

注意 lb:unknown —— 连负载均衡算法都没确定下来。 在正常工作的 CNAT 里这里应该显示 Maglev 相关的信息。

为什么它不是给人配的

把线索拼起来,CNAT 的定位就很清楚了:

text
  · snat-policy 有个专门的 k8s 模式
  · client / vip / real 这套术语直接来自 K8s Service 模型
  · 后端列表预期由控制面高频推送,不是人手工敲
  · CLI 的 translation 语法晦涩且多后端不work
  · 有 test cnat maglev 这种明显是给开发者的自测命令

CNAT 是 VPP 作为 Calico 数据面时的组件。 真实的使用路径是:

具体项目叫 Calico/VPPprojectcalico/vpp-dataplane)。 它用 VPP 替换掉 Linux 内核数据面,cnat 负责 Service 的负载均衡, 其他插件负责 Pod 网络、策略等。

这解释了为什么手工配不通

控制面在下发翻译时会一并配好 client、snat-policy、以及回程所需的状态。 只敲 cnat translation add 相当于只做了其中一步。

要真正把它跑起来,正确的路子是部署 Calico/VPP,而不是在 vppctl 里凑命令。 那超出了本教程的范围(需要一个 K8s 集群)。

什么时候会用到它

text
  ✓ 你在用 Calico/VPP 做 Kubernetes 的数据面
    → cnat 是其中的 Service 负载均衡组件,由 Felix 自动配置,
      你需要的是会看 show cnat session / translation 来排障

  ✗ 你想在 VPP 上做通用的四层负载均衡
    → 用 nat44 的负载均衡静态映射(第 2 章),
      虽然算法简单、改后端要断连接,但至少 CLI 配得通

  ✗ 你想要 Maglev 那样的一致性哈希
    → VPP 26.06 的 CLI 路径拿不到;
      要么走 Calico/VPP,要么用专门的 LB(LVS/Maglev 实现、云 LB)

排障时的可用命令

即使不能手工配置,这些 show 命令在 Calico/VPP 环境里是有用的:

bash
vppctl show cnat translation          # 有哪些 Service 翻译
vppctl show cnat translation <VIP>    # 某个 VIP 的后端列表
vppctl show cnat session              # 活跃会话
vppctl show cnat client               # 被跟踪的源地址前缀
vppctl show cnat snat-policy          # 当前 SNAT 策略
vppctl cnat log enable                # 打开日志

小结

定位Kubernetes Service 的负载均衡,Calico/VPP 的组件
算法Maglev 一致性哈希(后端变化只影响少量连接)
驱动方式控制面通过 API 下发,CLI 只是调试接口
实测结论去程 DNAT 正常,回程不还原,CLI 单独配不通
试过无效snat-policy 全族报 no snat policy for fib 0client add return/snat
好消息不崩 VPP
想做通用 LB第 2 章的 nat44 负载均衡映射

第 6 章到此结束。这两个插件放在一起看很有意思 —— 它们代表了 VPP NAT 家族的两个极端:

text
  pnat   最简单:两条规则,零状态,零智能
         你说改什么它就改什么,其余一概不管

  cnat   最复杂:Maglev 哈希、控制面驱动、为特定编排系统定制
         强大但不给人手工用

中间那一大片(nat44-ed / nat44-ei / det44 / nat64)才是日常真正会碰到的。

接下来第 7 章是最后一章:排错与性能。 把前六章散落的 show trace、错误计数器、各种坑串成一套可操作的排障方法, 并且讲清这个实验环境的性能数字为什么不能当生产参考。

实验环境基于 netlab + containerlab + VPP 26.06