从C10K到C10M高性能网络的探索与实践

2022-11-15,,,

高性能网络的场景下,C10K是一个具有里程碑意义的场景,15年前它给互联网领域带来了非常大的挑战。发展至今,我们已经进入C10M的场景进行网络性能优化。

这期间有怎样的发展和趋势?环绕着各类指标分别有哪些探索实践

C10K时代的问题与优化手段

首先带大家回想一下当年C10K场景中遇到的问题以及为了解决我们单机下高并发的承载能力所做的改进。在当时的年代。国内互联网的普及程度相对较低,C10K并没有给当时中国的互联网环境带来太大冲击,可是在全球互联网环境下大家開始意识到这个问题。为了解决该问题,首先的研究方向就是IO模型的优化。逐渐攻克了C10K的问题。

epoll、kqueue、iocp就是IO模型优化的一些最佳实践,这几种技术实现分别相应于不同的系统平台。以epoll为例。在它的基础上抽象了一些开发框架和库。为广大软件开发人员在软件开发带来了便利,比方libevent、libev等。随着当年在IO模型上的革命,衍生出了非常多至今为止我们都在大量使用的优秀开源软件,比方nginx、haproxy、squid等。通过大量的创新、实践和优化。使我们在今天能够非常轻易地解决一个大并发压力场景下的技术问题。

这里简单列了几点,较为经常使用的优化技术手段。

CPU亲和性&内存局域性

眼下我们使用的server主要是多路、多核心的x86平台。用于执行我们的软件代码,在非常多场景的业务需求下,都会涉及一定并发任务。不管是多进程模型还是多线程模型,都要把全部的调度任务交给操作系统。让操作系统帮我们分配硬件资源。我们经常使用的server操作系统都属于分时操作系统,调度模型都尽可能的追求公平,并没有为某一类任务做特别的优化,假设当前系统只执行某一特定任务的时候。默认的调度策略可能会导致一定程度上的性能损失。我执行一个A任务。第一个调度周期在0号核心上执行,第二个调度周期可能就跑到1号核心上去了。这样频繁的调度可能会造成大量的上下文切换,从而影响到一定的性能。

数据局域性是同样相似的问题。当前x86server以NUMA架构为主。这样的平台架构下,每一个CPU有属于自己的内存,假设当前CPU须要的数据须要到另外一颗CPU管理的内存获取,必定增加一些延时。所以我们尽可能的尝试让我们的任务和数据在始终在同样的CPU核心和同样的内存节点上,Linux提供了sched_set_affinity函数。我们能够在代码中。将我们的任务绑定在指定的CPU核心上。

一些Linux发行版也在用户态中提供了numactl和taskset工具,通过它们也非常easy让我们的程序执行在指定的节点上。

RSS、RPS、RFS、XPS

这些技术都是近些年来为了优化Linux网络方面的性能而增加的特性。RPS、RFS、XPS都是Google贡献给社区,RSS须要硬件的支持,眼下主流的网卡都已支持。即俗称的多队列网卡,充分利用多个CPU核心,让数据处理的压力分布到多个CPU核心上去。

RPS和RFS在linux2.6.35的版本号被增加,通常是成对使用的,在不支持RSS特性的网卡上。用软件来模拟相似的功能,而且将同样的数据流绑定到指定的核心上,尽可能提升网络方面处理的性能。

XPS特性在linux2.6.38的版本号中被增加,主要针对多队列网卡在发送数据时的优化,当你发送数据包时,能够依据CPU MAP来选择相应的网卡队列。低于指定的kernel版本号可能无法使用相关的特性。可是发行版已经backport这些特性。

IRQ 优化

关于IRQ的优化。这里主要有两点,第一点是关于中断合并。在比較早期的时候,网卡每收到一个数据包就会触发一个中断。假设小包的数据量特别大的时候,中断被触发的数量也变的十分可怕。

大部分的计算资源都被用于处理中断,导致性能下降。后来引入了NAPI和Newernewer NAPI特性,在系统较为繁忙的时候。一次中断触发后,接下来用轮循的方式读取兴许的数据包。以减少中断产生的数量。进而也提升了处理的效率。第二点是IRQ亲和性。和我们前面提到了CPU亲和性较为相似。是将不同的网卡队列中断处理绑定到指定的CPU核心上去,适用于拥有RSS特性的网卡。

