CNI封装导致的MTU问题:怎么算开销、怎么验证路径、怎么改与MSS钳制
MTU(最大传输单元) 是一条链路单次能承载的最大 IP 包字节数,以太网默认 1500。问题出在封装:VXLAN、IP-in-IP、WireGuard 都要在原始包外面再套一层包头,套完之后的包会超过底层链路能承载的大小,于是被丢弃或分片。
Pod 网络里的 MTU 问题有一个极为鲜明的特征:
小包正常 ping、健康检查、TCP 建连都通
大包不通 上传大文件卡死、TLS 握手偶发失败、镜像层拉不下来
之所以是这个表现,是因为 MTU 只在包「大到一定程度」时才起作用。懂得这一点,排查方向立刻从「网络不通」收敛到「MTU 不对」。
各封装的额外字节数(IPv4 外层):
IP-in-IP (IPIP) 20 字节 仅多一个外层 IPv4 头
VXLAN (IPv4) 50 字节 外层 IPv4 20 + UDP 8 + VXLAN 8 + 内层以太网 14
VXLAN (IPv6) 70 字节 外层 IPv6 40 + UDP 8 + VXLAN 8 + 内层以太网 14
WireGuard (IPv4) 60 字节 IPv4 20 + UDP 8 + WireGuard 32
WireGuard (IPv6) 80 字节 IPv6 40 + UDP 8 + WireGuard 32
Geneve 约 58 字节 基础头(bucket 内选项会变化,Cilium 按 58 计算)
IPsec 约 77 字节 按 128 位密钥估算,实际随算法与密钥长度变化
host-gw / 原生路由 0 字节 无封装,直接用底层 MTU
物理 MTU 为 1500 时,常见的 Pod MTU 结果:
无封装 / host-gw 1500
IP-in-IP 1480 1500 - 20
VXLAN 1450 1500 - 50
VXLAN(IPv6 外层) 1430 1500 - 70
Geneve 1442 1500 - 58
VXLAN + WireGuard 1390 / 1370 按 60 或 80 计,取决于采用哪家的开销
VXLAN + IPsec 1373 1500 - 50 - 77
不同厂商对同一个封装的取值并不完全一致,WireGuard 是最典型的一个:
Calico 文档 IPv4 外层 60 字节,IPv6 外层 80 字节
Cilium 源码 常量 WireguardOverhead = 80(按 32 + 40 + 8 计算,
外层头按 40 字节保守估计,不区分 v4/v6)
也就是说,照抄不同项目文档里的数字会差 20 字节。选型之后请以你实际使用的那个项目的文档为准,并以实测验证(见下一节)。
Cilium 的常量还给出了另外几个值:TunnelOverheadIPv4 = 50、TunnelOverheadIPv6 = 70、DsrTunnelOverhead = 12(GENEVE DSR 选项)、IPIPv4Overhead = 20、IPIPv6Overhead = 48、EncryptionIPsecOverhead = 77。
第一步:看接口的 MTU。
# 节点上的封装接口
ip link show | grep -E "flannel|cilium_vxlan|tunl0|vxlan|geneve"
ip -d link show flannel.1
ip -d link show cilium_vxlan
ip link show tunl0
# Pod 内的网卡
kubectl exec -it <pod> -- ip link show eth0
kubectl exec -it <pod> -- cat /sys/class/net/eth0/mtu
# 节点上的 veth 对
ip link show | grep -E "^[0-9]+: (veth|lxc)"
第二步:看路由的 MTU。 隧穿与加密模式下,Cilium 会把开销扣在路由上而不是设备上:
kubectl exec -it <pod> -- ip route show default
# default via 10.244.0.1 dev eth0 mtu 1380 ← 这个 mtu 才是实际生效值
kubectl exec -it <pod> -- ip route get 8.8.8.8
# 节点上
ip route show
遇到「Pod 的 eth0 MTU 是 1460,但路由显示 1380」不要困惑 —— 这是正常的,Cilium 在 WireGuard 场景下就是这样设计的:设备 MTU 保持底层链路的 MTU,开销扣在路由 MTU 上。判断实际可用大小时要看路由上的 mtu。
**第三步:用 ping 实测路径能承载多少字节。**这是最可靠的验证方式:
# -M do 禁止分片,-s 指定 ICMP 载荷大小(不含 28 字节的 IP+ICMP 头)
kubectl exec -it <pod> -- ping -M do -s 1472 -c 3 <对端IP> # 1500 - 28
kubectl exec -it <pod> -- ping -M do -s 1422 -c 3 <对端IP> # 1450 - 28,VXLAN
kubectl exec -it <pod> -- ping -M do -s 1452 -c 3 <对端IP> # 1480 - 28,IPIP
kubectl exec -it <pod> -- ping -M do -s 1352 -c 3 <对端IP> # 1380 - 28
# 二分法找临界值
for s in 1472 1442 1400 1350 1300; do
echo -n "$s: "
ping -M do -s $s -c 1 -W 2 <对端IP> >/dev/null 2>&1 && echo OK || echo FAIL
done
不要在 Pod 内部自己 ping 自己 —— 那走的是 loopback,MTU 是 65536,永远成功。必须跨节点 ping。
第四步:看 TCP 的 MSS。
# 建一条连接后看实际协商的 MSS
ss -ti | grep -A1 ESTAB
# 抓包看 SYN 里的 MSS 选项
tcpdump -i eth0 -nn -v 'tcp[tcpflags] & tcp-syn != 0'
第五步:确认 ICMP 有没有被挡。
# 排查 PMTUD 黑洞:路径上是否有 ICMP type 3 code 4(fragmentation needed)
tcpdump -i any -nn 'icmp and icmp[0] = 3 and icmp[1] = 4'
# 路径 MTU 探测
tracepath <对端IP>
tracepath -n <对端IP>
1. TCP 建连时双方协商 MSS,依据是本地接口 MTU
2. 中间路径的 MTU 更小,包发出去超限
3. 转发设备应该回一个 ICMP "fragmentation needed"(type 3 code 4)
4. 如果这个 ICMP 被防火墙/安全组挡掉,发送方永远收不到通知
5. 包不断重传、不断被丢 —— 连接挂住,直到超时
这就是PMTUD(路径 MTU 发现)黑洞。表现是「连得上,但一传数据就卡死」。云环境中安全组默认往往不放通 ICMP,所以黑洞很常见。
比改 MTU 更直接的解法是在 SYN 包里改 MSS,让双方从一开始就用小一点的段:
# 节点上手工加规则(回程方向也要加)
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --set-mss 1360
# 检查现有规则
iptables -t mangle -L FORWARD -n -v | grep TCPMSS
nft list ruleset | grep -i mss
大多数 CNI 已经自动做了这件事(Calico、Cilium、Flannel 都会在自己的规则链里钳制 MSS),所以通常不需要手工加。但要注意两点:MSS 钳制只对 TCP 生效,UDP 与 ICMP 不受保护;而且它只对经过该节点转发的流量生效。
Calico:
# Operator 安装
kubectl patch installation.operator.tigera.io default --type merge \
-p '{"spec":{"calicoNetwork":{"mtu":1440}}}'
# Manifest 安装
kubectl patch configmap/calico-config -n kube-system --type merge \
-p '{"data":{"veth_mtu": "1440"}}'
kubectl rollout restart daemonset calico-node -n kube-system
Calico 默认会自动探测:依据节点网卡 MTU 与启用的封装模式(vxlanEnabled / ipipEnabled / wireguardEnabled / wireguardEnabledV6)算出合适的值。
Cilium:
helm upgrade cilium oci://quay.io/cilium/charts/cilium 1.20.2 \
--namespace kube-system --reuse-values --set MTU=1450
# 或改 ConfigMap
kubectl -n kube-system edit configmap cilium-config # mtu: "1450"
kubectl -n kube-system rollout restart daemonset cilium
Cilium 的 MTU 值不会改动宿主网卡(eth0/ens0)的 MTU,改的是 cilium_net@cilium_host、cilium_host@cilium_net、cilium_vxlan 与 lxc_health 这些接口。设为 0 表示自动探测。
Flannel:
# net-conf.json 或 helm values 里指定 Backend.MTU
{
"Network": "10.244.0.0/16",
"Backend": { "Type": "vxlan", "MTU": 1400 }
}
kubectl -n kube-system edit configmap kube-flannel-cfg
kubectl -n kube-system rollout restart daemonset kube-flannel-ds
Flannel 会把结果写进 /run/flannel/subnet.env 的 FLANNEL_MTU,CNI 插件读它来设置 Pod 网卡;CNI 配置里显式的 delegate.mtu 会覆盖它。
# 确认 Flannel 实际下发值
cat /run/flannel/subnet.env
ip link show flannel.1
不要假设底层就是 1500:
GCE 1460 google 的 MTU 不是 1500
AKS 1400 网卡报 1500,但实际承载只有 1400
AWS Jumbo 9001 开启巨型帧后
OpenStack 1450 底层已是 VXLAN 时
AKS 尤其要注意:因为 WireGuard 会给包打上 DF 位,AKS 上必须按 1400 再减 60(IPv6 减 80)。
ping 通、TCP 建连通,但一传数据就卡住;或者换小文件正常、大文件必挂。ping -s 的值要减去 28。-s 是 ICMP 载荷长度,加上 20 字节 IP 头与 8 字节 ICMP 头才是实际 IP 包大小。验证 1500 的链路要 -s 1472,验证 1450 的 Pod MTU 要 -s 1422。少减 28 会得出错误结论。ip link show eth0 显示 1460 而 ip route show default 显示 mtu 1380 是正常的。判断实际可用值要看路由,不能只看网卡。WireguardOverhead 常量是 80。照抄错的数字会差 20 字节,导致「按文档配完了还是不通」。以实测为准。flannel.1 设备。某些版本会复用已存在的设备而不更新它的 MTU,导致 FLANNEL_MTU 与实际设备不一致,需要手工 ip link set dev flannel.1 mtu <值> 或干脆删掉设备让它重建。tracepath 与 traceroute 的结果要交叉验证。tracepath 会做路径 MTU 探测,但依赖 ICMP 回包;如果 ICMP 被部分过滤,结果会误导。配合 ping -M do 的二分法更可靠。ping -M do 二分法测的是端到端真实路径,这也是它比看配置更可靠的原因。network-troubleshooting — MTU在整体排查中的位置cni — 封装模式的来源calico — vxlan/ipip/wireguard 的 MTU 配置cilium — Geneve/VXLAN/WireGuard 的 MTU 模型flannel — vxlan 后端的 MTU 下发conntrack — 另一类「老连接正常、新连接异常」的问题ksniff — 抓包确认大包是否被丢弃