从NIO到Netty的线程模型演变

从NIO到Netty的线程模型演变

Netty基于NIO,有关NIO的内容,主要在下面这篇博客中做了阐述

学Netty前必须掌握的-JavaIO-BIO、NIO编程

原生NIO与Netty

原生NIO存在以下问题:

  1. NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
  2. 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
  3. 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
  4. 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 进行了封装,解决了上述问题。

  1. 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池
  2. 使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。
  3. 高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
  4. 安全:完整的 SSL/TLS 和 StartTLS 支持。
  5. 社区活跃、不断更新:版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入

注意:netty5版本出现重大BUG,已经被官网废弃,目前主流使用netty4,需要JDK6以上版本

Reactor_IO模型

线程模型基本介绍

注意:这里的线程模型与上一章的相比是更为抽象的表示,上一章的模型是Java基于这里所述线程模型的实现。

目前存在的线程模型有:传统阻塞型IO模型和Reactor模型

Netty 主要基于主从 Reactor 多线程模型做了一定的改进,在抛出Netty线程模型之前,我们首先搞清楚Reactor模型和传统阻塞IO模型如何工作

根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现

  1. 单 Reactor 单线程
  2. 单 Reactor 多线程
  3. 主从 Reactor 多线程

传统IO阻塞型:

从NIO到Netty的线程模型演变

橙色的框表示对象,蓝色的框表示线程,白色的框表示方法(API)

模型特点:

  1. 采用阻塞IO模式获取输入的数据
  2. 每个来自客户端的连接都要分配一个独立的线程完成数据读取、业务处理、数据发送

缺点:

  1. 当并发数很大,就会创建大量线程,占用大系统资源。
  2. 同时,如果连接上之后没有进行IO操作,该线程会被阻塞在read操作,造成服务资源浪费

Reactor模式

Reactor模式对传统模式进行了如下改进

  1. 采取了IO多路复用器,即多个连接共用一个Reactor,应用程序只需要一个Reactor(分发器)进行等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,处理线程从阻塞状态返回,开始进行业务处理
  2. 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理, 一个线程可以处理多个连接的业务

示意图

从NIO到Netty的线程模型演变

说明:

  1. Reactor 模式,通过一个或多个输入同时传递给服务处理器的模式(基于事件Event驱动
  2. 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程, 因此 Reactor 模式也叫 Dispatcher 分发器模式
  3. Reactor 模式使用 IO 复用监听事件,收到事件后,分发给某个线程(进程),这点是网络服务器高并发处理关键

核心组件:

  • Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。
  • EventHandlers:负责处理分发器分发过来的事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。

上面说过,Reactor有 3 种典型的实现,这里将逐个介绍

先来看看单 Reactor 单线程模式

单Reactor 单线程模式

从NIO到Netty的线程模型演变

说明:

  1. Select 是上一篇说NIO说I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求

  2. Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发

  3. 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接请求完成后的后续业务处理

  4. 如果不是建立连接事件,则 Reactor 会分发给对应的 Handler 来响应

  5. Handler 会完成 Read→业务处理→Send 的完整业务流程

缺点:本质上还是一个线程处理一个请求,如果客户端连接数量较多,该模型也会面临并发压力。

存在缺陷:

单Reactor单线程模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成,但是存在性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。此外,Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈,并且存在可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障

适合使用场景:客户端的数量有限,业务处理非常快速,比如 Redis 在业务处理的时间复杂度 O(1) 的情况

单Reactor多线程模型

从NIO到Netty的线程模型演变

说明:

与“单Reactor单线程”相比

将Handler也业务处理进行了解耦,Handler只负责响应事件,只保留基本的读写功能,通过read读取数据后,分发给Woker线程池,由线程池中的某个线程进行业务处理,线程处理完毕后将结果返回给对应Handler

handler 收到响应后,通过 send 将结果返回给相应client

优缺点:

优点:线程池机制可以充分发挥多核CPU的处理能力

缺点:Reactor还是单线程,连接量巨大的的情况下还是存在并发压力

主从Reactor多线程模型

注意这个模型下已经和netty的线程模型很接近了…

从NIO到Netty的线程模型演变

  • 这个模型将Handler与Reactor进行了解耦

  • Reactor 主线程 MainReactor 对象通过 select 监听连接事件, 收到事件后,通过 Acceptor 处理连接事件

  • 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor

  • SubReactor 将连接加入到连接队列,利用Selector进行监听,并创建 handler 进行各种事件处理

  • 当连接有新事件发生时, SubReactor 就会调用对应的Handler处理

  • Handler 通过 read 读取数据,分发给线程池的Worker线程处理

  • Worker 线程池分配独立的 worker 线程进行业务处理,并返回结果

  • Handler收到响应的结果后,再通过 send 将结果返回给 client

  • Reactor主线程可以对应多个 Reactor 子线程,即 MainRecator 可以关联多个 SubReactor

优缺点分析:

优点:

  • Reactor父线程与子线程的数据即交互简单,职责明确,父线程只需要接收新连接,子线程维护这些连接并控制后续事宜
  • 这种模型在许多项目中广泛被应用,经典如Nginx主从Reactor多进程模型

Netty_IO模型

从NIO到Netty的线程模型演变

说明:

  • Netty 抽象出两组线程池, BossGroup专门负责接收客户端的连接,类似Reactor主从模型中的Reactor主线程与Acceptor;WorkerGroup 专门负责网络的读写,类似Reactor主从模型中Reactor从线程中的SubReactor与Handler;Netty将这两部分全改造成了线程池。
  • BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
  • NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
  • NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个 NioEventLoop 都有一个 selector , 用于监听绑定在其上的 socket 的网络通讯
  • NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop
  • 每个 BossNioEventLoop 循环执行的步骤有 3 步
    1. 轮询 accept 事件
    2. 处理 accept 事件 , 与 client 建立连接 , 生成 NioScocketChannel , 并将其注册到某个 WorkerNIOEventLoop 上 的 selector
    3. 处理任务队列的任务,即 runAllTasks
  • 每个 WorkerNIOEventLoop 循环执行的步骤
    1. 轮询 read, write 事件
    2. 处理 i/o 事件, 即 read , write 事件,在对应 NioScocketChannel 处理,注意此处并没有把业务处理单独解耦出来。
    3. 处理任务队列的任务 , 即 runAllTasks
  • 每个WorkerNIOEventLoop 处理业务时,会使用pipeline(管道),pipeline 中包含了 channel , 即通过pipeline,可以获取到对应通道,管道中维护了很多的处理器