MTU 不匹配引起的丢包问题在 Kubernetes 集群里非常隐蔽。它的典型表现是:小包一切正常,服务偶尔能通,但一旦传输较大的数据就卡住;或者 TCP 握手成功、HTTP 响应头都收到了,body 却迟迟传不完。很多团队在排查这类问题时会怀疑应用层超时设置、DNS 解析,甚至怀疑 Kube-proxy 规则,绕了一大圈最后发现是网络层 MTU 配置不一致。这篇文章就把这类问题的原理、排查手法和修复方案一次讲清楚。

为什么 MTU 不匹配会导致丢包
先回顾基础概念。MTU(Maximum Transmission Unit)指的是链路层一次能传输的最大数据帧载荷,标准以太网默认是 1500 字节。当 IP 层要发送的数据报文超过出口接口的 MTU 时,有两条路可走:一是分片,把大包切成多个小包分别发送,到目的地再重组;二是直接丢弃。
走哪条路取决于 IP 头部的 DF(Don't Fragment)标志位。现代操作系统发送 TCP 报文时几乎都默认置位 DF,目的就是为了避免分片带来的性能损耗。这时候如果路径上某一跳的 MTU 小于报文长度,正常流程是这一跳返回一个 ICMP Destination Unreachable(Type 3,Code 4)报文,告诉源端路径上能通过的最大 MTU,源端收到后调整分段大小重发,这套机制叫 PMTUD(Path MTU Discovery)。
问题恰恰出在 ICMP 被丢弃的场景。很多云环境、防火墙策略、安全组默认丢弃 ICMP 报文,源端收不到反馈,只能不断以原尺寸重发,结果就是全部被丢弃,表现为传输黑洞:连接建立正常,数据一动就卡死。还有一种情况更隐蔽,就是收到的包因为封装后超出物理 MTU,在内核统计里表现为接口 InDiscards 或分片失败计数上涨,应用层只看到随机超时。
为什么容器集群更容易踩坑
裸机时代两台机器直连,MTU 都是 1500,基本不会出问题。但 Kubernetes 集群的网络栈叠加了多层封装,每加一层就吃掉一段头部空间。常见的封装开销如下表:
| 封装类型 | 额外头部开销 | 建议内层 MTU |
|---|---|---|
| IPIP | 20 字节 | 1480 |
| VXLAN | 50 字节(外层 ETH+IP+UDP+VXLAN) | 1450 |
| GRE | 4 至 24 字节 | 1476 |
| WireGuard | 60 字节(IPv4 场景) | 1440 |
| IPSec ESP(隧道模式) | 约 50 至 74 字节 | 视算法而定 |
如果物理网卡 MTU 是 1500,VXLAN 的 VTEP 设备 MTU 也设成 1500,那么 Pod 发出的 1500 字节报文经过 VXLAN 封装后实际变成 1550 字节,超过物理网卡承载能力。要么内核强行分片,要么因为 DF 位置位而丢弃。Flannel VXLAN 模式默认会把 flannel.1 接口设为 1450,就是为了预留这 50 字节。
另一个典型坑是云厂商托管集群叠加自建隧道。比如底层的 VPC 网络本身有 50 字节的隧道开销,实际可用 MTU 只有 1450,你在其上再跑一层 CNI 的 VXLAN,如果 CNI 仍按 1500 物理网卡计算并设置 1450 的接口 MTU,封装后就是 1500,又撞上了 VPC 的上限,继续丢包。这就要求逐层累减,最终 Pod 网络的 MTU 要等于物理 MTU 减去所有隧道开销之和。此外,跨可用区或跨机房链路的 MTU 也可能不同,排查时不能只盯着同网段节点。
实战排查步骤
第一步:确认各环节 MTU 配置
在异常链路两端的节点上执行 ip link show,列出所有接口的 MTU 值,重点看物理网卡、CNI 创建的网桥(如 cni0)、隧道设备(如 flannel.1、tunl0)以及 Pod 的 veth pair。检查思路是保证每一层封装后的总尺寸不超过下一层的 MTU。
# 查看所有接口的 MTU ip link show | grep -E 'mtu' # 只看关键设备的 MTU cat /sys/class/net/cni0/mtu cat /sys/class/net/flannel.1/mtu # 查看 Pod 内部接口 MTU kubectl exec -it mypod -- ip link show eth0
如果发现 Pod 内是 1500 而节点隧道接口是 1450,基本可以锁定方向。也可以直接进入 Pod 用 ip addr 确认,注意 veth 两端的 MTU 是独立设置的,CNI 配置错误时两端可能不一致。
第二步:用带 DF 位的 ping 二分定位
这是最关键的一步。利用 ping 的 -M do 参数强制置位 DF,配合 -s 指定载荷大小,从大到小逐级测试,找到路径上实际能通过的最大报文尺寸。ICMP 头部占 8 字节,IP 头部占 20 字节,所以载荷大小加 28 才等于实际 IP 报文长度。
# 从 Pod 内 ping 对端 Pod,DF 位置位,载荷 1472(对应 1500 字节报文) ping -M do -s 1472 10.244.1.23 # 如果失败,逐步减小载荷 ping -M do -s 1422 10.244.1.23 ping -M do -s 1372 10.244.1.23 # 也可以直接在节点间测试物理路径的可用 MTU ping -M do -s 1392 -c 3 192.168.1.20
如果 1372 能通、1392 不通,说明路径 MTU 大约在 1400 到 1420 之间,对照节点配置就能找到是哪一跳设置过大。也可以用二分法加速定位。相比之下,不带 -M do 的普通 ping 会允许分片,即使 MTU 不匹配也可能通过,这就是为什么有人反馈 ping 通但业务不通,一定要养成加 DF 位测试的习惯。
第三步:抓包和内核计数验证
当 ping 测试还不够确凿时,用 tcpdump 在转发路径上的节点抓包,观察报文是否到达、是否被分片、ICMP 是否有回传。同时在物理网卡上抓外层封装后的包,确认实际帧长。
# 在节点上抓 ICMP unreachable,验证 PMTUD 是否被阻断 tcpdump -i eth0 -n icmp and 'icmp[icmptype] == icmp-unreach' # 抓取发往目标 Pod 的大包,观察是否有分片标志 tcpdump -i eth0 -n host 10.244.1.23 and greater 1400 # 查看接口丢包统计 ip -s link show eth0
内核计数方面,netstat -i 和 ip -s link 输出中的 TX-OK 正常但 RX 端大量丢弃、或者 /proc/net/snmp 里 Ip:FragFails 持续增长,都是 MTU 问题的强信号。如果是 IPsec 环境,还要检查 xfrm 统计。多角度交叉验证后,问题定位就非常扎实了。
修复方案与注意事项
定位到问题后,修复思路是统一调小 MTU,让所有设备在封装后不超出物理链路上限。临时验证可以直接改接口:ip link set dev eth0 mtu 1450,能通之后再固化到配置。永久方案分几种:CNI 层面,Flannel 可在 ConfigMap 里设置 {"Network": "10.244.0.0/16", "Backend": {"Type": "vxlan"}} 并通过 ip-mtu 参数覆盖;Calico 可在 ippool 或全局 BGP 配置中指定 MTU;云厂商托管集群通常提供集群级网络配置项,改动后需要重建节点或滚动重启相关 Pod 才能生效。
有两点提醒。第一,修改 MTU 是有副作用的操作,调小会略微降低大流量传输的效率(头占比增大),对延迟敏感的小包业务几乎无感知。第二,环境里如果启用了巨型帧(Jumbo Frame,MTU 9000),排查时务必确认链路上每一跳都支持,任何一跳是 1500 都会成为瓶颈。建议在集群上线初期就用 DF 位 ping 把整条路径的 MTU 摸排一遍,并把各节点 MTU 校验加入巡检脚本,避免后期能力退化带来的隐蔽故障。MTU 问题虽然原理简单,但因为症状和应用层超时高度相似,往往是最容易被忽视的一类网络故障,掌握本文这套排查套路后基本可以在半小时内闭环。