大家好,我是易安!明天咱们谈一谈架构设计中的高性能架构波及到的底层思维。本文分为缓存架构,单服务器高性能模型,集群下的高性能模型三个局部,内容很干,心愿你仔细阅读。

高性能缓存架构

在某些简单的业务场景下,单纯依附存储系统的性能晋升不够的,典型的场景有:

  • 须要通过简单运算后得出的数据,存储系统无能为力

例如,一个论坛须要在首页展现以后有多少用户同时在线,如果应用MySQL来存储以后用户状态,则每次获取这个总数都要“count(*)”大量数据,这样的操作无论怎么优化MySQL,性能都不会太高。如果要实时展现用户同时在线数,则MySQL性能无奈撑持。

  • 读多写少的数据,存储系统有心无力

绝大部分在线业务都是读多写少。例如,微博、淘宝、微信这类互联网业务,读业务占了整体业务量的90%以上。以微博为例:一个明星发一条微博,可能几千万人来浏览。如果应用MySQL来存储微博,用户写微博只有一条insert语句,但每个用户浏览时都要select一次,即便有索引,几千万条select语句对MySQL数据库的压力也会十分大。

缓存就是为了补救存储系统在这些简单业务场景下的有余,其基本原理是将可能重复使用的数据放到内存中,一次生成、屡次应用,防止每次应用都去拜访存储系统。

缓存可能带来性能的大幅晋升,以Memcache为例,单台Memcache服务器简略的key-value查问可能达到TPS 50000以上,其根本的架构是:

缓存尽管可能大大加重存储系统的压力,但同时也给架构引入了更多复杂性。架构设计时如果没有针对缓存的复杂性进行解决,某些场景下甚至会导致整个零碎解体。上面,我来逐个剖析缓存的架构设计要点。

缓存穿透

缓存穿透 是指缓存没有发挥作用,业务零碎尽管去缓存查问数据,但缓存中没有数据,业务零碎须要再次去存储系统查问数据。通常状况下有两种状况:

1.存储数据不存在

第一种状况是被拜访的数据的确不存在。个别状况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查问的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查问一遍,而后返回数据不存在。缓存在这个场景中并没有起到分担存储系统拜访压力的作用。

通常状况下,业务上读取不存在的数据的申请量并不会太大,但如果呈现一些异常情况,例如被黑客攻击,成心大量拜访某些读取不存在数据的业务,有可能会将存储系统拖垮。

