大文件传一半卡死,网页能开却拉不动数据——问题可能出在 MTU
有一类服务器故障特别折磨人:小请求一切正常,大一点就出问题。典型症状包括:
- 能 ping 通、能 SSH 登录,但一执行
scp传大文件或git clone就卡住不动; - 网页首页能打开,但一访问图片或大文件就超时;
- API 的小请求响应正常,传大数据体就断;
ping 8.8.8.8正常,但ping -s 1500 8.8.8.8就没反应;- 某些客户端能连、某些连不上,或者换个网络环境就正常了。
这些症状看似五花八门,其实常常指向同一个根因:MTU(Maximum Transmission Unit,最大传输单元)不匹配,也就是"路径 MTU 问题"。它隐蔽是因为:小包没问题、本地没问题、看日志也看不出明显报错,只有当你开始传大一点的数据时,问题才出现。
先搞懂 MTU 和"分片"是怎么回事
以太网默认的 MTU 是 1500 字节,意思是单个数据帧的载荷最大 1500 字节。当应用要发送的数据超过这个值时,IP 层需要把它分片(fragmentation)成多个不超过 MTU 的小包分别发送,到达对端后再重组。
听起来很方便,但分片有一个致命特性:重组需要收到全部分片,丢任何一个分片,整个数据报就作废,需要重传。分片越多,丢一个的概率越大,整体效率越低。因此现代网络设计倾向于避免分片——由发送方根据"路径上最小的 MTU"(Path MTU,路径 MTU)来主动决定包的大小。
这就要说到 PMTUD(Path MTU Discovery,路径 MTU 发现)机制。它的原理很巧妙:发送方默认发大包并设置 IP 头里的 DF 位(Don't Fragment,禁止分片);如果路径上某个中间设备(比如隧道、VPN、路由器)的 MTU 更小、装不下这个包,它本该回一个 ICMP 消息"需要分片但 DF 位已设"(ICMP Type 3 Code 4),告诉发送方"把包改小点"。发送方收到后就降低包大小重试。
问题就出在这里:很多网络(尤其是云环境、防火墙、CDN)会出于安全考虑屏蔽或过滤 ICMP。一旦这个 ICMP 消息被吃掉,发送方永远收不到"把包改小"的提醒,于是不停地用大包发、发不出去、超时、重试……表现就是连接卡死、大文件传不动。这就是著名的"ICMP 黑洞"(ICMP black hole)。
什么时候会踩到 MTU 问题
对个人站长来说,以下场景特别容易触发:
- 服务器接了 VPN 或隧道(WireGuard、IPsec、GRE、PPTP)。隧道会额外套一层包头(WireGuard 约 60-80 字节,PPTP 更多),有效 MTU 从 1500 降到 1420 甚至更低。如果你的服务还在用默认 1500,发出的包就会超过隧道能承载的大小。
- 服务器在云上,客户端在另一个网络。两边的 MTU 可能不同,中间还隔着云厂商的网络设备。
- 用了 Docker 的 overlay 网络或 VXLAN。VXLAN 封装会额外占 50 字节,容器网络里的 MTU 常常要调小。
- 普通家宽 + PPPoE 拨号。PPPoE 会额外占 8 字节,导致有效 MTU 变成 1492 而非 1500。这是"我在家能连,换手机热点就连不上"的经典原因。
第一步:确认是不是 MTU 问题
最直观的测试方法是用 ping 配合 DF 位和指定包大小。Linux 下 -M do 表示设置 DF 位(Mac 上是 -D):
# 用小包 ping,应该通
ping -c 3 8.8.8.8
# 用 1472 字节(+28 字节 IP/ICMP 头 = 1500)测试
ping -c 3 -M do -s 1472 8.8.8.8
# 如果不通,逐步缩小包大小,找出能通过的临界值
ping -c 3 -M do -s 1400 8.8.8.8
ping -c 3 -M do -s 1420 8.8.8.8
判读规则:
- 小包通、1472 大包不通 → 高度怀疑 MTU 问题。
- 如果 1472 不通但 1400 通,说明这条路径的真实 MTU 大约在 1400~1472 之间。注意
-s指定的是 ICMP 数据部分,实际 IP 包大小 =-s 值 + 28。所以-s 1400对应 IP 包 1428。 - 如果收到的是
ping: local error: Message too long,那是本机就发不出去;如果是直接超时无响应,说明包发出去了但被中间设备静默丢弃——这就是 ICMP 黑洞的典型特征。
另一个更精确的工具是 traceroute,配合大包和 DF 位能定位是哪一跳开始出问题的:
traceroute --mtu 目标IP
它会报告路径上每一跳的 MTU,遇到黑洞的那一跳会显示 !F(需要分片)或直接超时。用 mtr 更直观,能看到逐跳的丢包:
mtr --report --report-cycles 20 目标IP
第二步:先看本机的 MTU 设置
确认问题存在后,检查本机网络接口的 MTU:
ip link show
# 或指定网卡
ip link show eth0
输出里会有 mtu 1500 这样的字段。常见需要调整的情况:
- 接了 WireGuard:通常应设为 1420 或更低;
- PPPoE 拨号:应设为 1492;
- VXLAN/overlay 网络:按封装开销相应调小(常见 1450);
- IPsec:视加密算法,往往需要 1400 甚至更低。
临时修改(重启失效,用于测试):
ip link set dev eth0 mtu 1420
改完立刻重测大包 ping 和实际业务,如果问题消失,就确认是 MTU 了,接着做持久化。
第三步:持久化 MTU 设置
临时改会随重启或网络重连失效,必须写进配置。几种持久化方式:
① systemd-networkd 用户(现代 Debian/Ubuntu 常见)——在对应的 .network 文件里加:
[Link]
MTUBytes=1420
改完 networkctl reload 或重启网络服务。
② ifupdown 用户——在 /etc/network/interfaces 的接口段里加:
auto eth0
iface eth0 inet static
mtu 1420
...
③ Netplan 用户(Ubuntu)——在 netplan YAML 里加 mtu: 1420,然后 netplan apply。
④ 云平台层面——有些云厂商在控制台/网卡设置里也能指定 MTU,这层设置优先级可能更高,改完记得两边一致。
一个稳妥的验证方式:改完持久化配置后手动重启网络或重启机器,ip link show 确认 MTU 生效,再跑一遍大包 ping。
别忘了这一层:容器和 Docker 的 MTU
如果问题只出现在容器里,宿主机却正常,那多半是容器网络 MTU 没跟着调。Docker 默认给容器网桥用 1500,如果宿主机因为隧道/VPN 需要 1420,容器里的流量照样会因为大包被丢而卡死。解决办法是在 /etc/docker/daemon.json 里指定:
{
"mtu": 1420
}
改完重启 Docker 服务(注意重启 Docker 会导致所有容器重启,选好时机)。对单个容器,也可以在 docker run 时用 --network 自定义网络的 --opt com.docker.network.driver.mtu=1420 来指定。
对于 overlay 网络或 Kubernetes 集群,MTU 更是必须显式配置的东西——VXLAN 封装开销固定,默认值往往会超。
第四步:如果改不了 MTU,就让 TCP 自己妥协
有时候你没法改中间网络设备的 MTU(比如是别人的网络、云平台不给改)。这时可以靠 TCP MSS 钳制(MSS clamping)来绕过。原理是:TCP 建立连接时会协商 MSS(Maximum Segment Size),你可以在自己的防火墙上改写经过的 TCP 握手包里的 MSS 值,把自己通告的 MSS 调小,让对方发出的包永远不会超过路径 MTU,从而根本不需要分片。
在 iptables 里:
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
这条规则只对经过 FORWARD 链的转发流量生效。如果问题出在本机自己发起的连接,需要放在 OUTPUT 链:
iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
--clamp-mss-to-pmtu 会自动根据当前路径 MTU 推算合适的 MSS,比硬编码一个值更省心。如果它不起作用(比如 PMTUD 本身被黑洞了),可以手动指定:
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --set-mss 1400
顺手确认一下 ICMP 有没有被挡
既然 MTU 问题的一大根源是 ICMP 被屏蔽,那有个好习惯:确保你自己的服务器不要屏蔽必要的 ICMP 类型。尤其是 Type 3 Code 4(需要分片)这个 ICMP 必须放行,否则你的服务器作为中间节点时,会成为别的连接的"黑洞制造者"。检查防火墙规则里有没有无差别地 DROP icmp。很多"一键安全脚本"会顺手把 ICMP 全禁了,这是好心办坏事的典型。
排查速查表
# 1. 判断是否 MTU 问题:小包通、大包不通
ping -c 3 -M do -s 1472 目标IP # 1500 大小
ping -c 3 -M do -s 1400 目标IP # 逐步缩小找临界
# 2. 定位是哪一跳出问题
traceroute --mtu 目标IP
mtr --report --report-cycles 20 目标IP
# 3. 查看/临时修改本机 MTU
ip link show eth0
ip link set dev eth0 mtu 1420
# 4. 绕过:MSS 钳制
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
# 5. Docker 容器网络 MTU
# /etc/docker/daemon.json -> { "mtu": 1420 }
写在最后
MTU 问题之所以让人头疼,是因为它的表现和根因隔得太远:你看到的是"传大文件卡死""图片加载不出来",而原因藏在网络协议的封装开销里。一旦掌握了"小包通、大包不通 → 怀疑 MTU"这个判据,再配合 ping DF 测试和 traceroute,定位就不再是玄学。
它的解法也很符合运维的一般规律:优先从根因解决(调整正确的 MTU 值),实在改不了再用兼容手段(MSS 钳制)绕过。尤其在你用了 VPN、隧道、Docker overlay 这些"叠加封装"的技术时,主动想一下"有效 MTU 还剩多少",很多莫名其妙的连接问题就会提前被消灭。