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的原理其实很朴素:
- 发送端在IP头里设置
DF=1,告诉路由器“别切我”。 - 如果某个路由器发现包超过出接口MTU,它直接丢包,并返回一个ICMPv6 Type 2(IPv4是Type 3 Code 4)消息,里面附上该接口的MTU值。
- 发送端收到ICMP后,把MTU缩小到那个值,重新发。
- 重复这个过程,直到包能顺利到达对端。
我个人习惯:在生产环境里,我会在应用层或传输层做一次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中断更多,协议栈开销更大。
| MTU | 10GB数据所需包数 | CPU开销(相对) |
|---|---|---|
| 1500 | 约710万 | 高 |
| 9000 | 约120万 | 低(约1/6) |
注意:巨型帧只在二层网络内有效。一旦跨路由器、跨公网,MTU会被重置为1500。所以别指望巨型帧能解决所有问题——它只适合数据中心内部的东西向流量。
3.4 一张图看懂IP分片与优化
下面这张图总结了IP分片的发生条件、PMTUD的工作流程,以及我们常用的优化手段。我画图时习惯把关键路径标红,把避坑点标黄,方便团队新人一眼看懂。
3.5 总结一下我的经验
IP分片这个事,说白了就是能避则避。高并发场景下,分片带来的丢包率上升、CPU开销增加、延迟抖动,都是实打实的性能杀手。我个人的做法是:
- 边界网关统一做MSS钳制,1400是个安全值,兼容绝大多数路径。
- 内网用巨型帧,但只在二层域内用,跨网段自动降级。
- UDP和QUIC场景,应用层主动做MTU探测,别偷懒。
- 监控ICMP丢包,一旦发现PMTUD失效,立刻排查中间设备。