从0开始学架构
李运华
资深技术专家
立即订阅
38968 人已学习
课程目录
已完结 59 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 照着做,你也能成为架构师!
免费
基础架构 (13讲)
01 | 架构到底是指什么?
02 | 架构设计的历史背景
03 | 架构设计的目的
04 | 复杂度来源:高性能
05 | 复杂度来源:高可用
06 | 复杂度来源:可扩展性
07 | 复杂度来源:低成本、安全、规模
08 | 架构设计三原则
09 | 架构设计原则案例
10 | 架构设计流程:识别复杂度
11 | 架构设计流程:设计备选方案
12 | 架构设计流程:评估和选择备选方案
13 | 架构设计流程:详细方案设计
高性能架构模式 (8讲)
14 | 高性能数据库集群:读写分离
15 | 高性能数据库集群:分库分表
16 | 高性能NoSQL
17 | 高性能缓存架构
18 | 单服务器高性能模式:PPC与TPC
19 | 单服务器高性能模式:Reactor与Proactor
20 | 高性能负载均衡:分类及架构
21 | 高性能负载均衡:算法
高可用架构模式 (10讲)
22 | 想成为架构师,你必须知道CAP理论
23 | 想成为架构师,你必须掌握的CAP细节
24 | FMEA方法,排除架构可用性隐患的利器
25 | 高可用存储架构:双机架构
26 | 高可用存储架构:集群和分区
27 | 如何设计计算高可用架构?
28 | 业务高可用的保障:异地多活架构
29 | 异地多活设计4大技巧
30 | 异地多活设计4步走
31 | 如何应对接口级的故障?
可扩展架构模式 (6讲)
32 | 可扩展架构的基本思想和模式
33 | 传统的可扩展架构模式:分层架构和SOA
34 | 深入理解微服务架构:银弹 or 焦油坑?
35 | 微服务架构最佳实践 - 方法篇
36 | 微服务架构最佳实践 - 基础设施篇
37 | 微内核架构详解
架构实战 (13讲)
38 | 架构师应该如何判断技术演进的方向?
39 | 互联网技术演进的模式
40 | 互联网架构模板:“存储层”技术
41 | 互联网架构模板:“开发层”和“服务层”技术
42 | 互联网架构模板:“网络层”技术
43 | 互联网架构模板:“用户层”和“业务层”技术
44 | 互联网架构模板:“平台”技术
45 | 架构重构内功心法第一式:有的放矢
46 | 架构重构内功心法第二式:合纵连横
47 | 架构重构内功心法第三式:运筹帷幄
48 | 再谈开源项目:如何选择、使用以及二次开发?
49 | 谈谈App架构的演进
50 | 架构实战:架构设计文档模板
特别放送 (7讲)
架构专栏特别放送 | “华仔,放学别走!”第1期
架构专栏特别放送 | “华仔,放学别走!” 第2期
如何高效地学习开源项目 | “华仔,放学别走!” 第3期
架构师成长之路 | “华仔,放学别走!” 第4期
架构师必读书单 | “华仔,放学别走!” 第5期
新书首发 | 《从零开始学架构》
致「从0开始学架构」专栏订阅用户
结束语 (1讲)
结束语 | 坚持,成就你的技术梦想
从0开始学架构
登录|注册

19 | 单服务器高性能模式:Reactor与Proactor

李运华 2018-06-09
专栏上一期我介绍了单服务器高性能的 PPC 和 TPC 模式,它们的优点是实现简单,缺点是都无法支撑高并发的场景,尤其是互联网发展到现在,各种海量用户业务的出现,PPC 和 TPC 完全无能为力。今天我将介绍可以应对高并发场景的单服务器高性能架构模式:Reactor 和 Proactor。

Reactor

