暗色模式

Linux MTU 与 IP 分片:从 ping -M do 到路径 MTU 发现与黑洞排查

技术教程
2026-10-02
2
0
本文要点
  • MTU 1500 限制的是 IP 层载荷,不是「数据」本身:ping -s 1472 才是刚好塞满 1500 的那个值(1472 + 20 字节 IP 头 + 8 字节 ICMP 头)。实测只多加 1 字节到 -s 1473,立刻报 ping: local error: message too long, mtu=1500——包根本没出网卡,是内核在发送前就拒了
  • DF 位决定「超了怎么办」:ping -M do 置上 DF(不许分片),超了就报错;不加 -M do 时内核在本机先切片再发。实测 2 个 3000 字节的 ping 让 /proc/net/snmp 的 FragOKs 从 8 涨到 10、FragCreates 从 24 涨到 30;回程方向 ReasmReqds 24→30、ReasmOKs 8→10,与 nstat 的绝对值逐项吻合
  • 分片长什么样:3000 字节被切成 1500 + 1500 + 68 三片,抓包实测偏移量 offset 0 / 1480 / 2960,前两片带 flags [+](后面还有)。每片最多塞 1480 字节数据,是因为分片偏移必须以 8 字节为单位
  • TCP 默认置 DF,靠路径 MTU 发现(PMTUD)探路:tracepath 第一跳就打印 pmtu 1500。一旦路径中间某段 MTU 更小、而「需要分片」的 ICMP 又被拦掉,就变成 黑洞:小包通、大包卡死、TCP 卡在重传
  • 本机三个相关开关实测值:ip_no_pmtu_disc=0(PMTUD 开启)、tcp_mtu_probing=0(探测更小 MTU 的兜底关闭)、tcp_base_mss=1024。容器、VPN、隧道场景的 MTU 事故,基本都能用这几行加抓包定位

Linux MTU 与 IP 分片:从 ping -M do 到路径 MTU 发现与黑洞排查

「ping 小包能通,传大文件就没反应」——这是网络排查里最经典的悬案之一。它几乎总是同一批主角:MTU、DF 位、分片、路径 MTU 发现。

这几个词背后的机制其实很简单,但它们在排障时特别容易被绕晕,原因有两个:

  • MTU 不是一个「数据大小」,它限制的是 IP 层载荷,而 ICMP、TCP、UDP 各自还要在里面扣掉自己的头。所以「MTU 1500」对应的 ping 尺寸是 1472,不是 1500。
  • 分不分片是发送端决定的,靠 IP 头里的 DF 位。置了 DF 就不许分片,路上遇到更小的 MTU 只能靠 ICMP 回信通知;ICMP 一旦被拦,故障就从「报错」变成「无响应」——这就是黑洞。

本文在一台 1 核 1.9G 的 Ubuntu 22.04 演示机(内核 5.15.0-30-generic,网卡 eth0,MTU 1500)上把这几件事逐条实测:先用 ping 卡出 1472/1473 这条边界,再用内核计数器看分片真实发生,最后抓包看 DF 位和分片偏移量,并说明 PMTUD 黑洞怎么定位。所有命令都是系统自带工具,不改任何配置。

先看这台机器的基础事实

ip -br link
ip link show dev eth0 | head -1
ip route get 223.5.5.5
echo "--- -s 1472:1500 - 20(IP 头) - 8(ICMP 头) = 刚好装得下 ---"
ping -M do -s 1472 -c 2 -W 2 223.5.5.5 2>&1
echo "--- -s 1473:只多 1 字节,DF 置位就发不出去 ---"
ping -M do -s 1473 -c 2 -W 2 223.5.5.5 2>&1

终端截图:ip link 显示 eth0 的 mtu 1500;ping -M do -s 1472 到 223.5.5.5 两个包全部收到;ping -M do -s 1473 则每个包都报 ping: local error: message too long, mtu=1500,2 packets transmitted, 0 received, +2 errors

三个值得停下来看的细节:

① mtu 1500 是接口属性,不是路径属性。 ip link show dev eth0 打印的 mtu 1500 qdisc fq_codel state UP 只说明本机这块网卡愿意发多大的 IP 包。路径上任何一跳的 MTU 更小,都不会体现在这行输出里——那要靠 PMTUD 或探测去发现。

② 1472 是「刚好」,1473 是「差 1 字节」。 ICMP echo 的载荷要套两层头:

层头部大小说明
IP 头20 字节不含选项时;带选项最大 60 字节
ICMP 头8 字节类型/代码/校验和/标识/序号
合计开销28 字节1500 - 28 = 1472

