Netty I/O模型和线程模型

目录

1.概述

1.1 为什么使用Netty

1.2 Netty的优势

1.3 Netty的常见使用场景

2.Netty高性能的原因

2.1 I/O模型

2.1.1 阻塞IO

2.1.2 IO复用模型

2.2 线程模型

2.2.1 线程模型1:传统阻塞 I/O 服务模型

2.2.2 线程模型2:Reactor 模式

2.2.2.1 单 Reactor 单线程

2.2.2.2 单 Reactor 多线程

2.2.2.3 主从 Reactor 多线程


1.概述

Netty的官网:Netty: 首页

Netty是一个异步 事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端

Netty I/O模型和线程模型_第1张图片

  • Netty是由JBoss提供的一个java开源框架,现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
  • 也就是说,Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。
  • Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。

1.1 为什么使用Netty

  1. 直接使用 NIO 的时候,它的复杂的类库和API让人头大,例如需要我们熟练掌握NIO的几大核心内容:Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
  2. 要编写出高质量的NIO程序需要熟练掌握多线程和网络编程技术,而这些工作量和难度都不小,例如客户端面临断连重连、网络闪断、半包卖写、失败缓存、网络拥塞和异常流的处理等等。
  3. NIO中有一些Bug:例如Epoll Bug,它会导致Selector 空轮询,最终导致CPU 100%。

1.2 Netty的优势

  • 设计:更优雅的设计简化了原生NIO中的复杂编程
  • 易用性:文档丰富的Javadoc、用户指南和示例
  • 性能:更好的吞吐量,更低的延迟;减少资源消耗;最小化不必要的内存拷贝(零拷贝--操作系统层面)
  • 安全:完整的SSL/TLS和StartTLS支持
  • 社区:社区活跃、不断更新

1.3 Netty的常见使用场景

  1. 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的应用有:阿里分布式服务框架 Dubbo 使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。
  2. 游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过 Netty 进行高性能的通信。
  3. 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨节点通信,它的 Netty Service 基于 Netty 框架的二次封装实现。

2.Netty高性能的原因

Netty 作为异步事件驱动的网络框架,高性能之处主要来自于其 I/O 模型和线程处理模型I/O 模型决定如何收发数据,线程模型决定如何处理数据。

2.1 I/O模型

用什么样的通道将数据发送给对方,BIO、NIO 或者 AIO,I/O 模型在很大程度上决定了框架的性能

2.1.1 阻塞IO

传统阻塞型I/O(BIO)

Netty I/O模型和线程模型_第2张图片

  1. 每个请求都需要独立的线程完成数据的读写和业务处理
  2. 当客户端请求并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
  3. 建立连接后,如果当前线程暂时没有数据可读,则线程就会阻塞在读取数据的操作上,造成线程资源浪费

2.1.2 IO复用模型

在 I/O 复用模型中,会用到 Select,这个函数也会使进程阻塞,但是和阻塞 I/O 所不同的是这个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作、多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

Netty的非阻塞I/O的实现关键是基于I/O复用模型,这里用Selector 对象表示:

Netty I/O模型和线程模型_第3张图片

  1. 当线程从一个客户端SocketChannel进行读写数据时,若没有数据可用时,该线程可以执行其他任务。
  2. 线程通常将非阳塞I/O的空闲时间用于在其他通道上执行I/O操作,所以单独的线程可以管理多个输入和输出通道。
  3. 由于读写操作都是非阻塞的,这就可以充分提升I/O线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。
  4. 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

传统的IO是面向字节流或字符流的,以流式的方式顺序地从一个stream中读取一个或多个字节,因此也就不能随意改变读取指针的位置。

在NIO中,抛弃了传统的IO流,而是引入Channel和Buffer的概念。在NIO中,只能从Channel中读取数据到Buffer中或将数据从Buffer中写入到channel。

基于Buffer操作不像传统IO的顺序操作,NIO中可以随意地读取任意位置的数据

2.2 线程模型

上面介绍了服务器如何基于I/O模型管理连接并获取输入数据,接着介绍一下决定服务器如何处理数据的线程模型。

