主题
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)推送,不是人手工敲
· 要求变更**不打断已有连接**传统方案是 iptables 或 ipvs,两者在规则数上万时性能都会明显下降。 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 / real | Service 的虚拟地址 / Pod 的真实地址 |
client | 需要被跟踪的源地址前缀(Pod 网段) |
snat-policy k8s | 专门为 Kubernetes 准备的一套 SNAT 策略 |
maglev | Google 的 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 没有崩溃,比 det44 和 pnat 都文明。
多后端的语法也没跑通
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 9000text
[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/VPP(projectcalico/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 0;client add return/snat |
| 好消息 | 不崩 VPP |
| 想做通用 LB | 用第 2 章的 nat44 负载均衡映射 |
第 6 章到此结束。这两个插件放在一起看很有意思 —— 它们代表了 VPP NAT 家族的两个极端:
text
pnat 最简单:两条规则,零状态,零智能
你说改什么它就改什么,其余一概不管
cnat 最复杂:Maglev 哈希、控制面驱动、为特定编排系统定制
强大但不给人手工用中间那一大片(nat44-ed / nat44-ei / det44 / nat64)才是日常真正会碰到的。
接下来第 7 章是最后一章:排错与性能。 把前六章散落的 show trace、错误计数器、各种坑串成一套可操作的排障方法, 并且讲清这个实验环境的性能数字为什么不能当生产参考。