PPC 模式最主要的问题就是每个连接都要创建进程(为了描述简洁,这里只以 PPC 和进程为例,实际上换成 TPC 和线程,原理是一样的),连接结束后进程就销毁了,这样做其实是很大的浪费。为了解决这个问题,一个自然而然的想法就是资源复用,即不再单独为每个连接创建进程,而是创建一个进程池,将连接分配给进程,一个进程可以处理多个连接的业务。
引入资源池的处理方式后,会引出一个新的问题:进程如何才能高效地处理多个连接的业务?当一个连接一个进程时,进程可以采用“read -> 业务处理 -> write”的处理流程,如果当前连接没有数据可以读,则进程就阻塞在 read 操作上。这种阻塞的方式在一个连接一个进程的场景下没有问题,但如果一个进程处理多个连接,进程阻塞在某个连接的 read 操作上,此时即使其他连接有数据可读,进程也无法去处理,很显然这样是无法做到高性能的。
解决这个问题的最简单的方式是将 read 操作改为非阻塞,然后进程不断地轮询多个连接。这种方式能够解决阻塞的问题,但解决的方式并不优雅。首先,轮询是要消耗 CPU 的;其次,如果一个进程处理几千上万的连接,则轮询的效率是很低的。
为了能够更好地解决上述问题,很容易可以想到,只有当连接上有数据的时候进程才去处理,这就是 I/O 多路复用技术的来源。
I/O 多路复用技术归纳起来有两个关键实现点:
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《从0开始学架构》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(78)

  • IO操作分两个阶段
    1、等待数据准备好(读到内核缓存)
    2、将数据从内核读到用户空间(进程空间)
    一般来说1花费的时间远远大于2。
    1上阻塞2上也阻塞的是同步阻塞IO
    1上非阻塞2阻塞的是同步非阻塞IO,这讲说的Reactor就是这种模型
    1上非阻塞2上非阻塞是异步非阻塞IO,这讲说的Proactor模型就是这种模型

    作者回复: 解释很清楚👍👍

    2018-06-10
    1
    237
  • Reactor与Proactor能不能这样打个比方:

    1、假如我们去饭店点餐,饭店人很多,如果我们付了钱后站在收银台等着饭端上来我们才离开,这就成了同步阻塞了。

    2、如果我们付了钱后给你一个号就可以离开,饭好了老板会叫号,你过来取。这就是Reactor模型。

    3、如果我们付了钱后给我一个号就可以坐到坐位上该干啥干啥,饭好了老板会把饭端上来送给你。这就是Proactor模型了。

    作者回复: 太形象了👍👍👍

    2018-06-11
    1
    211
  • 正是那朵玫瑰
    根据华仔之前对前浪微博消息中间件的分析,TPS定位在1380,QPS定位在13800,消息要高可靠(不能丢失消息),定位在常量连接海量请求的系统吧。基于此来分析下吧。

    1、单Reactor单进程/线程
    redis采用这种模式,原因是redis是基于内存的数据库,在处理业务会非常快,所以不会对IO读写进行过长时间的阻塞,但是如果redis开启同步持久化后,业务处理会变慢,阻塞了IO线程,也就无法处理更多的连接了,而我们的消息中间件需要消息的高可靠,必定要同步持久化,如果异步的话,就看异步持久化的时间间隔了,假设500ms持久化一次,那就有可能会丢失500ms的消息。当然华仔分析的无法利用多核cpu的特性也是一大缺点;虽然我们要求的TPS不算很高,但是QPS很高了,所以我觉得这种模式不合适
    2、单Reactor多进程/线程
    这种模式我觉得也不是和合适,虽然真正的业务处理在独立的线程了,IO线程并没有被阻塞,可以处理更多的连接和读写事件。我们的中间件可能不会面对海量的连接数,但是会面对大量的读请求,瓶颈是在处理读操作上,跟单Reactor单进程/线程差别不大;我倒觉得前一讲说的TPC prethread 模式是合适的,有独立的线程负责read-业务处理-send。
    3、多Reactor多进程/线程
    这种模式是最合适的了,不过华仔在讲解是read→业务处理→send,业务处理还是在IO线程上,如果业务处理的慢,还是会阻塞IO线程的,我觉得最好是业务处理放到独立的线程池里面去,这就变成了mainReactor负责监听连接,subReactor 负责IO读写,后面的业务线程池负责真正的业务处理,这样就既可以面对海量的连接,海量的请求也可以支撑。

    不知理解的是否正确?

    作者回复: 1. 分析正确,redis不太适合有的key的value特别大,这种情况会导致整个redis变慢,这种场景mc更好
    2. prethread确实可以,mysql就是这种模式
    3. 多reactor多线程再拆分业务线程,性能没有提升,复杂度提升不少,我还没见过这种方式。

    2018-06-09
    29
  • 空档滑行
    消息队列系统属于中间件系统,连接数相对固定,长链接为主,所以把accept分离出来的意义是不大的。消息中间件要保证数据持久性,所以入库操作应该是耗时最大的操作。综合起来我觉得单reactor,多线程/进程的方式比较合适。

    作者回复: 分析正确

    2018-06-10
    16
  • 正是那朵玫瑰
    感谢华仔,我也再实验了下netty4,其实handler的独立的线程池里面执行其实也没有问题,netty已经帮我们处理好了,当我们处理完业务,write数据的时候,会先放到一个队列里面,真正出站还是由io线程统一调度,这样就避免了netty3的问题!

    作者回复: 非常感谢,我明白了你说的情况,我再次验证了一下,写了一个独立线程处理业务的,确实如你所说,netty4两者都支持,并且做了线程安全处理,最终发送都是在io线程里面。

    如果我们用这种模式,可以自己控制业务线程,因为netty4已经帮我们封装了复杂度,看来我孤陋寡闻了😂

    不过我建议还是别无条件用这种模式,我们之前遇到的情况就是短时间内io确实很快,并发高,但如果业务处理慢,会积压请求数据,如果客户端请求是同步的,单个请求全流程时间不会减少;如果客户端请求是异步的,如果积压的时候宕机会丢较多数据。

    其实这种情况我理解单纯加大线程数就够了,例如5个io线程加20个业务线程能达到最优性能的话,我理解25个融合线程性能也差不多。

    我们之前有一个案例,http服务器业务处理线程配置了512个,后来发现其实配置128是最好的(48核),所以说并不是线程分开或者线程数量多性能就一定高。

    再次感谢你的认真钻研,我也学到了一个技术细节👍

    2018-06-15
    14
  • 赵正 Allen
    一直做网络通讯相关的开发,用过ACE,boost asio。谈谈我的一些愚见,reactor pattern 主要解决io事件检测和事件分派,其中,事件检测一般都是通过封装OS提供API实现,在Linux下最常用epoll,事件分派是将检测到的事件委托给特定的方法,一般通过接口继承或函数指针实现。除此之外,定时器,信号量也会集成到reactor框架中。
    多线程or多进程,实际工作中,基本多线程模型,可以单线程事件检测,多线程分派,也可以多线程轮流事件检测和分派。可以参考leader-follwers pattern。
    io模式,一般都使用non-block。
    与acceptor-connector模式结合使用,可进一步分离模块职责(即将 服务初始化与 服务逻辑分离. 由reactor统一进行事件驱动)

    附一个自己开发的reactor框架

    https://github.com/zhaozhencn/cute
    2018-06-10
    11
  • LinMoo
    请教两个问题 谢谢
    之前学习NIO和AIO的时候是这么描述的:进程请求IO(无论是硬盘还是网络IO),先让内核读取数据到内核缓存,然后从内核缓存读取到进程。这里面就有2个IO等待时间,第一个是读取到内核缓存,第二个是读取到进程。前者花费的时间远远大于后者。在第一个时间中进程不做等待就是NIO,即非阻塞。第二个时间中进程也不需要等待就是AIO,即异步。
    第一个问题:文章中说Reactor 是非阻塞同步网络模型,因为真正的 read 和 send 操作都需要用户进程同步操作。这里的read和send指的是我上面说的第二个时间吗?
    第二个问题:因为我理解你的“来了事件我来处理,处理完了我通知你”。这里的我来处理就是包括第一和第二个时间吗?

    感觉我之前被误解了,是我哪个地方理解不对吗?麻烦解答一下。

    作者回复: 你两个问题的理解都正确

    2018-06-09
    11
  • 赵正 Allen
    对于两组概念的理解,欢迎吐槽

    阻塞&非阻塞

    这一组概念并偏向于系统底层的实现,常与OS进程调度相关。 以socket为例,在阻塞模式下线程A调用recv函数,若此时接收缓冲区有数据,则立即返回,否则将进入”阻塞状态“(主动释放CPU控制权,由OS CPU调度程序重新调度并运行其它进程),直到”等待条件”为真,再由OS将此进程调度并重新投入运行。非阻塞模式则另辟蹊径,无论有无数据均立即返回(有数据则返回数据,无数据则返回错误值), 不会影响当前线程的状态。 从某种意义上讲,阻塞模式下,一个线程关联一个文件fd, 常引起线程切换与重新调度,对于高并发环境,这种代价太大。而非阻塞模式则解耦了“1线程关联1文件fd"。



    同步&异步

    调用与执行的分离即为异步,否则为同步。其实包括两个层面,其一为请求方(客户方),其二为执行方(服务方),抛开这两个概念单独讨论同步或异步是片面的。若请求方调用执行方的服务并等待服务结果,则为同步过程。但对于一些耗时或IO服务,服务执行时间往往较长或不可控,并可能导致降低整体服务质量,此时需要将调用与执行解耦。 有些经典设计模式常用于解决此问题: 1 command(命令模式)-- 将请求封装成命令对象,实现请求方对命令执行的透明化, 2 Active Object(主动对象)-- 对象内部驻留一个线程或线程池,用于执行具体服务,同时,对象对外提供服务接口,供请求方发起调用(可能获得Future对象)。
    2018-06-11
    10
  • 正是那朵玫瑰
    感谢华仔的解答,我看到在针对多reactor多线程模型,也有同学留言有疑问,我想请教下华仔,多reactor多线程模型中IO线程与业务处理在同一线程中,如果业务处理很耗时,定会阻塞IO线程,所以留言同学“衣申人”也说要不要将IO线程跟业务处理分开,华仔的答案是性能没有提升,复杂度提升很多,我还没见过这种处理方式,华仔对netty应该是很熟悉的,我的疑问是:在netty中boss线程池就是mainReactor,work线程池就是subReactor,正常在ChannelPipeline中添加ChannelHandler是在work线程即IO线程中串行执行,但是如果pipeline.addLast(group, "handler", new MyBusinessLogicHandler());这样的话,业务hangdle就会在group线程池里面执行了,这样不就是多reactor多线程模型中把IO线程和业务处理线程分开么?而且我在很多著名开源项目里面看到使用netty都是这样处理的,比如阿里的开源消息中间件rocketmq使用netty也是如此。华仔说没有见过这种处理方式,能否解答下?不知道是不是我理解错了

    作者回复: 非常感谢你的认真研究和提问,我对netty的原理有一些研究,也用过netty3,也看过一些源码,但也还达不到非常熟悉的地步,所以不管是网上的资料,还是我说的内容,都不要无条件相信,自己都需要思考,这点你做的很好👍

    回到问题本身,由于netty4线程模型和netty3相比做了改进,我拿netty4.1源码中的telnet样例,在handler和NioEventloop的processSelectedKey函数打了输出线程id的日志,从日志结果看,StringEncoder, StringDecoder, TelnetServerHandler都在NioEventLoop的线程里面处理的。

    如果handler在独立的线程中运行,返回结果处理会比较麻烦,如果返回结果在业务线程中处理,会出现netty3存在的问题,channel需要做多线程同步,各种状态处理很麻烦;如果返回结果还是在io线程处理,那业务线程如何将结果发送给io线程也涉及线程间同步,所以最终其实还不如在一个线程里面处理。

    2018-06-14
    6
  • 文竹
    服务基本上都是部署在Linux上的,所以仅能使用reactor。前浪微博的写QPS在千级别,读在万级别,一般单台稍微好点配置好点的机器都能承受这两个QPS,再加上这两个QPS因任务分配器被分摊到了多态机器,最终单台机器上的QPS并不高。所以使用单reactor多线程模式足矣。

    作者回复: 分析正确👍

    2018-08-19
    3
  • 周飞
    nodejs的异步模型是io线程池来监听io,然后通过管道通信来通知事件循环的线程。事件循环线程调用主线程注册的回调函数来实现的。不知道这种模式跟今天说的两种相比有什么优缺点啊

    作者回复: 我理解这就是Reactor模式

    2018-07-05
    3
  • Bayern
    我能不能这样理解reactor,IO操作用一个连接池来获取连接,处理用线程池来处理任务。将IO和计算解耦开。这样就避免了在IO和计算不平衡时造成的浪费,导致性能低下。老师,我这样理解对吗
    2018-06-10
    3
  • 潘宁
    Reactor和proactor主要是用来解决高并发的问题(ppc和tpc不能应对高并发)
    打个比方,我们去点心店吃碗面,首先先得去收银台点单付钱,同步阻塞的情况是:我点了碗辣酱加辣肉面,然后我就在收银台等着,等到面来了,我拿着面去吃了,后面所有的人都无法点单无法下单(即使后面的人点的是已经做好的大排面也不能付钱拿面,老子还没好 你们谁都不许动),而reactor(同步非阻塞)的情况是我点了碗辣酱加辣肉面,钱付好以后我就拿着号去座位上坐下了,等面好了后,服务员会叫“XXX号,你的面好了,自己来取”(服务员帮你送上来的叫proactor),这里收银台就是reactor或者叫dispatcher,店里会有一个小二定时的轮询去看XXX号的XXX面有没有好,好了以后就通知XXX你可以来拿面了,没好你就等着呗。多reactor就是把收钱 下面 通知的事分成几个人 由不同的人来做
    2018-08-16
    2
  • 沙亮亮
    根据unix网络编程上说的,select和poll都是轮询方式,epoll是注册方式。为什么您说select也不是轮询方式

    作者回复: 两个轮询不是一个意思,select和poll是收到通知后轮询socket列表看看哪个socket可以读,普通的socket轮询是指重复调用read操作

    2018-07-16
    2
  • 孙振超
    ppc和tpc时是每一个连接创建一个进程或线程,处理完请求后将其销毁,这样的性能比较低,为提升性能,首先考虑是链接处理完后不再销毁进程或线程,将这一部分的成本给降下来。改进后存在的问题是如果当前的链接没有请求又把进程或线程都给占住的情况下,新进来的链接就没有处理资源了。对此的解决方法是把io处理从阻塞改为非阻塞,这样当链接没有请求的时候可以去其他有请求的链接,这样改完后存在的问题有两个:一是寻找有请求的链接需要轮询需要耗费cpu而是当请求特别多的时候轮询一遍也需要耗费很长时间。基于这种情况引出了io多路复用,在处理进程和链接这之间加了一个中间人,将所有的链接都汇总到一个地方,处理进程都阻塞在中间人上,当某一个链接有请求进来了,就通知一个进程去处理。在具体的实现方式上根据中间人reactor的个数和处理请求进程的个数上有四种组合,用的比较多的还是多reactor和多进程。
    之前的留言中有一个类比成去餐厅吃饭的例子还是蛮恰当的,肯德基麦当劳里面是reactor模式,需要用户先领个号然后等叫号取餐;海底捞和大多数中餐厅就是paractor模式,下完单后服务员直接将食品送过来。


    回到文章中的问题,消息中间件的场景是链接数少请求量大,采用多进程或多线程来处理会比较好,对应单reactor还是多reactor应该都可以。

    作者回复: 功能是ok的,但复杂度不一样,参考架构设计的简单原则

    2018-07-14
    2
  • 衣申人
    华仔,我有两个疑问:
    1.单reactor多线程模式,业务处理之后,处理线程将结果传输给reactor线程去send,这个具体能怎么实现?reactor既要等待网络事件,又要等待业务线程的处理结果,然后作出响应,这个除了两边轮询还有更直接的方式吗?
    2.多reactor多线程模型,现在你给出的方案是连接线程与io线程分开,但io线程与业务处理在一起的。而有的资料建议将io线程和业务线程分开,你认为有这个必要吗?

    作者回复: 1. 处理线程将返回结果包装成一个事件,触发write事件,详细可以看看Doug Lee的NIO PPT,处理线程和业务线程共享selector,key这些对象
    2. io线程与业务线程分开就是单reactor多线程,多reactor如果再分开的话,性能没有提升,复杂度提升很多,我还没见过这种处理方式

    2018-06-10
    2
  • 祖彦龙
    I/O模型分为阻塞I/O,非阻塞I/O,I/O多路复用,信号驱动I/O,异步I/O五种类型。本文提到的reactor模式应属于,I/O多路复用;proactor模式应属于异步I/O。
    2018-11-17
    1
  • FelixSun
    小白有一个问题困扰了好几天,可能也是经验不足没接触过这方面,请问一下。这里反复说的连接和请求究竟是什么意思?我查了一些资料,用MySQL举例,是不是说,mysql的连接数就是指的连接,mysql在最大连接数下支持的一秒内的请求处理数量是指的请求?

    作者回复: 连接你理解为tcp连接,请求你理解为一次sql语句执行

    2018-11-14
    1
  • 李继鹏
    李老师能讲一下udp服务是否能用reactor模型吗?网上这方面的资料甚少

    作者回复: 支持的,netty的NioDatagramChannel了解一下,直接用epoll也可以,epoll只管监听socket,tcp和udp都可以,差别在于udp只有一个socket描述符,因此只能用单Reactor

    2018-08-24
    1
  • 21克
    佩服佩服
    2018-07-24
    1
收起评论
78
返回
顶部