注意 ping 的统计行写的是 1472(1500) bytes of data——括号里的 1500 才是实际发到线上的 IP 包长度,1472 只是你给的载荷大小。回报行里的 1480 bytes from 223.5.5.5 则是「1472 载荷 + 8 字节 ICMP 头」,别把它和 MTU 混起来。

③ 1473 的失败是「本机错误」,不是「对端不通」。 报错原文是 ping: local error: message too long, mtu=1500,统计行是 0 received, +2 errors——一个包都没发出去。因为 -M do 置上了 DF 位,内核发现「DF + 包长 1501 > MTU 1500」时无处可退:既不能分片,也不能发超长帧,于是直接把这个 write 判为失败并返回错误。定位这类问题时,「local error」这四个字就是分水岭:它说明故障在发送端,还没轮到网络。

DF 位:分不分片,是发送端说了算

IP 头里那 3 个 bit 的标志字段,其中一个就是 DF(Don't Fragment)。它的语义非常硬:

  • DF = 1:这个包在任何一跳都不许被分片。路径 MTU 装不下时,路由器应该回一个 ICMP「需要分片」(type 3, code 4),发送端据此把包改小重发。若这个 ICMP 被丢弃,发送端永远收不到反馈——就是黑洞。
  • DF = 0:装不下就在那一跳切片,每片独立转发,到终点再重组。转发路径上不感知,代价全都落在终点。

ping 的行为差异就在这里:-M do(do = 不许分片)置 DF,-M dont(默认)不置。所以同一台机器、同一个目标,加不加 -M do 是两种完全不同的故事。

TCP 永远置 DF,这是刻意的设计:分片对 TCP 是灾难(丢一片就得重传整个报文,且中间设备对分片的处理千奇百怪),所以 TCP 宁可自己按 MSS 切好再发,MSS 的默认算法就是「MTU - 40」——IPv4 20 字节 IP 头 + 20 字节 TCP 头,1500 - 40 = 1460。这条链子顺下来就是:

MTU 1500  →  MSS 1460(TCP 段最大数据量)  →  PMTUD 负责发现更小的 MTU

分片真的发生了:用内核计数器看

不分片的路径走不通时,可以把 DF 撤掉,让内核替你切。下面这段脚本在发送前后各读一次 /proc/net/snmp 的 Ip: 行——注意它是以 Ip: 开头的两行(第一行是表头,第二行才是数值),所以 tail -1 取的是数值行:

snap() { grep '^Ip:' /proc/net/snmp | tail -1 | awk '{printf "FragOKs=%s  FragCreates=%s  ReasmReqds=%s  ReasmOKs=%s\n", $18, $20, $15, $16}'; }
echo "--- 发送前 ---"; snap
ping -s 3000 -c 2 -W 2 223.5.5.5 2>&1 | tail -3
echo "--- 发送后 ---"; snap
echo "--- nstat 里的分片与重组计数 ---"
nstat -az | grep -E 'IpFrag|IpReasm'

终端截图:发送前 FragOKs=8 FragCreates=24 ReasmReqds=24 ReasmOKs=8;两个 3000 字节 ping 之后变成 FragOKs=10 FragCreates=30 ReasmReqds=30 ReasmOKs=10,nstat 输出的 IpFragOKs=10、IpFragCreates=30、IpReasmReqds=30、IpReasmOKs=10 与之完全一致

四个计数器的增量正好是 2、6、6、2,每一个都能对上号:

计数器含义本次增量为什么是这个数
FragOKs本机成功分片的数据报个数+2发了 2 个 3000 字节的 ping
FragCreates本机切出去的分片个数+6每包切 3 片,2 × 3 = 6
ReasmReqds收到的待重组分片个数+6对端同样得把 3000 字节的回应切 3 片,2 × 3 = 6
ReasmOKs重组成功的数据报个数+26 片重组回 2 个完整报文

这里最值得记住的是两个方向各有各的账:本机既在出方向分片(Frag*),又在入方向重组(Reasm*)。所以看到 FragCreates 涨并不代表「本机在给别人的包切片」,也可能只是你 ping 的包太大了。

顺带一提,nstat -az 打印的是绝对值(加 -a),不加 -a 时它给的是距上次运行的增量。上面截图里两种口径同时出现、数值互相印证,正好可以当交叉验证用。

分片长什么样:抓包看 DF 位与偏移量

计数器只告诉你「分了几片」,抓包才能看到每片的真面目。同样一台机器,两次发送,两条过滤条件:

echo "--- 1472 字节 + DF:整包出去,flags [DF] ---"
( sleep 1; ping -M do -s 1472 -c 1 -W 2 223.5.5.5 >/dev/null 2>&1 ) &
tcpdump -ni eth0 -c 2 -v 'icmp and host 223.5.5.5' 2>&1 | grep -E 'flags|ICMP echo'
wait
echo "--- 3000 字节不加 DF:被切成 offset 0 / 1480 / 2960 三片 ---"
( sleep 1; ping -s 3000 -c 1 -W 2 223.5.5.5 >/dev/null 2>&1 ) &
tcpdump -ni eth0 -c 3 -v 'host 223.5.5.5' 2>&1 | grep -E 'flags|ICMP echo'
wait

![终端截图:上半部分 1472 字节 ping 被 tcpdump 抓到 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto ICMP (1), length 1500);下半部分 3000 字节 ping 被拆成 offset 0 和 offset 1480 两片 flags [+]、以及 offset 2960 flags [none] 的收尾片](https://blog.astarry.cn/usr/uploads/2026/10/2075618682.png)

把这两段输出对照着读,IP 头里跟分片有关的三个字段全都在这里了:

  • flags [DF]:第一段的请求包。整包 1500 字节,id 0、offset 0、没有后续片。回包是 flags [none]——DF 是逐包设置的,对端回不回 DF 由它自己决定,别指望对称。
  • flags [+]:第二段的前两片。+ 表示 MF(More Fragments)置位,也就是「后面还有片」。
  • offset 1480 / offset 2960:分片偏移,单位是 8 字节,tcpdump 已经帮你换算成字节数了。这解释了为什么每片的数据部分必须是 8 的倍数:1500 字节的 IP 包去掉 20 字节头 = 1480 字节数据,正好是 8 的整数倍;最后一片 68 字节 = 20 头 + 48 数据,收尾不用凑整。
  • 同一个 id:三片的 id 都是 34850,终点靠 (源 IP, 目的 IP, 协议, id) 这四元组把片归拢到一起重组。id 只有 16 位,所以高速链路上 id 会绕回——这也是分片在高带宽下容易出问题的原因之一。

路径 MTU 发现:tracepath 看到的那一行

TCP 置 DF 之后并不能「凭感觉」猜路径 MTU,它靠 PMTUD:先按本机 MTU 发,被 ICMP「需要分片」打回就把 MSS 调小重试。tracepath 就是把这个过程显式跑一遍的工具(它逐跳发送、逐渐加大包长,直到收到「需要分片」):

tracepath -m 6 223.5.5.5
 1?: [LOCALHOST]                      pmtu 1500
 1:  ???                                                   0.221ms
 1:  ???                                                   0.184ms
 2:  43.248.3.129                                          3.978ms asymm  3
 3:  ???                                                   3.449ms asymm  4
 4:  ???                                                   3.815ms asymm  5
 5:  no reply
 6:  ???                                                   3.426ms asymm  8

第一行的 pmtu 1500 就是结论:从本机到第一跳这一段,能通过的最大 IP 包是 1500。后面每跳的 asymm N 表示「去程和回程的跳数不对称」,??? 表示该跳没回 ICMP 超时(很常见,不必当成故障)。如果路径中间存在 MTU 更小的一段,tracepath 会在对应跳上打印出更小的 pmtu 值——这就是它比 traceroute 更适合排查 MTU 问题的原因。

本机与 PMTUD 相关的三个开关(实测值):

for k in ip_no_pmtu_disc tcp_mtu_probing tcp_base_mss; do printf "%s = %s\n" "$k" "$(cat /proc/sys/net/ipv4/$k)"; done
ip_no_pmtu_disc = 0
tcp_mtu_probing = 0
tcp_base_mss = 1024
参数值含义
ip_no_pmtu_disc0PMTUD 正常工作(1 = 完全关闭,内核不再听 ICMP 的「需要分片」,也就更容易踩黑洞)
tcp_mtu_probing0关闭。开启后 TCP 会在疑似黑洞时主动下探更小的 MSS(tcp_base_mss 是起点)
tcp_base_mss1024探测起点

调整这些参数属于 sysctl 的范畴,写法与持久化方式见 Linux sysctl 内核参数调优:从 /proc/sys 到 sysctl.conf 永久配置。

PMTUD 黑洞:为什么「小包通、大包死」

把前面几块拼起来,黑洞的成因就一句话:发送端置了 DF,路径装不下,本该通知它的 ICMP 被拦了。

典型现场与判断方法:

现象大概率原因怎么确认
ping -s 1472 通,-s 1473 报 local error本机 MTU 就这么大对比 ip link show 的 mtu,看 1472/1473 这条边界
小包 ping 通,大包 ping 无响应(不是 local error)路径中间 MTU 更小且 ICMP 被拦tracepath 看哪一跳 pmtu 变小;对端抓包看包是否到达
SSH 能连、一传文件就卡死同上,TCP 的大段被丢看 TCP 是否停在重传;ss -ti 观察重传与 mss,方法见 Linux TCP 连接诊断:从 ss 到内核计数器
容器内访问外网 HTTPS 握手成功但传输停滞容器/veth 的 MTU 比宿主机小,而 DF 位由容器里的进程设置ip link show 比对两端 MTU;隧道类接口常见 1450/1420
VPN / 隧道建立后只有小包能过封装额外吃掉几十字节,而底层路径没被告知见下方封装开销表

隧道与封装会实打实地吃掉 MTU,这是容器和 VPN 场景里 MTU 事故的主要来源:

封装方式额外开销常见可用 MTU
PPPoE8 字节1492
IPIP20 字节1480
GRE24 字节1476
VXLAN50 字节1450
WireGuard60~80 字节1420

(这些是「物理层 1500 减去封装头」的常见取值,实际还要看具体配置,用 ip link show 看到的值才是准的。)

处理思路有三条,按侵入性从低到高:

  1. 放行 ICMP:让「需要分片」的报文能回到发送端,PMTUD 就能自愈。这是最正确的做法,但在很多云环境和「安全加固」脚本里,ICMP 恰恰是被整类封掉的。
  2. 主动下调 MTU/MSS:把隧道接口或相关连接的 MTU 改小,或者对经过的 TCP 连接做 MSS clamping(在 nftables/iptables 里改写 TCPMSS 字段),让双方一开始就用小段。防火墙规则的写法见 Linux 防火墙:从 iptables 到 nftables 完整指南。
  3. 开 tcp_mtu_probing=1:作为兜底,让 TCP 自己在疑似黑洞时下探。它治不了「路径真的装不下」的根因,但能让连接不至于彻底卡死。

顺带区分两个容易混的动作:「分片」是发送端的自愿行为,改 MTU 是让分片压根不需要发生。生产环境里值得追求的是后者——分片只在排障时用来确认链路能力。

排查清单

把这篇文章用到的命令按顺序排一遍,基本能覆盖 MTU 类故障的绝大多数场景:

ip -br link                                  # 各接口 MTU 一览(隧道接口一眼可见)
ip route get 1.1.1.1                         # 走哪条路由、从哪个源地址出去
ping -M do -s 1472 -c 2 <目标>               # 卡边界:local error 说明是本机 MTU 限制
ping -M do -s 1473 -c 2 <目标>               # 加 1 字节必失败,用来确认 1500 这条线
ping -s 3000 -c 2 <目标>                     # 撤掉 DF,看内核对超大包的处理
tracepath -m 8 <目标>                        # 逐跳看 pmtu,定位「哪一段变窄」
tcpdump -ni eth0 -c 5 -v 'icmp or (host <目标>)'   # 看 flags [DF] / [+] / offset
nstat -az | grep -E 'IpFrag|IpReasm'         # 分片与重组的累计账
ss -ti state established                     # TCP 侧的重传与 mss(配合看卡死)

几个判读原则:

  • local error 是本机拦下的,包没出去,问题不在网络。
  • FragCreates 涨是「本机切了包」,不代表链路有问题;它涨得很快反而说明有应用在发超大包。
  • ReasmFails 涨才是危险信号:收到了分片却拼不起来(丢片、超时),通常伴随性能下降。
  • 路径 MTU 小于两端接口 MTU 时,问题一定出现在中间,tracepath 比 traceroute 更值得先跑。

小结

MTU 与分片这一套机制,理解成本主要在于「把三个数字分清」:

  • 1500 是 IP 载荷上限,1472 是 ICMP 能用的载荷,1460 是 TCP 的 MSS;
  • DF 位决定超限时报错还是分片,TCP 永远置 DF,所以 TCP 的故障表现是「卡死」而不是「报错」;
  • PMTUD 是 TCP 用来发现更小 MTU 的机制,它依赖 ICMP,所以拦 ICMP 等于关掉这个安全网。

演示机上的三条实测证据可以当作模板复用:ping -M do -s 1472/1473 卡边界、/proc/net/snmp 的 Frag*/Reasm* 计数器对账、tcpdump -v 看 DF 位与 offset 偏移量。三样都是系统自带工具,不装任何东西,也不改任何配置。

相关阅读:

发表评论

暂无评论,快来抢沙发吧!