Netty(二)_IO模型如何演变到Netty模型
前置知识:
上一篇主要着重于Java中的IO模型,这把我们脱离Java这个包袱,分析一下更普遍一下的模型,然后正式进入netty!
原生NIO与Netty
原生NIO存在以下问题:
- NIO 的类库和 API 比较复杂:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
- 需要处理很多问题:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。(这些在netty中都有handler进行处理)
- JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决。
关于Epoll Bug
正常情况下,
selector.select()
操作是阻塞的,只有被监听的fd(文件描述符或通道)有读写操作时,才被唤醒但是,在这个bug中,没有任何channel有读写请求,但是
select()
操作依旧被唤醒很显然,这种情况下,会造成死循环导致爆CPU。
Netty的改进:
Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。
- 设计优雅:适用于各种传输类型的统一API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 (单线程,一个或多个线程池)
- 使用方便:…后面看API就知道。
- 通过合理的线程模型实现高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制(利用OS零拷贝技术)。
- 安全:完整的 SSL/TLS 和 StartTLS 支持。 (即支持HTTPS)
- 社区活跃、不断更新:版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入
注意:netty5版本出现重大BUG,已经被官网废弃,目前主流使用netty4,需要JDK6以上版本
Reactor_IO模型
线程模型基本介绍
注意:这里的线程模型与上一章的相比是更为抽象的表示,上一章的模型是Java基于这里所述线程模型的实现。
目前存在的线程模型有:传统同步阻塞型IO模型和同步非阻塞Reactor模型。
Netty 主要基于主从 Reactor 多线程模型做了一定的改进,在抛出Netty线程模型之前,我们首先搞清楚Reactor模型和传统阻塞IO模型如何工作。
根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现
- 单 Reactor 单线程
- 单 Reactor 多线程
- 主从 Reactor 多线程
传统IO阻塞型:
橙色的框表示对象,蓝色的框表示线程,白色的框表示方法(API),你可以把这个Handler泛指成服务端针对Channel(Socket)的一切操作,这一切操作都在一个线程当中。
模型特点:
- 采用阻塞IO模式获取输入的数据
- 每个来自客户端的连接都要分配一个独立的线程完成数据读取、业务处理、数据发送。注意,数据读取、业务处理、数据回送都是由一个线程进行完成!
缺点:
- 当并发数很大,就会创建大量线程,占用大系统资源。
- 同时,如果连接上之后没有进行IO操作,该线程会被阻塞在read操作,线程得不到释放。造成服务资源浪费
Reactor模式大致示意图
说明:
Reactor模式对传统模式进行了如下改进
- 采取了IO多路复用器,即多个连接共用一个Reactor,应用程序只需要一个Reactor(分发器)进行等待,无需阻塞等待所有连接。即把接收连接和业务处理分离开了。
- 当某个连接有新的数据可以处理时,即通道发生事件了,操作系统通知应用程序,处理线程(Handler)从阻塞状态返回,开始进行业务处理。注意这一步从OS到我们应用程序的感觉。还有,不同Handler之间的业务逻辑的独立的。
- 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给某个线程进行处理, 一个线程可以处理多个连接的业务,即根据通道事件的不同,选取对应事件的Handler进行处理。多个连接肯定都有可能发生同一事件,那自然一个Handler处理多个连接通道的同一事件了。
- 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程, 因此 Reactor 模式也叫 Dispatcher 分发器模式
核心组件:
- Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序(Handler)来对 IO 事件做出反应。
- EventHandlers:负责处理分发器分发过来的事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。
上面说过,Reactor有 3 种典型的实现,这里将逐个介绍
单Reactor 单线程模式
说明:
- Reactor 对象通过 select() 监控(轮询)客户端请求事件,收到事件后通过 dispatch() 进行分发
- 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建对应的一个 Handler 对象处理连接请求完成后的后续业务处理
- 如果不是建立连接事件,则 Reactor 会分发给对应的 Handler 来响应 。
- Handler 会完成 Read→业务处理→Send 的完整业务流程
这个单Reactor本质上还是一个线程处理一个请求,甚至全部流程也都在一个线程之中,只不过对比之前说的BIO,增加的select轮询事件罢了,把BIO的死循环读取通道改成轮询。性能提高看起来并不明显。
总结一下它存在的缺陷:
单Reactor单线程模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成,但是存在性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。此外,Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,所以它是个同步阻塞模型,很容易导致性能瓶颈,并且存在可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
适合使用场景:客户端的数量有限,业务处理非常快速,比如 Redis 在业务处理的时间复杂度 O(1) 的情况
单Reactor多线程模型
说明:
与“单Reactor单线程”相比,很简单:
将Handler与业务处理进行了解耦,Handler只负责响应事件,只保留基本的读写功能,通过read读取数据后,分发给Woker线程池,由线程池中的某个线程进行业务处理,线程处理完毕后将结果返回给对应Handler
handler 收到响应后,通过 send 将结果返回给相应client。
一句话说,也就是原来个一个线程负责一切,现在额外抽出一个线程负责业务处理。
优点:线程池机制可以充分发挥多核CPU的处理能力
缺点:Reactor还是单线程,连接量巨大的的情况下还是存在并发压力
主从Reactor多线程模型
注意这个模型下已经和netty的线程模型很接近了…
- 这个模型将Handler与Reactor进行了解耦
- Reactor 主线程 MainReactor 对象通过 select 监听连接事件, 收到事件后,通过 Acceptor 处理连接事件。注意看图,select(轮询)监听连接到accept得到连接是一个线程中完成。
- 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor,即主线程将连接(Socket或理解成Java Channel)分发给从线程进行管理。
- SubReactor 线程将连接加入到连接队列,利用Selector进行监听通道事件,并创建 handler 对象在内存中负责进行事件处理
- 当连接有新事件发生时, SubReactor 就会调用对应的Handler处理。注意,一旦连接建立,后续该连接的读写事件都与Reactor主线程无关了。
- Handler 还是只负责对通道读写,具体业务逻辑线程池的Worker线程接管
- Reactor主线程可以对应多个 Reactor 子线程,即 MainRecator 可以关联多个 SubReactor
小结一下,你会发现监听通道事件这件事上可以有多个线程了,业务处理甚至都抬出了线程池!这样便颠覆了之前一个线程处理一切的模式。
其优点:
- Reactor父线程与子线程的数据即交互简单,职责明确,父线程只需要接收新连接,子线程维护这些连接并控制后续事宜
- 这种模型在许多项目中广泛被应用,经典如Nginx
Netty_IO模型
线程我们终于可以说说netty的模型了。
netyy模型主要是根据Reactor主从模型做了一些改造和具体实现。
Netty模型大致示意图
这个模型非常重要,我在后面做的Netty文章和源码分析都是基于它,并且它足够复杂,这里也只是做一点简单的说明:
Netty 抽象出两组线程池, BossGroup专门负责接收客户端的连接,类似Reactor主从模型中的Reactor主线程与Acceptor;WorkerGroup 专门负责网络的读写,类似Reactor主从模型中Reactor从线程中的SubReactor与Handler;Netty将这两部分全改造成了线程池。并且,你可以随意定制这两个线程池的数量!
一般情况都是1个 BossGroup 加 n个WorkerGroup
BossGroup 和 WorkerGroup 在netty中类型都是 NioEventLoopGroup。
NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
这里的事件循环表示一个不断循环的执行任务的线程, 每个 NioEventLoop 都有一个 selector , 用于监听绑定在其上的 socket 的通道发生了什么事件
每个 BossNioEventLoop 循环执行的步骤有 3 步
- selector轮询 accept 事件,即轮询是否有连接事件发生。
- 处理 accept 事件 , 与 client 建立连接 , 生成 NioScocketChannel(netty对JavaNewIO中ServerSocketChannel的封装, 并将其注册到某个 WorkerNIOEventLoop上的selector,后续该连接的事情它就不管了。
- 处理任务队列的任务,即 runAllTasks ,这里的任务队列指的是暂时无法处理的连接,将之放到队列里,等空闲了再去处理。
WorkerNIOEventLoop 循环执行的步骤与BossNioEventLoop类似了
- 轮询绑定其上的通道是否有 read, write 事件
- 处理 i/o 事件, 即 read , write 事件,pipeLine中handler处理业务逻辑
- 处理任务队列的任务 , 即 runAllTasks
每个WorkerNIOEventLoop 处理业务时,会使用pipeline(管道),pipeline 中包含了 channel , 即通过pipeline,可以获取到对应通道,管道中维护了很多的处理器(即对某个事件的处理逻辑)…
其中原委,且听下回分解吧,到目前为止,我们对netty应该就有个大概的认识了,后面开始详细阐述了!