服务器入坑指北:MTU 与分片的取舍
小包通、大包不通:
ping能通,一传文件就断;有一个方向的连接能建起来,却传不了数据;把"改小 MTU"当成万能药,改完有些通了、有些还是不通。
顺序是:先把载荷上限算出来 → 再用"禁止分片"的探针找到分界点 → 分清是分片问题还是路径 MTU 发现失效 → 最后才决定改哪一层。
这篇给三段可直接粘贴的内容:第一段只读(量与现读)、第二段只读(探针定界)、第三段含写操作(改 MTU 与相关开关,标回滚点)。这里的量关系全部可演算,来源是协议规范而不是经验。取值用占位符,命令名、参数名与常量照写。
载荷上限是算出来的,不是试出来的
载荷上限 = MTU - IP 头 - 传输层头。
三个数记住,就能把"探针该用多大"变成算术:
1500 - 20 - 20 = 1460:IPv4 报头是固定 20 字节(不含选项),TCP 报头最小也是 20 字节——所以这是以太网 MTU 下的 TCP 载荷上限(也就是常说的 MSS);1500 - 40 - 20 = 1440:IPv6 基本报头固定 40 字节,TCP 仍是 20 字节——同样 1500 的链路上,IPv6 每包比 IPv4 少 20 字节;1500 - 20 - 8 = 1472:ICMP 回显报头只有 8 字节——所以回显探针能填到 1472。
这三个减项分别是什么头:20是 IPv4 固定头、40是 IPv6 固定头、20是 TCP 最小头、8是 ICMP 回显头。都取最小值——因为选项会再占空间,取最小值得到的是上限。
判据:探针大小与 TCP 载荷不是一回事(一个按 8 字节头算,一个按 20 字节头算)——忘记这一点,就会拿 1472 去解释 TCP 的传输失败。
量与现读块
ip link show <接口> # 现读本端接口的 MTU
ip route get <目标> # 看这个目标走哪条路、出口是哪个接口
sysctl net.ipv4.ip_no_pmtu_disc # 现读:非 0 即关闭了路径 MTU 发现
nstat -s # 读"超限丢弃"一类计数:是否真有包因为太大被丢
tracepath <目标> # 现探路径 MTU 与每一跳的值
判据:本端 MTU 只是"这一跳"的量,路径 MTU 是整条路上最小的那一跳——两者不一定相等;nstat 里"超限丢弃"的计数在涨,才说明确实有包因为太大被丢,而不是被别的原因拦掉。
片数怎么算
当载荷超过链路 MTU 时,包会被分片;分片偏移字段(fragment offset)以 8 字节为单位记录,因此每个分片(最后一片除外)的载荷长度必须是 8 的倍数:
片数 = 总长 / 每片载荷,向上取整。
算一遍:总长 3000 字节、每片载荷 1480 字节(这个数能被 8 整除:1480 = 8 × 185):
- 3000 / 1480 = 2.027…,向上取整 → 3 片;
- 三片对账:1480 + 1480 + 40 = 3000 ✓ 正好补齐;
- 若每片载荷取 1500 / 1480 之外的数,注意后面的片要重新算——不是整除时,最后一片会更小。
判据:偏移不是 8 的倍数就会被拒——所以"每片塞多少"不能随手定。
探针定界块
ping -M do -s <字节数> <目标> # 禁止分片:成功/失败的分界点就是路径 MTU
ping -M dont -s <字节数> <目标> # 允许分片:对照组
从大往小试(先试 1472 这类上限,再往下):
ping -M do -s 1472 <目标>成功、ping -M do -s 1473 <目标>失败 → 分界点在这里;路径 MTU = 1472 + 20 + 8 = 1500(回显载荷加 IPv4 头与 ICMP 头);- 两个方向的去与回可能不同,所以要各测一次;
- 同一大小、允许分片就通、禁止分片不通 ⇒ 这就把问题定在 MTU 上:
-M dont是对照组。
为什么"禁止分片"的包会被丢:IPv4 报头里有一个DF(禁止分片)位;置了它的包若超过链路 MTU,会被丢弃,并由中间设备回一条fragmentation needed(需要分片) 的不可达通知——这就是"小包通、大包不通"的机制来源。
两种故障要分开
| 现象 | 结论 | 怎么判 |
|---|---|---|
| 只有大包不通,且没有任何回执 | 路径 MTU 发现失效(那类通知被拦掉了) | 换一条路径试:换路径就好 → 是这条链路上的通知被拦 |
| 在同一条链路上稳定复现 | 链路 MTU 配错(本端或对端) | 换路径照旧 → 是端点的配置问题 |
判据:换一条路径试——这一条动作就能把两类故障分开。相关的取值语义:net.ipv4.ip_no_pmtu_disc 非 0 即关闭路径 MTU 发现;net.ipv4.tcp_mtu_probing 的取值语义是 0 关闭 / 1 检测到"黑洞"才开 / 2 始终开(以现读为准);若你的内核提供 net.ipv4.mtu_expires,它表达探到的路径 MTU 缓存多久——较新内核上可能已无此项,以现读为准;net.ipv4.tcp_base_mss 是探测的起始段长(与探测配套)。 |
改与读回块
在你动手改 MTU 之前,先把这条代价看清:改小 MTU 是用吞吐换连通——每一包里头的头占比上升,同样数据量需要更多的包,这是取舍,不是免费的修复。优先修"通知被拦",而不是把 MTU 一律改小。
ip link show <接口> # 改前:记录原值(回滚依据)
ip link set <接口> mtu <字节数> # 改本端 MTU(高风险写操作,需回滚点)
ping -M do -s <字节数> <目标> # 读回:新的分界点在哪里
nstat -s # 读回:超限丢弃计数还在不在涨
读回是独立的一步,不许省:改完要回跑探针确认新的分界点,并复核吞吐相关的计数——"通是通了、吞吐掉了"必须看得见。改 MTU 与改相关内核开关都属高风险写操作,需回滚点。
现象到动作的判定表
| 现象 | 结论 | 动作 |
|---|---|---|
| 小包通、大包不通 | 大小相关,不是随机丢 | 用 ping -M do -s <字节数> <目标> 从大往小找分界点 |
| 允许分片就通、禁止分片不通 | 问题在 MTU 上 | 记录该分界点,算回路径 MTU |
| 换路径就好 | 路径 MTU 发现这条链的问题 | 查 DF 那类通知为什么被拦,而不是先改 MTU |
| 换路径照旧 | 端点配置问题 | 核对本端与对端的 MTU |
| 改小 MTU 后通了但吞吐下降 | 用吞吐换了连通 | 记下这个代价,评估是否还有更划算的修法 |
改完怎么验证:每项一条判据
- 探针的分界点等于预期的路径 MTU;
- 两个方向各测一次;
- 允许与禁止分片两个对照的结论一致;
- 超限丢弃计数不再增长;
- 有回滚形态与原值记录。
这篇不解决什么
- 讲端口冲突与监听地址的边界的那篇是听没听、听在哪;本篇是包能不能过去——两篇都在"连不上"这一大类里,但层不同。
- 讲网卡中断与软中断分布的那篇是收包的核间分布;本篇是包的大小与分片。
- 讲连接跟踪表满了会怎样的那篇是连接状态表打满;本篇不涉及状态表。
踩坑清单
- 把"大包不通"一律归为 MTU:先分清是丢还是被拒。
- 只测一个方向:去与回可能是两条路径。
- 只改本端不看对端:两头的 MTU 都要核对。
- 拿经验值当上限:该算——
MTU - 头。 - 忘记 ICMP 的头是 8 字节:探针大小与 TCP 载荷不是一回事。
- 分片偏移不是 8 的倍数:那样会被直接拒掉。
- 把"一律改小 MTU"当修复:吞吐代价没算。
- 只看"通不通"不看超限计数:不知道有没有真丢。
- 改完不换路径复核:两类故障分不开。
- 不知道
DF位的作用:看到"需要分片"的通知也不知道它在说什么。 - 忽略
net.ipv4.ip_no_pmtu_disc的现读值:它是 0 还是非 0,结论完全不同。 - 把路径 MTU 缓存当永久有效:缓存有有效期,**而且能不能调、叫什么名字都随内核版本不同,
- 点赞
- 收藏
- 关注作者
评论(0)