主题
五元组与会话表
NAT 是有状态的。这句话的全部含义就是:它维护一张表,把"内网视角的连接"和"外网视角的连接" 对应起来。这一篇把 VPP 那张表逐字段拆开。
先说五元组
要唯一标识一条网络连接,需要五个值:
text
┌─────────────┬─────────────┬──────────┬──────────┬──────────┐
│ 源 IP │ 目的 IP │ 源端口 │ 目的端口 │ 协议 │
├─────────────┼─────────────┼──────────┼──────────┼──────────┤
│ 10.10.0.11 │203.0.113.12 │ 52515 │ 9001 │ UDP │
└─────────────┴─────────────┴──────────┴──────────┴──────────┘为什么是五个?因为少任何一个都会撞车:
- 只有源和目的 IP → 同一台主机开两个浏览器标签访问同一个网站,分不开
- 加上端口,但不带协议 → TCP 的 53 端口和 UDP 的 53 端口会被当成同一条连接
NAT 会话不完全等于五元组
一个常见误解是"NAT 表就是五元组表"。实际上不同的 NAT 类型,索引用的字段数量不一样, 这正是下一篇 EI/ED 的核心。VPP 的 nat44-ed 用全部五元组,nat44-ei 只用其中三个。 先记住这个伏笔。
VPP 会话表的完整结构
把上一章那条会话再贴一次,这次逐行注释:
text
NAT44 ED sessions:
-------- thread 0 vpp_main: 1 sessions -------- ← ①
i2o 10.10.0.11 proto TCP port 39637 fib 0 ← ②
o2i 203.0.113.1 proto TCP port 39637 fib 0 ← ③
external host 203.0.113.11:9000 ← ④
i2o flow: match: saddr 10.10.0.11 sport 39637
daddr 203.0.113.11 dport 9000 proto TCP fib_idx 0
rewrite: saddr 203.0.113.1 sport 39637
daddr 203.0.113.11 dport 9000 txfib 0 ← ⑤
o2i flow: match: saddr 203.0.113.11 sport 9000
daddr 203.0.113.1 dport 39637 proto TCP fib_idx 0
rewrite: saddr 203.0.113.11 daddr 10.10.0.11
dport 39637 txfib 0 ← ⑥
index 0 ← ⑦
last heard 484.35 ← ⑧
timeout in 235.88 ← ⑨
total pkts 9, total bytes 541 ← ⑩
dynamic translation ← ⑪① 会话是按线程分片的
thread 0 vpp_main 说明这条会话属于 0 号线程。VPP 多 worker 运行时, 每条会话只属于一个 worker,各 worker 的会话表互不共享,也不加锁。
这带来一个必须理解的约束:同一条流的来回两个方向,必须被同一个 worker 处理, 否则回程包会在另一个 worker 的表里查不到会话。VPP 靠一致性哈希(RSS / 软件哈希) 保证同一条流的双向包落到同一个 worker 上。
这是一类真实故障的根源
如果哈希算法在两个方向上算出的结果不一致(比如网卡 RSS 只哈希了源地址), 就会出现"去程能出去,回程被丢"的诡异现象,而且是部分连接受影响。 第 7 章会讲怎么用 show nat44 hash tables 和 show interface rx-placement 定位。
②③ 两个索引键
这是同一条会话的两个"入口",对应两个查找方向:
text
i2o 键(内→外查) o2i 键(外→内查)
┌──────────────────┐ ┌──────────────────┐
│ 10.10.0.11:39637 │ │ 203.0.113.1:39637│
│ TCP, fib 0 │ │ TCP, fib 0 │
└──────────────────┘ └──────────────────┘
↓ ↓
└────── 同一条会话 ───────┘fib 0 是路由表(VRF)编号。多租户场景下不同租户可以用相同的内网地址段, 靠 fib 编号区分 —— 第 2 章的 VRF 一节会用到。
④ external host:ED 的身份证
text
external host 203.0.113.11:9000这一行说:这条会话是专门为了和 203.0.113.11 的 9000 端口通信而建的。
这就是 "Endpoint-Dependent"(端点相关)的全部含义。同一个内网 10.10.0.11:39637 如果去访问另一台服务器 203.0.113.12:9001,会新建另一条会话,可能分到不同的外部端口。
nat44-ei 的会话表里就没有这一行。这个差别的后果很大,值得单独一篇: 端点无关 vs 端点相关。
⑤⑥ 两个方向的 match + rewrite
这是会话表最核心的部分,也是最能体现"NAT 是一对改写而不是一个改写"的地方。
text
i2o(出方向)
match : saddr 10.10.0.11 sport 39637 daddr 203.0.113.11 dport 9000
↓ 命中后 ↓ 目的不变
rewrite : saddr 203.0.113.1 sport 39637 daddr 203.0.113.11 dport 9000
★改源 保持
o2i(回方向)
match : saddr 203.0.113.11 sport 9000 daddr 203.0.113.1 dport 39637
↓ 源不变 ↓ 命中后
rewrite : saddr 203.0.113.11 daddr 10.10.0.11 dport 39637
保持 ★改目的两个方向严格互为镜像。VPP 在建会话的那一刻就把两个方向的规则都算好存下来, 后续包只需查表 + 套用预先算好的改写,不需要再做任何决策。这是它能跑得快的原因之一。
排错时先看这四行
遇到"NAT 行为不对"的问题,直接看这四行 match/rewrite。 它们是 NAT 对这条流的全部意图,比推测配置效果可靠得多。
⑦⑧⑨⑩⑪ 元数据
| 字段 | 含义 | 排错用途 |
|---|---|---|
index 0 | 会话在表里的槽位号 | 配合 nat44 del session 精确删除 |
last heard 484.35 | 最后一次收到该流的包的时间戳(VPP 启动后秒数) | 判断流是不是已经静默 |
timeout in 235.88 | 还有多少秒就老化 | 判断是不是"表还没到期但对端已经断了" |
total pkts / bytes | 累计包数和字节数 | 判断流量是否真的在走;pkts 停止增长说明流已死 |
dynamic translation | 会话来源:动态建立 | 对比 static translation(静态映射产生的) |
快路径与慢路径
会话表带来一个自然的优化:首包要建会话(慢),后续包只需查表(快)。 VPP 把这两条路径做成了不同的 graph node。
从实测 trace 里能直接看到差别。下面是 ping -c 3 抓到的节点序列 (同一次 ping 的三个包 ICMP id 相同,所以是同一条会话):
真实的节点序列(show trace 输出整理后):
text
Packet 1(首包,in2out)
→ af-packet-input → ethernet-input → ip4-input → ip4-sv-reassembly-feature
→ nat-pre-in2out → nat44-ed-in2out → nat44-ed-in2out-slowpath ← 多了这一跳
→ ip4-lookup → ip4-rewrite → host-eth2-output → host-eth2-tx
Packet 2(回包,out2in)
→ af-packet-input → ethernet-input → ip4-input → ip4-sv-reassembly-feature
→ nat-pre-out2in → nat44-ed-out2in ← 回程分支
→ ip4-lookup → ip4-rewrite → host-eth1-output → host-eth1-tx
Packet 3(第二个请求,in2out)
→ af-packet-input → ethernet-input → ip4-input → ip4-sv-reassembly-feature
→ nat-pre-in2out → nat44-ed-in2out ← 没有 slowpath 了
→ ip4-lookup → ip4-rewrite → host-eth2-output → host-eth2-tx自己验证快慢路径
bash
./labctl reset base && ./labctl apply base 01
./labctl vppctl base clear trace
./labctl vppctl base trace add af-packet-input 20
ssh chenzhong@192.168.49.182 \
'docker exec clab-natbase-h1 ping -c 3 -i 0.3 -W 1 203.0.113.12'
./labctl vppctl base show trace想只看节点序列不看细节,可以过滤一下:
bash
./labctl vppctl base show trace \
| grep -E '^Packet |^[0-9]{2}:[0-9]{2}:[0-9]{2}:[0-9]{6}: '应该观察到
- 第 1 个包有
nat44-ed-in2out-slowpath,第 3、5 个包没有 - 回包走的是
nat-pre-out2in→nat44-ed-out2in,出接口变成host-eth1 - 回包的 NAT 节点里写着
NAT44_OUT2IN_ED_FAST_PATH ... via o2if—— 因为出方向的首包已经把 o2i 规则一起建好了,回包天生就在快路径上
这个设计还有一个重要含义:慢路径的成本远高于快路径。 一条长连接摊下来几乎没有 NAT 开销,但如果流量模式是大量短连接(每个连接只有几个包), NAT 设备就要不停地走慢路径 —— 这是 CGN 设备的主要压力来源,也是评估 NAT 性能时 "新建会话速率(CPS)"比"吞吐(bps)"更关键的原因。
会话表的容量
bash
./labctl vppctl base show nat44 summarytext
max translations per thread: 64512 fib 0
transitory tcp LRU min session timeout 417 (now 177)
total sessions: 2 (timed out: 0)
tcp sessions:
total: 2 (timed out: 0)
established: 0 (timed out: 0)
transitory: 2 (timed out: 0)
udp sessions:
total: 0 (timed out: 0)
icmp sessions:
total: 0 (timed out: 0)
other sessions:
total: 0 (timed out: 0)几个要点:
max translations per thread: 64512—— 每个线程的会话上限,默认值。 注意是"per thread",8 个 worker 就是 8 × 64512。可以在启用插件时调:nat44 plugin enable sessions 1000000- TCP 会话分 established 和 transitory —— 已建立的连接和处于握手/挥手中间态的连接, 两者超时时间差了三十倍(7440s vs 240s)。下一篇细讲。
timed out计数 —— 已经超时但还没被回收的会话。VPP 是懒回收: 不跑定时器扫全表,而是在需要腾位置时顺手清理过期项(那个 LRU 就是干这个的)。
手工删会话
调试时经常需要"把这条流的状态清掉,重新走一次慢路径":
bash
# 按内网侧删
./labctl vppctl base nat44 del session in 10.10.0.11:39637 tcp \
external-host 203.0.113.11:9000
# 按外网侧删
./labctl vppctl base nat44 del session out 203.0.113.1:39637 tcp \
external-host 203.0.113.11:9000nat44-ed 必须带 external-host
不带会直接失败:
text
nat44 del session: nat44_del_session returned -6道理就在这一页讲的会话键上:ED 的键包含外网端, 少了它这个键就是不完整的,当然找不到。外网端的地址端口从 show nat44 sessions 的 external host 那一行读。
(nat44-ei 的键里没有外网端,所以它的 nat44 ei del session 不需要这个参数。)
观察一次强制重建
- 建一条会话,记下端口:bash
ssh chenzhong@192.168.49.182 \ 'docker exec clab-natbase-h1 nc -w 2 203.0.113.11 9000' show nat44 sessions找到它- 删掉它,再连一次,会话表里会出现一条全新的(
index变了,端口可能也变了)
小结
一条 NAT 会话 = 两个索引键 + 两个方向的改写规则 + 一组元数据。
text
┌─────────────── 一条会话 ───────────────┐
│ │
i2o 键 ────→│ i2o: match … → rewrite … │←──── o2i 键
│ o2i: match … → rewrite … │
│ external host / timeout / pkts / … │
└────────────────────────────────────────┘
↑ 建于首包(慢路径)
后续包只查表(快路径)理解了这张表,NAT 的大部分行为都能推导出来。下一篇讲那个被我们埋了两次的伏笔: external host 这一行在与不在,会带来什么天差地别的后果。