为什么MSS都比MTU小?

1,732 阅读10分钟

不知道你第一次看到二层的MTU和三层TCP的MSS有没有这样一个困惑,既然网络层已经可以通过MTU进行拆包了那为什么传输层还要一个MSS呢?反正我一开始的时候是困惑的。

为了说明今天的问题,先来解释一下MTU和MSS是什么。

MTU

MTU(Maximum Transmission Unit)最大传输单元,是数据链路层能够传输的最大字节数,一般情况下是1500字节,我们可以通过ifconfig或者ip addr命令来查看,如下:

# ip addr
...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
...

可以看到mtu 1500,这个就表示我这台机器二层(数据链路层)允许通过的最大数据是1500字节。那为什么是1500字节呢?这个似乎没有一个官方的说法,但我们可以推测一下。假如我们随便取个数,比如200,我们知道IP头是20字节(假设没有option的情况下, 另外IPv6的IP头是40字节,这里不考虑IPv6的情况),实际传输的数据就是200字节-20字节IP头-20字节TCP头=160字节,有效传输比就是 160/200 = 80%,而如果是1500字节,有效传输比就是(1500-20-20)/1500=97%。所以,之所以设置成1500字节应该是为了有效传输率考虑的。

我们也可以手动修改MTU的大小,例如:

root@debian:~# ip link set eth0 mtu 1400
# 或者
root@debian:~# ifconfig eth0 mtu 1500 up

这里面有几个细节要注意:

1.MTU不包含数据链路层的头

这是因为MTU是链路层的限制,而拆包发生在IP层,这个时候还没有到数据链路层,也就还没有数据链路层的头了,这个过程也符合代码编写的习惯,先判断数据是否符合要求,再决定是发送还是报错。

2. MTU并不是越大越好

首先,每台机器都有限制,并不能设置无限大,在linux源码中我们可以找到一些线索,在头文件linux/include/uapi/linux/if_ether.h中有下面两个常量。

#define ETH_MIN_MTU  68    /* Min IPv4 MTU per RFC791  */
#define ETH_MAX_MTU  0xFFFFU    /* 65535, same as IP_MAX_MTU  */

在Linux中最大可以设置到65535字节,最小可以设置到68字节,而在我的Mac上只能设置到1500字节,如果超过会报一个Error: mtu greater than device maximum。其次,由于网络包会经过非常多的中间设备,而这些中间设备一般都不会超过约定的值,所以设置太大也没有意义。

MSS

MSS(Maximum Segment Size)最大报文段,是属于TCP协议里的一个选项,用来控制TCP能发送数据段的大小。我们可以通过Wireshark抓包来查看,如下:

图片

可以看到MSS在往反的两个方向上可能是不一样的。要说明的是MSS是在TCP头的Option中的,比如上面那个例子我们展开Transmission如下:

图片

MSS会比MTU大吗?

正常情况下是不会的,我们知道,一个网络包从一端发出去到接收端中间可能会经历很多网络设备,比如各种路由器和交换机,这些网络设备的MTU可能有大有小,TCP可以通过PMTU探测感知到链路中比MSS小的MTU,最终取最小的MTU并在此基础上减去20字节的IP头和20字节的TCP头得到最终的MSS,但如果没有开启PMTU探测就可能感知不到链路中的MTU,这时就有可能出现MSS比MTU大(PMTU探测会在后面详细讲)。

前面介绍了什么是MTU和MSS,距离回答我们今天的问题还差一点。我们首先来回顾一下,一个网络包从发送端到接收端都经历了些什么?

首先,我们的应用程序调用send()方法将要发送的数据发送出去,接着被发送的这部分数据会从用户态拷贝到内核态,然后会被内核的网络协议栈接管,网络协议栈拿到数据之后会依次套上TCP头、IP头、数据链路层的头,然后将数据往网卡中写。接着,数据会经过路由器,经过光猫,数据被转换成电信号发送到运营商,然后经过各种骨干线路到达数据中心的机房。接着,经过机房的核心交换、二、三层交换机到达某一台服务器,服务器的网卡会将数据收进来,交给网络协议栈,网络协议栈依次去掉链路层头、IP头、TCP头,找到TCP目标端口对应的应用,将数据交给应用程序,由内核态切换到用户态,整个网络包的传输就完成了。这里只列举了几个关键部分,真实的网络传输比这个要复杂得多。

通过上面数据包发送的过程,我们可以看到,每一个数据包发送的过程都是很复杂的,中间要经历内核态和用户态的切换,还要经过各种网络设备。所以,在实际优化网络性能时经常会考虑一次尽量多的发送数据,这样可以减少上面的传输过程,也可以节省传输过程所携带的各层头信息,从而提升网络的传输效率。但考虑到我们的网络资源以及中间各种设备的性能不一样,单个数据包并不是越大越好,这才出现了像MTU这类限制数据传输大小的规则。

