TCP/IP栈简介
当用户需向网络发数据时,实际上是通过APP来完成这项工作。APP向一个描述了对端连接的fd写数据。
位于OS内核的TCP/IP协议栈,从fd收到数据,完成TCP分段,加TCP、IP、Ethernet Header。在加这些Header时,也涉及到一些内容的计算,例如校验和,序列号。
最后,OS内核通过网卡的驱动,告知网卡需发的数据,这里的数据是长度合适、且封装了各种协议头的数据。网卡会再加一些其他数据确保传输的可靠性。最后,数据由网卡从网线发出去,经各个网络转发设备送到对端。
对端,即数据的收端,过程类似,方向反。网卡从网线收到数据,通知OS内核来取数据,位于OS内核的TCP/IP栈完成校验,剥离TCP、IP、Ethernet头部,拼接数据。最后将完整的数据传递给APP。APP仍是通过一个fd读取数据。
所以,可将网络传输在OS内的整个过程分为3个部分:
- User area:APP发和收数据
- Kernel area:TCP/IP栈和OS内核对用户数据的封装解封装
- Device area:网卡实际的收发数据
User area和Device area的工作相对简单,复杂的网络协议的处理主要在Kernel area。Kernel area的任何处理都需CPU完成。若单位时间要传递的数据越多,CPU需进行的运算就越多。
网络带宽这些年有了很大的提升,以太网从最开始的10M到现在100G,提升了一万倍。CPU虽也有很大的发展,但单核CPU的频率并未提升这么多。CPU的核数虽增加了很多,但把一个数据流交给多个CPU核心去处理本身有一定的挑战,另一方面计算机的需处理的任务越来越复杂,尤其是引入了虚拟化后,计算机上不仅跑APP,还需跑容器、虚拟机,CPU本身的负荷可能已很重。
以太网速度的提升>CPU的计算速度的提升,使得CPU能用来处理单个网络包的时间变少了。若CPU不能及时处理数据,那必然会影响网络传输的latency和吞吐。因此需一些技术来降低CPU处理单个网络包的时间。
DMA-Direct Memory Access。
DMA可用于数据收发,是一个通用的技术,它有一个独立于CPU的DMA控制器。在数据copy时,CPU只需告诉DMA控制器,copy数据的起始地址、数据长度,之后将总线控制权交给DMA控制器,无需CPU的介入,完成数据copy。
使用DMA,在网卡从内存copy数据(发),和网卡向内存copy数据(收)时,只需很少的CPU介入。
RSS-Receive Side Scaling
这项加速技术只在数据收时有效。具备RSS能力的网卡,有多个收队列,网卡可用不同的收队列来收不同的网络流,再将这些队列分配到不同的CPU核上进行处理,充分利用多核处理器的能力,将数据收的负荷分散开,提高网络传输的效率。
RSS虽能更好的利用多核CPU,但一方面,数据的分发需考虑TCP连接、NUMA等因素,本身较为复杂。另一方面,它增加了网络传输对CPU的影响,前面说过计算机本身有计算任务,不可能只用来收发数据。在实际使用时,通常会将RSS限定在有限的几个CPU核上,以隔离网络传输带来的CPU影响。
NAPI
New API,是Linux OS针对网络收的优化。硬件I/O与CPU的交互一般有中断和轮询2种方式。中断的CPU代价较大,但实时性好,且无需CPU一直值守,而轮询需CPU定期查询I/O,需CPU一直值守,且不是真正的实时。对于网卡来说,一个繁忙的网络,每次数据包到达,若都采用中断,频繁的中断会影响OS整体效率。而对于一个流量小的网络,若采用轮询,一个是实时性差,会导致Latency上升,另一方面CPU需一直值守,CPU效率低。
NAPI根据不同的场景,采用不同的方式作为CPU和网卡的交互方法,在大网络流量时,采用轮询读取网卡数据,小网络流量时采用中断,从而提高CPU的效率。
Checksum offload
很多网络协议,例如IP、TCP、UDP都有自己的checksum。校验和的计算(发数据包)和验证(收数据包)通过CPU完成。这对CPU的影响很大,因为校验和需每个byte的数据都参与计算。对于一个100G带宽的网络,需CPU最多每s计算大约12G的数据。
为减轻这部分影响,现在的网卡,都支持校验和的计算和验证。OS内核在封装数据包时,可跳过校验和。网卡收到数据包后,根据网络协议的规则,进行计算,再将校验和填入相应的位置。
因Checksum offload的存在,在用tcpdump之类的抓包分析工具时,有时会发现抓到的包提示checksum incorrect。tcpdump抓到的网络包就是OS内核发给网卡的网络包,若校验和放到网卡去计算,那么tcpdump抓到包的时刻,校验和还未被计算出来,自然看到的是错误的值。
Scatter/Gather
这项加速只能用于数据的发。Scatter/Gather本身也是OS里面一个通用的技术,也叫做vector addressing。数据的读取方,不需从一段连续的内存读取数据,而是可从多个离散的内存地址读取数据。例如,OS内核在收到APP传来的原始数据时,可保持这段数据不动。之后在另一块内存中计算出各层协议的Header。最后通知网卡驱动,从这2块内存中将数据copy过去。SG可减少不必要的内存copy操作。
SG需Checksum offload的支持,因为现在数据是离散的,OS内核不太容易计算Checksum。
网络功能卸载
为适应高速网络,现代网卡中普遍卸载了部分 L3-L4 层的处理逻辑(e.g. 校验和计算、传输层分片重组等),来减轻 Host CPU 的处理负担。甚至有些网卡(e.g. RDMA 网卡)还将整个 L4 层的处理都卸载到硬件上,以完全解放 Host CPU。得益于这些硬件卸载技术,Host OS 的网络协议栈处理才能与现有的高速网络相匹配。
现在大多数网卡都具有LRO/GRO功能。即,网卡收包时将同一流的小包合并成大包 (tcpdump抓包可以看到>MTU 1500bytes的数据包)交给内核协议栈。
打开LRO/GRO功能是由网卡进行分包组包,关闭LRO/GRO是由内核进行分包组包
TSO
TCP Offload,用于数据发。与TCP紧密相关。APP可传递任意长度数据给TCP。TCP位于传输层并不会直接将整段用户数据交给下层协议去传输。因为TCP本身是一个可靠的传输协议,而下层协议,IP/Ethernet都不是可靠的,下层协议在数据传输过程中可能会丢失数据。TCP不仅需确保传输的可靠性,为了保证效率,还需尽量提高传输成功率。TCP的办法就是化整为零,各个击破。
在开始后面的描述之前,先说2个相近且容易混淆的词。一个是分段,一个是分片。TCP在将用户数据传给IP层之前,会先将大段的数据根据MSS(Maximum Segment Size)分成多个小段,此过程是分段,分出来的数据是Segments。IP因为MTU(Maximum Transmission Unit)的限制,会将上层传过来的且超过MTU的数据,分成多个分片,此过程是分片,分出来的数据是Fragments。这2个过程都是大块的数据分成多个小块数据,区别就是一个在TCP(L4),一个在IP(L3)完成。
MTU是以太网数据链路层中约定的数据载荷部分最大长度,数据不超过它时就无需分片。
MSS是传输层的概念,由于数据往往很大,会超出MTU,网络将很大的数据载荷分割为多个分片发出去。TCP为了IP层不用分片主动将数据包切割为MSS大小。MSS = MTU - IP header头大小 - TCP 头大小
接着回来,若TCP直接传输整段数据给下层协议,假设是15000byte的用户数据,网卡的MTU是1500,考虑到Header,IP层会将数据分成11个IP Fragments在网络上传输,为了描述简单,假设分成了10个IP Fragments。假设每个IP packet的传输成功率是90%,因为TCP有自己的校验和,在数据的收端,IP必须将完整的15000byte的用户数据收完,且拼接传给TCP,才算收端成功收到数据。这样的话,传输一次成功传输的概率是(90%)^10=34%。一旦TCP收端未成功收到数据,发端就需重传整段数据15000byte。假设发4次,即总共60000byte,传输的成功率能上升到80%。
若TCP本身就将数据分成小段,一段一段传输呢?前面说过,TCP是根据MSS完成分段,MSS通常是根据MTU计算,以确保一个TCP Segment不必在IP层再进行分片。为了描述简单,还是抛开网络协议的头部,现在TCP将应用层的15000byte数据在自己这里分成了10个Segments。每个Segment对应一个IP packet,成功率还是90%。若Segment发失败了,TCP只需重传当前Segment,之前已成功发的TCP Segment不必重传。这样,对于每个Segment,只要发2次成功率就能达到99%。假设每个Segment发2次,相应的应用层数据总共发2次,即30000byte,传输的成功率可达到(99%)^10=90%。TCP 分段后再传输,需发的数据量更少,成功率反而更高。当然实际中,因为TCP 分段,会对每个TCP Segment增加TCP 头部,相应传输的数据会更多一点,但前面的分析结果不受这点数据量的影响。所以,TCP 分段对于TCP的可靠来说是必须的。
但同时,也有缺点。TCP 分段后,相当于对于一段数据,分成了若干个TCP Segments,每个Segment都有自己的TCP头部,这若干个TCP头部,都需CPU去计算checksum,sequence等。同时,每个TCP Segment还会有自己的IP头部,也需CPU去计算IP头部的内容。所以可预见的是,TCP 分段后,CPU的负担增加了许多。
TSO就是将TCP 分段的工作,卸载到网卡来完成。有了TSO,OS只需传给硬件网卡一个大的TCP数据(当然是包在Ethernet Header和IP Header内,且不超过64K)。网卡会代替TCP/IP栈完成TCP 分段。这样,就消除了TCP 分段带来的CPU负担。
另一个好处在DMA。虽说每次DMA操作,不需CPU太多的介入,但仍需CPU配置DMA控制器。DMA的特点在于,无论传输数据的长短,配置工作量是一样的。若OS内核自己完成TCP 分段,那么就有若干个TCP Segments需通过DMA传给网卡。而采用TSO,因为传输的是一大段数据,只需配置一次DMA,就可将数据copy到网卡。这也减轻了CPU的负担。
支持TSO的网卡,仍会按照TCP/IP将数据包生成好并发出去。对于外界OS来说,感受不到TSO的存在。下图是TSO和非TSO下,TCP/IP栈对数据的处理过程对比。
TSO带来提升明显,一方面,更多的CPU被释放出来完成别的工作,另一方面,网络吞吐不受CPU负荷的影响,若没TSO,当CPU性能不好或CPU本身负荷较大时,CPU来不及处理数据,会导致网络吞吐量下降,延时上升。
TSO需SG和Checksum offload的支持。因为TCP/IP栈并不知最终的数据包是什么样,自然也无法完成校验和计算。
小结
以上是Linux OS和周边硬件针对网络加速的一些方案,主要思想还是减少CPU处理数据包的时间,最开始说过,网络传输的核心问题是CPU可用来处理每个网络包的时间变短了,所以减少CPU处理数据包所需的时间是最直观的解决方法。下一篇再看看另一种思路的几个相应技术方案,参考的一些链接也一起放到下一篇。
上一篇看了一些网络加速的技术,它们致力于减少处理每个数据包所需的CPU时间,要么是把一些网络协议的运算卸载到网卡,要么是减少数据copy对CPU资源的消耗。在上一篇说过,网络加速要解决的核心问题是CPU可用来处理每个网络包的时间变短了。因为带宽的增速比CPU处理速度的增速大,所以相同的CPU时间,需处理的数据包更多了。若换一个思路:减少数据包的数量,那相同的CPU时间,需处理的数据包更少了,分给每个数据包的时间也更就多了。这次看看相关的一些技术。
Jumbo Frames
以太网提出时是按照1500byteMTU设计的,即Ethernet Frame的payload最大是1500byte。为何是1500byte?这是一个效率和可靠性的折中选择。因为单个包越长,效率肯定越高,但相应的丢包概率也越大。反过来,单个包越小,效率更低,因为有效数据占整个数据比例更低,不过相应的丢包概率也更小。 因此,IEEE802.3规定了以太网的MTU是1500。
网络传输时,MTU必须匹配,MTU1500向MTU9000的机器发数据没问题。但反过来,MTU9000向MTU1500的机器发数据,因为数据太长,MTU1500的机器识别不了会丢包。因此,数据的收发端MTU必须匹配。另一方面,互联网从几十年前就开始构建,为了统一标准,增加兼容性,整个互联网都是根据IEEE802.3规定的MTU1500来构建。
但现在的网络设备可靠性有了很大的提升,可稳定传输更大的网络包。Jumbo Frames就是MTU为9000byte的Ethernet Frames。对于Jumbo Frames来说,每个数据包的有效数据占比更多,因为网络协议的头部长度是固定的,数据包变长了只能是有效数据更多了。另一方面,以10G网络为例,MTU1500需CPU每s处理超过800,000个网络包,而MTU9000只需CPU每s处理140,000个网络包。因此,在MTU9000下,单位时间CPU需处理的网络包更少了,留给CPU处理每个网络包的时间更多了。
支持Jumbo Frames需相应的硬件,最新的硬件基本都支持了,只需简单的配置即可。但Jumbo Frames在实际使用时有一定局限性。因为Jumbo Frames提出时,互联网已按照MTU1500搭建完了,而MTU又必须匹配,改造全网基本不太可能。所以Jumbo Frames一般只在数据中心内部网络使用,如:内部存储网络。连接互联网的MTU一般还设置为1500。
GSO
Generic 分段 Offload,只在数据发时有效。GSO的作者Herbert Xu说过“If we can't use a larger MTU, we can go for the next-best thing: pretend that we're using a larger MTU.”有点像,现在我不能吃烧鸡,那老板来2片素鸡,比什么也没有强点。既然互联网是基于MTU 1500构建,在互联网上传输的网络包必须遵循MTU 1500,那若在OS里面尽量晚进行IP 分片,在TCP/IP栈里就会有一段“路径”,其上传递的数据是一个payload超过1500byte的网络包,相当于在传递一个大MTU的数据。在这段“路径”上,CPU需处理更少的数据包,相应的留给CPU处理每个网络包的时间就更多了。
其实上一篇介绍的TSO也有此思想,从APP到网卡之间,一直都不进行TCP 分段和IP 分片,数据包最大可到64K。但,TSO只支持TCP,且需硬件网卡的支持,而GSO就是为其他场合提出。其实严格来说,除了TCP,其他的数据大包变小包都是发生在IP层,因此属于IP 分片,所以这里叫GS(egmentation)O并非很恰当。
因为不依赖硬件,又要尽可能晚的分段或分片,所以GSO选择在发给网卡驱动的前一刻将大包分成多个小包。这样,虽网卡收到的还是多个小的数据包,但在TCP/IP栈里面,如下图所示,还是有一段路径,CPU需处理少量的大包。
因为在发给网卡驱动的前一刻完成,所以GSO可作为TSO的备份。在发给网卡驱动时检查网卡是否支持TSO,若支持,将大包直接传给网卡驱动。若不支持,再做GSO。
根据LinuxFoundation的文档,在MTU1500时,使用GSO可使得网络吞吐提升17.5%。
skb_is_gso
static inline bool skb_is_gso(const struct sk_buff *skb)
{
return skb_shinfo(skb)->gso_size;
}
skb_is_gso的逻辑很简单,返回skb_shinfo(skb)->gso_size,所以进入GSO处理逻辑的必要条件之一是skb_shinfo(skb)->gso_size不为0,那么此字段的含义是什么呢?gso_size表示生产GSO大包时的数据包长度,一般时mss的整数倍。下面看skb_gso_ok,如果此函数返回False,就可以进入GSO处理逻辑。
static inline bool skb_gso_ok(struct sk_buff *skb, netdev_features_t features)
{
return net_gso_ok(features, skb_shinfo(skb)->gso_type) &&
(!skb_has_frag_list(skb) || (features & NETIF_F_FRAGLIST));
}
skb_shinfo(skb)->gso_type包括SKB_GSO_TCPv4, SKB_GSO_UDPv4,同时NETIF_F_XXX的标志也增加了相应的bit,标识设备是否支持TSO, GSO, e.g.
NETIF_F_TSO = SKB_GSO_TCPV4 << NETIF_F_GSO_SHIFT
NETIF_F_UFO = SKB_GSO_UDPV4 << NETIF_F_GSO_SHIFT
#define NETIF_F_GSO_SHIFT 16
通过以上三个函数分析,以下三个情况需要协议栈负责GSO。
LRO
Large Receive Offload,或又称为RSC(Receive Side Coalescing),只在数据收时有效。LRO是TSO的逆方向实现,是指网卡将同一个TCP连接的TCP Segments合并成一个大的TCP包,再传给OS。避免了OS处理合并多个小包,减少了CPU的运算时间,且在TCP/IP栈,CPU需处理更少的数据包。与TSO一样,LRO也需网卡的支持。
但与TSO不一样的是,LRO并没那么好用。因为TSO发生在数据的发方,发方掌握了数据的全部信息,发方可按照自己的判断控制发的流程。而LRO发生在数据的收方,且是相对于数据发方的异步收,所以LRO只能基于当前获取到的有限数据和信息做出合并,存在一定的困难。这就像拆一个东西很容易,但要重新组装回去很难一样。
LRO可能会丢失重要的数据,例如数据发方在Header加了一些字段来区分不同的网络包。合并可能导致这些字段的丢失,因为合并后只有一个Header了。而且当OS需转发数据时,合并后的网络包可能需重被分段/片。再重分成小包,原来Header里面的差异字段就彻底丢失了。因为LRO的局限性,在一些最新的网卡上,LRO已被删除了。
GRO
GRO本身是通过包聚合的方式,让CPU一次能处理更多的数据,从而降低CPU的使用,在高速场景下(估计800Mbit以上),CPU可能成为瓶颈,开启GRO,节省的CPU资源才较明显,是能提高极限吞吐的。
但其实在一般用户场景,系统吞吐很难达到500mbit/s,开启GRO,能节省的CPU资源其实非常有限, 因此关闭GRO也没啥影响。
因此,可以在低速场景下 (如:500Mbit一下)关闭GRO来规避此问题。
Generic Receive Offload,是GSO在收端的对应。GRO的作者与GSO是同一个人,都是Herbert Xu。不像GSO作为TSO的替补,GRO逐渐取代了LRO。因为GRO运行在OS内核,掌握的信息更多,GRO可用更加严格的规则来合并数据包。因为合并时更严格,所以可避免关键的信息丢失。另一方面,在一些需转发的场合,GRO可利用GSO的代码来重新分段。
其他的优点还有,GRO也更加通用,不仅不依赖硬件设备,还支持TCP以外的协议。
UFO
UDP 分片 offload。不像TCP,UDP没有分段的过程,APP发给UDP多长的数据(当然要控制在64K以内),UDP都会转给IP层。IP层会根据MTU进行分片。UFO使得网络设备,例如网卡,可将一个超长的UDP数据段(超过MTU),分成多个IPv4分片。因为在网卡做了,所以,CPU的运算量被节省下来了。
不过,在最新的linux内核中,UFO已被弃用了。因为除了TSO,其他的offload基本上都是在IP层做分片,那UFO也没有必要单独存在,因此它与GSO合并表示了。
tx-udp_tnl-分段
Overlay网络,例如VxLAN,现在应用的越来越多。Overlay网络可使得用户不受物理网络的限制,进而创建、配置并管理所需的虚拟网络连接。同时Overlay可让多个租户共用一个物理网络,提高网络的利用率。Overlay网络有很多种,但最具有代表性的是VxLAN。VxLAN是一个MAC in UDP的设计,具体格式如下所示。
从VxLAN的格式可看出,以VxLAN为代表的Overlay网络在性能上存在2个问题。一个是Overhead的增加,VxLAN在原始的Ethernet Frame上再包了一层Ethernet+IP+UDP+VXLAN,这样每个Ethernet Frame比原来要多传输50个byte。所以可预见的是,Overlay网络的效率必然要低于Underlay网络。另一个问题比传50个byte更为严重,那就是需处理这额外的50个byte。这50个byte包括了4个Header,每个Header都涉及到copy,计算,都需消耗CPU。而现在迫切的问题在于CPU可用来处理每个数据包的时间更少了。
首先,VxLAN的这50个byte是没法避免的。其次,那就只能降低它的影响。这里仍可采用Jumbo Frames的思想,因为50个byte是固定的,那数据包越大,50byte带来的影响就相对越小。
先来看一下虚拟机的网络连接图。虚拟机通过QEMU连接到位于宿主机的TAP设备,之后再通过虚机交换机转到VTEP(VxLAN Tunnel EndPoint),封装VxLAN格式,发给宿主机网卡。
理想情况就是,一大段VxLAN数据直接传给网卡,由网卡去完成剩下的分片,分段,并对分成的小的网络包分别封装VxLAN,计算校验和等工作。这样VxLAN对虚机网络带来影响就可降到最低。实际中,这是可能的,但需一系列的前提条件。
首先,虚拟机要把大的网络包发到宿主机。因为虚拟机里面也运行了一个OS,也有自己的TCP/IP栈,所以虚拟机完全有能力自己就把大的网络包分成多个小的网络包。从前面介绍的内容看,只有TSO才能真正将一个大的网络包发到网卡。GSO在发到网卡时,已在进入驱动的前一刻将大的网络包分成了若干个小的数据包。所以这里要求:虚机的网卡支持TSO(Virtio默认支持),且打开TSO(默认打开),同时虚机发出的是TCP数据。
之后,经过QEMU,虚拟交换机的转发,VTEP的封装,此大的TCP数据被封装成了VxLAN格式。50个byte的VxLAN数据被加到了此大的TCP数据上。接下来问题来了,这本来是个TCP数据,但因为做了VxLAN的封装,现在看起来像是个UDP的数据。若OS不做任何处理,按照前面的介绍,那就应该走GSO做IP 分片,并在发给网卡的前一刻分成多个小包。这样,若网卡本来支持TSO现在就用不上了。且更加严重的是,现在还没做TCP 分段。在上一篇花了很大的篇幅介绍其必要性的TCP 分段在这里也丢失了。
对于现代的网卡,除了TSO,GSO等offload选项外,还多了一个选项tx-udp_tnl-分段。若此选项打开,OS自己会识别封装成VxLAN的UDP数据是一个tunnel数据,且OS会直接把这一大段VxLAN数据丢给网卡去处理。在网卡里面,网卡会针对内层的TCP数据,完成TCP 分段。之后再为每个TCP Segment加上VxLAN封装(50byte),如下图右所示。这样,VxLAN封装对于虚拟机网络来说,影响降到了最低。
从前面描述看,要达成上述的效果,需宿主机网卡同时支持TSO和tx-udp_tnl-分段。若这2者任意一个不支持或都不支持。那么OS内核会调用GSO,将封装成VxLAN格式的大段TCP数据,在发给网卡驱动前完成TCP 分段,且为每个TCP Segment加上VxLAN封装。如下图左所示。
若关闭虚拟机内的TSO,或虚拟机内发的是UDP数据。那么在虚拟机的TCP/IP栈会调用GSO,发给虚拟机网卡驱动的前一刻,完成了分段、分片。虚拟机最终发到QEMU的数据包就是多个小的数据包。此时候,无论宿主机怎么配置,都需处理多个小的网络包,并对他们做VxLAN封装。
总结
零零碎碎说了这么多,这些网络加速技术一般不需使用者去配置。因为若支持的话,默认都是打开的。使用时大家只需确认自己的OS是否带有这些功能。又或在调试一些问题时,看看是否是因为这些功能引起的,若是的话,手动关闭它们。