反应堆开发模型被绝大多数高性能服务器所选择,上一篇所介绍的IO多路复用是它的实现基础。定时触发功能一般是服务器必备组件,反应堆模型每每还不得不将定时器的管理囊括在内。本篇将介绍反应堆模型的特色和用法。node
首先咱们要谈谈,网络编程界为何须要反应堆?有了IO复用,有了epoll,咱们已经可使服务器并发几十万链接的同时,维持高TPS了,难道这还不够吗?nginx
个人答案是,技术层面足够了,但在软件工程层面倒是不够的。程序员
程序使用IO复用的难点在哪里呢?1个请求虽然由屡次IO处理完成,但相比传统的单线程完整处理请求生命期的方法,IO复用在人的大脑思惟中并不天然,由于,程序员编程中,处理请求A的时候,假定A请求必须通过多个IO操做A1-An(两次IO间可能间隔很长时间),每通过一次IO操做,再调用IO复用时,IO复用的调用返回里,很是可能再也不有A,而是返回了请求B。即请求A会常常被请求B打断,处理请求B时,又被C打断。这种思惟下,编程容易出错。redis
形象的说,传统编程方法就好像是到了银行营业厅里,每一个窗口前排了长队,业务员们在窗口后一个个的解决客户们的请求。一个业务员能够尽情思考着客户A依次提出的问题,例如:编程
“我要买2万XX理财产品。“数组
“看清楚了,5万起售。”服务器
“等等,查下我活期余额。”网络
“余额5万。”数据结构
“那就买 5万吧。”多线程
业务员开始录入信息。
”对了,XX理财产品年利率8%?”
“是预期8%,最低无利息保本。“
”早不说,拜拜,我去买余额宝。“
业务员无表情的删着已经录入的信息进行事务回滚。
”下一个!“
用了IO复用则是大师业务员开始挑战极限,在超大营业厅里给客户们人手一个牌子,黑压压的客户们都在大厅中,有问题时举牌申请提问,大师目光敏锐点名指定某人提问,该客户迅速获得大师的答复后,要通过一段时间思考,查查本身的银袋子,咨询下LD,才能再次进行下一个提问,直到获得完整的满意答复退出大厅。例如:大师刚指导A填写转账单的某一项,B又来申请兑换泰铢,给了B兑换单后,C又来办理定转活,而后D与F在争抢有限的圆珠笔时出现了不和谐现象,被大师叫停业务,暂时等待。
这就是基于事件驱动的IO复用编程比起传统1线程1请求的方式来,有难度的设计点了,客户们都是上帝,既不能出错,还不能厚此薄彼。
当没有反应堆时,咱们可能的设计方法是这样的:大师把每一个客户的提问都记录下来,当客户A提问时,首先查阅A以前问过什么作过什么,这叫联系上下文,而后再根据上下文和当前提问查阅有关的银行规章制度,有针对性的回答A,并把回答也记录下来。当圆满回答了A的全部问题后,删除A的全部记录。
回到码农生涯,即,某一瞬间,服务器共有10万个并发链接,此时,一次IO复用接口的调用返回了100个活跃的链接等待处理。先根据这100个链接找出其对应的对象,这并不难,epoll的返回链接数据结构里就有这样的指针能够用。接着,循环的处理每个链接,找出这个对象此刻的上下文状态,再使用read、write这样的网络IO获取这次的操做内容,结合上下文状态查询此时应当选择哪一个业务方法处理,调用相应方法完成操做后,若请求结束,则删除对象及其上下文。
这样,咱们就陷入了面向过程编程方法之中了,在面向应用、快速响应为王的移动互联网时代,这样作迟早得把本身玩死。咱们的主程序须要关注各类不一样类型的请求,在不一样状态下,对于不一样的请求命令选择不一样的业务处理方法。这会致使随着请求类型的增长,请求状态的增长,请求命令的增长,主程序复杂度快速膨胀,致使维护愈来愈困难,苦逼的程序员不再敢轻易接新需求、重构。
反应堆是解决上述软件工程问题的一种途径,它也许并不优雅,开发效率上也不是最高的,但其执行效率与面向过程的使用IO复用却几乎是等价的,因此,不管是nginx、memcached、redis等等这些高性能组件的代名词,都义无反顾的一头扎进了反应堆的怀抱中。
反应堆模式能够在软件工程层面,将事件驱动框架分离出具体业务,将不一样类型请求之间用OO的思想分离。一般,反应堆不只使用IO复用处理网络事件驱动,还会实现定时器来处理时间事件的驱动(请求的超时处理或者定时任务的处理),就像下面的示意图:
这幅图有5点意思:
(1)处理应用时基于OO思想,不一样的类型的请求处理间是分离的。例如,A类型请求是用户注册请求,B类型请求是查询用户头像,那么当咱们把用户头像新增多种分辨率图片时,更改B类型请求的代码处理逻辑时,彻底不涉及A类型请求代码的修改。
(2)应用处理请求的逻辑,与事件分发框架彻底分离。什么意思呢?即写应用处理时,不用去管什么时候调用IO复用,不用去管什么调用epoll_wait,去处理它返回的多个socket链接。应用代码中,只关心如何读取、发送socket上的数据,如何处理业务逻辑。事件分发框架有一个抽象的事件接口,全部的应用必须实现抽象的事件接口,经过这种抽象才把应用与框架进行分离。
(3)反应堆上提供注册、移除事件方法,供应用代码使用,而分发事件方法,一般是循环的调用而已,是否提供给应用代码调用,仍是由框架简单粗暴的直接循环使用,这是框架的自由。
(4)IO多路复用也是一个抽象,它能够是具体的select,也能够是epoll,它们只必须提供采集到某一瞬间全部待监控链接中活跃的链接。
(5)定时器也是由反应堆对象使用,它必须至少提供4个方法,包括添加、删除定时器事件,这该由应用代码调用。最近超时时间是须要的,这会被反应堆对象使用,用于确认select或者epoll_wait执行时的阻塞超时时间,防止IO的等待影响了定时事件的处理。遍历也是由反应堆框架使用,用于处理定时事件。
下面用极简流程来形象说明下反应堆是如何处理一个请求的,下图中桔色部分皆为反应堆的分发事件流程:
能够看到,分发IO、定时器事件都由反应堆框架来完成,应用代码只会关注于如何处理可读、可写事件。
固然,上图是极度简化的流程,实际上要处理的异常状况都没有列入。
这里能够看到,为何定时器集合须要提供最近超时事件距离如今的时间?由于,调用epoll_wait或者select时,并不可以始终传入-1做为timeout参数。由于,咱们的服务器主营业务每每是网络请求处理,若是网络请求不多时,那么CPU的全部时间都会被频繁却又没必要要的epoll_wait调用所占用。在服务器闲时使进程的CPU利用率下降是颇有意义的,它可使服务器上其余进程获得更多的执行机会,也能够延长服务器的寿命,还能够省电。这样,就须要传入准确的timeout最大阻塞时间给epoll_wait了。
什么样的timeout时间才是准确的呢?这等价于,咱们须要准确的分析,什么样的时段进程能够真正休息,进入sleep状态?
一个没有意义的答案是:不须要进程执行任务的时间段内是能够休息的。
这就要求咱们仔细想一想,进程作了哪几类任务,例如:
一、全部网络包的处理,例如TCP链接的创建、读写、关闭,基本上全部的正常请求都由网络包来驱动的。对这类任务而言,没有新的网络分组到达本机时,就是可使进程休息的时段。
二、定时器的管理,它与网络、IO复用无关,虽然它们在业务上可能有相关性。定时器里的事件须要及时的触发执行,不能由于其余缘由,例如阻塞在epoll_wait上时耽误了定时事件的处理。当一段时间内,能够预判没有定时事件达到触发条件时(这也是提供接口查询最近一个定时事件距当下的时间的意义所在),对定时任务的管理而言,进程就能够休息了。
三、其余类型的任务,例如磁盘IO执行完成,或者收到其余进程的signal信号,等等,这些任务明显不须要执行的时间段内,进程能够休息。
因而,使用反应堆模型的进程代码中,一般除了epoll_wait这样的IO复用外,其余调用都会基于无阻塞的方式使用。因此,epoll_wait的timeout超时时间,就是除网络外,其余任务所能容许的进程睡眠时间。而只考虑常见的定时器任务时,就像上图中那样,只须要定时器集合可以提供最近超时事件到如今的时间便可。
从这里也能够推导出,定时器集合一般会采用有序容器这样的数据结构,好处是:
一、容易取到最近超时事件的时间。
二、能够从最近超时事件开始,向后依次遍历已经超时的事件,直到第一个没有超时的事件为止便可中止遍历,不用所有遍历到。
所以,粗暴的采用无序的数据结构,例如普通的链表,一般是不足取的。但事无绝对,redis就是用了个毫无顺序的链表,缘由何在?由于redis的客户端链接没有超时概念,因此对于并发的成千上万个连上,都不会由于超时被断开。redis的定时器惟一的用途在于定时的将内存数据刷到磁盘上,这样的定时事件一般只有个位数,其性能可有可无。
若是定时事件很是多,综合插入、遍历、删除的使用频率,使用树的机会最多,例如小根堆(libevent)、二叉平衡树(nginx红黑树)。固然,场景特殊时,尽能够用有序数组、跳跃表等等实现。
综上所述,反应堆模型开发效率上比起直接使用IO复用要高,它一般是单线程的,设计目标是但愿单线程使用一颗CPU的所有资源,但也有附带优势,即每一个事件处理中不少时候能够不考虑共享资源的互斥访问。但是缺点也是明显的,如今的硬件发展,已经再也不遵循摩尔定律,CPU的频率受制于材料的限制再也不有大的提高,而改成是从核数的增长上提高能力,当程序须要使用多核资源时,反应堆模型就会悲剧,为什么呢?
若是程序业务很简单,例如只是简单的访问一些提供了并发访问的服务,就能够直接开启多个反应堆,每一个反应堆对应一颗CPU核心,这些反应堆上跑的请求互不相关,这是彻底能够利用多核的。例如Nginx这样的http静态服务器。
若是程序比较复杂,例如一块内存数据的处理但愿由多核共同完成,这样反应堆模型就很难作到了,须要昂贵的代价,引入许多复杂的机制。因此,你们就能够理解像redis、nodejs这样的服务,为何只能是单线程,为何memcached简单些的服务确能够是多线程。