这种状况的解决办法比较简单,如果查问存储系统的数据没有找到,则间接设置一个默认值(能够是空值,也能够是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会持续拜访存储系统。

2.缓存数据生成消耗大量工夫或者资源

第二种状况是存储系统中存在数据,但生成缓存数据须要消耗较长时间或者消耗大量资源。如果刚好在业务拜访的时候缓存生效了,那么也会呈现缓存没有发挥作用,拜访压力全副集中在存储系统上的状况。

典型的就是电商的商品分页,假如咱们在某个电商平台上抉择“手机”这个类别查看,因为数据微小,不能把所有数据都缓存起来,只能依照分页来进行缓存,因为难以预测用户到底会拜访哪些分页,因而业务上最简略的就是每次点击分页的时候按分页计算和生成缓存。通常状况下这样实现是根本满足要求的,然而如果被竞争对手用爬虫来遍历的时候,零碎性能就可能呈现问题。

具体的场景有:

  • 分页缓存的有效期设置为1天,因为设置太长时间的话,缓存不能反馈实在的数据。
  • 通常状况下,用户不会从第1页到最初1页全副看完,个别用户拜访集中在前10页,因而第10页当前的缓存过期生效的可能性很大。
  • 竞争对手每周来爬取数据,爬虫会将所有分类的所有数据全副遍历,从第1页到最初1页全副都会读取,此时很多分页缓存可能都生效了。
  • 因为很多分页都没有缓存数据,从数据库中生成缓存数据又十分消耗性能(order by limit操作),因而爬虫会将整个数据库全副拖慢。

这种状况并没有太好的解决方案,因为爬虫会遍历所有的数据,而且什么时候来爬取也是不确定的,可能是每天都来,也可能是每周,也可能是一个月来一次,咱们也不可能为了应答爬虫而将所有数据永恒缓存。通常的应答计划要么就是辨认爬虫而后禁止拜访,但这可能会影响SEO和推广;要么就是做好监控,发现问题后及时处理,因为爬虫不是攻打,不会进行暴力毁坏,对系统的影响是逐渐的,监控发现问题后有工夫进行解决。

缓存雪崩

缓存雪崩 是指当缓存生效(过期)后引起零碎性能急剧下降的状况。当缓存过期被革除后,业务零碎须要从新生成缓存,因而须要再次拜访存储系统,再次进行运算,这个解决步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务零碎来说,几百毫秒内可能会接到几百上千个申请。因为旧的缓存曾经被革除,新的缓存还未生成,并且解决这些申请的线程都不晓得另外有一个线程正在生成缓存,因而所有的申请都会去从新生成缓存,都会去拜访存储系统,从而对存储系统造成微小的性能压力。这些压力又会拖慢整个零碎,重大的会造成数据库宕机,从而造成一系列连锁反应,造成整个零碎解体。

缓存雪崩的常见解决办法有两种: 更新锁机制后盾更新机制

1.更新锁

对缓存更新操作进行加锁爱护,保障只有一个线程可能进行缓存更新,未能获取更新锁的线程要么期待锁开释后从新读取缓存,要么就返回空值或者默认值。

对于采纳分布式集群的业务零碎,因为存在几十上百台服务器,即便单台服务器只有一个线程更新缓存,但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。因而分布式集群的业务零碎要实现更新锁机制,须要用到分布式锁,如ZooKeeper。

2.后盾更新

由后盾线程来更新缓存,而不是由业务线程来更新缓存,缓存自身的有效期设置为永恒,后盾线程定时更新缓存。

后盾定时机制须要思考一种非凡的场景,当缓存零碎内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程自身又不会去更新缓存,因而业务上看到的景象就是数据丢了。解决的形式有两种:

  • 后盾线程除了定时更新缓存,还要频繁地去读取缓存(例如,1秒或者100毫秒读取一次),如果发现缓存被“踢了”就立即更新缓存,这种形式实现简略,但读取工夫距离不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务拜访都拿不到真正的数据而是一个空的缓存值,用户体验个别。
  • 业务线程发现缓存生效后,通过音讯队列发送一条音讯告诉后盾线程更新缓存。可能会呈现多个业务线程都发送了缓存更新音讯,但其实对后盾线程没有影响,后盾线程收到音讯后更新缓存前能够判断缓存是否存在,存在就不执行更新操作。这种形式实现依赖音讯队列,复杂度会高一些,但缓存更新更及时,用户体验更好。

后盾更新既适应单机多线程的场景,也适宜分布式集群的场景,相比更新锁机制要简略一些。

后盾更新机制还适宜业务刚上线的时候进行缓存预热。缓存预热指零碎上线后,将相干的缓存数据间接加载到缓存零碎,而不是期待用户拜访才来触发缓存加载。

缓存热点

尽管缓存零碎自身的性能比拟高,但对于一些特地热点的数据,如果大部分甚至所有的业务申请都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。例如,某明星微博公布“咱们”来宣告恋爱了,短时间内上千万的用户都会来围观。

缓存热点的解决方案就是复制多份缓存正本,将申请扩散到多个缓存服务器上,加重缓存热点导致的单台缓存服务器压力。以微博为例,对于粉丝数超过100万的明星,每条微博都能够生成100份缓存,缓存的数据是一样的,通过在缓存的key外面加上编号进行辨别,每次读缓存时都随机读取其中某份缓存。

缓存正本设计有一个细节须要留神,就是不同的缓存正本不要设置对立的过期工夫,否则就会呈现所有缓存正本同时生成同时生效的状况,从而引发缓存雪崩效应。正确的做法是设定一个过期工夫范畴,不同的缓存正本的过期工夫是指定范畴内的随机值。

因为缓存的各种拜访策略和存储的拜访策略是相干的,因而下面的各种缓存设计方案通常状况下都是集成在存储拜访计划中,能够采纳“程序代码实现”的中间层形式,也能够采纳独立的中间件来实现。

单服务器下的高性能模型

高性能是每个程序员的谋求,无论咱们是做一个零碎还是写一行代码,都心愿可能达到高性能的成果,而高性能又是最简单的一环,磁盘、操作系统、CPU、内存、缓存、网络、编程语言、架构等,每个都有可能影响零碎达到高性能,一行不失当的debug日志,就可能将服务器的性能从TPS 30000升高到8000;一个tcp\_nodelay参数,就可能将响应工夫从2毫秒缩短到40毫秒。因而,要做到高性能计算是一件很简单很有挑战的事件,软件系统开发过程中的不同阶段都关系着高性能最终是否可能实现。

站在架构师的角度,当然须要特地关注高性能架构的设计。高性能架构设计次要集中在两方面:

  • 尽量晋升单服务器的性能,将单服务器的性能施展到极致。
  • 如果单服务器无奈撑持性能,设计服务器集群计划。

除了以上两点,最终零碎是否实现高性能,还和具体的实现及编码相干。但架构设计是高性能的根底,如果架构设计没有做到高性能,则前面的具体实现和编码能晋升的空间是无限的。形象地说,架构设计决定了零碎性能的下限,实现细节决定了零碎性能的上限。

单服务器高性能的要害之一就是 服务器采取的并发模型,并发模型有如下两个要害设计点:

  • 服务器如何治理连贯。
  • 服务器如何解决申请。

以上两个设计点最终都和操作系统的I/O模型及过程模型相干。

  • I/O模型:阻塞、非阻塞、同步、异步。
  • 过程模型:单过程、多过程、多线程。

在上面具体介绍并发模型时会用到下面这些根底的知识点,所以我倡议你先检测一下对这些基础知识的掌握情况,更多内容你能够参考《UNIX网络编程》三卷本。

PPC

PPC是Process Per Connection的缩写,其含意是指每次有新的连贯就新建一个过程去专门解决这个连贯的申请,这是传统的UNIX网络服务器所采纳的模型。根本的流程图是:

  • 父过程承受连贯(图中accept)。
  • 父过程“fork”子过程(图中fork)。
  • 子过程解决连贯的读写申请(图中子过程read、业务解决、write)。
  • 子过程敞开连贯(图中子过程中的close)。

留神,图中有一个小细节,父过程“fork”子过程后,间接调用了close,看起来如同是敞开了连贯,其实只是将连贯的文件描述符援用计数减一,真正的敞开连贯是等子过程也调用close后,连贯对应的文件描述符援用计数变为0后,操作系统才会真正敞开连贯,更多细节请参考《UNIX网络编程:卷一》。

PPC模式实现简略,比拟适宜服务器的连接数没那么多的状况,例如数据库服务器。对于一般的业务服务器,在互联网衰亡之前,因为服务器的访问量和并发量并没有那么大,这种模式其实运作得也挺好,世界上第一个web服务器CERN httpd就采纳了这种模式(具体你能够参考 https://en.wikipedia.org/wiki/CERN\_httpd)。互联网衰亡后,服务器的并发和访问量从几十剧增到成千上万,这种模式的弊病就凸显进去了,次要体现在这几个方面:

  • fork代价高:站在操作系统的角度,创立一个过程的代价是很高的,须要调配很多内核资源,须要将内存映像从父过程复制到子过程。即便当初的操作系统在复制内存映像时用到了Copy on Write(写时复制)技术,总体来说创立过程的代价还是很大的。
  • 父子过程通信简单:父过程“fork”子过程时,文件描述符能够通过内存映像复制从父过程传到子过程,但“fork”实现后,父子过程通信就比拟麻烦了,须要采纳IPC(Interprocess Communication)之类的过程通信计划。例如,子过程须要在close之前通知父过程本人解决了多少个申请以撑持父过程进行全局的统计,那么子过程和父过程必须采纳IPC计划来传递信息。
  • 反对的并发连贯数量无限:如果每个连贯存活工夫比拟长,而且新的连贯又源源不断的进来,则过程数量会越来越多,操作系统过程调度和切换的频率也越来越高,零碎的压力也会越来越大。因而,个别状况下,PPC计划能解决的并发连贯数量最大也就几百。

prefork

PPC模式中,当连贯进来时才fork新过程来解决连贯申请,因为fork过程代价高,用户拜访时可能感觉比较慢,prefork模式的呈现就是为了解决这个问题。

顾名思义,prefork就是提前创立过程(pre-fork)。零碎在启动的时候就事后创立好过程,而后才开始承受用户的申请,当有新的连贯进来的时候,就能够省去fork过程的操作,让用户拜访更快、体验更好。prefork的根本示意图是:

prefork的实现要害就是多个子过程都accept同一个socket,当有新的连贯进入时,操作系统保障只有一个过程能最初accept胜利。但这里也存在一个小小的问题:“惊群”景象,就是指尽管只有一个子过程能accept胜利,但所有阻塞在accept上的子过程都会被唤醒,这样就导致了不必要的过程调度和上下文切换了。侥幸的是,操作系统能够解决这个问题,例如Linux 2.6版本后内核曾经解决了accept惊群问题。

prefork模式和PPC一样,还是存在父子过程通信简单、反对的并发连贯数量无限的问题,因而目前理论利用也不多。Apache服务器提供了MPM prefork模式,举荐在须要可靠性或者与旧软件兼容的站点时采纳这种模式,默认状况下最大反对256个并发连贯。

TPC

TPC是Thread Per Connection的缩写,其含意是指每次有新的连贯就新建一个线程去专门解决这个连贯的申请。与过程相比,线程更轻量级,创立线程的耗费比过程要少得多;同时多线程是共享过程内存空间的,线程通信相比过程通信更简略。因而,TPC实际上是解决或者弱化了PPC fork代价高的问题和父子过程通信简单的问题。

TPC的根本流程是:

  • 父过程承受连贯(图中accept)。
  • 父过程创立子线程(图中pthread)。
  • 子线程解决连贯的读写申请(图中子线程read、业务解决、write)。
  • 子线程敞开连贯(图中子线程中的close)。

留神,和PPC相比,主过程不必“close”连贯了。起因是在于子线程是共享主过程的过程空间的,连贯的文件描述符并没有被复制,因而只须要一次close即可。

TPC尽管解决了fork代价高和过程通信简单的问题,然而也引入了新的问题,具体表现在:

  • 创立线程尽管比创立过程代价低,但并不是没有代价,高并发时(例如每秒上万连贯)还是有性能问题。
  • 毋庸过程间通信,然而线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题。
  • 多线程会呈现相互影响的状况,某个线程出现异常时,可能导致整个过程退出(例如内存越界)。

除了引入了新的问题,TPC还是存在CPU线程调度和切换代价的问题。因而,TPC计划实质上和PPC计划根本相似,在并发几百连贯的场景下,反而更多地是采纳PPC的计划,因为PPC计划不会有死锁的危险,也不会多过程相互影响,稳定性更高。

prethread

TPC模式中,当连贯进来时才创立新的线程来解决连贯申请,尽管创立线程比创立过程要更加轻量级,但还是有肯定的代价,而prethread模式就是为了解决这个问题。

和prefork相似,prethread模式会事后创立线程,而后才开始承受用户的申请,当有新的连贯进来的时候,就能够省去创立线程的操作,让用户感觉更快、体验更好。

因为多线程之间数据共享和通信比拟不便,因而实际上prethread的实现形式相比prefork要灵便一些,常见的实现形式有上面几种:

  • 主过程accept,而后将连贯交给某个线程解决。
  • 子线程都尝试去accept,最终只有一个线程accept胜利,计划的根本示意图如下:

Apache服务器的MPM worker模式实质上就是一种prethread计划,但略微做了改良。Apache服务器会首先创立多个过程,每个过程外面再创立多个线程,这样做次要是为了思考稳定性,即:即便某个子过程外面的某个线程异样导致整个子过程退出,还会有其余子过程持续提供服务,不会导致整个服务器全副挂掉。

prethread实践上能够比prefork反对更多的并发连贯,Apache服务器MPM worker模式默认反对16 × 25 = 400 个并发解决线程。

Reactor

PPC模式最次要的问题就是每个连贯都要创立过程(为了形容简洁,这里只以PPC和过程为例,实际上换成TPC和线程,原理是一样的),连贯完结后过程就销毁了,这样做其实是很大的节约。为了解决这个问题,一个自然而然的想法就是资源复用,即不再独自为每个连贯创立过程,而是创立一个过程池,将连贯调配给过程,一个过程能够解决多个连贯的业务。

引入资源池的解决形式后,会引出一个新的问题:过程如何能力高效地解决多个连贯的业务?当一个连贯一个过程时,过程能够采纳“read -> 业务解决 -> write”的解决流程,如果以后连贯没有数据能够读,则过程就阻塞在read操作上。这种阻塞的形式在一个连贯一个过程的场景下没有问题,但如果一个过程解决多个连贯,过程阻塞在某个连贯的read操作上,此时即便其余连贯有数据可读,过程也无奈去解决,很显然这样是无奈做到高性能的。

解决这个问题的最简略的形式是将read操作改为非阻塞,而后过程一直地轮询多个连贯。这种形式可能解决阻塞的问题,但解决的形式并不优雅。首先,轮询是要耗费CPU的;其次,如果一个过程解决几千上万的连贯,则轮询的效率是很低的。

为了可能更好地解决上述问题,很容易能够想到,只有当连贯上有数据的时候过程才去解决,这就是I/O多路复用技术的起源。

I/O多路复用技术归纳起来有两个要害实现点:

  • 当多条连贯共用一个阻塞对象后,过程只须要在一个阻塞对象上期待,而无须再轮询所有连贯,常见的实现形式有select、epoll、kqueue等。
  • 当某条连贯有新的数据能够解决时,操作系统会告诉过程,过程从阻塞状态返回,开始进行业务解决。

I/O多路复用联合线程池,完满地解决了PPC和TPC的问题,而且“大神们”给它取了一个很牛的名字:Reactor,中文是“反应堆”。联想到“核反应堆”,听起来就很吓人,实际上这里的“反馈”不是聚变、裂变反馈的意思,而是“ 事件反馈”的意思,能够艰深地了解为“ 来了一个事件我就有相应的反馈”,这里的“我”就是Reactor,具体的反馈就是咱们写的代码,Reactor会依据事件类型来调用相应的代码进行解决。Reactor模式也叫Dispatcher模式(在很多开源的零碎外面会看到这个名称的类,其实就是实现Reactor模式的),更加贴近模式自身的含意,即I/O多路复用对立监听事件,收到事件后调配(Dispatch)给某个过程。

Reactor模式的外围组成部分包含Reactor和解决资源池(过程池或线程池),其中Reactor负责监听和调配事件,解决资源池负责处理事件。初看Reactor的实现是比较简单的,但实际上联合不同的业务场景,Reactor模式的具体实现计划灵便多变,次要体现在:

  • Reactor的数量能够变动:能够是一个Reactor,也能够是多个Reactor。
  • 资源池的数量能够变动:以过程为例,能够是单个过程,也能够是多个过程(线程相似)。

将下面两个因素排列组合一下,实践上能够有4种抉择,但因为“多Reactor单过程”实现计划相比“单Reactor单过程”计划,既简单又没有性能劣势,因而“多Reactor单过程”计划仅仅是一个实践上的计划,理论没有利用。

最终Reactor模式有这三种典型的实现计划:

  • 单Reactor单过程/线程。
  • 单Reactor多线程。
  • 多Reactor多过程/线程。

以上计划具体抉择过程还是线程,更多地是和编程语言及平台相干。例如,Java语言个别应用线程(例如,Netty),C语言应用过程和线程都能够。例如,Nginx应用过程,Memcache应用线程。

1.单Reactor单过程/线程

单Reactor单过程/线程的计划示意图如下(以过程为例):

留神,select、accept、read、send是规范的网络编程API,dispatch和“业务解决”是须要实现的操作,其余计划示意图相似。

具体阐明一下这个计划:

  • Reactor对象通过select监控连贯事件,收到事件后通过dispatch进行散发。
  • 如果是连贯建设的事件,则由Acceptor解决,Acceptor通过accept承受连贯,并创立一个Handler来解决连贯后续的各种事件。
  • 如果不是连贯建设事件,则Reactor会调用连贯对应的Handler(第2步中创立的Handler)来进行响应。
  • Handler会实现read->业务解决->send的残缺业务流程。

单Reactor单过程的模式长处就是很简略,没有过程间通信,没有过程竞争,全副都在同一个过程内实现。但其毛病也是非常明显,具体表现有:

  • 只有一个过程,无奈施展多核CPU的性能;只能采取部署多个零碎来利用多核CPU,但这样会带来运维复杂度,原本只有保护一个零碎,用这种形式须要在一台机器上保护多套零碎。
  • Handler在解决某个连贯上的业务时,整个过程无奈解决其余连贯的事件,很容易导致性能瓶颈。

因而,单Reactor单过程的计划在实践中利用场景不多, 只实用于业务解决十分疾速的场景,目前比拟驰名的开源软件中应用单Reactor单过程的是Redis。

须要留神的是,C语言编写零碎的个别应用单Reactor单过程,因为没有必要在过程中再创立线程;而Java语言编写的个别应用单Reactor单线程,因为Java虚拟机是一个过程,虚拟机中有很多线程,业务线程只是其中的一个线程而已。

2.单Reactor多线程

为了克服单Reactor单过程/线程计划的毛病,引入多过程/多线程是不言而喻的,这就产生了第2个计划:单Reactor多线程。

单Reactor多线程计划示意图是:

我来介绍一下这个计划:

  • 主线程中,Reactor对象通过select监控连贯事件,收到事件后通过dispatch进行散发。
  • 如果是连贯建设的事件,则由Acceptor解决,Acceptor通过accept承受连贯,并创立一个Handler来解决连贯后续的各种事件。
  • 如果不是连贯建设事件,则Reactor会调用连贯对应的Handler(第2步中创立的Handler)来进行响应。
  • Handler只负责响应事件,不进行业务解决;Handler通过read读取到数据后,会发给Processor进行业务解决。
  • Processor会在独立的子线程中实现真正的业务解决,而后将响应后果发给主过程的Handler解决;Handler收到响应后通过send将响应后果返回给client。

单Reator多线程计划可能充分利用多核多CPU的解决能力,但同时也存在上面的问题:

  • 多线程数据共享和拜访比较复杂。例如,子线程实现业务解决后,要把后果传递给主线程的Reactor进行发送,这里波及共享数据的互斥和爱护机制。以Java的NIO为例,Selector是线程平安的,然而通过Selector.selectKeys()返回的键的汇合是非线程平安的,对selected keys的解决必须单线程解决或者采取同步措施进行爱护。
  • Reactor承当所有事件的监听和响应,只在主线程中运行,霎时高并发时会成为性能瓶颈。

你可能会发现,我只列出了“单Reactor多线程”计划,没有列出“单Reactor多过程”计划,这是什么起因呢?次要起因在于如果采纳多过程,子过程实现业务解决后,将后果返回给父过程,并告诉父过程发送给哪个client,这是很麻烦的事件。因为父过程只是通过Reactor监听各个连贯上的事件而后进行调配,子过程与父过程通信时并不是一个连贯。如果要将父过程和子过程之间的通信模仿为一个连贯,并退出Reactor进行监听,则是比较复杂的。而采纳多线程时,因为多线程是共享数据的,因而线程间通信是十分不便的。尽管要额定思考线程间共享数据时的同步问题,但这个复杂度比过程间通信的复杂度要低很多。

3.多Reactor多过程/线程

为了解决单Reactor多线程的问题,最直观的办法就是将单Reactor改为多Reactor,这就产生了第3个计划:多Reactor多过程/线程。

多Reactor多过程/线程计划示意图是(以过程为例):

计划具体阐明如下:

  • 父过程中mainReactor对象通过select监控连贯建设事件,收到事件后通过Acceptor接管,将新的连贯调配给某个子过程。
  • 子过程的subReactor将mainReactor调配的连贯退出连贯队列进行监听,并创立一个Handler用于解决连贯的各种事件。
  • 当有新的事件产生时,subReactor会调用连贯对应的Handler(即第2步中创立的Handler)来进行响应。
  • Handler实现read→业务解决→send的残缺业务流程。

多Reactor多过程/线程的计划看起来比单Reactor多线程要简单,但理论实现时反而更加简略,次要起因是:

  • 父过程和子过程的职责十分明确,父过程只负责接管新连贯,子过程负责实现后续的业务解决。
  • 父过程和子过程的交互很简略,父过程只须要把新连贯传给子过程,子过程毋庸返回数据。
  • 子过程之间是相互独立的,毋庸同步共享之类的解决(这里仅限于网络模型相干的select、read、send等毋庸同步共享,“业务解决”还是有可能须要同步共享的)。

目前驰名的开源零碎Nginx采纳的是多Reactor多过程,采纳多Reactor多线程的实现有Memcache和Netty。

我多说一句,Nginx采纳的是多Reactor多过程的模式,但计划与规范的多Reactor多过程有差别。具体差别体现为主过程中仅仅创立了监听端口,并没有创立mainReactor来“accept”连贯,而是由子过程的Reactor来“accept”连贯,通过锁来管制一次只有一个子过程进行“accept”,子过程“accept”新连贯后就放到本人的Reactor进行解决,不会再调配给其余子过程,更多细节请查阅相干材料或浏览Nginx源码。

Proactor

Reactor是非阻塞同步网络模型,因为真正的read和send操作都须要用户进程同步操作。这里的“同步”指用户过程在执行read和send这类I/O操作的时候是同步的,如果把I/O操作改为异步就可能进一步晋升性能,这就是异步网络模型Proactor。

Proactor中文翻译为“前摄器”比拟难了解,与其相似的单词是proactive,含意为“被动的”,因而咱们照猫画虎翻译为“被动器”反而更好了解。Reactor能够了解为“来了事件我告诉你,你来解决”,而Proactor能够了解为“ 来了事件我来解决,解决完了我告诉你”。这里的“我”就是操作系统内核,“事件”就是有新连贯、有数据可读、有数据可写的这些I/O事件,“你”就是咱们的程序代码。

Proactor模型示意图是:

具体介绍一下Proactor计划:

  • Proactor Initiator负责创立Proactor和Handler,并将Proactor和Handler都通过Asynchronous Operation Processor注册到内核。
  • Asynchronous Operation Processor负责解决注册申请,并实现I/O操作。
  • Asynchronous Operation Processor实现I/O操作后告诉Proactor。
  • Proactor依据不同的事件类型回调不同的Handler进行业务解决。
  • Handler实现业务解决,Handler也能够注册新的Handler到内核过程。

实践上Proactor比Reactor效率要高一些,异步I/O可能充分利用DMA个性,让I/O操作与计算重叠,但要实现真正的异步I/O,操作系统须要做大量的工作。目前Windows下通过IOCP实现了真正的异步I/O,而在Linux零碎下的AIO并不欠缺,因而在Linux下实现高并发网络编程时都是以Reactor模式为主。所以即便Boost.Asio号称实现了Proactor模型,其实它在Windows下采纳IOCP,而在Linux下是用Reactor模式(采纳epoll)模仿进去的异步模型。

高性能集群架构

单服务器无论如何优化,无论采纳多好的硬件,总会有一个性能天花板,当单服务器的性能无奈满足业务需要时,就须要设计高性能集群来晋升零碎整体的解决性能。

高性能集群的实质很简略,通过减少更多的服务器来晋升零碎整体的计算能力。因为计算自身存在一个特点:同样的输出数据和逻辑,无论在哪台服务器上执行,都应该失去雷同的输入。因而高性能集群设计的复杂度次要体现在任务分配这部分,须要设计正当的任务分配策略,将计算任务分配到多台服务器上执行。

高性能集群的复杂性次要体现在须要减少一个工作分配器,以及为工作抉择一个适合的任务分配算法。对于工作分配器,当初更风行的通用叫法是“负载均衡器”。但这个名称有肯定的误导性,会让人潜意识里认为任务分配的目标是要放弃各个计算单元的负载达到平衡状态。而实际上任务分配并不只是思考计算单元的负载平衡,不同的任务分配算法指标是不一样的,有的基于负载思考,有的基于性能(吞吐量、响应工夫)思考,有的基于业务思考。思考到“负载平衡”曾经成为了事实上的规范术语,这里我也用“负载平衡”来代替“任务分配”,但请你时刻记住, 负载平衡不只是为了计算单元的负载达到平衡状态

负载平衡分类

常见的负载平衡零碎包含3种:DNS负载平衡、硬件负载平衡和软件负载平衡。

DNS负载平衡

DNS是最简略也是最常见的负载平衡形式,个别用来实现天文级别的平衡。例如,南方的用户拜访北京的机房,北方的用户拜访深圳的机房。DNS负载平衡的实质是DNS解析同一个域名能够返回不同的IP地址。例如,同样是www.baidu.com,南方用户解析后获取的地址是61.135.165.224(这是北京机房的IP),北方用户解析后获取的地址是14.215.177.38(这是深圳机房的IP)。

上面是DNS负载平衡的简略示意图:

DNS负载平衡实现简略、成本低,但也存在粒度太粗、负载平衡算法少等毛病。仔细分析一下优缺点,其长处有:

  • 简略、成本低:负载平衡工作交给DNS服务器解决,毋庸本人开发或者保护负载平衡设施。
  • 就近拜访,晋升访问速度:DNS解析时能够依据申请起源IP,解析成间隔用户最近的服务器地址,能够放慢访问速度,改善性能。

毛病有:

  • 更新不及时:DNS缓存的工夫比拟长,批改DNS配置后,因为缓存的起因,还是有很多用户会持续拜访批改前的IP,这样的拜访会失败,达不到负载平衡的目标,并且也影响用户失常应用业务。
  • 扩展性差:DNS负载平衡的控制权在域名商那里,无奈依据业务特点针对其做更多的定制化性能和扩大个性。
  • 调配策略比较简单:DNS负载平衡反对的算法少;不能辨别服务器的差别(不能依据零碎与服务的状态来判断负载);也无奈感知后端服务器的状态。

针对DNS负载平衡的一些毛病,对于时延和故障敏感的业务,有一些公司本人实现了HTTP-DNS的性能,即应用HTTP协定实现一个公有的DNS零碎。这样的计划和通用的DNS优缺点正好相同。

硬件负载平衡

硬件负载平衡是通过独自的硬件设施来实现负载平衡性能,这类设施和路由器、交换机相似,能够了解为一个用于负载平衡的根底网络设备。目前业界典型的硬件负载平衡设施有两款:F5和A10。这类设施性能强劲、功能强大,但价格都不便宜,个别只有“土豪”公司才会思考应用此类设施。一般业务量级的公司一是累赘不起,二是业务量没那么大,用这些设施也是节约。

硬件负载平衡的长处是:

  • 功能强大:全面反对各层级的负载平衡,反对全面的负载平衡算法,反对全局负载平衡。
  • 性能弱小:比照一下,软件负载平衡反对到10万级并发曾经很厉害了,硬件负载平衡能够反对100万以上的并发。
  • 稳定性高:商用硬件负载平衡,通过了良好的严格测试,通过大规模应用,稳定性高。
  • 反对平安防护:硬件平衡设施除具备负载平衡性能外,还具备防火墙、防DDoS攻打等平安性能。

硬件负载平衡的毛病是:

  • 价格昂贵:最一般的一台F5就是一台“马6”,好一点的就是“Q7”了。
  • 扩大能力差:硬件设施,能够依据业务进行配置,但无奈进行扩大和定制。

软件负载平衡

软件负载平衡通过负载平衡软件来实现负载平衡性能,常见的有Nginx和LVS,其中Nginx是软件的7层负载平衡,LVS是Linux内核的4层负载平衡。4层和7层的区别就在于 协定灵活性,Nginx反对HTTP、E-mail协定;而LVS是4层负载平衡,和协定无关,简直所有利用都能够做,例如,聊天、数据库等。

软件和硬件的最次要区别就在于性能,硬件负载平衡性能远远高于软件负载平衡性能。Nginx的性能是万级,个别的Linux服务器上装一个Nginx大略能到5万/秒;LVS的性能是十万级,据说可达到80万/秒;而F5性能是百万级,从200万/秒到800万/秒都有(数据起源网络,仅供参考,如需采纳请依据理论业务场景进行性能测试)。当然,软件负载平衡的最大劣势是便宜,一台一般的Linux服务器批发价大略就是1万元左右,相比F5的价格,那就是自行车和宝马的区别了。

除了应用开源的零碎进行负载平衡,如果业务比拟非凡,也可能基于开源零碎进行定制(例如,Nginx插件),甚至进行自研。

上面是Nginx的负载平衡架构示意图:

软件负载平衡的长处:

  • 简略:无论是部署还是保护都比较简单。
  • 便宜:只有买个Linux服务器,装上软件即可。
  • 灵便:4层和7层负载平衡能够依据业务进行抉择;也能够依据业务进行比拟不便的扩大,例如,能够通过Nginx的插件来实现业务的定制化性能。

其实上面的毛病都是和硬件负载平衡相比的,并不是说软件负载平衡没法用。

  • 性能个别:一个Nginx大概能撑持5万并发。
  • 性能没有硬件负载平衡那么弱小。
  • 个别不具备防火墙和防DDoS攻打等平安性能。

负载平衡典型架构

后面咱们介绍了3种常见的负载平衡机制:DNS负载平衡、硬件负载平衡、软件负载平衡,每种形式都有一些优缺点,但并不意味着在理论利用中只能基于它们的优缺点进行非此即彼的抉择,反而是基于它们的优缺点进行组合应用。具体来说,组合的 根本准则 为:DNS负载平衡用于实现天文级别的负载平衡;硬件负载平衡用于实现集群级别的负载平衡;软件负载平衡用于实现机器级别的负载平衡。

我以一个假想的实例来阐明一下这种组合形式,如下图所示。

整个零碎的负载平衡分为三层。

  • 天文级别负载平衡:www.xxx.com部署在北京、广州、上海三个机房,当用户拜访时,DNS会依据用户的地理位置来决定返回哪个机房的IP,图中返回了广州机房的IP地址,这样用户就拜访到广州机房了。
  • 集群级别负载平衡:广州机房的负载平衡用的是F5设施,F5收到用户申请后,进行集群级别的负载平衡,将用户申请发给3个本地集群中的一个,咱们假如F5将用户申请发给了“广州集群2”。
  • 机器级别的负载平衡:广州集群2的负载平衡用的是Nginx,Nginx收到用户申请后,将用户申请发送给集群外面的某台服务器,服务器解决用户的业务申请并返回业务响应。

须要留神的是,上图只是一个示例,个别在大型业务场景下才会这样用,如果业务量没这么大,则没有必要严格照搬这套架构。例如,一个大学的论坛,齐全能够不须要DNS负载平衡,也不须要F5设施,只须要用Nginx作为一个简略的负载平衡就足够了。

接下来我介绍一下负载平衡算法以及它们的优缺点:

轮询

负载平衡零碎收到申请后,依照程序轮流调配到服务器上。

轮询是最简略的一个策略,毋庸关注服务器自身的状态,例如:

  • 某个服务器以后因为触发了程序bug进入了死循环导致CPU负载很高,负载平衡零碎是不感知的,还是会持续将申请源源不断地发送给它。
  • 集群中有新的机器是32核的,老的机器是16核的,负载平衡零碎也是不关注的,新老机器调配的工作数是一样的。

须要留神的是负载平衡零碎毋庸关注“服务器自身状态”,这里的关键词是“自身”。也就是说, 只有服务器在运行,运行状态是不关注的。但如果服务器间接宕机了,或者服务器和负载平衡零碎断连了,这时负载平衡零碎是可能感知的,也须要做出相应的解决。例如,将服务器从可调配服务器列表中删除,否则就会呈现服务器都宕机了,工作还一直地调配给它,这显著是不合理的。

总而言之,“简略”是轮询算法的长处,也是它的毛病。

加权轮询

负载平衡零碎依据服务器权重进行任务分配,这里的权重个别是依据硬件配置进行动态配置的,采纳动静的形式计算会更加符合业务,但复杂度也会更高。

加权轮询是轮询的一种非凡模式,其次要目标就是为了 解决不同服务器解决能力有差别的问题。例如,集群中有新的机器是32核的,老的机器是16核的,那么实践上咱们能够假如新机器的解决能力是老机器的2倍,负载平衡零碎就能够依照2:1的比例调配更多的工作给新机器,从而充分利用新机器的性能。

加权轮询解决了轮询算法中无奈依据服务器的配置差别进行任务分配的问题,但同样存在无奈依据服务器的状态差别进行任务分配的问题。

负载最低优先

负载平衡零碎将任务分配给以后负载最低的服务器,这里的负载依据不同的工作类型和业务场景,能够用不同的指标来掂量。例如:

  • LVS这种4层网络负载平衡设施,能够以“连接数”来判断服务器的状态,服务器连接数越大,表明服务器压力越大。
  • Nginx这种7层网络负载零碎,能够以“HTTP申请数”来判断服务器状态(Nginx内置的负载平衡算法不反对这种形式,须要进行扩大)。
  • 如果咱们本人开发负载平衡零碎,能够依据业务特点来抉择指标掂量零碎压力。如果是CPU密集型,能够以“CPU负载”来掂量零碎压力;如果是I/O密集型,能够以“I/O负载”来掂量零碎压力。

负载最低优先的算法解决了轮询算法中无奈感知服务器状态的问题,由此带来的代价是复杂度要减少很多。例如:

  • 起码连接数优先的算法要求负载平衡零碎统计每个服务器以后建设的连贯,其利用场景仅限于负载平衡接管的任何连贯申请都会转发给服务器进行解决,否则如果负载平衡零碎和服务器之间是固定的连接池形式,就不适宜采取这种算法。例如,LVS能够采取这种算法进行负载平衡,而一个通过连接池的形式连贯MySQL集群的负载平衡零碎就不适宜采取这种算法进行负载平衡。
  • CPU负载最低优先的算法要求负载平衡零碎以某种形式收集每个服务器的CPU负载,而且要确定是以1分钟的负载为规范,还是以15分钟的负载为规范,不存在1分钟必定比15分钟要好或者差。不同业务最优的工夫距离是不一样的,工夫距离太短容易造成频繁稳定,工夫距离太长又可能造成峰值来长期响应迟缓。

负载最低优先算法基本上可能比拟完满地解决轮询算法的毛病,因为采纳这种算法后,负载平衡零碎须要感知服务器以后的运行状态。当然,其代价是复杂度大幅回升。艰深来讲,轮询可能是5行代码就能实现的算法,而负载最低优先算法可能要1000行能力实现,甚至须要负载平衡零碎和服务器都要开发代码。负载最低优先算法如果自身没有设计好,或者不适宜业务的运行特点,算法自身就可能成为性能的瓶颈,或者引发很多莫名其妙的问题。所以负载最低优先算法尽管成果看起来很美妙,但实际上真正利用的场景反而没有轮询(包含加权轮询)那么多。

性能最优类

负载最低优先类算法是站在服务器的角度来进行调配的,而性能最优优先类算法则是站在客户端的角度来进行调配的,优先将任务分配给处理速度最快的服务器,通过这种形式达到最快响应客户端的目标。

和负载最低优先类算法相似,性能最优优先类算法实质上也是感知了服务器的状态,只是通过响应工夫这个内部规范来掂量服务器状态而已。因而性能最优优先类算法存在的问题和负载最低优先类算法相似,复杂度都很高,次要体现在:

  • 负载平衡零碎须要收集和剖析每个服务器每个工作的响应工夫,在大量工作解决的场景下,这种收集和统计自身也会耗费较多的性能。
  • 为了缩小这种统计上的耗费,能够采取采样的形式来统计,即不统计所有工作的响应工夫,而是抽样统计局部工作的响应工夫来估算整体工作的响应工夫。采样统计尽管可能缩小性能耗费,但使得复杂度进一步回升,因为要确定适合的 采样率,采样率太低会导致后果不精确,采样率太高会导致性能耗费较大,找到适合的采样率也是一件简单的事件。
  • 无论是全副统计还是采样统计,都须要抉择适合的 周期:是10秒内性能最优,还是1分钟内性能最优,还是5分钟内性能最优……没有放之四海而皆准的周期,须要依据理论业务进行判断和抉择,这也是一件比较复杂的事件,甚至呈现零碎上线后须要一直地调优能力达到最优设计。

Hash类

负载平衡零碎依据工作中的某些要害信息进行Hash运算,将雷同Hash值的申请调配到同一台服务器上,这样做的目标次要是为了满足特定的业务需要。例如:

  • 源地址Hash

将来源于同一个源IP地址的任务分配给同一个服务器进行解决,适宜于存在事务、会话的业务。例如,当咱们通过浏览器登录网上银行时,会生成一个会话信息,这个会话是长期的,敞开浏览器后就生效。网上银行后盾毋庸长久化会话信息,只须要在某台服务器上长期保留这个会话就能够了,但须要保障用户在会话存在期间,每次都能拜访到同一个服务器,这种业务场景就能够用源地址Hash来实现。

  • ID Hash

将某个ID标识的业务调配到同一个服务器中进行解决,这里的ID个别是临时性数据的ID(如session id)。例如,上述的网上银行登录的例子,用session id hash同样能够实现同一个会话期间,用户每次都是拜访到同一台服务器的目标。

总结

本文讲述了高性能架构设计中缓存设计须要留神的几个关键点,常见的缓存问题,并围绕单台服务器的高性能模式,介绍了PPC,TPC的模式,之后介绍了实用于互联网海量用户的Reactor, Proactor模式,这两种模式是反对高并发的架构模型,最初咱们谈了集群下的负载平衡以及典型架构。如果本文对你有帮忙的话,欢送点赞分享,这对我持续分享&创作优质文章十分重要。感激 !

本文由mdnice多平台公布