TCP/IP协议栈简介
当用户需要向网络发送数据的时候,用户实际上是通过应用程序来完成这项工作。应用程序向一个描述了对端连接的文件描述符(File Description)写数据。
之后位于操作系统内核的TCP/IP协议栈,从文件描述符收到数据,完成TCP分段(如果是TCP连接的话),加TCP,IP,Ethernet Header。在加这些Header的时候,也涉及到一些内容的计算,例如校验和,序列号。
最后,操作系统内核通过网卡的驱动,告知网卡需要发送的数据,这里的数据是长度合适,并且封装了各种协议头的网络数据。网卡会再加一些其他数据确保传输的可靠性。最后,网络数据由网卡从网线(如果是有线连接的话)发出去,经过各个网络转发设备送到对端。
对端,也就是网络数据的接收端,有个类似的过程,不过方向是反的。网卡从网线接收到数据,通知系统内核来取数据,位于系统内核的TCP/IP协议栈完成校验,剥离TCP、IP、Ethernet头部,拼接数据。最后将完整的数据传递给应用程序,或者说最终用户。用户程序仍然是通过一个文件描述符读取数据。
所以,可以将网络传输在操作系统内的整个过程分为三个部分:
- User area:应用程序发送和接收数据
- Kernel area:TCP/IP协议栈和系统内核对用户数据的封装解封装
- Device area:网卡实际的发送并接收网络数据
从前面的描述可以看出,User area和Device area的工作都相对简单,而对于复杂的网络协议的处理主要在Kernel area。Kernel area的任何处理都是需要CPU完成的。很显然,如果单位时间要传递的数据越多,CPU需要进行的运算就越多。
网络带宽这些年有了很大的提升,以太网从最开始的10M到现在100G,提升了一万倍。虽然CPU这些年也有很大的发展,但是单核CPU的频率并没有提升这么多。有人可能会说CPU的核数增加了很多,但是把一个网络数据流交给多个CPU核心去处理本身有一定的挑战,另一方面计算机的需要处理的任务越来越复杂,尤其是引入了虚拟化之后,计算机上不仅跑应用程序,还需要跑容器,虚拟机,CPU本身的负荷可能就已经很重。
以太网速度的提升大于CPU的计算速度的提升,使得CPU能够用来处理单个网络包的时间变少了。如果CPU不能及时处理网络数据,那必然会影响网络传输的延时(latency)和吞吐量(throughput)。因此需要一些技术/方案来降低CPU处理单个网络包的时间。这次就过一下现在计算机系统中常见的网络加速技术,当然还是浅谈。
DMA
DMA全称是Direct Memory Access。DMA可以同时应用于网络数据的发送和接收。DMA本身是一个通用的技术,它有一个独立于CPU的DMA控制器。在数据拷贝的时候,CPU只需要告诉DMA控制器,拷贝数据的起始地址,数据长度,之后将总线控制权交给DMA控制器,就可以不需要CPU的介入,完成数据拷贝。
使用DMA,在网卡从内存拷贝数据(发送),和网卡向内存拷贝数据(接收)时,只需要很少的CPU介入。
RSS
RSS全称是Receive Side Scaling,从名字上可以看出,这项加速技术只在网络数据接收时有效。具备RSS能力的网卡,有多个接收队列,网卡可以用不同的接收队列来接收不同的网络流,再将这些队列分配到不同的CPU核上进行处理,充分利用多核处理器的能力,将网络数据接收的负荷分散开,从而提高网络传输的效率。
RSS虽然能更好的利用多核CPU,但是一方面,网络数据的分发需要考虑TCP连接,NUMA等因素,本身较为复杂。另一方面,它增加了网络传输对CPU的影响,前面说过计算机本身有计算任务,不可能只用来收发网络数据。在实际使用的时候,通常会将RSS限定在有限的几个CPU核上,以隔离网络传输带来的CPU影响。
NAPI
NAPI全称是New API,这是Linux系统针对网络接收的优化。硬件I/O与CPU的交互一般有中断和轮询两种方式。中断的CPU代价较大,但是实时性好,且不需要CPU一直值守,而轮询需要CPU定期查询I/O,需要CPU一直值守,并且不是真正的实时。对于网卡来说,一个繁忙的网络,每次网络数据包到达,如果都采用中断,这样频繁的中断会影响系统的整体效率。而对于一个流量小的网络,如果采用轮询,一个是实时性差,会导致延时(Latency)上升,另一方面CPU需要一直值守,CPU效率低。
NAPI根据不同的场景,采用不同的方式作为CPU和网卡的交互方法,在大网络流量的时候,采用轮询的方式,读取网卡数据,小网络流量的时候则采用中断的方式,从而提高CPU的效率。
Checksum offload
很多网络协议,例如IP、TCP、UDP都有自己的校验和(checksum)。传统上,校验和的计算(发送数据包)和验证(接收数据包)是通过CPU完成的。这对CPU的影响很大,因为校验和需要每个字节的数据都参与计算。对于一个100G带宽的网络,需要CPU最多每秒计算大约12G的数据。
为了减轻这部分的影响,现在的网卡,都支持校验和的计算和验证。系统内核在封装网络数据包的时候,可以跳过校验和。网卡收到网络数据包之后,根据网络协议的规则,进行计算,再将校验和填入相应的位置。
因为Checksum offload的存在,在用tcpdump之类的抓包分析工具时,有时会发现抓到的包提示校验和错误(checksum incorrect)。tcpdump抓到的网络包就是系统内核发给网卡的网络包,如果校验和放到网卡去计算,那么tcpdump抓到包的时刻,校验和还没有被计算出来,自然看到的是错误的值。
Scatter/Gather
这项加速只能用于网络数据的发送。Scatter/Gather本身也是操作系统里面一个通用的技术,也叫做vector addressing。简单来说,就是数据在传输的过程中,数据的读取方,不需要从一段连续的内存读取数据,而是可以从多个离散的内存地址读取数据。例如,系统内核在收到应用程序传来的原始数据时,可以保持这段数据不动。之后在另一块内存中计算出各层协议的Header。最后通知网卡驱动,从这两块内存中将数据拷贝过去。SG可以减少不必要的内存拷贝操作。
SG需要Checksum offload的支持,因为现在数据是离散的,系统内核不太容易计算Checksum。
TSO
TSO全称是TCP Segmentation Offload,它只能用于网络数据的发送。从名字可以看出,这是一个与TCP协议紧密相关的方法。
应用程序可以传递任意长度数据给TCP。TCP位于传输层并不会直接将整段用户数据交给下层协议去传输。因为TCP本身是一个可靠的传输协议,而下层协议,IP/Ethernet都不是可靠的,下层协议在数据传输过程中可能会丢失数据。TCP不仅需要确保传输的可靠性,为了保证效率,还需要尽量提高传输的成功率。TCP的办法就是化整为零,各个击破。
在开始后面的描述之前,先说两个相近且容易混淆的词。一个是Segmentation(分段),一个是Fragmentation(分片)。TCP协议在将用户数据传给IP层之前,会先将大段的数据根据MSS(Maximum Segment Size)分成多个小段,这个过程是Segmentation,分出来的数据是Segments。IP协议因为MTU(Maximum Transmission Unit)的限制,会将上层传过来的并且超过MTU的数据,分成多个分片,这个过程是Fragmentation,分出来的数据是Fragments。这两个过程都是大块的数据分成多个小块数据,区别就是一个在TCP(L4),一个在IP(L3)完成。
接着回来,如果TCP直接传输整段数据给下层协议,假设是15000字节的用户数据,网卡的MTU是1500,考虑到Header,IP层会将数据分成11个IP Fragments在网络上传输,为了描述简单,我们就假设分成了10个IP Fragments。假设每个IP packet的传输成功率是90%,因为TCP协议有自己的校验和,在数据的接收端,IP协议必须将完整的15000字节的用户数据收完,并且拼接传给TCP,才算接收端成功收到数据。这样的话,传输一次成功传输的概率是(90%)^10=34%。一旦TCP接收端没有成功收到数据,发送端就需要重新将整段数据15000字节再发一次。假设发送4次,也就是总共60000字节,传输的成功率能上升到80%。
如果TCP协议本身就将数据分成小段,一段一段传输呢?前面说过,TCP是根据MSS完成Segmentation,MSS通常是根据MTU计算,以确保一个TCP Segment不必在IP协议层再进行Fragmentation。为了描述简单,我们还是抛开网络协议的头部,现在TCP将应用层的15000字节数据在自己这里分成了10个Segments。每个Segment对应一个IP packet,成功率还是90%。如果Segment发送失败了,TCP只需要重传当前Segment,之前已经成功发送的TCP Segment不必重传。这样,对于每个Segment,只要发送2次成功率就能达到99%。假设每个Segment发送2次,相应的应用层数据总共发送2次,也就是30000字节,传输的成功率可以达到(99%)^10=90%。也就是说TCP Segmentation之后再传输,需要发送的数据量更少,成功率反而更高。当然实际中,因为TCP Segmentation,会对每个TCP Segment增加TCP 头部,相应传输的数据会更多一点,但是前面的分析结果不受这点数据量的影响。所以,TCP Segmentation对于TCP的可靠来说是必须的。
但同时,它也有自身的缺点。TCP Segmentation之后,相当于对于一段数据,分成了若干个TCP Segments,每个Segment都有自己的TCP头部,这若干个TCP头部,都需要CPU去计算checksum,sequence等。同时,每个TCP Segment还会有自己的IP协议头部,也需要CPU去计算IP协议头部的内容。所以可以预见的是,TCP Segmentation之后,CPU的负担增加了许多。
TSO就是将TCP Segmentation的工作,卸载(offload)到网卡来完成。 有了TSO,操作系统只需要传给硬件网卡一个大的TCP数据(当然是包在Ethernet Header和IP Header内,且不超过64K)。网卡会代替TCP/IP协议栈完成TCP Segmentation。这样,就消除了TCP Segmentation带来的CPU负担。
另一个好处在DMA。虽然说每次DMA操作,不需要CPU太多的介入,但是仍然需要CPU配置DMA控制器。DMA的特点在于,无论传输数据的长短,配置工作量是一样的。如果系统内核自己完成TCP Segmentation,那么就有若干个TCP Segments需要通过DMA传给网卡。而采用TSO,因为传输的是一大段数据,只需要配置一次DMA,就可以将数据拷贝到网卡。这也一定程度减轻了CPU的负担。
支持TSO的网卡,仍然会按照TCP/IP协议将网络数据包生成好并发送出去。对于外界系统来说,感受不到TSO的存在。下图是TSO和非TSO下,TCP/IP协议栈对网络数据的处理过程对比。
TSO带来的提升是明显的,一方面,更多的CPU被释放出来完成别的工作,另一方面,网络吞吐量(throughput)不受CPU负荷的影响,如果没有TSO,当CPU性能不好或者CPU本身负荷已经较大时,CPU将来不及处理足够的网络数据,会导致网络吞吐量下降,延时上升。
TSO需要SG和Checksum offload的支持。因为TCP/IP协议栈并不知道最终的网络数据包是什么样,自然也没办法完成校验和计算。
小结
以上是Linux系统和周边硬件针对网络加速的一些方案,主要思想还是减少CPU处理网络数据包的时间,最开始我们说过,网络传输的核心问题是CPU可以用来处理每个网络包的时间变短了,所以减少CPU处理网络数据包所需的时间是最直观的解决方法。