请选择 进入手机版 | 继续访问电脑版

默认
发表评论 2
高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索

1、前言


本系列的上篇文章中,我们简要探讨了C10M问题的由来以及基本的技术实现思路(详见《高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了》),本文内容由京东的资深架构师闫国旗分享,以分享者多年的实践和总结,进一步探讨解决C10M问题的理论可行性。

对于中小型应用场景来说,实现单机10万、单机百万、单机千万的负载能力,对团队的运营成本以及产品投入其实差别不是太大。但对于大量、海量用户规模的互联网应用来说,动辄需要部署数千、甚至数万台服务器,如果能将单机性能提升10倍甚至百倍,无论是从硬件投入还是运营成本上来看都能带来非常可观的成本削减,这样的技术变革带来的潜在效益是非常诱人的。

术语解释:C10M是指单机1000万网络并发连接和数据处理能力,C10K是指单机1万网络并发连接和数据处理能力。

2、C10K问题系列文章


本文是C10K问题系列文章中的第4篇,总目录如下:


3、分享者介绍


a.png 闫国旗

- 2012年加入京东,担任资深架构师、京东架构委员会委员;
- 在京东主要负责京东云的基础服务的建设;
- 10年Linux使用经验,专注于SDN、数据库、网络安全、分布式计算等技术领域,拥有丰富架构设计及研发经验;
- 曾在著名开源社区UnixCenter担任核心成员,负责核心服务的维护及在线技术课程的编撰;
- 在移动互联网领域有较为丰富创业积累,是云计算、移动电子商务、在线教育等领域的先行者。

4、内容概述


在高性能网络的场景下,C10K是一个具有里程碑意义的场景(详见《高性能网络编程(二):上一个10年,著名的C10K并发连接问题》),15年前它给互联网领域带来了很大的挑战。发展至今,我们已经进入C10M的场景进行网络性能优化(详见《高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了》)。这期间有怎样的发展和趋势?围绕着各类指标分别有哪些探索和实践?以下正文将一一探讨。

5、C10K时代的问题与优化手段以及带来的启发


首先带大家回顾一下当年C10K场景中遇到的问题以及为了解决我们单机下高并发的承载能力所做的改进(有关C10K问题来龙去脉请参见本系列文章的《高性能网络编程(二):上一个10年,著名的C10K并发连接问题》)。在当时的年代,国内互联网的普及程度相对较低,C10K并没有给当时中国的互联网环境带来太大冲击,但是在全球互联网环境下大家开始意识到这个问题。为了解决该问题,首先的研究方向就是IO模型的优化,逐渐解决了C10K的问题。

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

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

1CPU亲和性 & 内存局域性


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

数据局域性是同样类似的问题。当前x86服务器以NUMA架构为主,这种平台架构下,每个CPU有属于自己的内存,如果当前CPU需要的数据需要到另外一颗CPU管理的内存获取,必然增加一些延时。所以我们尽可能的尝试让我们的任务和数据在始终在相同的CPU核心和相同的内存节点上,Linux提供了sched_set_affinity函数,我们可以在代码中,将我们的任务绑定在指定的CPU核心上。一些Linux发行版也在用户态中提供了numactl和taskset工具,通过它们也很容易让我们的程序运行在指定的节点上。

2RSS、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这些特性。

3IRQ 优化


关于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存在的一些问题。这些特性都是在一定的场景下才可以发挥其性能效率,在不明确自己的需求的时候,开启这些特性反而可能造成性能下降。

4Kernel 优化


关于Kernel的网络相关优化我们就不过多的介绍了,主要的内核网络参数的调整在以下两处:net.ipv4.*参数和net.core.*参数。

主要用于调节一些超时控制及缓存等,通过搜索引擎我们能很容易找到关于这些参数调优的文章,但是修改这些参数是否能带来性能的提升,或者会有什么弊端,建议详细的阅读kernel文档,并且多做一些测试来验证。

6、更深入的探索和总结


从上节里回顾解决C10K性能问题的思路里,为我们尝试解决C10M问题提供了诸多启发。接下来,我们着重了解如何去更进一步提升我们单机网络吞吐以及网络处理性能的技术和手段。

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

