2KB项目,专业的源码交易网站 帮助 收藏 每日签到

高性能服务器架构

  • 时间:2019-01-23 18:45 编辑:2KB 来源:2KB.COM 阅读:327
  • 扫一扫,手机访问
  • 分享
摘要: 英文原文:Hig
英文原文:High-Performance Server Architecture

引言

本文档的目的是为了同大家分享多年来我在开发一种特定类型的应用时形成的一些观点,而“服务器”只是对这类应用程序的一个不是那么恰如其分的称谓。更准确的说,我将描述的是一大类的程序,这类程序的设计使得它们能够在每秒钟内处理数量十分巨大的离散消息或请求。网络服务器是最为常见的同此定义吻合的软件,但是,并非所有同此定义吻合的程序绝对可以称作是服务器。然而,“高性能请求处理程序”这种称谓又很难让人接受,所以,为了行文简单起见,我就用“服务器”这个词了事了。

尽管在单个程序中进行多任务处理现在早已司空见惯了,但我将不会对“适度并行”的应用程序进行讨论。就在现在,你阅读本文档所用的浏览器可能正在以并行的方式做着一些事情,但是如此低水平的并行真是不会带来任何值得关注的挑战。真正值得关注的挑战出现在处理请求的架构本身是总体性能的限制性因素的时候,此时对架构进行改善就能够真正地提高性能。运行在主频为几G赫兹的CPU和上G内存的环境中,通过DSL线路同时进行着6个下载任务的浏览器,往往就不属于这种情况。这里的重点并不在于象是用吸管喝着饮料似的应用程序,而是在于象是通过消防拴来喝水的应用程序,这类程序处于马上就要突破硬件能力的边缘地带,你对这类程序的设计起着至关重要的作用。

毫无疑问有些人会反对我的意见和建议,或者认为他们有更好的办法。这非常好。这里我可不是想要发出什么上帝之声;这些只是我发现正合我意的方法,但合意的标准并不仅是它们在性能方面的表现不错,而且还包括后期对这些代码进行调试和扩展的难度也不高这个标准。你的衡量标准可能有所不同。如果你发现有别的方法更适合你那就太棒了,但是要警告的是,我在本文中建议的作为替代方案的几乎所有方法我都试过了,而且其结果都很令人气恼和不可接受。你所钟爱的观点要是放到本文中作为其中的故事之一可能也会非常的合适,如果你怂恿我把这些故事写出来,无辜的读者可能会被我烦死的。你可不想伤害读者,对吧?

本文剩下的部分将围绕着我称之为“性能低下的四骑士”的四个方面的内容来进行:

  1. 数据拷贝
  2. 上下文切换
  3. 内存分配
  4. 锁的争用

本文最后还包含了一个包罗万象的部分,但是这四个是最大的性能杀手。如果你能在不拷贝数据、无需上下文切换、不用进行内存分配而且不会引起对锁的争用的情况下处理绝大多数请求,那么你的服务器性能一定会非常好,即使有些小地方做得不对也没有太大的关系。

数据拷贝

因为一个非常简单的原因,这一小节本来可以写得非常简短:绝大多数人都已经有过这方面的教训了。每个人都知道,数据拷贝很不好;这很显然,对吧?嗯,真地很对,正是因为你在你的计算领域生涯的早期就有过这方面的教训了所以很显然,并且之所以你有这方面教训是因为早在几十年前就有人开始提出数据拷贝这个词了。我知道我的情况就是这样的,但我有点跑题了。现如今,在每个学校的课程和各种非正规的指南中都会对数据拷贝进行讨论。即使那些做销售的都已经弄明白了,“零拷贝”是个不错的时髦词。

尽管后知后觉显然认为数据拷贝很不好,但是,貌似人们还是没有弄明白其中的一些细微之处。其中最重要的一点就是,数据拷贝往往发生地很隐蔽,形式上也有所伪装。你真的了解你所调用的驱动程序或者代码库里面到底有没有进行数据拷贝吗?可能情况比你所想的要复杂一些。请你猜猜看PC中“程序控制的I/O”指的是什么。哈希函数就是一个不是隐蔽的而是经过伪装的数据拷贝的例子,它具有数据拷贝的所有内存访问开销,而且还涉及了大量的计算过程。 只要指出来哈希实际上是个“数据拷贝再加其它操作“的过程,那么貌似有一点很显然,就是要避免使用哈希函数了,但是,我知道至少有一群高人会把这个问题解决掉。如果你真想消除数据拷贝,不管是因为它们真地会损害性能,还是因为你就是想把“零拷贝操作”写入你在黑客大会上的幻灯片里,你将需要对很多并不没有大张旗鼓告诉你但其实真的包含了数据拷贝的很多东西一直追查到底。