这里再说说关于网络卸载的优化,眼下主要有TSO、GSO、LRO、GRO这几个特性。先说说TSO,以太网MTU一般为1500,减掉TCP/IP的包头,TCP的MaxSegment Size为1460,通常情况下协议栈会对超过1460的TCP Payload进行分段。保证最后生成的IP包不超过MTU的大小,对于支持TSO/GSO的网卡来说,协议栈就不再须要这样了,能够将更大的TCPPayload发送给网卡驱动,然后由网卡进行封包操作。通过这个手段,将须要在CPU上的计算offload到网卡上,进一步提升总体的性能。

GSO为TSO的升级版,不在局限于TCP协议。

LRO和TSO的工作路径正好相反。在频繁收到小包时,每次一个小包都要向协议栈传递,对多个TCPPayload包进行合并,然后再传递给协议栈。以此来提升协议栈处理的效率。GRO为LRO的升级版本号,攻克了LRO存在的一些问题。这些特性都是在一定的场景下才干够发挥其性能效率,在不明白自己的需求的时候,开启这些特性反而可能造成性能下降。

Kernel 优化

关于Kernel的网络相关优化我们就不过多的介绍了,基本的内核网络參数的调整在下面两处:net.ipv4.*參数和net.core.*參数。主要用于调节一些超时控制及缓存等。通过搜索引擎我们能非常easy找到关于这些參数调优的文章,可是改动这些參数能否带来性能的提升。或者会有什么弊端,建议具体的阅读kernel文档。而且多做一些測试来验证。

更深入的探索和实践

接下来,我们着重了解怎样去更进一步提升我们单机网络吞吐以及网络处理性能的技术和手段。

计算机硬件做为当前IT发展的重要组成部分。作为软件开发人员,我们更应该掌握这部分的内容。学习了解我们的软件怎样在操作系统中执行,操作系统又怎样分配我们的硬件资源。

硬件

CPU

CPU是计算机系统中最核心、最关键的部件。在当前的x86server领域我们接触到主要还是Intel的芯片。

索性我们就以IntelXeon 2600系列举例。

Intel Xeon 2600系列的CPU已经公布了3代,第4代产品2016年Q1也即将面市,图例中均选取了4代产品最高端的型号。

图一为该系列CPU的核心数量统计,从第一代的8核心发展到即将上市的22核心,若干年前。这是非常可怕的事情。装配该型号CPU的双路server,再开启超线程。轻而易举达到80多个核心。就多核处理器的发展历程来讲。核心数量逐年提升,主频基本稳定在一定的范围内,不是说单核主频不再重要,而是说在当前的需求场景下。多核心才是更符合我们需求的处理器。

图1

不不过核心数量,像LLC缓存的容量、内存带宽都有非常大的提升,分别达到了55MB和76.8GB/s。

内存

关于内存,可能它的发展历程并没有像CPU或者其它硬件这样耀眼夺目。可能大家更关心的就是价格吧。

眼下在server领域,DDR3内存仍是主流。DDR4内存由于成本等问题并没有大面积普及。这里列举了IDF15的一些数据。从Intel的销售市场调研报告来看,在明年Q2左右会看到很多其它的serverCPU支持DDR4,可是PC机的普及可能还须要一段过渡时间。

网络

当年我们可能只使用一台server就能满足我们的业务需求,可是随着业务规模的扩大,单台server的能力已经远不能支撑如今的业务,所谓的分布式扩展。便被大家推了上现。所以如今的业务对网络的依赖越来越高。关于网络硬件。我们也以Inter系列的网卡来举例,总结一下眼下比較成熟的特性,像RSS特性,前面也提到了,这个特性是须要硬件支持的,眼下大部分中小企业可能还是千兆网络为主,像82559这类的网卡,都已经支持了比較多的队列。

今年新出的X710芯片也是正相应着云计算或者虚拟化的需求,提供很多其它相关的特性。如virtualfunction,SR-IOV。tunnel protocol offload等等。随着云计算的发展。未来包括这些特性的网卡将会成为主流。