1CPU


CPU是计算机系统中最核心、最关键的部件。在当前的x86服务器领域我们接触到主要还是Intel的芯片。索性我们就以IntelXeon 2600系列举例。

Intel Xeon 2600系列的CPU已经发布了3代,第4代产品2016年Q1也即将面市,图例中均选取了4代产品最高端的型号。图一为该系列CPU的核心数量统计,从第一代的8核心发展到即将上市的22核心,若干年前,这是很可怕的事情。装配该型号CPU的双路服务器,再开启超线程,轻而易举达到80多个核心。就多核处理器的发展历程来讲,核心数量逐年提升,主频基本稳定在一定的范围内,不是说单核主频不再重要,而是说在当前的需求场景下,多核心才是更符合我们需求的处理器。

2.png

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

2内存


关于内存,可能它的发展历程并没有像CPU或者其他硬件这样耀眼夺目。可能大家更关心的就是价格吧。目前在服务器领域,DDR3内存仍是主流,DDR4内存因为成本等问题并没有大面积普及。这里列举了IDF15的一些数据,从Intel的销售市场调研报告来看,在明年Q2左右会看到更多的服务器CPU支持DDR4,但是PC机的普及可能还需要一段过渡时间。

3网络


当年我们可能仅仅使用一台服务器就能满足我们的业务需求,但是随着业务规模的扩大,单台服务器的能力已经远不能支撑现在的业务,所谓的分布式扩展,便被大家推了上现,所以现在的业务对网络的依赖越来越高。关于网络硬件,我们也以Inter系列的网卡来举例,总结一下目前比较成熟的特性,像RSS特性,前面也提到了,这个特性是需要硬件支持的,目前大部分中小企业可能还是千兆网络为主,像82559这类的网卡,都已经支持了比较多的队列。今年新出的X710芯片也是正对应着云计算或者虚拟化的需求,提供更多相关的特性,如virtualfunction,SR-IOV,tunnel protocol offload等等。随着云计算的发展,未来包含这些特性的网卡将会成为主流。

4那么该如何更好的利用硬件特性来帮助我们解决C10M问题?


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

首先就是如何处理我们的数据包,使其速度更快,效率更高。其次让我们的任务按业务逻辑进行调度,而不仅仅是我起个并发模型,让操作系统帮我们调度。第三是关于数据访问,也就是和内存交互的一些技巧。