经实践验证过的避免数据拷贝的方法就是使用间接法,传递缓冲区描述符(或者是一个缓冲区描述符组成的链)而不是仅仅传递缓冲区指针。每个描述符一般都由以的几个部分组成:

  • 一个指针以及整个缓冲区的长度。
  • 一个指针和长度,或者是缓冲区中真正填充了数据部分的。
  • 指向列表中其它缓冲区描述符的前向和后向指针。
  • 一个引用计数

现在,不用通过拷贝一段数据来确保这些数据能够呆在内存中了,代码可以很简单地对适当的缓冲区描述符中的引用计数加一。在某些情况下这种做法会相当地成功,包括在典型的网络协议栈的运作模式中也没问题,但是,这种做法也有可能会成为一件让你大为头疼的事情。一般来说,要在缓冲区描述符链的开头或者结尾部分添加新的缓冲区很容易,同样为整个缓冲区增加引用以及立即撤销为整个链分配的内存也很容易。在中间部分添加新缓冲区、一点一点的撤销已分配的内存或者引用部分缓冲区这三种操作每一个都会让你的日子越来越难过。要想对缓冲区进行分割或者合并只会把你逼疯。

然而,实际上我并不建议在所有情况下都采用这种方法。为什么不建议呢?因为采用这种方法后,每次想查看报头部分的时候你都不得不对描述符链进行遍历,这么做真是痛苦了。这里真的还有比数据拷贝更加糟糕的事情。我发现,要做的最好的事情就是找出程序里的大对象,比如数据块,确保象前文所述那样,为这些数据块独立分配内存,这样就不需要对它们进行拷贝了,至于剩下其它的东西就不要操那么多心了。

这就是我对数据拷贝要说一下我的最后一个观点:在避免数据拷贝时不要做得太过火。我看到过太多代码,为了避免数据拷贝而它们把某些事情搞得更糟了,比如,它们会迫使系统进行上下文切换或者会打断数据规模较大的I/O请求。数据拷贝代价比较高,当你正在寻找需要避免冗余操作的地方时,其中首要的就是应该看看有没有出现数据拷贝的地方。但是,有一点会减少这么做对你的回报。仔细排查代码,然后就是为了排除掉最后的几个数据拷贝而把代码搞到复杂了两倍多,这通常是对时间的一种浪费,这些时间本可以更好地花在别的地方。

上下文切换

鉴于每个人都认为数据拷贝显然不好,我经常惊叹于竟然有那么多人会完全忽略上下文切换对性能的影响。按照我的经验来看,在高负载的情况下,同数据拷贝相比,实际上上下文切换实际才是更多的导致系统“完全失灵”的元凶;系统开始在来回从一个线程到另一个线程的切换中所花的时间比线程真正做有用的工作所花的时间还要多。令人惊奇的是,从某个角度讲,引起系统过度进行上下文切换的元凶十分显而易见。上下文切换的头号原因就是活跃线程数超过了处理器的总数。随着活跃线程数同处理器总数比值的增大,上下文切换的数量也会增大。如果幸运,这种增加会是线性的,但通常都是成指数级增长。这个非常简单事实可以解释出每个连接都用一个线程来处理的多线程设计为什么伸缩性会非常之差。可伸缩系统唯一比较现实的方案就是限制活跃线程的总数,让该数(在一般情况下)小于或等于处理器的总数。这种方案有一种比较多见的经过修改的版本就是只使用一个线程;尽管这么做的确能够完全避免上下文胡乱切换,而且还不用再使用锁了,但它也无法利用多CPU来提高总吞吐量了,所以除非所设计的程序是非CPU密集型的(通常是网络I/O密集型的),一般大家都不采用这种方案。

一个“适度使用线程”的程序要做的第一件事就是找出如何让一个线程同时处理多个连接的办法。这通常意味着要在前台使用select/poll API、异步I/O、信号或者完成端口,而后端使用一个事件驱动的结构。关于到底哪种前台API才是最好的,已经发生过许多类似于“宗教之争”的争论,而且这种争论还会持续下去。Dan Kegel所写的C10K论文是这个领域中最好的参考资料。我个人认为,各种select/poll API和信号都是些丑陋的伎俩,因此我比较偏爱AIO或者完成端口,但这实际上并没有那么重要。所以这些方案,也许要将除select()外,用起来都相当不错,真地都不会做太多的事情来解决发生在你的程序前端最外层之外的任何问题。

