/** * 谨献给Yoyo * * 原文出处:https://www.toptal.com/software/guide-to-multi-processing-network-server-models * @author dogstar.huang <chanzonghuang@gmail.com> 2016-04-02 */
做为多年来一直在编写高性能网络代码的人(个人博士论文主题是适配多核系统分布式应用的高速缓存服务),如今我看到了不少彻底不知道或忽略讨论网络服务模型基本原理的教程。所以,本文旨在但愿能为你们提供有用的概览以及网络服务模型的比较,以揭开编写高性能网络代码的神秘面纱。
程序员
本文主要针对“系统程序员”,即与他们的应用程序的底层细节工做、实现网络服务代码的后端开发。这一般是在C++或C来完成,虽然时下大部分现代语言和框架经过各类级别的效率提供了体面的底层功能。算法
既然经过增长内核更容易扩展CPU,我会把这做为常识,而本质倒是调整软件以便最大化使用这些内核。所以,问题就变成如何在能在多个CPU上并行执行的线程(或进程)中分区软件。后端
我也将理所固然地认为读者意识到,“并发”基本上意味着“多任务处理”,即一些代码实例(不管是相同或不一样的代码,这并不重要),在同一时间是活跃的。并发能够在单个CPU上实现,而且一般前现代时期是这样的。具体地,并发能够经过在单个CPU上的多个进程或线程之间快速切换来实现。这是老式的、单CPU系统如何管理在同一时间运行众多应用程序的方式,在某种程度上,用户会以为应用程序是在同时执行,尽管实际上并无。另外一方面,平行度,从字面上看具体意味着代码经过多个CPU或CPU内核在同一时间执行。缓存
出于这个讨论的目的,假如咱们谈论线程或全过程,它基本上是不相关的。现代操做系统(而Windows显然是个例外)把进程看待像线程同样轻量级(或在某些状况下,反之亦然,线程都得到了功能,这使得它们像进程同样重量级)。现在,进程和线程之间的主要区别是在跨进程或跨线程通讯和数据共享的功能。其中,进程和线程之间的区别是很重要的,我会进行适当的备注,不然,在这些部分能够安全地考虑“线程”和“过程”是能够互换的。安全
这篇文章具体处理网络服务代码,这部分须要实现如下三个任务:服务器
关于跨进程分区分区这三个任务,这里有几个广泛的网络服务模型,即:网络
这些都是在学术界使用的网络服务模型的名字,我记得“在野外”的同义词发现至少其中的一些。(名字自己,固然,并非那么重要的 -- 真正的价值是如何洞悉代码是怎么回事。)数据结构
这些网络服务模型,每个都会在下面的部分中进一步说明。多线程
MP的网络服务模型是每一个人都会首选用来学习的一个,特别是学习多线程的时候。在MP模型中,有一个“master”进程,接收链接(任务#1)。一旦创建了链接,主进程建立一个新的进程,并把链接的socket传给它,因此一个进程一个链接。这个新的进程而后一般和此链接以简单、连续、锁步的方式工做:进程从链接中读取一些东西(任务#2),而后作一些计算(任务#3),而后写一些东西给它(再次 任务#2)。架构
模型MP是很容易实现的,并且实际工做极为出色只要进程总数维持很低很低。有多低?答案取决于任务#2和任务#3蕴含了什么。经验法则,能够说进程数或线程数不该超过CPU内核的两倍。一旦有在同一时间激活太多进程,操做系统则趋于花费了太多在于时间抖动(即,围绕可用的CPU内核上平衡进程或线程)和这样的应用一般最终花费几乎全部的CPU的一次在“SYS”(或内核)代码,实际上却作了一点点真正有用的工做。
优势:实现很简单,只要链接数不多能够工做得很是好。
缺点:若是进程数增加太大则趋于使得操做系统过载太重,而且可能会有延迟抖动网络IO等待,直到有效载荷(计算)阶段结束。
该SPED网络服务器模型,因最近一些高调的网络服务应用程序,如Nginx而出名。基本上,它在同一个进程作了这三项任务,在它们之间之间复用。为了提升效率,它须要像epoll和kqueue的一些至关先进的核心功能。在这种模型下,代码是由传入的链接和数据“事件”驱动,而且实现了一个看起来像这样的“事件循环”:
全部这一切都在一个单一的进程中完成,而且能够很是有效地完成,由于它彻底避免了进程之间的上下文切换,这一般会形成MP模型严重的性能问题。这里惟一的上下文切换来自系统调用,而这些又经过仅做用于有某些事件绑定的具体链接而使得切换最小化。该模型能够同时处理数万的链接,只要有效载荷工做(任务#3)不是太复杂或是资源密集型的。
尽管这种方式有两大缺点:
一、因为三个任务都在一个单一的循环迭代中顺序进行,有效载荷工做(任务#3)和全部东西都是同步完成的,也就是说,若是它须要很长的时间来计算到由客户端接收的数据的响应,当正在作这点时其余东西都会中止,而这会在延迟中引入潜在的巨大波动。
二、只使用一个CPU内核。这样再次是有好处,绝对限制了来自操做系统要求的上下文切换数量,从而提升了总体性能,但有明显的不足就是其余任何可用的CPU内核都无事可作。
这是对于须要更先进的模型的理由。
优势:能够是具备高性能,在操做系统易于实现(即,须要最少许的OS干预)。只须要一个CPU内核。
缺点:仅利用单个CPU(无论可用的数量)。若是有效载荷工做不统一,会致使非均匀的响应延迟。
该SEDA网络服务模型有点复杂。它把复杂的,事件驱动的应用程序分解到一组由队列链接的阶段。尽管若是不仔细实现,它的性能会跟MP状况中同一问题而受到影响。它的工做原理是这样的:
有效载荷工做(任务#3)会尽量地分红多个阶段,或模块。每一个模块实现了驻留在其本身单独的进程中单个特定功能(可认为是“微服务”或“微内核”),而且这些模块经由消息队列相互通讯。此架构能够表示为节点图,其中节点是进程,边是消息队列。
一个单一进程执行任务#1(一般遵循SPED模型),它将新链接交付于特定的条目点节点。这些节点能够是传递数据给其余节点进行计算,或者也能够是实现有效载荷处理(任务3#)的纯网络节点(任务#2)。一般没有“master”进程(例如,一个收集并汇集响应,并将其经过链接发送返回),由于每个节点均可以经过自身进行响应。
理论上,这种模式能够是任意复杂的,由于节点图可能具备循环,链接到其余相似的应用程序,或是链接到其实是在远程系统上执行的节点。但在实践中,即便有定义良好的消息和高效的队列,它会变得笨拙难以思考,而且把系统的行为做为一个总体来推理。相比于SPED的模式,来往传递的消息可能会破坏该模型的性能,若是每一个节点的工做都是很简短的话。该模型的效率显然比SPED模型的要低,因此它一般采用在有效载荷的工做复杂且耗时的状况。
优势:软件架构师最终的梦想:一切都分割成整齐而又独立的模块。
缺点:复杂度随模块数量而爆炸,而且消息队列仍然比直接内存共享慢得多。
该AMPED网络服务是SEDA驯服的,更易于模型的一个版本。没有过多不一样的模块和进程,也没有多过的消息队列。下面是它如何工做的:
这里最重要的是,有效负载工做是在一个固定的(一般配置的)数量的进程中进行,这独立于链接的数量。这样的好处是,有效负载能够是任意复杂,而且也不会影响网络IO(这是很好的等待时间)。并且还可能带来更高的安全性,由于只有一个进程在作网络IO。
优势:网络IO和有效载荷的工做分离很是清晰。
缺点:为在进程之间来回传递数据利用消息队列,而这根据不一样协议的性质,可能成为瓶颈。
该SYMPED网络服务模型在许多方面是网络服务模型的“圣杯”,由于它就像有独立SPED“worker”进程的多个实例。它是经过由单一进程循环接收链接,而后将它们传递到工做进程得以实现,每个都有一个像SPED的事件循环。这有一些很是有利的后果:
事实上,这一点,也是最新版Nginx在作的;它们生产出少许工做进程,每一个运行一个事件循环。为了使事情变得更好,大多数操做系统都提供了一个可由多个进程在一个独立的TCP端口侦听传入链接的功能,省去了为某个特定进程决定与网络链接工做的须要。若是你正在使用的应用程序能够经过这种方式来实现,我建议这样作。
优势:经过像SPED那样循环可控制的数量,严格提升CPU使用率天花板。
缺点:因为每一个过程有一个像SPED那样的循环,若是有效载荷工做是不均匀的,等待时间能够再次变化,就像与正常SPED模型那样。
除了为您的应用选择最佳的构架模型外,这里还有可用于进一步提升网络代码性能的一些低级招数。下面简短列出了一些更有效的技巧:
一、避免动态内存分配。做为一个解释,简单地看流行的内存分配代码 - 他们使用复杂的数据结构,互斥,并其中只是简单地这么多的代码(例如,jemalloc大概是450KiB左右的C代码!)。上面大部分的模型可用彻底静态的(或预先分配)网络和/或仅在须要的地方改变线程之间全部权缓冲器来实现。
二、使用操做系统能够提供最大值。大多数操做系统容许多个进程监听一个单一socket,并在套接字直到接收到第一个字节(或甚至是第一个完整的请求!)时链接将不被接受时那里实现功能收到。若是能够请使用sendfile()。
三、了解您正在使用的网络协议!例如,禁用Nagle算法一般是有意义的,而且若是(再)链接率高禁止持续是有意义的。学习TCP拥塞控制算法,看看它是否有意义去尝试较一个新的。
在将来的博客文章,我能够更多地谈论这些,以及其余技术和实用的技巧。但如今,这里但愿能为编写高性能网络代码提供关于的架构选择一个有用的信息基础,和它们的相对优点和劣势。
------------------------