1. 一次“小包正常、大包卡死”的故障,把封装问题顶到台前
大概两个月前,有个朋友的项目组找上我,说他们内外网之间的文件传输经常卡死,但是网页、SSH、聊天消息这些应用又完全正常。第一反应谁都会想到防火墙、DNS、路由策略,我也按这个顺序查了一圈,配置全都没问题。直到我随手敲了一条ping -M do -s 1472,才意识到问题的根子根本不在应用层,而是出在网络层协议封装最基础的一个环节上——MTU。
这条命令的意思很直白:发送一个1472字节的ICMP载荷,同时设置禁止分片标志。1472这个数字不是随便取的,以太网标准MTU是1500字节,减去IP头20字节和ICMP头8字节,刚好剩下1472字节,这是理论上“不触发IP分片”的最大载荷。如果这个大小的包能通,说明路径MTU至少是1500;如果通不过,那问题就大了。
结果果然不通。路径上的某个中间环节MTU被卡在更低的位置,而TCP协商出来的分段大小又没来得及适配,于是大包到了那个环节就被丢弃。这就是典型的分片黑洞问题。这篇文章我想围绕“网络层协议封装”这件事展开,把IP头字段、MTU、分片重组、TCP分段、Overlay二次封装这些概念串起来讲。内容不深奥,但确实是我在实际项目里反复踩过坑之后总结出来的,适合运维、后端开发、网络工程师,以及所有被“大包不通小包通”折磨过的人。
1.1 封装不是“套一层头”这么简单
很多刚入门的朋友会把网络数据发送理解成“包一个信封扔出去”,这个类比方向没错,但真正落地时要比这复杂得多。应用层产生数据后,传输层会加上TCP或UDP头,比如TCP头里面有源端口、目的端口、序列号、确认号,这些信息保证数据能按顺序、不丢失地到达对端进程。
接下来就是网络层的事。网络层拿到上层传下来的数据段,再加一个IP头,里面最关键的是源IP、目的IP、TTL、协议号等字段。这个IP头解决的问题是在互联网范围内怎么把数据从一台机器送到另一台机器。至于物理上怎么送,网络层不用管,它把封装好的IP包继续往下交。
到了数据链路层,还会再套一层以太网头,里面有源MAC、目的MAC和类型字段。只有到了这一步,数据才真正有了在局域网内“找到邻居”的能力。每一层都只认识自己那一层的地址和信息,这就是分层协议最核心的价值。而所谓“网络层协议封装”,本质上就是发生在网络层这一跳上的加头动作,同时它还要负责和上下两层之间做好交接。
1.2 从抓包里亲眼看到“层层加壳”
我建议大家遇到封装问题先用抓包工具看一下,比看任何原理都直观。用tcpdump在发送端抓一个HTTP请求,展开看的时候能明显看到:最外层是以太网帧头,接下来是IP头,再往里是TCP头,最后是应用数据。数据包的大小就是这么多层头的总和。
有一次我给一个客户排查慢请求,光看应用日志完全找不到线索,抓包展开IP层才发现,客户端发出去的包全被加上了分片标识,每个分片都在1000字节左右,这意味着中间有链路MTU设置不对,导致原本一个完整的TCP段被切成了好几片,接收端还要花费额外时间做重组。整个请求光分片重组就浪费了几十毫秒,对于大文件传输来说这个损耗会被放大到肉眼可见的程度。
所以我的建议是,当你开始怀疑网络层封装出问题时,先别猜,先抓包。抓包工具会把每一层的封装结构完完整整地摊在面前,你只需要学会读它。
2. IP头封装的秘密:每个字段都在决定数据包命运
网络层协议封装的核心就是构造IP头。IPv4头标准长度是20字节,不算选项的话,它就像一张运输单,里面记录了包裹从哪来、到哪去、存活多久、有没有被拆过。我拆开讲几个真正影响排错的字段。
2.1 版本号、头长、总长度:看起来基础,关键时刻救你一命
版本号字段占4位,IPv4填4,IPv6填6。这个字段不是摆设,抓包工具靠它识别协议族;有些路由器在收到一个版本号对不上的包时会直接丢弃,这是保护机制。
头部长度字段(IHL)也是4位,它告诉你IP头到底有多长。标准是20字节,但如果带了选项字段,头就会变长,最大能到60字节。我见过不少人在手写抓包过滤器时忽略这个字段,直接按偏移量读TCP头,结果TCP头根本没在预期位置,分析全错。
总长度字段是16位,占2字节,表示整个IP报文长度,最大65535字节。这个字段对分片重组意义重大,接收端靠它判断分片是否拼齐全。有一次我排查一个UDP组播丢包问题,发现某些包的IP总长度字段比实际抓到的帧长度还大,这就是因为前一跳分片之后,中间某个设备把分片丢了,接收端只有碎片没有完整包,重组自然失败。
2.2 标识、标志和片偏移:分片现场的三驾马车
这是IP封装里最容易被误解的三件套。标识(Identification)是一个16位ID,同一个原始IP包分出来的所有分片会带上相同的ID,接收端靠这个把一家人认回来。标志字段里有三个位,最关键的是DF(Don‘t Fragment)位和MF(More Fragments)位。DF位设成1代表“不许拆”,MF位设成1代表“后面还有分片”。片偏移(Fragment Offset)字段记录的是这个分片在原始IP载荷中的位置,单位是8字节。
举个例子:一个IP包总长2000字节,去掉20字节IP头,载荷部分1980字节。如果它要通过一个MTU为1500的链路,就会被拆成两个分片:第一个分片能装下1480字节载荷(IP头20字节加上1480字节,正好1500),标识是100,MF位是1,片偏移是0;第二个分片装剩下的500字节载荷,标识还是100,MF位是0,片偏移等于185(1480除以8)。接收端看到这些分片后,会按照片偏移把数据重新拼成原始IP包。
这里有一个非常常见的网络故障模式:某个防火墙或者路由器在没有发送ICMP“需要分片但DF置位”消息的情况下,悄悄丢弃了超过MTU的包。发送端傻等回复,最后应用超时。这就是前面说的分片黑洞。遇到这种问题,知道分片三件套的工作逻辑是必须的。
2.3 TTL、协议号、校验和,和一些值得说的函数
TTL字段是8位,代表这个包最多能经过多少跳。每经过一个路由器,TTL减1,减到0就丢弃并回送ICMP超时消息。traceroute命令能显示每一跳路由,原理就是不断发送一个TTL很小的探测包,让沿途路由器一层层暴露自己。但TTL从来不是当“剩余秒数”用的,千万不要把TTL理解成时间。
协议号字段标识上层协议,TCP是6,UDP是17,ICMP是1。路由器转发时不需要知道上层协议,但防火墙和负载均衡器会非常关心这个字段,很多ACL规则就是按协议号过滤的。头部校验和字段只覆盖IP头本身,计算方式是逐16位累加后取反。这个字段有两个值得注意的点:一是IPv6干脆删掉了它,因为上层协议和链路层都做了校验,IP层再算一遍过于浪费;二是经过路由器的IP包TTL会变,TTL变了校验和就必须重算,所以每一跳都要做一次计算,这也是“为什么受限于硬件处理能力”的一个优化点。
3. MTU下的分片与重组:用完整复盘理解网络层边界
回到文章开头的故障,这次我不直接给结论,把排查链路完整走一遍。
3.1 症状描述和初步判断
现场的状况是:跨机房传输的备份文件经常中断,重试几次偶尔能传完,但速度极不稳定。网页、SSH正常,说明连通性没有问题。项目组有人怀疑是磁盘IO问题,也有人怀疑是应用线程池不够,我把日志翻了一遍,发现传输失败前应用层没有任何异常报错,TCP连接也没有明显的RST。
这个症状组合很容易让人想到MTU而不是应用问题。因为小包(比如TCP握手包、DNS查询)即使超过链路MTU通常也能被正常转发,或者至少不会触发分片黑洞;而大包一旦超过MTU又设置了DF位,就会被中间设备静默丢弃。表现出来就是“小应用都正常,大文件必失败”。
3.2 用ping探出路径MTU,一步一步缩小范围
我用的是最传统的探测法:从1472字节开始,逐步加大ICMP载荷长度。因为需要控制DF位,Linux下用-M do参数,macOS用-D,Windows用-f。命令长这样:
ping -c 3 -M do -s 1472 192.0.2.1 ping -c 3 -M do -s 1400 192.0.2.1 ping -c 3 -M do -s 1300 192.0.2.1如果1472通,说明路径MTU至少1500;如果1400通而1472不通,说明路径MTU在1428到1500之间;如果1300通而1400不通,那路径MTU大概率在1328到1428之间。我当时测出来是1400能通、1472不通,于是判断路径MTU应该在1472到1500之间,继续把长度往上收,最后定位到了1452字节,也就是有效载荷1452,加28字节IP头,路径MTU是1480。
这已经足够说明问题了:中间某段链路MTU被设成了1480,而不是标准1500。路径MTU探测在多数操作系统里默认是开启的,但很多网络设备为了保证安全直接丢弃了ICMP“需要分片”消息,导致探测失效,这也是这个故障能藏这么久的原因。
3.3 修复:调整接口MTU,或者让TCP自己适配
定位到路径MTU之后,修复其实有两个方向。一个是把涉及这一段路径的所有接口MTU统一改成1480,但这在跨机房场景里往往不现实,因为你控制不了别人家设备的配置。另一个更通用的办法是调整TCP的MSS值,让TCP分段大小主动适应这个MTU,而不是等到IP层去分片。
在我的场景里,比较简单粗暴的做法是在客户端发起连接的那台设备上,设置iptables的TCP MSS钳制规则,把所有经过本机且方向为外发的TCP SYN包里的MSS改成比1400更保守的值。命令大致是:
iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu这句的意思是把TCP握手时的MSS值钳到路径MTU允许的范围内,TCP就会按照这个更小的MSS做分段,IP层不再因为DF位而丢包。这个方案对下游设备完全透明,也是我在生产环境里最常用的一招。
4. 当封装遇到上层分段和Overlay:两个最容易忽视的坑
MTU和IP分片讲完之后,还得往下沉一层,因为网络层封装并不是孤立存在的,它上面站着TCP分段,下面站着物理链路的帧限制,旁边还可能站着各种叠加封装。这几层的关系弄错,排错时就会绕远路。
4.1 TCP的MSS和IP分片到底什么关系
先说一个很多人搞混的点:TCP分段和IP分片是两回事。TCP层根据MSS把应用数据切成一个个TCP段,每个段加上TCP头再交给IP层,由IP层加上IP头后发送。这个过程发生在发送端,是发送端主动控制大小的行为。
而IP分片发生在链路中间。如果IP包已经发出去了,到了某个MTU更小的链路上,路由器发现包太大,才被迫把这个包拆成几个分片。所以TCP分段是“发送前主动适配”,IP分片是“传输中被逼无奈”。
TCP握手时会协商MSS。以标准1500字节MTU为例,TCP头加IP头总共40字节,所以MSS默认是1460字节。如果路径MTU变成了1480,MSS就应该降到1440甚至更低。但如果中间设备的TCPMSS钳制没有生效,TCP还是按1460分段,每个IP包就是1500字节,超过路径MTU,DF置位后被丢弃,传输就会一直卡住。这也是为什么很多MTU故障最终要在TCP层解决,而不是去命令IP层不要分片。
4.2 Overlay封装给网络层多套了一层“衣服”
现在稍微复杂点的架构里都有Overlay网络的身影。跨主机容器网络、分布式存储内部通信、云平台虚拟网络,都会在原有IP包外面再包一层额外的头部。这个过程本质上就是把一个完整的网络层数据包当成载荷,再封装一层新的网络层头,让它在另一个网络域里传输。
问题来了:外面的封装是有代价的,至少几十字节的头开销。假设物理链路MTU还是1500,节点A发送一个标准的1500字节IP包到另一个节点,如果中间网络要求加一层Overlay封装,那么原始IP包加上外层头之后总长度就超过了1500,又会触发分片或者被丢弃。
很多人在这种架构里遇到“容器间通信大包失败、小包正常”的诡异情况,原因就是这个。一台容器里的服务收包正常,但一旦响应报文大到4000多字节,Overlay节点没法处理,直接丢弃。我处理过一个类似的案例,最后把容器虚拟网卡的MTU从1500调小到1400,同时把容器内TCPMSS钳制也配合调整,问题就消失了。核心思路是:多一层封装,就要给上层预留空间,不要让数据包长到物理层的极限。
4.3 用实验理解“分段和分片的坐标不同”
为了帮助团队理解这件事,我做了一个很简单的小实验。在发送端启动一个UDP程序,一次性发送一个6000字节的缓冲区出去。UDP不关心MSS,它只会把这一大块数据直接交给IP层。抓包结果显示,这6000字节被IP层分成了4个分片,每个分片大概1500字节左右,分片ID全部相同。
随后我改用TCP发送同样的数据量,抓包看到的情况完全不一样:TCP层把数据切成若干个1460字节的TCP段,每个TCP段独立成为一个IP包,从头到尾没有出现IP分片。原因很简单,TCP在发送前已经协商好MSS,根本不给IP层分片的机会。
这个实验对团队的帮助很大,它解释了为什么压测环境里UDP大包传输总是比别人慢、总是有很多丢包,而TCP虽然也有问题但至少能通过重传兜底。UDP一旦出现分片,任何一个分片丢了,整个数据报就废了,接收端只能丢弃所有分片。这是网络层封装机制的一个天然弱点。
5. 排错工具箱:用抓包反推封装细节的实战方法
前面讲的都是原理,最后这部分是真正的实操集锦。当你在生产环境里遇到封装相关问题,下面这些命令和习惯能帮你少走很多弯路。
5.1 最常用的三个探测命令,以及它们的进阶用法
基础版是三件套:ping、traceroute、ip link。但普通ping只能验证连通性,要用带参数的方式探测MTU。
Linux下推荐这样:
ping -M do -s 1400 目标IP ping -M do -s 1472 目标IP如果小包能通大包不能通,再结合下面这条看看路径:
traceroute --mtu 目标IPtraceroute的--mtu参数会在每一跳做MTU探测,中间哪个设备回包说“需要分片”,你就能直接看到是哪一跳在做限制。这个命令在一些发行版里面依赖UDP探测包,如果中间防火墙挡UDP,可以试试-I参数用ICMP探测。Windows上对应的命令是tracert,但MTU探测支持得没有Linux好,我在Windows上一般直接多敲几次不同长度的ping -f -l。
ip link用来查看本机各接口的MTU配置,这个不用多说。真正容易被忽略的是虚拟接口,比如容器网卡、Overlay网卡,它们的MTU往往和物理网卡不一致,查的时候要注意。
5.2 tcpdump和Wireshark里看封装的核心技巧
抓包类问题,我通常分两步。第一步确认是不是真的发生了分片,可以在Linux下这样抓:
tcpdump -ni eth0 'ip[6:2] & 0x3fff != 0 or ip[6] & 0x20'这条过滤规则的意思是,抓出IP头偏移6字节处长度为2字节的字段(标志位加片偏移)不等于0的包,或者MF位(0x20)被置位的包。能抓到这类包,就说明链路里确实存在分片流量,接下来再回到MTU的排查上。
第二步是用Wireshark展开看具体字段。展开IP层后,重点看Identification、Flags、Fragment offset三个字段。如果看到一串包的Identification都是同一个数字,那它们就是同一个原始包的分片。你再看看Flags里DF位是不是置位,如果置位了还分片,那说明某个设备无视DF位强行分片,这种行为在某些老旧设备上确实存在,是比黑洞更隐蔽的坑。
打开Wireshark时顺便看一眼Statistics菜单里的Packet Lengths统计,如果看到大量长度在1500左右甚至超过1500的包,也需要警惕MTU配置。超过1500的包只能出现在巨型帧环境或者分片环境里,正常情况下很难见到。
5.3 写在最后的经验库
我这几年总结下来,和网络层协议封装有关的问题,超过一半是MTU配置不一致造成的。你不需要记住IP头所有字段的二进制位,但必须建立起一个意识:数据包每经过一层封装就多一层头,每经过一条链路就要重新检查大小上限。写配置之前先确认物理MTU,做虚拟化网络之前先想清楚Overlay开销,排查大包问题先ping一下1472。
有时候我自己也会在工位上贴一张小纸条:1472、1460、1400。1472是1500MTU下ICMP最大载荷,1460是标准TCP MSS,1400是很多保守组网喜欢设置的兜底值。这三个数字能解决我八成以上的封装排错场景。另外,强烈建议把路径MTU探测相关的ICMP消息放行规则检查一遍,很多保密性要求比较高的网络会把它误伤,这等于把排错的手脚捆上了一大半。
网络层协议封装说难不难,说简单也不简单。它不像应用层问题那样有日志可以翻,也不像数据库问题那样有慢查询可以查。它就是一张默默运转的运输网,大多数时候不出声,一旦出声,就是大包传输卡死、视频会议花屏、文件上传中断这类让所有人头疼的问题。把这些封装细节理解和掌握住,你至少能让这类故障在手里少转几圈。