怎样更好的利用硬件特性

如今主流的硬件性能已经非常强大了,可是我们的应用软件,真的能够充分利用这些硬件吗?怎样更进一步把这些硬件特性利用起来,我们摸索出一系列的技术的段,先聊聊比較关键的三点:数据包处理、任务调度和数据存储。

首先就是怎样处理我们的数据包,使其速度更快。效率更高。其次让我们的任务按业务逻辑进行调度,而不不过我起个并发模型。让操作系统帮我们调度。

第三是关于数据訪问。也就是和内存交互的一些技巧。

在2013年在Shmoocon会议上,Robert提出”kernel不是一个万能的解决方式,它正是一个问题所在。”这样一个命题。随着互联网的发展。Linux已经成为server领域上不可或缺的角色。可是它的初衷并非针对某一类应用场景进行特殊的优化和适配。本质上来说还是一个分时系统,希望更公平地服务众多的用户以和任务。但正是由于它的设计目标是如此。所以在过去的发展中也主要是解决这些问题。进而所造成的一个问题就是系统越来越庞大,功能越来越多,逻辑越来越臃肿,尽管通过一些手段有的优化,可是总体的架构对于我们上层的软件开发人员来说还是比較复杂的。

图2是从linux foundation的一篇文章kernel_flow中截取的。我们能够看到在用户态中,从调用write()等API到将数据发送到网卡上经过了相当多的逻辑处理,在非常多需求场景中。能够省略一定的逻辑上整个的东西,复杂的实现,一定程度上对软件开发人员的理解造成了障碍。

图2

这里重点讲两个瓶颈点。第一个就是全局的队列,在我们在写用户态网络程序中,对同一个网络端口,仅同意一个监听实例,接收的数据包由一个队列来维护。并发的短连接请求较大时,会对这个队列造成较大的竞争压力,成为一个非常大瓶颈点。至少在linuxkernel 3.9版本号之前是这样,在3.9的版本号合并了一个非常关键的特性SO_REUSEPORT。支持多个进程或线程监听同样的端口。每一个实例分配一个独立的队列,一定程度上缓解这个问题。用更easy理解的角度来描写叙述,就是支持了我们在用户态上对一个网络端口,能够有多个进程或线程去监听它。

正是由于有这样一个特性,我们能够依据CPU的核心数量来进行端口监听实例的选择。进一步优化网络连接处理的性能。

第二点也是一个比較大的问题,在linuxkernel中有一个全局的连接表,用于维护TCP连接状态。这个表在维护大量的TCP连接时。会造成相当严重的资源竞争。

总的来说。有锁的地方。有资源占用的地方都可能会成为瓶颈点。

对于前面提到的问题,是我们为了实现高性能网络优先要解决的。眼下。在业界来说解决这些问题主要有下面两套方案。第一套方案就是说更进一步在linuxkernel中优化网络协议栈处理的业务逻辑和结构。可是这里会有一个问题。前提你要有一个相对有一定kerne经验的团队做这件事情,而且对它足够了解。可是对于大部分企业来说是非常难具备这样的一个团队的。

另外一个解决方式。让你的数据包接近用户态,尽可能的和kernel少做交互。眼下我们选择的是另外一种方式,尽可能不让kernel做太多事情,至少在网络数据包处理这块。

关于网络数据包处理这块,怎样让它更接近用户态,主要有两点。第一点。在收到数据包之后不进协议栈,把数据包的内存直接映射到用户态。让我们的程序在用户态直接能够看到这些数据。

这样就绕过了kernel的处理。第二个其这间利用了linuxUIO,这个特性叫UIO,通过这个模块框架我们能够在驱动程序收到数据包之后,直接放到用户态的内存空间中,也同样达到了绕过协议栈的目的。

大家也可能想到一个问题,我尽管绕过了Linux协议栈,数据包直接达到用户态,对于大部分比較业务应用来说。没有太大的意义,为什么?由于此时应用程序看到的数据包不过内存中的一段二进制,获取IP等信息。不知怎样获取。和原有的socket编程方式全然不兼容。在一些特殊专用领域的,它们可能会有一些特有的流程来处理这些数据。而且不须要协议栈的支持,比如IDS这类功能。

