主题
端口分配与会话老化
前面留了两个问题:那个外部端口是怎么挑出来的?会话什么时候被回收? 这一篇回答它们 —— 顺带解释一类最难查的 NAT 故障。
端口是怎么挑的:先试着不改
你可能已经注意到,好几次实测里源端口都原样保留了:
text
i2o 10.10.0.11 proto TCP port 39637
o2i 203.0.113.1 proto TCP port 39637 ← 一模一样这不是巧合。NAT 的端口分配策略是端口保持优先(port preservation): 能不改就不改。原因有二 —— 一是省事,二是有些老旧协议会在载荷里带自己的端口号, 改了会出问题(虽然正确做法是用 ALG 处理,但少改总是少一分风险)。
那什么时候必须改?冲突的时候。
制造一次端口冲突
让 h1 和 h2 用同一个源端口 30000 访问同一个目的:
bash
ssh chenzhong@192.168.49.182 '
docker exec clab-natbase-h1 sh -c "echo x | nc -u -p 30000 -w 1 203.0.113.11 9001"
docker exec clab-natbase-h2 sh -c "echo x | nc -u -p 30000 -w 1 203.0.113.11 9001"'text
[s1 udp/9001] 我看到你的地址是 203.0.113.1:30000 ← h1,保留了
[s1 udp/9001] 我看到你的地址是 203.0.113.1:49895 ← h2,被迫改了会话表:
text
i2o 10.10.0.11 proto UDP port 30000
o2i 203.0.113.1 proto UDP port 30000 ← 先到先得
i2o 10.10.0.12 proto UDP port 30000
o2i 203.0.113.1 proto UDP port 49895 ← 只好另挑一个为什么第二个必须改?看 o2i 方向就明白了。如果两条会话都用 203.0.113.1:30000, 那么 s1 回过来的包(目的 203.0.113.1:30000)到底该发给 h1 还是 h2?没法判断。 o2i 键必须唯一,这是硬约束。
text
o2i 键必须唯一
┌──────────────────────┬──────────────────┐
│ 203.0.113.1:30000 │ → 10.10.0.11 │ ✓
├──────────────────────┼──────────────────┤
│ 203.0.113.1:30000 │ → 10.10.0.12 │ ✗ 冲突!
└──────────────────────┴──────────────────┘
所以第二条只能换端口ED 的端口复用优势
这里能看到 ED 的一个实际好处。ED 的 o2i 键包含外网端,所以:
text
ED 的 o2i 键(唯一性要求宽松)
┌──────────────────────┬──────────────────────┬────────────┐
│ 外部地址端口 │ 外网端 │ → 内网 │
├──────────────────────┼──────────────────────┼────────────┤
│ 203.0.113.1:23456 │ 203.0.113.11:9001 │ 10.10.0.11 │ ✓
│ 203.0.113.1:23456 │ 203.0.113.12:9001 │ 10.10.0.11 │ ✓ 不冲突
└──────────────────────┴──────────────────────┴────────────┘
↑ 目的不同,键就不同这正是上一篇里 ED 能对两个目的复用同一个外部端口 23456 的原因。 换成 EI,一个外部端口一旦被某个内网端点占用,就对所有目的都被占用了。
结论:ED 的端口利用率高于 EI。 运营商级 NAT 要在有限的公网地址上撑起几万用户, 端口是最稀缺的资源 —— 这是 CGN 普遍选择端点相关(对称型)的现实原因, 哪怕它牺牲了 P2P 体验。
一个公网地址到底能撑多少用户
端口号是 16 位,理论上 65536 个。但:
text
65536 理论总数
-1024 0-1023 是知名端口,通常不用于源端口
──────
64512 可用池
一个用户同时能开多少个会话?
· 轻度浏览:几十条
· 打开一个现代网页:单页就可能几十上百条(各种资源、追踪、长连接)
· 挂着几个 App 后台同步:持续几十条
· 用 P2P 下载:能到上千条
按每用户平均 100 条并发会话算:
64512 / 100 ≈ 645 个用户 / 每个公网地址这个数字解释了两件事:
- 为什么运营商 NAT 要用地址池而不是单地址 —— 一个地址几百个用户, 一个城域网几十万用户,需要成百上千个公网地址。
- 为什么 P2P 下载会被运营商限制 —— 一个重度 P2P 用户能吃掉几十个普通用户的端口配额。
VPP 的会话上限是另一个独立限制
bash
./labctl vppctl base show nat44 summarytext
max translations per thread: 64512 fib 0注意这是每线程的会话数上限,和端口数是两回事:
- 端口耗尽 → 某个池地址上没有可用端口了,新会话拿不到映射
- 会话表满 → 表里塞不下更多条目了,无论端口够不够
调整方式是在启用插件时指定:
bash
vppctl nat44 plugin enable sessions 1000000会话老化:四种超时
会话不能永久保留,否则表迟早撑爆。VPP 按协议和状态分了四类超时:
bash
./labctl vppctl base show nat timeoutstext
udp timeout: 300sec
tcp-established timeout: 7440sec
tcp-transitory timeout: 240sec
icmp timeout: 60sectext
┌──────────────────┬────────┬────────────────────────────────────────┐
│ 类型 │ 默认值 │ 为什么是这个数量级 │
├──────────────────┼────────┼────────────────────────────────────────┤
│ tcp-established │ 7440s │ ≈124 分钟。RFC 5382 要求至少 2 小时 4 分。│
│ │ │ TCP 连接可以长时间静默(SSH 挂着不动), │
│ │ │ 提前回收会导致连接莫名其妙断掉。 │
├──────────────────┼────────┼────────────────────────────────────────┤
│ tcp-transitory │ 240s │ 握手中、挥手中的连接。这些状态本就该很快 │
│ │ │ 结束,长时间停留说明对端已经没了。 │
├──────────────────┼────────┼────────────────────────────────────────┤
│ udp │ 300s │ UDP 无连接,只能靠"多久没流量"来判断死活。 │
│ │ │ RFC 4787 建议不低于 2 分钟。 │
├──────────────────┼────────┼────────────────────────────────────────┤
│ icmp │ 60s │ ping / traceroute 都是短命交互。 │
└──────────────────┴────────┴────────────────────────────────────────┘TCP 的两个超时差了三十倍,这个设计很关键:大部分表项压力来自半开连接 (SYN 发出去没人应、FIN 之后对端消失)。给它们一个短超时,能让表快速自愈; 而真正建立好的连接享受长超时,不会被误伤。
会话表里能直接看到剩余时间:
text
last heard 484.35 ← 最后一次见到这条流的包
timeout in 235.88 ← 还有 235 秒老化调整超时
bash
# 单独调 UDP
./labctl vppctl base set nat timeout udp 120
# TCP 两个必须一起给
./labctl vppctl base set nat timeout tcp-established 3600 tcp-transitory 120
# 恢复默认
./labctl vppctl base set nat timeout reset调短超时是有代价的
"表快满了,把超时调短点" 是个很自然的念头,但要想清楚代价:
- 调短 tcp-established → 静默的长连接(SSH、数据库连接池、MQTT)会被悄悄回收。 症状是用户抱怨"放着不动一会儿就断了",而且只有闲置久的连接受影响,极难复现。
- 调短 udp → 依赖 NAT 保活的应用(VoIP 注册、游戏心跳)需要更频繁地发保活包, 否则通话中途单向没声音。
更好的做法通常是加地址池、加会话上限,而不是压缩超时。
懒回收:为什么会话数会"超过"上限
VPP 不跑定时器去扫全表 —— 那样在几百万会话时开销不可接受。它用的是懒回收: 只在需要腾位置时,顺手清理过期项。
bash
./labctl vppctl base show nat44 summarytext
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)注意 timed out: 那个计数 —— 它统计的是已经过期但还占着位置的会话。 这个数字大于零是正常的,不是故障。那个 LRU 就是懒回收用的最近最少使用链表。
一类最难查的故障:会话被提前回收
把上面几件事串起来,就能理解 NAT 故障里最讨厌的一类:
症状是"长连接用着用着就废了,重连一下就好"。排查要点:
show nat44 summary看会话总数是不是逼近上限show errors | grep -i nat看no translation计数是不是在涨- 对着
show nat timeouts和应用的保活间隔比一比 —— 保活间隔必须明显小于 NAT 超时
经验值
应用的保活间隔取 NAT 超时的一半以内比较安全。 比如 UDP 超时 300 秒,保活最好 60~120 秒发一次。
反过来,如果你是运维 NAT 设备的一方,收到"长连接会断"的报障, 先问对方保活间隔是多少,再对照自己的超时配置 —— 十有八九对不上。
顺带:EI 能看到每个地址的端口占用
一个小差异。nat44-ei 的地址显示会带上端口占用统计:
bash
./labctl vppctl base show nat44 ei addressestext
NAT44 pool addresses:
203.0.113.1
tenant VRF independent
0 busy other ports
0 busy udp ports
0 busy tcp ports
0 busy icmp portsnat44-ed 的 show nat44 addresses 没有这几行。想在 ED 下判断端口压力, 就得靠 show nat44 summary 的会话数,或者 show errors 里的分配失败计数。
小结
| 问题 | 答案 |
|---|---|
| 端口怎么挑? | 优先保留原端口,冲突时才另选 |
| 什么算冲突? | o2i 键重复。ED 的键含外网端,所以冲突概率更低、端口利用率更高 |
| 一个公网地址能撑多少人? | 数量级几百,取决于人均并发会话数 |
| 会话什么时候回收? | 四种超时:TCP 已建立 7440s / TCP 中间态 240s / UDP 300s / ICMP 60s |
| 回收怎么执行? | 懒回收 + LRU,不扫全表;timed out 计数大于零是正常的 |
| 最难查的故障? | 保活间隔 > NAT 超时,导致长连接被静默回收 |
第 1 章到此结束。你现在手上有了拆解任何 NAT 行为所需要的全部概念: 改写字段、会话表结构、EI/ED 索引差异、端口分配、老化机制。
第 2 章开始,我们把这些概念一个个落到 VPP 的具体配置上。