在高性能网络的场景下,C10K是一个具备里程碑意义的场景,15年前它给互联网领域带来了很大的挑战。发展至今,咱们已经进入C10M的场景进行网络性能优化。这期间有怎样的发展和趋势?围绕着各种指标分别有哪些探索和实践?12月20日在北京举办的“七牛架构师实践日”沙龙中,来自京东的资深架构师闫国旗为你们作了题为“从C10K到C10M高性能网络的探索与实践”的分享,如下是对他演讲内容的文字实录。linux
首先带你们回顾一下当年C10K场景中遇到的问题以及为了解决咱们单机下高并发的承载能力所作的改进。在当时的年代,国内互联网的普及程度相对较低,C10K并无给当时中国的互联网环境带来太大冲击,可是在全球互联网环境下你们开始意识到这个问题。为了解决该问题,首先的研究方向就是IO模型的优化,逐渐解决了C10K的问题。nginx
epoll、kqueue、iocp就是IO模型优化的一些最佳实践,这几种技术实现分别对应于不一样的系统平台。以epoll为例,在它的基础上抽象了一些开发框架和库,为广大软件开发者在软件开发带来了便利,好比libevent、libev等。随着当年在IO模型上的革命,衍生出了不少至今为止咱们都在大量使用的优秀开源软件,好比nginx、haproxy、squid等,经过大量的创新、实践和优化,使咱们在今天可以很轻易地解决一个大并发压力场景下的技术问题。算法
这里简单列了几点,较为经常使用的优化技术手段。数据库
目前咱们使用的服务器主要是多路、多核心的x86平台。用于运行咱们的软件代码,在不少场景的业务需求下,都会涉及必定并发任务,不管是多进程模型仍是多线程模型,都要把全部的调度任务交给操做系统,让操做系统帮咱们分配硬件资源。咱们经常使用的服务器操做系统都属于分时操做系统,调度模型都尽量的追求公平,并无为某一类任务作特别的优化,若是当前系统仅仅运行某一特定任务的时候,默认的调度策略可能会致使必定程度上的性能损失。我运行一个A任务,第一个调度周期在0号核心上运行,第二个调度周期可能就跑到1号核心上去了,这样频繁的调度可能会形成大量的上下文切换,从而影响到必定的性能。编程
数据局域性是一样相似的问题。当前x86服务器以NUMA架构为主,这种平台架构下,每一个CPU有属于本身的内存,若是当前CPU须要的数据须要到另一颗CPU管理的内存获取,必然增长一些延时。因此咱们尽量的尝试让咱们的任务和数据在始终在相同的CPU核心和相同的内存节点上,Linux提供了sched_set_affinity函数,咱们能够在代码中,将咱们的任务绑定在指定的CPU核心上。一些Linux发行版也在用户态中提供了numactl和taskset工具,经过它们也很容易让咱们的程序运行在指定的节点上。缓存
这些技术都是近些年来为了优化Linux网络方面的性能而添加的特性,RPS、RFS、XPS都是Google贡献给社区,RSS须要硬件的支持,目前主流的网卡都已支持,即俗称的多队列网卡,充分利用多个CPU核心,让数据处理的压力分布到多个CPU核心上去。RPS和RFS在linux2.6.35的版本被加入,通常是成对使用的,在不支持RSS特性的网卡上,用软件来模拟相似的功能,而且将相同的数据流绑定到指定的核心上,尽量提高网络方面处理的性能。XPS特性在linux2.6.38的版本中被加入,主要针对多队列网卡在发送数据时的优化,当你发送数据包时,能够根据CPU MAP来选择对应的网卡队列,低于指定的kernel版本可能没法使用相关的特性,可是发行版已经backport这些特性。安全
关于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的网络相关优化咱们就不过多的介绍了,主要的内核网络参数的调整在如下两处:net.ipv4.*参数和net.core.*参数。主要用于调节一些超时控制及缓存等,经过搜索引擎咱们能很容易找到关于这些参数调优的文章,可是修改这些参数是否能带来性能的提高,或者会有什么弊端,建议详细的阅读kernel文档,而且多作一些测试来验证。网络
接下来,咱们着重了解如何去更进一步提高咱们单机网络吞吐以及网络处理性能的技术和手段。
计算机硬件作为当前IT发展的重要组成部分。做为软件开发者,咱们更应该掌握这部分的内容,学习了解咱们的软件如何在操做系统中运行,操做系统又怎样分配咱们的硬件资源。
CPU是计算机系统中最核心、最关键的部件。在当前的x86服务器领域咱们接触到主要仍是Intel的芯片。索性咱们就以IntelXeon 2600系列举例。
Intel Xeon 2600系列的CPU已经发布了3代,第4代产品2016年Q1也即将面市,图例中均选取了4代产品最高端的型号。图一为该系列CPU的核心数量统计,从第一代的8核心发展到即将上市的22核心,若干年前,这是很可怕的事情。装配该型号CPU的双路服务器,再开启超线程,垂手可得达到80多个核心。就多核处理器的发展历程来说,核心数量逐年提高,主频基本稳定在必定的范围内,不是说单核主频再也不重要,而是说在当前的需求场景下,多核心才是更符合咱们需求的处理器。
图1
不只仅是核心数量,像LLC缓存的容量、内存带宽都有很大的提高,分别达到了55MB和76.8GB/s。
关于内存,可能它的发展历程并无像CPU或者其余硬件这样耀眼夺目。可能你们更关心的就是价格吧。目前在服务器领域,DDR3内存还是主流,DDR4内存由于成本等问题并无大面积普及。这里列举了IDF15的一些数据,从Intel的销售市场调研报告来看,在明年Q2左右会看到更多的服务器CPU支持DDR4,可是PC机的普及可能还须要一段过渡时间。
当年咱们可能仅仅使用一台服务器就能知足咱们的业务需求,可是随着业务规模的扩大,单台服务器的能力已经远不能支撑如今的业务,所谓的分布式扩展,便被你们推了上现,因此如今的业务对网络的依赖愈来愈高。关于网络硬件,咱们也以Inter系列的网卡来举例,总结一下目前比较成熟的特性,像RSS特性,前面也提到了,这个特性是须要硬件支持的,目前大部分中小企业可能仍是千兆网络为主,像82559这类的网卡,都已经支持了比较多的队列。今年新出的X710芯片也是正对应着云计算或者虚拟化的需求,提供更多相关的特性,如virtualfunction,SR-IOV,tunnel protocol offload等等。随着云计算的发展,将来包含这些特性的网卡将会成为主流。
如今主流的硬件性能已经很强大了,可是咱们的应用软件,真的可以充分利用这些硬件吗?如何更进一步把这些硬件特性利用起来,咱们摸索出一系列的技术的段,先聊聊比较关键的三点:数据包处理、任务调度和数据存储。
首先就是如何处理咱们的数据包,使其速度更快,效率更高。其次让咱们的任务按业务逻辑进行调度,而不只仅是我起个并发模型,让操做系统帮咱们调度。第三是关于数据访问,也就是和内存交互的一些技巧。
在2013年在Shmoocon会议上,Robert提出”kernel不是一个万能的解决方案,它正是一个问题所在。”这样一个命题。随着互联网的发展,Linux已经成为服务器领域上不可或缺的角色。可是它的初衷并非针对某一类应用场景进行特殊的优化和适配,本质上来讲仍是一个分时系统,但愿更公平地服务众多的用户以和任务。但正是由于它的设计目标是如此,因此在过去的发展中也主要是解决这些问题。进而所形成的一个问题就是系统愈来愈庞大,功能愈来愈多,逻辑愈来愈臃肿,虽然经过一些手段有的优化,可是总体的架构对于咱们上层的软件开发者来讲仍是比较复杂的。
图2是从linux foundation的一篇文章kernel_flow中截取的,咱们能够看到在用户态中,从调用write()等API到将数据发送到网卡上通过了至关多的逻辑处理,在不少需求场景中,能够省略必定的逻辑上整个的东西,复杂的实现,必定程度上对软件开发者的理解形成了障碍。
图2
这里重点讲两个瓶颈点,第一个就是全局的队列,在咱们在写用户态网络程序中,对同一个网络端口,仅容许一个监听实例,接收的数据包由一个队列来维护,并发的短链接请求较大时,会对这个队列形成较大的竞争压力,成为一个很大瓶颈点,至少在linuxkernel 3.9版本以前是这样,在3.9的版本合并了一个很关键的特性SO_REUSEPORT,支持多个进程或线程监听相同的端口,每一个实例分配一个独立的队列,必定程度上缓解这个问题。用更容易理解的角度来描述,就是支持了咱们在用户态上对一个网络端口,能够有多个进程或线程去监听它。正是由于有这样一个特性,咱们能够根据CPU的核心数量来进行端口监听实例的选择,进一步优化网络链接处理的性能。第二点也是一个比较大的问题,在linuxkernel中有一个全局的链接表,用于维护TCP链接状态,这个表在维护大量的TCP链接时,会形成至关严重的资源竞争。总的来讲,有锁的地方,有资源占用的地方均可能会成为瓶颈点。
对于前面提到的问题,是咱们为了实现高性能网络优先要解决的。目前,在业界来讲解决这些问题主要有如下两套方案。第一套方案就是说更进一步在linuxkernel中优化网络协议栈处理的业务逻辑和结构,可是这里会有一个问题,前提你要有一个相对有必定kerne经验的团队作这件事情,并且对它足够了解,可是对于大部分企业来讲是很难具有这样的一个团队的。另一个解决方案,让你的数据包接近用户态,尽量的和kernel少作交互,目前咱们选择的是第二种方式,尽量不让kernel作太多事情,至少在网络数据包处理这块。
关于网络数据包处理这块,如何让它更接近用户态,主要有两点。第一点,在收到数据包以后不进协议栈,把数据包的内存直接映射到用户态,让咱们的程序在用户态直接能够看到这些数据。这样就绕过了kernel的处理。第二个其这间利用了linuxUIO,这个特性叫UIO,经过这个模块框架咱们能够在驱动程序收到数据包以后,直接放到用户态的内存空间中,也一样达到了绕过协议栈的目的。
你们也可能想到一个问题,我虽然绕过了Linux协议栈,数据包直接达到用户态,对于大部分比较业务应用来讲,没有太大的意义,为何?由于此时应用程序看到的数据包仅仅是内存中的一段二进制,获取IP等信息,不知如何获取,和原有的socket编程方式彻底不兼容。在一些特殊专用领域的,它们可能会有一些特有的流程来处理这些数据,并且不须要协议栈的支持,例如IDS这类功能。因此咱们须要这样一个协议栈,而且支持传统的Socket框架,避免带来应用程序修改为本。咱们知道协议栈从BSD等系统最初的实现,发展到如今,经历了几十个年头,功能愈来愈复杂,规模愈来愈庞大。像前面提到采用绕过kernel默认协议栈的方案的话,协议栈是永远不可避免的。不管是购买一个商业的协议栈,移植开源协议栈,或者本身从新实现。
如今一些大公司都已经进行了相关的尝试,而且带到了生产环境中。对于协议栈,仍是有一些优化的空间,就TCP协议来讲,它的场景实际上是针对不可控的互联网而产生的,它所面临的问题也主要是延时不一样,距离不一样等等广域网的场景,好比滑动窗口,TCP拥塞算法等等。对于如今的互联网业务而言,每套业务系统都有不少的业务层次,除了接入层须要直接面向互联网和用户打交道,其他大量的数据交互都发生在机房内部或机房之间,就算跨两个异地的机房,之间的网络延时也不过几毫秒,并不像互联网的环境那样的复杂,也正因如此,不少特性在这些场景中看来有些“多余”。也正是由于这些特性,对于网络性能而言也是有必定的损耗,也给咱们更多的空间,根据咱们本身的业务对其进行定制和优化。
扩展性,这个也是你们都面临的一个问题。例如,数据流量进来,须要对它进行采样统计分析,或者作一些更精细化的流量调度,如此众多的业务需求,若是像前面提到的在linuxkernel内部作优化,作扩展的话,挑战仍是蛮大的。正是由于咱们有如此多的业务需求,因此咱们选择了在kernel以外作这样的事情。
咱们的服务器常常会有一种现象,CPU0 使用率很高,但大部分的CPU核心很空闲,咱们在编程的时候,多进程模型或多线程模型,只是描述程序的并发模型,和系统的多核调度并无太多直接的联系。这些调度模型是否可以充分利用多核,就像前面提到的CPU亲和性,将某个进程或线程绑定在指定的CPU核心,充分利用CPU核心的缓存,减小在上下文切换等。这里产生了另一个问题,虽然这种方式是能解决对于CPU多核利用率不佳的问题,但这种硬性关联没法解决咱们业务上的需求,也就是说结合业务,咱们更了解应该怎样调度咱们计算资源和数据。
刚刚提到了多进程和多线程,有些同窗说用协程的模型也能够来解决这个问题吧?但协程只是让开发者更容易使用的一种封装,在进程内模拟并发,并自行管理上下文,比系统提供的要轻一些,但最终仍是落在如今的调度模型上。这里有一个比较有争议的建议,不见得在任何场景中都适用。
前面已经提到过,对于在一条网络链接上的数据处理,尽量保持在一个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卡带来的。做为软件开发人员不只仅代码写的漂亮,质量高,必定要对硬件有必定的了解,作运维的同窗不只须要对硬件,对中间件,对服务器了解的同时,也同时多看看业务方有怎样的需求,怎么样把资源和业务结合的更好,更好的为咱们的用户提供更好的服务。