2.2.1 线程模型1:传统阻塞 I/O 服务模型

Netty I/O模型和线程模型_第4张图片

特点:

  • 采用阻塞式I/O模型获取输入数据
  • 每个连接都需要独立的线程完成数据输入,业务处理,数据返回的完整操作

存在问题:

  • 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
  • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在读取数据(read)操作上,造成线程资源浪费

2.2.2 线程模型2:Reactor 模式

针对传统阻塞I/O服务模型的 2 个缺点,比较常见的有如下解决方案:

  • 基于I/O复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
  • 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。

而Reactor模式基本设计思想就是I/O复用结合线程池:

Netty I/O模型和线程模型_第5张图片

Reactor 模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。

服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。即I/O多路复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术之一。

Reactor模式中有2个关键组成

  • Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对I/O事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
  • Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际负责人。Reactor 通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作

根据Reactor的数量和Handler的数量不同,有3种典型的实现

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

可以这样理解,Reactor 就是一个执行 while(true){selector.select();...} 循环的线程,会源源不断的产生新的事件,称作反应堆很贴切。

2.2.2.1 单 Reactor 单线程

Netty I/O模型和线程模型_第6张图片

其中Select 是前面I/O复用模型介绍的标准网络编程API,可以实现应用程序通过一个阻塞对象监听多路连接请求。

说明:

  1. Reactor 对象通过Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发
  2. 如果是建立连接请求事件,则由Acceptor 通过 Accept 处理连接请求,然后创建一个Handler 对象处理连接完成后的后续业务处理
  3. 如果不是建立连接事件,则Reactor 会分发调用连接对应的 Handler 来响应
  4. Handler 会完成Read一业务处理一Send的完整业务流程
  • 优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成。
  • 缺点:性能问题,只有一个线程,无法完全发挥多核CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。当其中某个handler阻塞时,会导致其他所有的 client的 handler 都得不到执行,并目更严重的是,handler 的阻塞也会导致整个服务不能接收新的 client 请求(因为acceptor 也被阻塞了)。因为有这么多的缺陷,因此单线程Reactor 模型用的比较少。
  • 使用场景:客户端的数量有限,业务处理非常快速,比如Redis。
2.2.2.2 单 Reactor 多线程

Netty I/O模型和线程模型_第7张图片

说明:

  1. Reactor 对象通过Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发
  2. 如果是建立连接请求事件,则由Acceptor 通过 Accept 处理连接请求,然后创建一个Handler 对象处理连接完成后续的各种事件
  3. 如果不是建立连接事件,则Reactor 会分发调用连接对应的Handler 来响应
  4. Handler 只负责响应事件,不做具体业务处理,通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理
  5. Worker 线程池会分配独立的线程完成真正的业务处理,然后将响应结果发给 Handler 进行处理
  6. Handler 收到响应结果后通过Send 将响应结果返回给 Client
  • 优点:可以充分利用多核CPU的处理能力。
  • 缺点:
    • ①多线程数据共享和访问比较复杂。
    • ②Reactor 承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈。
2.2.2.3 主从 Reactor 多线程

Netty I/O模型和线程模型_第8张图片

针对单 Reactor 多线程模型中,Reactor 在单线程中运行,高并发场景下容易成为性能瓶颈,可以让Reator在多线程中运行。

方案说明:

  1. Reactor 主线程 MainReactor 对象通过 Select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件
  2. Acceptor 处理建立连接事件后,MainReactor 将连接分配给 SubReactor (Reactor 子线程) 进行处理
  3. SubReactor将连接加入连接队列进行监听,并创建一个Handler 用于处理各种连接事件
  4. 当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应
  5. Handler通过Read读取数据后,会分发给后面的 Worker 线程池进行业务处理
  6. Worker 线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给 Handler 进行处理
  7. Handler 收到响应结果后通过 Send 将响应结果返回给 Client

优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。

这种模型在许多项目中广泛使用,包括 Nginx主从Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。

你可能感兴趣的:(Netty,Netty)