IP层拆包存在什么问题?

在TCP层通过MSS来决定数据是否需要拆包,所以TCP层会保证每个数据包的大小不大于MSS的大小,这么做的意义在哪里呢?

我们假设系统MTU是1500字节,此时我们发送1600字节数据(不包含IP和TCP头),很明显根据IP层的拆包规则,会拆成两个包,第一个包1500字节,第二个包160字节,为什么第二个包是160字节呢?由于每个包IP头都需要占20个字节,第一个包1500字节,实际有效数据是1460字节(因为还有一个IP头和TCP头各占20字节)。还剩下140字节,加上20字节的IP头发送出去,所以是160字节。到这里为止,如果你不熟悉网络层(IP层或者叫三层)可能是晕的,比如会疑惑为什么第二个包不用包含TCP的头呢?我们假设TCP没有MSS限制,下面我画了一张图:

图片

可以看到,当数据从传输层流入到网络层(IP层)发现数据链路层的MTU是1500字节,没法容纳1600字节的数据,将数据拆成了两个包发送出去,当到达目地机器IP层再将数据重组,然后交给传输层。

网络分层的底层逻辑就是每一层需要保证每一层的数据完整性,对于上面的例子来说,我们发送1600字节数据,IP层发生了拆包,那IP层就要解决数据包的重组,从而保证上层(TCP)发送和接收到的数据是一致的。从TCP的视角来看,我发了1600字节数据,就要保证接收端能收到1600字节的数据。

通过上面的分析,我们可发现一个问题,假如一个包发送失败了,触发了TCP的重传(要注意的是这里的重传是重传所有1600字节的数据),那这个包又会在IP层拆包然后重组,很显然这个效率是不高的。另外一方面,我们通过上面的例子可以看出来,IP拆包之后每个包都要额外携带20字节的IP头,这在传送大文件时会占用大量带宽。

所以,TCP协议为了避免IP拆包,都会在三次握手的时候就协商MSS,从MSS的大小我们也可以看出,为了保证传输效率,MSS的值刚好是去掉IP和TCP的头。这里要注意,MSS的大小是不包含IP和TCP头的,而MTU是包含IP和TCP头的,但MTU不包含链路层的头,原因我们在前面已经讲过了。

MTU一经确定不再改变?

实际上MTU并不是确定了之后就一直不变的,每个端上查看到的MTU并非是最终的MTU大小,这个怎么理解呢?

大部情况下,网络发送端都需要经过各种中间设备才能到达目标机器,这些中间设备可能是路由器、交换机、中间代理服务器等等,这些设备的MTU大小可能有大有小,更糟糕的是,每次走的路径不一样MTU大小可能也不一样。那么,假如我们的发送端的MTU是1500,中间某个交换机或者路由器的MTU是200字节,当数据包到达这个设备的时候IP层就会触发拆包,我们上面详细分析了IP层拆包存在的问题。所以,很明显如果出现这种情况网络传输效率会大幅降低。那么有什么办法可以解决这个问题呢?

我们可以思考一下,要解决这个问题其实只要找到链路中最小的MTU就可以了,那么,我们如何感知中间设备的MTU呢?有一个叫ICMP的协议可以在中间设备出现异常的时候将异常返回,从而发送端可以感知到,关于ICMP协议这里不展开,有兴趣可以自行去了解。

我们可以通过ping来模拟一下,如下:

root@debian:~# ping -s 1700 -M do www.seepre.com
PING www.seepre.com (106.12.210.214) 1700(1728) bytes of data.
ping: local error: message too long, mtu=1500
ping: local error: message too long, mtu=1500
ping: local error: message too long, mtu=1500

其中,-s表示发送多少字节,ping会填充对应的字节数将包发出去。-M表示回显MTU相关的错误信息。最终我们看到返回了错误,提示说我们发送的数据超长了,并且提示当前链路中MTU的大小。

这样的话,我们就可以拿到整个链路中最小的那MTU了,传输层便可以根据最小的MTU来设置自己的MSS,比如,前面的例子,假如中间某个设备的MTU仅为200字节,那MSS就设置成160字节,这样TCP每次发送的数据包就不会超过200字节,也就不会触发IP层的拆包了。实际上,TCP协议已经实现了链路MTU的探测,叫做PMTU,原理就是设置IP报头DF不分片位置为不分片,这样当遇到比MSS小的MTU的设备,这个设备就会返回一个ICMP报文,里面携带了错误消息和可接受的MTU大小。

但是,如果你仔细想想,这事似乎没那么简单。假如,某一时刻,出现了一条更优的链路,经过的中间设备最小的MTU也可以达到1500字节了,这时候TCP还是按照200字节在传输,那岂不白白浪费了吗?这个又涉及到TCP的拥塞控制的内容了,在拥塞控制部分再详细分析。