3、IP分片与重组优化:分片原理、MTU发现机制、避免分片的最佳实践

说到IP分片,我脑子里立刻浮现出几年前的一次线上事故。那会儿我们一个视频推流服务在跨机房传输时,突然出现大量丢包和延迟抖动。排查到最后,罪魁祸首就是IP分片。你想想看,一个好好的数据包,硬生生被切成几片,只要丢一片,整个包就废了。从那以后,我对分片这件事就特别敏感。

3.1 分片原理:为什么IP要“切包”?

IP层在设计时就没考虑过“一次发多大”的问题。它只管把数据从A送到B,至于链路层能扛多大的帧,那是链路层的事。以太网默认MTU是1500字节,如果上层(比如TCP)交给IP的数据包超过这个值,IP就得动手切。

分片的核心逻辑其实不复杂:

  • 分片发生在源端或中间路由器——只要下一跳接口的MTU比包小,就得切。
  • 每个分片都是一个独立的IP包——有自己的IP头,只是通过Identification、Fragment Offset、MF标志位来标识归属。
  • 重组只在最终目的地进行——中间路由器不负责拼回去。

关键字段速查表

字段作用我的经验
Identification (16bit)标识原始IP包同一个原始包的所有分片,这个值相同
Fragment Offset (13bit)分片在原始包中的偏移量单位是8字节,所以分片数据长度必须是8的倍数
MF (More Fragments)是否还有后续分片最后一个分片MF=0,其他MF=1
DF (Don't Fragment)禁止分片标志设了DF还超MTU?直接丢包+ICMP通知

举个例子。假设TCP交给IP一个3000字节的数据块,加上20字节IP头,总长3020。以太网MTU是1500,所以IP头+数据最多1480。那就要切:

  • 分片1:IP头(20) + 数据(1480),Offset=0,MF=1
  • 分片2:IP头(20) + 数据(1480),Offset=185(1480/8),MF=1
  • 分片3:IP头(20) + 数据(40),Offset=370(2960/8),MF=0

嗯,这里要注意:分片越多,丢包概率越大。任何一个分片丢了,整个原始包都得重传。这就是为什么高并发场景下,分片是性能杀手。

3.2 MTU发现机制:别让路由器替你切

既然分片这么坑,那最好的办法就是——不让它发生。怎么做?让发送端提前知道整条路径上的最小MTU,然后主动把包控制在那个尺寸以内。这就是路径MTU发现(PMTUD)。

PMTUD的原理其实很朴素:

  1. 发送端在IP头里设置DF=1,告诉路由器“别切我”。
  2. 如果某个路由器发现包超过出接口MTU,它直接丢包,并返回一个ICMPv6 Type 2(IPv4是Type 3 Code 4)消息,里面附上该接口的MTU值。
  3. 发送端收到ICMP后,把MTU缩小到那个值,重新发。
  4. 重复这个过程,直到包能顺利到达对端。

我个人习惯:在生产环境里,我会在应用层或传输层做一次PMTUD的“预探测”。比如在服务启动时,主动发几个不同大小的探测包,把路径MTU摸清楚,后续直接按这个值发。这样能避免业务流量在运行时频繁触发ICMP,减少延迟抖动。

但PMTUD有个坑——ICMP被防火墙过滤。我曾经遇到过,内网到公网的路径上,某个中间设备把ICMP全丢了。结果发送端收不到“包太大”的通知,就一直重传,一直丢,业务直接卡死。那叫一个惨。

避坑指南:我曾经在云环境里排查过一个诡异问题——跨AZ的数据库同步偶尔超时。查了三天,最后发现是云厂商的虚拟网关默认过滤了ICMP Type 3 Code 4。解决方案:要么在安全组里放行这类ICMP,要么干脆在应用层用TCP的MSS钳制来绕过PMTUD。

3.3 避免分片的最佳实践:从源头掐断

说白了,避免分片就两条路:要么让包变小,要么让链路MTU变大。但在高并发场景下,我更推荐前者——可控、稳定、不依赖底层设施。

3.3.1 TCP MSS Clamping:最常用的手段

TCP在三次握手时,会协商MSS(最大段大小)。MSS = 路径MTU - IP头(20) - TCP头(20)。如果我们在中间设备(比如负载均衡、网关)上把MSS钳制到一个安全值,那TCP就不会生成超过MTU的段,IP层自然不用分片。

# Linux上通过iptables做MSS钳制
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
         -j TCPMSS --set-mss 1400

# 或者用ebpf更精细地控制
# 我一般会在边界网关上统一做,内网用巨型帧,外网用标准MTU

MSS钳制的好处是对应用透明,改完配置就行,代码不用动。我习惯把MSS设为1400,这样即使路径上有PPPoE(MTU 1492)或VXLAN(额外50字节开销),也能安全通过。

3.3.2 应用层主动控制:更精细,但更麻烦

有些场景下,TCP MSS钳制不够用。比如UDP传输、QUIC、或者自定义协议。这时候就得应用层自己管了。

  • UDP场景:我建议应用层做MTU探测,然后主动把UDP payload控制在1200-1400字节。别贪多,留点余量给IP头和可能的隧道开销。
  • QUIC场景:QUIC本身有路径MTU探测机制(DPLPMTUD),但默认值偏保守。我一般会调大初始探测包,加速收敛。
  • 自定义协议:如果你们团队自己写传输协议,记得把MTU发现和分片避免作为基础能力内置进去。别问我怎么知道的……

3.3.3 巨型帧:内网场景的利器

如果网络环境可控(比如同一个机房、同一个VPC),把MTU从1500调到9000,能显著提升吞吐。原因很简单:同样发10GB数据,用1500 MTU要切更多包,CPU中断更多,协议栈开销更大。

MTU10GB数据所需包数CPU开销(相对)
1500约710万高
9000约120万低(约1/6)

注意:巨型帧只在二层网络内有效。一旦跨路由器、跨公网,MTU会被重置为1500。所以别指望巨型帧能解决所有问题——它只适合数据中心内部的东西向流量。

3.4 一张图看懂IP分片与优化

下面这张图总结了IP分片的发生条件、PMTUD的工作流程,以及我们常用的优化手段。我画图时习惯把关键路径标红,把避坑点标黄,方便团队新人一眼看懂。

IP分片与优化全景图 发送端 路由器(MTU检查) 接收端 数据包(DF=1) 通过(包≤MTU) 分片发生 包>MTU且DF=0 多个分片 重组 PMTUD触发 包>MTU且DF=1 ICMP “包太大” 优化手段 ① TCP MSS Clamping:中间设备强制MSS=1400 ② 应用层控制:UDP payload ≤ 1400,QUIC启用DPLPMTUD ③ 巨型帧:内网MTU调至9000,减少包数量 ④ 确保ICMP不被过滤:安全组放行Type 3 Code 4

3.5 总结一下我的经验

IP分片这个事,说白了就是能避则避。高并发场景下,分片带来的丢包率上升、CPU开销增加、延迟抖动,都是实打实的性能杀手。我个人的做法是:

  • 边界网关统一做MSS钳制,1400是个安全值,兼容绝大多数路径。
  • 内网用巨型帧,但只在二层域内用,跨网段自动降级。
  • UDP和QUIC场景,应用层主动做MTU探测,别偷懒。
  • 监控ICMP丢包,一旦发现PMTUD失效,立刻排查中间设备。