大家好,欢迎来到IT知识分享网。
文章目录
Reactor 模式
目前存在的线程模型有:传统阻塞 I/O
服务模型 和 Reactor
模式。
Reactor 模式是基于事件驱动开发的,核心组成部分包括 Reactor 和线程池,其中 Reactor 负责监听和分发事件,线程池负责处理事件。Reactor 分为三种模型:
- 单线程模型 (单 Reactor 单线程)
- 多线程模型 (单 Reactor 多线程)
- 主从多线程模型 (多 Reactor 多线程)
单 Reactor 单线程
Reactor 和 Handler 都在同一个线程中执行,即 select 多路复用、事件分发和事件处理都是在同一个线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求或应答/响应消息。
- 优点:模型简单,没有多线程频繁切换、资源竞争的问题,全部都在一个线程中完成。
- 缺点:性能问题:一个 NIO 线程同时处理成百上千的链路,存在性能问题,无法完全发挥多核
CPU
的性能,当Handler
在处理某个连接上的业务时,如果发生阻塞,其他所有 Handler 都得不到执行,将无法输入输出,也无法建立连接。 - 缺点:可靠性问题:若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。
- 使用场景:客户端的数量有限,业务处理非常快速。
Select
是前面I/O
复用模型介绍的标准网络编程API
,可以实现应用程序通过一个阻塞对象监听多路连接请求;Reactor
对象通过Select
监控客户端请求事件,收到事件后通过Dispatch
进行分发;- 如果是建立连接请求事件,则由
Acceptor
通过Accept
处理连接请求,然后创建一个Handler
对象处理连接完成后的后续业务处理; - 如果不是建立连接事件,则
Reactor
会分发调用连接对应的Handler
来响应; Handler
会完成Read
→ 业务处理 →Send
的完整业务流程。
单 Reactor 多线程
一个 NIO 线程(Acceptor)只负责服务端监听,接收客户端的 TCP 连接请求;NIO 线程池负责网络 IO 的操作,即消息的读取、解码、编码和发送;1 个 NIO 线程可以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个 Acceptor 线程可能会存在性能不足问题。
- 优点:可以充分的利用多核
cpu
的处理能力。 - 缺点:多线程数据共享和访问比较复杂。
Reactor
承担所有的事件的监听和处理响应,它是单线程运行,在高并发场景容易出现性能瓶颈。也就是说Reactor
主线程存在网络 IO 瓶颈。
Reactor
对象通过Select
监控客户端请求事件,收到事件后,通过Dispatch
进行分发;- 如果是建立连接请求,则由
Acceptor
通过accept
处理连接请求,然后创建一个Handler
对象处理完成连接后的各种事件; - 如果是处理请求,则由
Reactor
分发调用连接对应的handler
来处理(也就是说连接已建立,后续客户端再来请求就基本是处理请求了,直接调用之前为这个连接创建好的 handler 来处理); handler
只负责读取数据、响应事件,不做具体的业务处理(这样不会使 handler 阻塞太久),通过read
读取数据后,会分发给后面的worker
线程池的某个线程处理业务,Reactor 线程可以处理其他事件。【业务处理是最费时的,所以将业务处理交给线程池去执行】;worker
线程池会分配独立线程完成真正的业务,并将结果返回给handler
;handler
收到响应后,通过send
将结果返回给client
。
主从 Reactor 多线程
可以将 Reactor 线程拆分为多个子 Reactor 线程;同时引入多个 Selector 选择器,并为每一个 SubReactor 引入一个线程,一个线程负责一个选择器的事件轮询。
主 Reactor 线程用于绑定监听端口,接收客户端连接。当连接建立,将 SocketChannel 从主 Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上, 用于处理 I/O 的读写等操作,从而保证 mainReactor 只负责接入认证、握手等操作。
- 优点:父线程与子线程的数据交互简单,职责明确。父线程只需要接收新连接,之后交给子线程完成后续的业务处理。父线程与子线程的数据交互简单,
Reactor
主线程只需要把新连接传给子线程,子线程无需返回数据给主线程。 - 缺点:编程复杂度较高。
Reactor
主线程MainReactor
对象通过select
监听连接事件,收到事件后,通过Acceptor
处理连接事件;当Acceptor
处理连接事件后,MainReactor
将连接分配给SubReactor
;subReactor
将连接加入到连接队列进行监听,并创建handler
进行各种事件处理;- 当有新事件发生时,
subReactor
就会调用对应的handler
处理; handler
通过read
读取数据,分发给后面的worker
线程处理;worker
线程池分配独立的worker
线程进行业务处理,并返回结果;handler
收到响应的结果后,再通过send
将结果返回给client
;Reactor
主线程可以对应多个Reactor
子线程。
Netty 线程模型
Netty 主要基于主从 Reactor
多线程模型并做了一定的改进,其中主从 Reactor
多线程模型有多个 Reactor
。内部实现了两个线程池,boss 线程池和 work 线程池,其中 boss 线程池的线程负责处理请求的 accept 事件,当接收到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 work 线程池。work 线程池负责轮询并处理 read 和 write 事件,由对应的 Handler 处理。
BossGroup
线程维护Selector
,只关注Accecpt
;- 当接收到
Accept
事件,获取到对应的SocketChannel
,封装成NIOScoketChannel
并注册到Worker
线程(事件循环)进行维护,并创建对应 Handler 加入到通道; - 当
Worker
线程监听到Selector
中通道发生自己感兴趣的事件后,就由handler
进行处理。
BossGroup
表示主Reactor
可以有多个(在 Netty 编程时我们通常会把 boss 线程池设置成单个线程),WorkerGroup
则代表 SubReactor
一样可以有多个(work 线程池则会直接使用默认的 CPU 核心数 * 2
个线程数)。
-
Netty
抽象出两组线程池 ,BossGroup
负责接收客户端的连接,WorkerGroup
负责网络 IO 读写;BossGroup
和WorkerGroup
类型都是NioEventLoopGroup
; -
NioEventLoopGroup
相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是NioEventLoop
; -
NioEventLoop
表示一个不断循环的执行处理任务的线程,每个NioEventLoop
都有一个Selector
,用于监听绑定在其上的socket
的网络通讯; -
NioEventLoopGroup
可以有多个线程,即可以含有多个NioEventLoop
; -
每个
BossGroup
下面的NioEventLoop
循环执行的步骤有3
步:- 轮询
accept
事件; - 处理
accept
事件,与client
建立连接,生成NioScocketChannel
,并将其注册到某个workerGroup
NIOEventLoop
上的Selector
; - 继续处理任务队列的任务,即
runAllTasks
;
- 轮询
-
每个
WorkerGroup
的NIOEventLoop
循环执行的步骤- 轮询
read
,write
事件; - 处理
I/O
事件,即read
,write
事件,在对应NioScocketChannel
处理; - 处理任务队列的任务,即
runAllTasks
;
- 轮询
-
每个
Worker
的NIOEventLoop
处理业务时,会使用pipeline
(管道),pipeline
中包含了channel
,即通过pipeline
可以获取到对应通道,管道中维护了很多的处理器。
NioEventLoopGroup
下包含多个 NioEventLoop
;每个 NioEventLoop
中包含有一个 Selector
,一个 taskQueue
;每个 NioEventLoop
的 Selector
上可以注册监听多个 NioChannel
;每个 NioChannel
只会绑定在唯一的 NioEventLoop
上;每个请求对应一个 SocketChannel
,每个 NioSocketChannel
都绑定有一个自己的 pipeline
,里面包含一连串的 Handler
。
1、串行化设计避免线程竞争
Netty 采用串行化设计理念,从消息的读取->解码->处理->编码->发送,始终由单个 IO 线程 NioEventLoop 负责。整个流程不会进行线程上下文切换,数据无并发修改风险。
一个NioEventLoop 聚合一个多路复用器 selector,因此可以处理多个客户端连接。
Netty 只负责提供和管理“IO线程”,其他的业务线程模型由用户自己集成。时间可控的简单业务建议直接在“IO线程”上处理,复杂和时间不可控的业务建议投递到业务线程池中处理。
2、定时任务与时间轮
NioEventLoop中的Thread线程按照时间轮中的步骤不断循环执行:
- 在时间片Tirck内执行selector.select()轮询监听IO事件;
- 处理监听到的就绪IO事件;
- 执行任务队列taskQueue/delayTaskQueue中的非IO任务。
Redis 线程模型
Redis 基于 Reactor 模式开发了自己的文件事件处理器,由 4 个部分组成:多个 Socket 套接字、I/O 多路复用程序、文件事件分派器(dispatcher)、各种事件处理器。
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,IO 多路复用程序会监听多个 socket,将产生事件的 socket 放入队列中排队,文件事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
Redis 使用 epoll 作为 I/O 多路复用技术的实现。并且,Redis自身的事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间。
套接字:socket 连接,也就是客户端连接。当一个套接字准备好执行连接、写入、读取、关闭等操作时, 就会产生一个相应的文件事件。
I/O 多路复用程序:使用 epoll 实现,会根据当前系统自动选择最佳的方式。负责监听多个套接字,当套接字产生事件时,会向文件事件分派器传送那些产生了事件的套接字。当多个文件事件并发出现时, I/O 多路复用程序会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字:当上一个套接字产生的事件被处理完毕之后,才会继续传送下一个套接字。
文件事件分派器:接收 I/O 多路复用程序传来的套接字, 并根据套接字的事件类型来为套接字关联不同的事件处理器。
事件处理器:事件处理器就是一个个函数, 定义了某个事件发生时, 服务器应该执行的动作。例如:建立连接、命令查询、命令写入、连接关闭等等。包含连接应答处理器、命令请求处理器、命令回复处理器。
Redis 的单线程
值得注意的是,Redis 正常处理客户端请求的核心流程,也就是执行命令阶段,一直都是单线程的。即使 Redis 6.0 再次引入了多线程,也主要是用于网络 IO 的,也就是接收命令和写回结果阶段,执行命令的核心流程依旧是单线程串行处理。
这也是 Redis 设计者的初衷,单线程编程更简单且容易维护,同时避免了线程的上下文切换以及死锁的问题,更重要的是无需使用加锁等方式进行线程同步,保证了数据安全。
再比如说 AOF 重写也是这个道理,Redis 是 fork
一个子进程 bgrewriteaof
进行后台重写,而不是创建线程进行处理,因为如果使用的是线程,多线程之间会共享内存,这样修改共享数据时就需要加锁来保证数据安全,会降低性能。而子进程则一开始通过拷贝页表的方式与父进程共享物理内存,在发生共享数据修改时则会触发 Copy-On-Write
写时复制,于是父子进程就会有独立的数据副本,就不用加锁来保证数据安全。
正是因为 IO 多路复用模型使得服务器得以支撑百万级的并发连接。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/23558.html