事件驱动的多线程服务器最简单的概念模型以队列为中心;一个或多个“监听者”线程读取请求并将其放入队列之中,然后由一个或多个“工作者”线程将请求从队列中取出并对它们进行处理。从概念上讲,这是个好模型,但太多人真的就按照这种方式来编码了。为什么这么做不对?因为导致上下文切换的第二号原因就是将工作从一个线程传递给另外一个线程。有些人甚至会让原先的线程来发送对请求的响应,这样势必造成处理每个请求时不是发生一次而是两次上下文切换,这可真是错上加错啊。这里很重要的一点就是,要采用一种“对称”的方法,一个给定的线程可以在根本不引起上下文切换的情况下,可以在刚开始时是监听者的身份,随后其身份可以变换为工作者,然后再次成为监听者。这种方法到底需要在线程间分配所有的连接还是需要让所有的线程按次序排队成为所有连接的监听者,似乎并不太重要了。

通常即使对于将来的下一刻,也很难知道系统中到底有多少活跃的线程。毕竟请求可能会在任何时刻从任意一个连接中发过来,还有专用于处理各种维护任务的“背景”线程也可能会挑选在那个时刻醒过来。如果你不知道到底有多少个线程是活跃的,那你怎么才能做到限制系统中应该有多少个活跃线程呢?从我的经验来看,最简单同时也是最有效的方法之一就是:采用一个老式的计数信号量,当每个线程在做“真正的工作”时,它必须持有该信号量。如果活跃线程数已经达到上限,那么处于侦听模式的每个线程在醒来的时候,可能会导致一次额外的线程切换,随后就会阻塞在该信号量之上,但是一旦所有侦听模式的线程都以这种方式进入阻塞状态,那么直到现有线程之一“退出活跃状态”之前,它们就不会再对系统资源进行争用了,因此它们对系统性能的影响可以忽略不计。更重要的是,这种方法还处理了维护线程,这些线程在大多数的时间中都处于休眠模式,所以不会计入活动线程计数,这种处理方式比其它的方案要更加的优雅。

既然将请求的处理过程分为了两个阶段(监听者和工作者)并由多个线程来为这两个阶段服务,那么将处理过程更进一步分为多于两个的阶段就是很自然的事情了。按照最简单的形式,请求的处理就变成了先在一个方向完成一个阶段的处理过程,然后再在另外一个方向上(为了响应请求)进行另外一个阶段的处理。然而,死去可能会变得更加复杂;有一个阶段可能会代表着在涉及不同阶段的两个处理路径上进行“分叉”,或者该阶段可能会产生一个响应(比如,该响应是个缓存中的值)而无需进行下一个阶段的处理了。因此,每个阶段都需要能够为请求指定“下个阶段应该干什么了”。这里有三种可能,由每个阶段的分发函数的返回值来表示:

  • 该请求需要接着传递到另外一个阶段(在返回值里用一个ID或指针来表示这个阶段)。
  • 该请求已经处理完毕(用一个专门的“请求处理完毕”返回值来表示)。
  • 该请求被阻塞(用一个专门的“请求被阻塞”返回值来表示)。这等价于一种情况,只是该请求仍未释放,随后会在另外一个线程中接着对其进行处理。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。


2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务

  • 全部评论(0)
资讯详情页最新发布上方横幅
最新发布的资讯信息
【计算机/互联网|】Nginx出现502错误(2020-01-20 21:02)
【计算机/互联网|】网站运营全智能软手V0.1版发布(2020-01-20 12:16)
【计算机/互联网|】淘宝这是怎么了?(2020-01-19 19:15)
【行业动态|】谷歌关闭小米智能摄像头,因为窃听器显示了陌生人家中的照片(2020-01-15 09:42)
【行业动态|】据报道谷歌新闻终止了数字杂志,退还主动订阅(2020-01-15 09:39)
【行业动态|】康佳将OLED电视带到美国与LG和索尼竞争(2020-01-15 09:38)
【行业动态|】2020年最佳AV接收机(2020-01-15 09:35)
【行业动态|】2020年最佳流媒体设备:Roku,Apple TV,Firebar,Chromecast等(2020-01-15 09:31)
【行业动态|】CES 2020预览:更多的流媒体服务和订阅即将到来(2020-01-08 21:41)
【行业动态|】从埃隆·马斯克到杰夫·贝佐斯,这30位人物定义了2010年代(2020-01-01 15:14)
联系我们

Q Q: 7090832

电话:400-0011-990

邮箱:7090832@qq.com

时间:9:00-23:00

联系客服
商家入住 服务咨询 投拆建议 联系客服
0577-67068160
手机版

扫一扫进手机版
返回顶部