在2013年在Shmoocon会议上,Robert提出“kernel不是一个万能的解决方案,它正是一个问题所在”(详见:http://www.52im.net/thread-568-1-1.html)这样一个命题。随着互联网的发展,Linux已经成为服务器领域上不可或缺的角色。但是它的初衷并不是针对某一类应用场景进行特殊的优化和适配,本质上来说还是一个分时系统,希望更公平地服务众多的用户以和任务。但正是因为它的设计目标是如此,所以在过去的发展中也主要是解决这些问题。进而所造成的一个问题就是系统越来越庞大,功能越来越多,逻辑越来越臃肿,虽然通过一些手段有的优化,但是整体的架构对于我们上层的软件开发者来说还是比较复杂的。

下图是从linux foundation的一篇文章kernel_flow中截取的(原图地址点此进入),我们可以看到在用户态中,从调用write()等API到将数据发送到网卡上经过了相当多的逻辑处理,在很多需求场景中,可以省略一定的逻辑上整个的东西,复杂的实现,一定程度上对软件开发者的理解造成了障碍。

Network_data_flow_through_kernel.png

这里重点讲两个瓶颈点,第一个就是全局的队列,在我们在写用户态网络程序中,对同一个网络端口,仅允许一个监听实例,接收的数据包由一个队列来维护,并发的短连接请求较大时,会对这个队列造成较大的竞争压力,成为一个很大瓶颈点,至少在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.png

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

Hugepage主要解决的问题就是TLB Miss的问题。TLB是需要靠硬件实现的,因为成本很高,所以它的容量没有像其他的存储呈指数级的增长。但是我们的内存几十G,上百G都已经是常态了。这种不对称的存在,造成了大内存在4k页面的时候产生了大量的TLB Miss。目前解决的手段就是使用更大的内存页,也就是hugepage,虽然可以换来性能提升,但是也引入了另外一个问题,就是内存利用率的问题,同时要求你对大页内存的使用进行数据结构上的优化。

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

7、本文小结


前面提到了这些都是我们在做高性能网络框架这个项目中遇到的一些问题,在业内交流汲取的经验,以及我们在实践上所带来的一些积累。就目前整体的IT产业的发展,无论软件还是硬件来说,它的发展在向一个目标前进,软件开发人员需要了解硬件的工作原理,硬件接触较多的同学比如运维也要更了解你的业务是怎样的实现,软硬结合会是未来持续方向之一。虽然说在当前这个云计算大潮下,很多软件人员只需要把代码写好,直接发布出去了,这样对你的开发是带来一定便利性。但也逃避不了这样的一个角色,帮助你让你的软件更好的运行在底层平台上。

8086刚问世的那个时代,主CPU是没有浮点运算的,要用携处理器来支持。随着历史发展,所有功能都在向CPU内迁移,就像FSB,逐渐取消了,内存管理功能移到了CPU内部,这是CPU整个处理器产业的发展。当然对我们来说是很好的,我们可以使用更大的带宽在CPU和内存之间进行交互。我国近几年对于网络安全的发展尤其看重,比如全站HTTPS,我们知道这种场景的话需要很大计算资源进行加密解密,但是这部分的运算让通用CPU来做的话,性能并不是特别理想,所以又引入了的协处理器的概念,当然已经不是当年的那个协处理器,而用一块辅助的硬件加速卡帮我们做SSL Offload的事情。使用GPU进行加速处理,这个也是我们调研的方向,就是我们希望用GPU这样一个比较优秀的并行架构能够帮助我们在网络处理上,可能达到更好的效果,另外提到关于英特尔的phi卡,天河2号,连续几年拿到超算排名,不可否认有一部分是由phi卡带来的。作为软件开发人员不仅仅代码写的漂亮,质量高,一定要对硬件有一定的了解,做运维的同学不仅需要对硬件,对中间件,对服务器了解的同时,也同时多看看业务方有怎样的需求,怎么样把资源和业务结合的更好,更好的为我们的用户提供更好的服务。

(原文链接:http://blog.qiniu.com/archives/4941,有改动)

更多文章


TCP/IP详解 - 第11章·UDP:用户数据报协议
TCP/IP详解 - 第17章·TCP:传输控制协议
TCP/IP详解 - 第18章·TCP连接的建立与终止
TCP/IP详解 - 第21章·TCP的超时与重传
技术往事:改变世界的TCP/IP协议(珍贵多图、手机慎点)
通俗易懂-深入理解TCP协议(上):理论基础
通俗易懂-深入理解TCP协议(下):RTT、滑动窗口、拥塞处理
理论经典:TCP协议的3次握手与4次挥手过程详解
理论联系实际:Wireshark抓包分析TCP 3次握手、4次挥手过程
计算机网络通讯协议关系图(中文珍藏版)
UDP中一个包的大小最大能多大?
Java新一代网络编程模型AIO原理及Linux系统AIO介绍
NIO框架入门(一):服务端基于Netty4的UDP双向通信Demo演示
NIO框架入门(二):服务端基于MINA2的UDP双向通信Demo演示
NIO框架入门(三):iOS与MINA2、Netty4的跨平台UDP双向通信实战
NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战
P2P技术详解(一):NAT详解——详细原理、P2P简介
P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解
P2P技术详解(三):P2P技术之STUN、TURN、ICE详解
高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少
高性能网络编程(二):上一个10年,著名的C10K并发连接问题
高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了
高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索
>> 更多同类文章 ……

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

上一篇:高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了下一篇:简述传输层协议TCP和UDP的区别

本帖已收录至以下技术专辑

评论 2
这系列文章算是长见识了,这篇有点高深的样纸
感谢分享,学习了。

Processed in 0.250000 second(s), 38 queries , Gzip On.

返回顶部