【死磕 NIO】— Reactor 模式就一定意味着高性能吗?
大家好,我是大明哥,我又来了。
为什么是 Reactor
一般所有的网络服务,一般分为如下几个步骤:
读请求(read request)
读解析(read decode)
处理程序(process service)
应答编码 (encode reply)
发送应答(send reply)
接下来,大明哥就来分析解决这个问题的最佳实践。
单线程模式
对于很多小伙伴来说,最简单,最传统的方式就是一个方法来处理所有的请求,这种实现方式最简单,也是最保险的方式。
这种方式实现起来虽然简单,但是性能不行,如果其中有一个请求因为某种原因阻塞了,则他后面的所有请求都会阻塞在那里,同时他也没法利用多 CPU 的性能,性能严重不足。
多线程模式
单线程的性能肯定不行,那就调整为多线程方式。
每来一个请求就会创建一个线程来处理,这种方式虽然不会像 单线程模式 一样,一个线程会阻塞所有的请求,但是他依然很大的问题:
当客户端多,并发大的时候,需要创建大量线程来处理,线程的创建和销毁也很消耗资源,会导致整个系统的的资源占用较大
同样无法应对高性能和高并发
线程池模式
既然多线程模式需要创建这么多线程,那么我们控制创建线程的个数,采用资源复用 线程池 的方式,也就是我们不需要再为每一个连接创建一个线程,而是创建一个线程池,将连接分配给线程,然后一个线程可以处理多个链接。
这种线程池的方式虽然解决了系统资源占用的问题,但是他依然带了了一个新的问题,每一个线程如何高效地处理请求呢?在上篇文章中 【死磕NIO】— 阻塞IO,非阻塞IO,IO复用,信号驱动IO,异步IO,这你真的分的清楚吗?我们提到过在单个线程中如果当前连接在进行read
操作时,如果没有数据可读,则会发生阻塞,那么线程就没有办法继续处理其他连接的业务了。那么怎么解决?将 read
操作改为非阻塞的方式,既然改为了非阻塞方式,那线程如何知道read
操作有数据可读了呢?
第一种方式,则是不断的去轮询,但是轮询要消耗 CPU的,而且随着轮询的线程多了,轮询的效率会越来越低
第二种方式,事件驱动。当线程关心的事件发生了,比如
read
有数据可读了,则通知相对应的线程进行处理
Reactor 模式
第二种方式就是 I/O多路复用
。I/O多路复用就是通过一种机制,一个线程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知线程进行相应的读写操作。目前支持 IO多路复用技术有:
Linux:
select
、poll
、epoll
MAC:kqueue
Windows:
select
监听线程帮助我们监听哪些线程的事件已发生,发生后则通知相对应的线程进行处理,这样就可以避免进行很多无用的操作。对处理线程而言,整个处理过程只有调用 select
、poll
、epoll
的时候才会阻塞,其他时段,他可以处理其他的事情,这样整个线程会被充分利用起来,这样就高效很多了。
什么是 Reactor模式
上面讲了 Reactor 模式的演变,那什么是 Reactor 模式呢?
wiki上是这样定义的:
Reactor 模式也叫做反应器设计模式,它是一种为处理服务请求并发提交到一个或者多个服务处理程序的事件设计模式。当请求抵达后,服务处理程序使用解多路分配策略,然后同步地派发这些请求至相关的请求处理程序。
简要概括就是: 将消息放到了一个队列中,通过异步线程池对其进行消费。暂时理解成下面这个样子:
对于Reactor模式来说,他并没队列,每当有一个 Event 输入到 Server端时,Service Handler 会将其转发(dispatch)相对应的handler进行处理。
Reactor的组件主要包括三个:
Reactor:派发器,将 client端的事件分发给相对应的Handler
Acceptor:请求连接器,Reactor 接收到 client 连接事件后,会将其转发给 Acceptor,Acceptor 则会接受 Client 的连接,建立对应的Handler,并向 Reactor注册此Handler
Handler:请求处理器,负责事件的处理。
模型大致如下图:
Reactor 模式
Reactor 模型中的Reactor可以是多个也可以是单个,Handler同样可以是单线程也可以是多线程,所以组合的模式大致有如下四种:
单Reactor单线程/进程
单Reactor多线程/进程
多Reactor单线程/进程
多Reactor多线程/进程
其中第三种多Reactor单线程并没有什么实际的意思,所以大明哥重点介绍第一、二、四种。
单Reactor单线程/进程
Reactor 线程通过 select (IO多路复用接口)监听事件,收到事件后通过Dispatch 来分发事件,事件会分发给Acceptor和Handler 两个组件,具体是哪个组件要看事件的类型。
如果事件类型为建立连接,则将事件分发给Acceptor,Acceptor会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件。
如果时间类型不是建立连接,则将该事件交由当前连接的Handler来处理。
优缺点
优点:该模型是将所有处理逻辑放在一个线程中实现,模型简单,没有多线程、进程通信、竞争的问题
缺点
由于只有一个线程,无法充分利用CPU,性能堪忧。同时Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。
还有一个比较严重的可靠性问题,如果线程意外终止,或者进入死循环,则会导致整个线程都无法接受和处理事件了,造成节点故障。
单Reactor多线程/进程
单线程存在性能瓶颈,那我们就引入多线程方案。
Reactor 接受请求后,根据请求类型来进行分发,分发逻辑与 单Reactor单线程 模型一样,不同之处在于Handler不在进行业务处理了,它只负责接受和发送,Handler接受数据后,会将数据发送给 Worker 线程池中的线程处理,该线程才是处理业务的真正线程,线程将业务处理完成后,将数据发送给Handler,然后Handler 再send出去。
优缺点
优点:由于Handler使用了多线程模式,则可以利用充分利用CPU的性能
缺点:
Handler使用多线程模式,则会涉及到数据共享的问题,需要考虑互斥,实现肯定比 单Reactor单线程模式复杂一些
单Reactor,一个线程处理事件监听、分发、响应,对于高并发场景,容易造成性能瓶颈
多Reactor多线程/进程
单Reactor多线程模式解决了Handler单线程的性能问题,但是Reactor还是单线程的,对于高并发场景还是会有性能瓶颈,所以需要对Reactor调整为 多线程模式。
主线程中的MainReactor对象通过select监听事件,接收到事件后通过Dispatch进行分发,如果事件类型为建立连接则将事件分发给Acceptor 进行连接建立
如果收到的事件不是连接,则他将事件分发个某个SubReactor,SubrReactor 将连接加入到连接队列进行监听,并创建Handler进行各种事件处理
如果有新的事件发生,SubReactor 则会调用当前连接的Handler来进行处理。Handler 通过read 读取数据后,将数据发送给Worker线程进行处理,Worker线程池则会分配线程进行业务处理,处理完成后返回结果,Handler接受结果后,通过send发送给客户端
优缺点
优点:该模式主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理,同时主线程和子线程的交互也很简单,子线程接收主线程的连接后,只管业务处理即可,无须关注主线程
缺点:模型复杂
这种模式适用于高并发场景,广泛运用于各种项目中,如大名鼎鼎的Netty。
Reactor 优缺点
Reactor模式有如下优点:
响应快,不必为单个同步时间所阻塞
可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销
扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源
复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性
虽然Reactor有诸多优点,但是由于他的IO读写数据时还是在同一个线程中实现的,如果当前线程出现了一个长时间的IO数据读写,则会影响其他的client。那怎么解决呢?请静候下一篇文章。
参考资料
【死磕 NIO】— Reactor 模式就一定意味着高性能吗?的更多相关文章
- 【死磕 NIO】— Proactor模式是什么?很牛逼吗?
大家好,我是大明哥. 上篇文章我们分析了高性能 IO模型Reactor模式,了解了什么是Reactor 模式以及它的三种常见的模式,这篇文章,大明再介绍另外一种高性能IO模型: Proactor. 为 ...
- 【死磕 NIO】— 深入分析Buffer
大家好,我是大明哥,今天我们来看看 Buffer. 上面几篇文章详细介绍了 IO 相关的一些基本概念,如阻塞.非阻塞.同步.异步的区别,Reactor 模式.Proactor 模式.以下是这几篇文章的 ...
- 【死磕NIO】— 阻塞、非阻塞、同步、异步,傻傻分不清楚
万事从最基本的开始. 要想完全掌握 NIO,并不是掌握上面文章([死磕NIO]- NIO基础详解)中的三大组件就可以了,我们还需要掌握一些基本概念,如什么是 IO,5 种IO模型的区别,什么是阻塞&a ...
- 【死磕NIO】— 阻塞IO,非阻塞IO,IO复用,信号驱动IO,异步IO,这你真的分的清楚吗?
通过上篇文章([死磕NIO]- 阻塞.非阻塞.同步.异步,傻傻分不清楚),我想你应该能够区分了什么是阻塞.非阻塞.异步.非异步了,这篇文章我们来彻底弄清楚什么是阻塞IO,非阻塞IO,IO复用,信号驱动 ...
- 【死磕NIO】— 跨进程文件锁:FileLock
大家好,我是大明哥,一个专注于[死磕 Java]系列创作的程序员. [死磕 Java ]系列为作者「chenssy」 倾情打造的 Java 系列文章,深入分析 Java 相关技术核心原理及源码 死磕 ...
- Java NIO Reactor模式
一.NIO介绍: NIO模型: 1.Channel为连接通道,相当于一个客户端与服务器的一个连接,Selector为通道管理器,将Channel注册到Selector上,Selector管理着这些Ch ...
- JAVA BIO,NIO,Reactor模式总结
传统同步阻塞I/O(BIO) 在NIO之前编写服务器使用的是同步阻塞I/O(Blocking I/O).下面是一个典型的线程池客服端服务器示例代码,这段代码在连接数急剧上升的情况下,这个服务器代码就会 ...
- 【死磕NIO】— 探索 SocketChannel 的核心原理
大家好,我是大明哥,一个专注于[死磕 Java]系列创作的程序员. [死磕 Java ]系列为作者「chenssy」 倾情打造的 Java 系列文章,深入分析 Java 相关技术核心原理及源码. 死磕 ...
- 【死磕NIO】— NIO基础详解
Netty 是基于Java NIO 封装的网络通讯框架,只有充分理解了 Java NIO 才能理解好Netty的底层设计.Java NIO 由三个核心组件组件: Buffer Channel Sele ...
随机推荐
- golang map实现原理
这篇文章主要讲 map 的赋值.删除.查询.扩容的具体执行过程,仍然是从底层的角度展开.结合源码,看完本文一定会彻底明白 map 底层原理. 我要说明的是,这里对 map 的基本用法涉及比较少,我相信 ...
- AN INTEGER FORMULA FOR FIBONACCI NUMBERS
https://blog.paulhankin.net/fibonacci/ This code, somewhat surprisingly, generates Fibonacci numbers ...
- Python之requests模块-session
http协议本身是无状态的,为了让请求之间保持状态,有了session和cookie机制.requests也提供了相应的方法去操纵它们. requests中的session对象能够让我们跨http请求 ...
- Linux下Sed替换时无法解析变量
1.问题描述 用sed替换文件中的IP时,想替换成$es_ip中的值,但是却不能解析这个变量$es_ip sed -ri 's/([0-9]{1,3}\.){3}[0-9]{1,3}/$es_ip/g ...
- 文件流转换为url
/** * 文件流转换为url * @param {} data //文件流 */ export function getObjectURL(data) { var url = null ...
- Selenium系列5-XPath路径表达式
Xpath介绍 XPath 使用路径表达式在 XML 文档中进行导航 XPath 使用路径表达式来选取 XML 文档中的节点或者节点集.这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似. ...
- Java大数操作
Java的Math包中提供了两个类用于对大数进行操作: BigInteger类,用于大整数的操作 BigDecimal类,用于大的小数操作 BigInteger类 Java中的基本类型中,表示整数的有 ...
- POJ2251——Dungeon Master(三维BFS)
和迷宫问题区别不大,相比于POJ1321的棋盘问题,这里的BFS是三维的,即从4个方向变为6个方向. 用上队列的进出操作较为轻松. #include<iostream> #include& ...
- swiper-wrapper轮滑组件(多组轮滑界面)间隔无效问题
在多组此种轮滑效果出现时,你需要加两个属性值,即 new Swiper('.swiper-container', { slidesPerView: 3, slidesPerColumn: 2, spa ...
- 重磅来袭!!!Elasticsearch7.14.1(ES 7.14.1)与Springboot2.5.4的整合
1. 概述 前面我们聊了 Elasticsearch(ES)集群的搭建,今天我们来聊一下,Elasticsearch(ES)集群如何与 Springboot 进行整合. Elasticsearch(E ...