所以我们须要这样一个协议栈,而且支持传统的Socket框架,避免带来应用程序改动成本。我们知道协议栈从BSD等系统最初的实现。发展到如今,经历了几十个年头。功能越来越复杂,规模越来越庞大。像前面提到採用绕过kernel默认协议栈的方案的话,协议栈是永远不可避免的。

不管是购买一个商业的协议栈,移植开源协议栈,或者自己又一次实现。

如今一些大公司都已经进行了相关的尝试,而且带到了生产环境中。对于协议栈,还是有一些优化的空间,就TCP协议来说,它的场景事实上是针对不可控的互联网而产生的,它所面临的问题也主要是延时不同。距离不同等等广域网的场景,比方滑动窗体。TCP拥塞算法等等。对于如今的互联网业务而言,每套业务系统都有非常多的业务层次,除了接入层须要直接面向互联网和用户打交道,其余大量的数据交互都发生在机房内部或机房之间,就算跨两个异地的机房,之间的网络延时也不过几毫秒。并不像互联网的环境那样的复杂,也正因如此,非常多特性在这些场景中看来有些“多余”。

也正是由于这些特性,对于网络性能而言也是有一定的损耗,也给我们很多其它的空间,依据我们自己的业务对其进行定制和优化。

扩展性,这个也是大家都面临的一个问题。

比如,数据流量进来。须要对它进行採样统计分析。或者做一些更精细化的流量调度,如此众多的业务需求,假设像前面提到的在linuxkernel内部做优化。做扩展的话,挑战还是蛮大的。正是由于我们有如此多的业务需求。所以我们选择了在kernel之外做这样的事情。

我们的server经常会有一种现象,CPU0 使用率非常高。但大部分的CPU核心非常空暇,我们在编程的时候。多进程模型或多线程模型,不过描写叙述程序的并发模型。和系统的多核调度并没有太多直接的联系。

这些调度模型能否够充分利用多核,就像前面提到的CPU亲和性,将某个进程或线程绑定在指定的CPU核心。充分利用CPU核心的缓存。减少在上下文切换等。这里产生了另外一个问题。尽管这样的方式是能解决对于CPU多核利用率不佳的问题。但这样的硬性关联无法解决我们业务上的需求,也就是说结合业务,我们更了解应该怎样调度我们计算资源和数据。

刚刚提到了多进程和多线程。有些同学说用协程的模型也能够来解决问题吧?但协程不过让开发人员更easy使用的一种封装,在进程内模拟并发,并自行管理上下文,比系统提供的要轻一些,但终于还是落在如今的调度模型上。这里有一个比較有争议的建议。不见得在不论什么场景中都适用。

前面已经提到过,对于在一条网络连接上的数据处理,尽可能保持在一个CPU核心上。以此换取CPUCache的利用率。网络连接流保持在一个核心上。能够减少在网络连接流上处理的优化成本,假设背道而驰的话。可能我接收数据包的是CPU0核心,发送数据包的是CPU1核心,这样可能会引入业务处理上的复杂度。

无锁数据结构,其是很多其它的都是编程上的一些技巧,尽管我们用的是多核平台。尽可能让每一个核心做自己的事情,可是不可避免在业务的需求上还是有跨CPU核心或实例的通信过程,这类通信是无法避免的,我们唯独尽可能保证不要有锁的出现。不是用锁解决资源问题不好。而是这样的上下文中,解决数据一致性可能成本会颇高,得不偿失,特别是在本文提到的需求场景下。

保证你的数据结构尽可能在同样的CPU核心上进行处理。对于一个数据包相关的数据结构或者哈希表。在同样的类型实例上都保存一份,尽管增加了一些内存占用,但减少了资源冲突的概率。

图3是一些常见的延时,当中一部分的数据是在我的laptop上測试得出的,另外一部分是引用JeffDean。我们能够看到对于一个CPU寄存器或缓冲的訪问。基本都在1个时钟周期内就能够完毕了,延时是非常低的,对于L1缓存来说至少须要4个时钟周期,LLC至少要26个时钟周期,到我们内存訪问的话就是更是拉开相当大的数量级。也是说明前面为什么多次提到尽可能充分利用CPU的缓存,而不是说尽可能把数据放在内存里,由于内存訪问的代价。在这里看来有些昂贵的。的确是这样的。可是说对于IO设备,之间的差距则变得更大了。

