LINUX MTU/MSS(1500/1460)那些事:原理、排查与实战
前两天有同事跑过来问我:同一台服务器,网页访问都正常,但从这台机器往远端上传大文件总是到一半就卡住,小文件又没事。他第一反应是带宽不够,我让他用带DF标志的大包ping了一下对端,结果很快出来了——这是典型的MTU问题。这类问题在Linux服务器运维里太常见了,尤其是在跨运营商、跨机房、走隧道或者PPPoE拨号的场景下。MTU和MSS这两个参数,一个是二层和三层边界的硬限制,一个是TCP协议栈里的软协商,1500和1460之间那40字节的差额,背后藏着的是IP头和TCP头的基本结构。这篇文章会把它们彻底讲透,并给出可复现的排查命令和修改方法,适合刚接触网络基础的学生、Linux运维新手,以及被“大包不通小包通”折磨过的同行。
1. 先搞懂1500和1460差的那40个字节去哪了
1.1 一个数据包从网卡出去的完整旅程
要理解MTU,必须先建立一个分层视角。当你在Linux里执行一次TCP发送,应用层数据先落到TCP层,TCP会加上一个TCP头;接着落到IP层,IP会加上一个IP头;最后落到网卡驱动,网卡会把这个完整的三层报文封装进以太网帧,再通过物理链路发出去。
MTU的全称是Maximum Transmission Unit,它的准确定义是“接口在不用分片的情况下,能够承载的最大IP数据报长度”。注意这里说的是IP数据报,也就是IP头加上IP载荷的总长度,不包含以太网帧头和帧尾。这就是为什么你看到接口MTU是1500,但同样的数据包到了抓包软件里,Frame长度却显示1518——那多出来的18字节,是14字节的以太网头(源MAC6字节、目的MAC6字节、Type字段2字节)加上4字节的Frame Check Sequence帧校验序列。
换句话说,MTU是三层概念里的上限,而1500这个经典数值,来源于以太网v2标准对帧载荷长度的约束。之所以定在1500,是兼顾了信道占用时间、错误重传代价和内存管理效率的折中结果,这个数值从80年代一直沿用到今天,已经成了绝大多数网络设备默认三层载荷上限。
1.2 TCP头加IP头等于40字节,这是1460的由来
那1460又是怎么来的?1500减去40,这40字节通常是20字节的IP头加20字节的TCP头。20字节的IP头是IPv4最基础的变长头部,不携带任何Option选项;20字节的TCP头同样也是最标准形态,没有时间戳、没有SACK选项,只带最基本的序列号、确认号、窗口、标志位等字段。
MSS的全称是Maximum Segment Size,属于TCP的选项字段,在TCP三次握手阶段,双方通过SYN报文里的MSS选项告诉对方:我这边能接收的TCP载荷最大是多少。计算方式就是本端接口MTU减去IP头长度再减去TCP头长度。如果本端MTU是1500,那么MSS通常就是1460。
这里有个容易绕晕的点:MSS不是新加限制,而是TCP为了“不给IP层添麻烦”主动把段变小。因为IP层一旦需要对数据包做分片,就很有可能导致性能下降甚至丢包后重传效率急剧降低。TCP宁可在一开始就把单个数据段控制在一个完美的尺寸内,让IP层不必分片。
1.3 这套换算关系在不同场景下怎么变
搞懂“40字节损耗”之后,各种变体就都能推导了。如果接口MTU是1492(典型的PPPoE场景),那TCP MSS就是1492减40等于1452。如果走GRE隧道,外层IP头加GRE头通常要吃掉24字节,内层数据就得缩水到1476,TCP MSS就变成1436。如果是IPv6环境,基础IP头是40字节,所以同样是1500的MTU,IPv6的MSS是1500减40(IPv6头)再减20(TCP头)等于1440。
我用表格把这个对应关系列出来,照着查就行:
| 场景 | 接口MTU | TCP MSS推荐值(基础IP头+TCP头) |
|---|---|---|
| 标准以太网 / IPv4 | 1500 | 1460 |
| 标准以太网 / IPv6 | 1500 | 1440 |
| PPPoE拨号 / IPv4 | 1492 | 1452 |
| GRE隧道内层 / IPv4 | 1476 | 1436 |
| 标准VLAN标签影响(802.1Q头4字节,实际帧载荷仍为1500) | 1500(L3不变) | 1460,但二层帧上限需考虑帧长为1522 |
记住一点:这些都是“典型值”,MSS最终是在握手时协商出来的,取收发两端看到的路径上较小值。真正决定能否传通的,是整条链路上最小的那个MTU值,也就是路径MTU。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 大包不通小包通:MTU问题的典型故障画像
2.1 PMTU黑洞是怎么产生的
PMTU黑洞这说法听起来玄乎,其实捅破就一层窗户纸。假设你内网服务器MTU是1500,要走PPPoE链路出去,PPPoE这段链路MTU只能到1492。当1500字节的IP报文到达PPPoE链路时,路由器发现包太大,按照协议它有两种选择:要么分片再转发,要么直接丢弃并回送一个ICMP错误消息(Type 3 Code 4,需要分片但DF置位)。
问题就出在这个ICMP通知上。很多网络设备防攻击策略会把ICMP包一股脑丢掉,服务器发出的DF=1的1500字节包被丢掉了,服务器又永远收不到“你需要把报文改小”的通知,于是只能傻等超时重传,重传的包依然是1500字节,继续被丢。客户端和应用层看到的表现就是:连接能建立(握手包小),但数据传输到某个大小就完全卡住。
为什么“网页能开,但大文件传不动”?因为网页交互是小包、短连接,单次请求和响应都远低于MTU限制,根本触发不到路径MTU黑洞。而文件上传下载是连续大流量,很快打出1500字节的大包,一发即丢。
2.2 用ping大包复现问题,操作比你想的要简单
几乎所有Linux发行版都自带ping工具,带DF标志的大包ping是首选的MTU探测手段。命令写法如下,核心在于-M do表示禁止本地做分片(Don't Fragment),-s指定ICMP载荷大小:
bash复制# 不带DF标志,1500字节载荷(会允许中间设备分片)
ping -s 1500 -c 3 目标IP
# 带DF标志,载荷大小从1472开始试(1472 + 28字节IP/ICMP头 = 1500)
ping -M do -s 1472 -c 3 目标IP
# 再往小一点试,比如1452(对应PPPoE链路1500-28)
ping -M do -s 1452 -c 3 目标IP
注意一点:ping的-s参数指定的是ICMP载荷大小,不是IP包大小。一个完整IP包等于IP头(20字节)加ICMP头(8字节)加ICMP载荷。所以ping -s 1472时,IP包大小是1472+8+20=1500,刚好对上以太网MTU。很多人在这里算错,用-s 1500去ping,其实已经超出了1500字节链路的上限。
如果你测的结果是1472通、1473不通,那就说明这条路径的MTU正好是1500,再往大调没有任何意义。如果是1472不通,就把数值往下降,找到能通过的最大值,再加上28字节头部,就是这段路径的实际MTU。
2.3 分片和分段,别再混为一谈
排查这类问题之前,一定先把概念立住。分片(Fragmentation)发生在IP层,IPv4允许一个超过链路MTU的数据报被拆成多片传输,到达对端再重组,前提是DF位不置1。分段(Segmentation)发生在TCP层,是指把应用层大块数据按MSS切成多个TCP段,每个段各有完整的TCP头和IP头。
Linux的TCP协议栈在本地发送时,会主动执行分段,让每个IP包都不超过出接口MTU。但问题往往出在中间路径上:你本机发送的包大小是依据本机网卡MTU来切的,中间要是有一条更小的链路,IP层就必须介入分片。分片会带来两个问题:一是RFC 879里强调只有第一个分片携带TCP头,后续分片不携带,中间任何一个分片丢失,整个TCP段都要重传;二是一些中间设备对分片报文处理性能极差,甚至直接丢片。
所以说,TCP分段是聪明的预防行为,IP分片是无奈的被动应对。MSS机制的存在,就是为了让全网数据段大小刚好适配路径上的最小MTU,尽量做到“永不触发IP分片”。
3. Linux下检测整条链路MTU的三种实测方法
3.1 ping探测法,手动锁定最小MTU
这个方法建议配合Tracert使用。先用tracepath直接看路径MTU,再用分段ping去验证。常规的逐跳测试可以这样写:
bash复制# 查看路径MTU,自动逐跳探测
tracepath -n 目标IP
tracepath会输出每一跳的MTU,并在某跳MTU比上一跳更小时明确标注出来。这个命令网络新手用起来很友好,不需要记那么多参数。但要注意,它依赖UDP和ICMP,有些网络节点不回包,会显示超时,这并不代表链路断了。
如果想要更精确地定位是哪一跳链路限制了MTU,就需要多轮ping加traceroute组合。先用大包通到目标的整段,确认真实MTU,再逐步ping到每一跳,找到第一个丢大包的那个网关,基本就是祸首:
bash复制# 逐跳ping大包,测到每一跳的MTU表现
for i in $(seq 1 15); do
echo "=== TTL $i ===";
ping -M do -s 1472 -c 1 -t $i 目标IP 2>&1;
done
这段脚本可以配合traceroute -T使用,它发送的是TCP SYN包,很多放行ICMP的安全策略也会放行TCP,反而能绕过一部分干扰。
3.2 traceroute的MTU发现模式
Linux下的traceroute有一个跟MTU相关但平时用得少的功能,就是对UDP探测包设置DF位,然后根据返回的ICMP“需要分片”消息中携带的下一跳MTU值来推算路径MTU:
bash复制# 不是所有traceroute版本都支持,但iputils版本基本可用
traceroute --mtu -n 目标IP
输出里每一跳后面会显示类似F=1500或PMTU=1492的信息,意思是这一跳通告的路径MTU是1500或1492。这个值来自中间设备返回的ICMP错误消息,属于最直接的通告信息。实测中遇到报文过大但ICMP被防火墙拦掉时,这个命令同样会卡住,属于无误报但信息不全。
相比之下,tracepath对ICMP被过滤场景会有更多容错,它会尝试用UDP包从小往大枚举,属于动态探测。稳妥做法是两个命令交叉验证,再用ping大包法最终拍板。
3.3 用tcpdump看真实MSS协商
ping能验证IP层的MTU是否可以通过,但TCP层的MSS最终协商成了多少,最好直接抓包验证。这个方法非常适合定位“三层MTU没问题,但TCP吞吐还是上不去”的疑难杂症:
bash复制# 监听80端口,捕获TCP握手报文
tcpdump -i eth0 -nn 'tcp port 80 and (tcp[tcpflags] & tcp-syn) != 0' -c 10
你看握手包里会有类似mss 1460的选项。假如服务器网卡MTU是1500,但协商出来MSS只有1452,那说明某一端知道路径上存在PPPoE这类小MTU链路,主动把MSS降了。如果两端都通告1460,但实测大包还是不通,那问题就出在中间路由的ICMP策略上,要回到防火墙和黑洞问题去找。
这里有个经验之谈:本地发出报文的MSS是根据本机出接口MTU计算的,不是根据对端通告值计算。对端通告的MSS是“我能收多少”,两端协商时取min(本端计算值,对端通告值),但最终每条方向上的实际MSS还会受到路径MTU的钳制,路由器可以在转发SYN包时改写MSS字段,这就是后面会提到的MSS钳制技术。
4. 哪些场景容易把MTU“搞小”了
4.1 PPPoE拨号:经典的1500变1492
家庭宽带和不少办公网络还在用PPPoE拨号。PPPoE协议架构是PPP报文外面再套一层以太网帧,PPP帧头加PPPoE头加起来要占掉8个字节(2字节PPPoE头+6字节PPP协议头),留给IP报文的载荷就只剩下1500-8=1492。
问题在于,服务器端网卡通常还是1500,服务器往出发的包一进入PPPoE链路就必须被分片或者被丢弃。如果这个网络里的拨号设备(光猫、路由器)没有做MSS钳制,那TCP握手时双方还会协商出1460的MSS,数据传输时就会反复触发黑洞问题。
所以给这类网络排查故障时,直接测网关IP的MTU,命令是:
bash复制# 拨号后的接口通常叫ppp0,先确认本机MTU
ip link show ppp0
如果显示mtu 1492,那问题基本就从这里开始。Linux下拨号上网接口的MTU一般都自动配好,但后端服务器不会自动感知,需要在路由或防火墙上做手工钳制,下面第5节会有完整操作。
4.2 隧道封装:GRE / IPsec / VXLAN额外吃掉header
隧道类技术是MTU问题的重灾区。核心原理很简单:一个完整的IP报文要作为“乘客数据”再装进一个新的“外衣”里,外衣本身有头部开销。以GRE为例,外层新加的IP头20字节加GRE头4字节(标准无key时),一共24字节;VXLAN的话是50字节到54字节;IPsec在传输模式和隧道模式下的开销又不相同。
这些场景下,如果一个节点只修改了物理网卡MTU,没有把隧道接口的MTU同步降下来,就会出现“数据能进去,但出隧道口时超限”的尴尬境地。建议做法是:先在物理链路基础上减掉隧道封装开销,设置隧道接口MTU,并同步调整MSS。
| 封装技术 | 额外开销(典型值) | 假设物理MTU 1500时,隧道接口MTU建议值 |
|---|---|---|
| GRE(标准) | 24字节 | 1476 |
| IPsec 隧道模式(ESP AES等) | 约52-60字节 | 1440左右 |
| VXLAN(标准) | 50字节 | 1450 |
| PPPoE | 8字节 | 1492 |
不过这些都是理论值,实际还要看负载均衡策略、是否启用扩展头、硬件卸载等因素。开箱评估最好的办法,还是第3节里那个逐跳探测流程。
4.3 蓝牙BLE MTU:同名完全不同概念的另一种MTU
热词里出现了“ble蓝牙中mtu”,这里也顺带扫个盲。蓝牙BLE的MTU指的是ATT层单次PDU的最大载荷长度,它跟TCP/IP网络里的MTU完全是两回事,只是都用了“最大传输单元”这个词。BLE 4.0时代默认MTU只有23字节,数据吞吐很低;BLE 4.2之后通过连接参数协商可以把MTU调到247字节甚至更高,这样可以显著提升小数据块传输效率。
理解“MTU”的含义一定要咬文嚼字:它永远是“某层协议在某个链路上单次能传输的最大载荷”。放在以太网是1500,放在Wi-Fi是2290或更大,放在BLE ATT层最初是23,放在串口上可能是512。每个技术栈都有自己的MTU,它们之间不通用,别用一个栈的概念去套另一个栈。
4.4 巨型帧:9000不是越大越好
数据中心内部为了减少CPU中断和协议处理开销,会开启Jumbo Frame巨型帧,典型值为9000。它的好处很明显:同样传输1GB数据需要处理的帧数量大幅减少,网络吞吐和CPU占用率都更优。
但巨型帧的坑在于,它要求整条二层链路的所有设备全部支持并且统一开启。一个交换机端口MTU是1500,另一个是9000,中间一旦出现MTU不匹配,现象就是数据在某个交换机口上被默不作声地丢弃。老规矩,大包通不了小包通。所以开启巨型帧前,要么把二层所有端口全部配齐,要么用VLAN隔离出独享巨型帧的网段,千万别在一个广播域里混跑两种MTU配置。
5. Linux下的修改与MSS钳制实操
5.1 直接改接口MTU,用ip命令而不是ifconfig
iproute2套件是Linux网络配置的第一选择,查看接口MTU只需:
bash复制ip link show eth0
临时修改MTU直接这样:
bash复制# 修改eth0的MTU为1400,立即生效,重启失效
sudo ip link set dev eth0 mtu 1400
改完查看确认:
bash复制ip link show eth0
ifconfig也能改,sudo ifconfig eth0 mtu 1400,但它显示的信息格式较老,且很多新版发行版默认已经不装net-tools了。我现在基本只用ip命令。要特别提醒的坑是:修改MTU后,已经建立的TCP连接不会自动感知MTU变小,除非启用PMTUD机制重新探测,否则已建立的连接还是会按旧分段传输,跑出一堆分片重传。一般修改MTU后,建议重启相关服务或重启网络。
5.2 按路由修改MTU,精确控制某个目标网段
有些场景不能改全局的接口MTU,比如同一出口既要访问外网标准以太网,又有一条隧道网段需要更小MTU。这种情况下,可以给路由表项单独指定MTU:
bash复制# 到目标网段走某个网关,同时指定MTU
sudo ip route add 10.10.20.0/24 via 192.168.1.1 dev eth0 mtu 1400
查看路由表确认:
bash复制ip route show
这种路由级MTU非常灵活,但动态路由协议(OSPF、BGP)在写路由时会把它覆盖掉。而且Linux的路由MTU是“建议值”,实际发出报文时,如果接口MTU更小,还是以接口MTU为准。所以路由级MTU适合特殊路径控制,不能替代接口级配置。
5.3 iptables TCPMSS:把MSS钳到链路上限
MSS钳制(MSS Clamping)是所有PPPoE和隧道网络的核心操作。思路是在TCP握手报文穿越路由器时,把SYN包里的MSS选项强行改写为一个不超过链路MTU的安全值。这样TCP数据包从一开始就不再触碰分片临界点。
路由器上针对PPPoE外网接口,常见写法:
bash复制# 对所有经过ppp0出口的TCP流,把MSS钳制为1452
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -o ppp0 -j TCPMSS --set-mss 1452
# 入方向也要做
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -i ppp0 -j TCPMSS --set-mss 1452
如果你不知道自己链路的精确MTU,还可以让iptables自动计算:
bash复制sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -o ppp0 -j TCPMSS --clamp-mss-to-pmtu
--clamp-mss-to-pmtu的语义是从路由表里取该出口的PMTU,减去40字节设置MSS,省去手工计算。对于隧道路由器,更稳妥的做法是明确指定数值。如果服务器本身就是PPPoE拨号节点,需要在本地INPUT链做同样钳制:
bash复制sudo iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -o ppp0 -j TCPMSS --set-mss 1452
nftables语法略有不同,但策略一致:在mangle表抓到携带SYN的TCP包,改写MSS选项。
5.4 写进配置文件,让配置重启后依然生效
临时修改只在当前运行期有效。不同发行版的持久化方式不同:
NetworkManager管理的系统,可以用nmcli修改:
bash复制sudo nmcli connection modify eth0 802-3-ethernet.mtu 1400
sudo nmcli connection up eth0
systemd-networkd管理的系统,编辑/etc/systemd/network/下对应网络文件,加入:
ini复制[Link]
MTUBytes=1400
然后重启网络服务:
bash复制sudo systemctl restart systemd-networkd
老一点用network-scripts的CentOS系列,在/etc/sysconfig/network-scripts/ifcfg-eth0中加一行:
code复制MTU=1400
再systemctl restart network生效。这些都是手工干预,如果你使用DHCP自动获取地址,DHCP服务器也可以下发MTU选项(DHCP Option 26),客户端收到后会自动应用。多数宽带光猫会给PPPoE连接下发1492,给LAN口下发1500,这就是为什么有时候拨号设备后面的networking接口MTU看起来“不太一样”的原因。
6. 几个我踩过且必须提醒的坑
6.1 检查OSPF邻居状态时,先怀疑MTU
热词里有一条“思科ospf dr的mtu大于bdr的mtu”,这说明问的人已经把MTU问题和OSPF关联起来了。实际情况中,OSPF邻居卡在EXSTART或EXCHANGE状态,很多时候不是因为DR/BDR选举,而是接口MTU不匹配。OSPF会在DD报文中携带接口MTU,双方MTU不一致时,邻居关系无法正常建立。
像这类动态路由协议问题,我的排查顺序是:先看接口MTU是否一致,再看Hello/Dead计时器,最后才怀疑认证和区域配置。交换机和Linux服务器之间也常出现这种现象,配合两端ip link和交换机show interface查看MTU,比在OSPF配置里翻半天更快。
6.2 “接口MTU”和“路径MTU”是两个维度,别糊在一起用
接口MTU是你本机出接口能扛多大包;路径MTU是这台服务器到目标之间所有链路的最小MTU。两者可能相同,也可能差很多。排查时先把两个概念拆开:本机接口MTU通过ip link show看,路径MTU通过tracepath和分段ping测。本机接口MTU调得再大,路径上任何一个中继设备MTU不够,照样丢包;反之,本机接口MTU如果设小了,即使路径支持更大的包,也会白白增加分段消耗。
我之前见过生产环境里有人把服务器MTU从1500改成9000,结果忘了交换机下联端口还是1500,全网断片了几分钟。这就是典型的只改本机不改路径,概念没打通。
6.3 Wireshark里看到1518或1522,不用怀疑抓包有问题
很多初学者抓包时看到Frame长度1518,第一反应是“怪了,接口MTU明明是1500”。这其实是没区分层导致的。Wireshark显示的帧长度包含以太网帧头14字节和帧校验FCS 4字节(有些抓包环境不抓FCS,可能显示1514)。加VLAN时再多个4字节,变成1522。所以只要IP层Payload不超过1500,就完全符合MTU限制,链路无恙。
在Linux上查看IP包大小可以用tcpdump -nn -l -e看length字段,那个才是IP报文长度。IP length等于MTU时是正常的,超过MTU一定说明本机或上游设备没按规矩分段。
6.4 最后的快速自查清单
遇到“网页正常、大文件传输卡死、SSH能连但SCP失败”这类现象,按这个清单走一遍:
- 本机接口MTU是多少:
ip link show - 分段ping测路径MTU:
ping -M do -s 1472 对端IP,逐步降值 - tracepath看全路径:
tracepath -n 对端IP - 确认对端接口MTU:登录对端机器跑
ip link show - 检查中间设备或云安全组是否拦截ICMP大包通知
- 是否跨PPPoE链路:查询拨号接口
ip link show ppp0 - 是否走隧道:确认隧道接口MTU有没有同步下调
- TCP握手时MSS协商值:tcpdump抓包看SYN包
这一套下来,90%以上的MTU问题都能定位。
回到开头那个同事的问题,他最后把服务器到公网出接口的MTU改为1400,同时做了MSS钳制,大文件传输恢复了正常。MTU这个问题,原理不复杂,但牵扯的分层概念特别多,稍不留神就会在IP分片、TCP分段、接口MTU和路径MTU之间绕晕。我写这篇,其实就是想把这条链路重新理一遍。搞清楚1500和1460之间的40字节去哪了,搞清楚你发的包沿途要过哪些比它更窄的桥,MTU相关的坑基本就能避开一大半。