图3

前面提到的都是关于任务调度以及关于对数据包处理上的优化的关键点,最后这一点主要关于内存的技巧。第一个是尽可能的不要使用多级的指针嵌套,怎么理解呢?在我们传统中实现,都会抽象非常多结构体,通过我们的业务抽象出非常多层次。然后通过指针一级级关联下去,从这个角度看没有什么问题。通过指针能够非常方便对数据进行索引和处理,避免数据处理的拷贝。但最大的问题也是发生在这里。多级的指针检索,有非常大的机率触发你的CacheMiss。对于网络数据处理,尽可能将你的数据结构以及数据业务层面尽可能抽象更加扁平化一些,这是一个取舍的问题,也是在高性能面前寻求一个平衡点。

Hugepage主要解决的问题就是TLB Miss的问题。TLB是须要靠硬件实现的,由于成本非常高。所以它的容量没有像其它的存储呈指数级的增长。

可是我们的内存几十G,上百G都已经是常态了。

这样的不正确称的存在。造成了大内存在4k页面的时候产生了大量的TLB Miss。眼下解决的手段就是使用更大的内存页,也就是hugepage,尽管能够换来性能提升,可是也引入了另外一个问题,就是内存利用率的问题,同一时候要求你对大页内存的使用进行数据结构上的优化。

最后一个内存预分配的问题。事实上在一些软件中能够看到它的影子,像一些数据库的实现,我们能够看到预分配内存的场景,它的引入解决的问题是什么呢?假设须要内存,进行数据存储的时候。频繁去申请释放内存,在内存管理就会把总体的性能拉下来,提前进行内存预分配。能够一定程度上减少这方面的开销。还是说要对内存进行更精细化的管理。避免在内存分配上引入一些性能损失或容量损失等等。

前面提到了这些都是我们在做高性能网络框架这个项目中遇到的一些问题,在业内交流汲取的经验,以及我们在实践上所带来的一些积累。就眼下总体的IT产业的发展。不管软件还是硬件来说,它的发展在向一个目标前进。软件开发人员须要了解硬件的工作原理,硬件接触较多的同学比方运维也要更了解你的业务是怎样的实现,软硬结合会是未来持续方向之中的一个。

尽管说在当前这个云计算大潮下。非常多软件人员只须要把代码写好,直接公布出去了,这样对你的开发是带来一定便利性。但也逃避不了这样的一个角色。帮助你让你的软件更好的执行在底层平台上。

8086刚问世的那个时代,主CPU是没有浮点运算的,要用携处理器来支持。随着历史发展。全部功能都在向CPU内迁移,就像FSB,逐渐取消了。内存管理功能移到了CPU内部,这是CPU整个处理器产业的发展。当然对我们来说是非常好的,我们能够使用更大的带宽在CPU和内存之间进行交互。

我国近几年对于网络安全的发展尤其看重,比方全站HTTPS,我们知道这样的场景的话须要非常大计算资源进行加密解密,可是这部分的运算让通用CPU来做的话。性能并非特别理想。所以又引入了的协处理器的概念,当然已经不是当年的那个协处理器,而用一块辅助的硬件加速卡帮我们做SSL Offload的事情。使用GPU进行加速处理,这个也是我们调研的方向,就是我们希望用GPU这样一个比較优秀的并行架构能够帮助我们在网络处理上。可能达到更好的效果,另外提到关于英特尔的phi卡。天河2号,连续几年拿到超算排名,不可否认有一部分是由phi卡带来的。作为软件开发人员不只代码写的美丽,质量高。一定要对硬件有一定的了解。做运维的同学不仅须要对硬件,对中间件,对server了解的同一时候。也同一时候多看看业务方有怎样的需求,怎么样把资源和业务结合的更好,更好的为我们的用户提供更好的服务。

从C10K到C10M高性能网络的探索与实践的相关教程结束。

《从C10K到C10M高性能网络的探索与实践.doc》

下载本文的Word格式文档,以方便收藏与打印。