写点什么

Netty 高并发处理架构设计介绍

  • 2022 年 9 月 05 日
    江苏
  • 本文字数:6863 字

    阅读完需:约 23 分钟

Netty高并发处理架构设计介绍

前言

Netty 是大名鼎鼎的通信框架,几乎很多耳熟能详的开发框架和中间件,其底层通信都是采用了 Netty,比如:Dubbo、RocketMQ、Hadoop。就连早期版本使用 Java NIO 作为底层通信框架的 Zookeeper 现在也改为采用 Netty 了,由此可见 Netty 的魅力。

Java 已经有了一个原生的 NIO 框架,为什么还会出现 Netty 呢,原因主要有以下几点:

  • Java 的 NIO 还不够高效,其底层使用 selector,而 Netty 使用 Linux 下最高效的 I/O 模式 epoll。

  • Selector 多路复用的开发模式较为复杂,需要在程序中自己轮询,而且 SelectionKey 需要自己进行删除的管理,比较容易出错,而且由很多阻塞操作(select),Java 自带的 AIO 更加难用。Netty 是全异步操作,并且将底层 IO 操作全部封装,简化开发。

  • Java 的 NIO 内存管理采用 ByteBuffer,ByteBuffer 是出了名的难用,在使用的时候要是忘记 flip()很容易出错。Netty 提供的 ByteBuf 就好用了很多,其采用读写,双 Index,更加易用。


1. Netty 的线程模型

1.1 Netty 的线程模型

Netty 的线程模型主要基于主从 Reactors 多线程模型,但是做了一定的修改,其中主从 Reactor 多线程模型有主从两个 Reactor:

  • mainReactor 负责客户端的连接请求,并将请求转交给 SubReactor;

  • subReactor 负责相应通道的 IO 读写请求;

  • 非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。

这里引用 Doug Lee 大神的 Reactor 介绍——Scalable IO in Java 里面关于主从 Reactor 多线程模型的图:

需要注意的是:虽然 Netty 的线程模型基于主从 Reactor 多线程,借用了 MainReactor 和 SubReactor 的结构。但是实际实现上 SubReactor 和 Worker 线程在同一个线程池中。

1.2 Netty 线程实例代码分析

EventLoopGroup bossGroup = new NioEventLoopGroup(10);EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();b.group(bossGroup,workerGroup);b.channel(NioServerSocketChannel.class);
复制代码

上面代码中的 bossGroup 和 workerGroup Bootstrap 构造方法中传入的两个对象,这两个 group 均是 EventLoopGroup,也就是事件循环组,这里暂且把它认为是线程池

  • NioEventLoopGroup:NIO 的事件循环组,如果不指定初始化线程数量,将默认初始化 CPU 内核数 * 2 个线程

private static final int DEFAULT_EVENT_LOOP_THREADS;static {    DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(            "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
if (logger.isDebugEnabled()) { logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS); }}/** * @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, Object...) */protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) { super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);}
复制代码
  • bossGroup: 事件循环组只是在 Bind 某个端口后,获得其中一个线程作为 mainReactor,专门处理端口的 Accept 事件,每个端口对应一个 Boss 线程

  • workerGroup: 事件循环组负责处理真正的逻辑会被各个 subReactor 和 worker 线程充分利用

1.3 Netty Reactor 工作框架

Server 端包含 1 个 Boss NioEventLoopGroup 和 1 个 Worker NioEventLoopGroup

NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环处理器 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。

每个 Boss NioEventLoop 循环执行的任务包含 3 步:

  1. 轮询 Accept 事件。

  2. 处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上

  3. 处理任务队列中的任务 runAllTasks。任务队列中的任务包括用户调用 eventloop.execute schedule 执行的任务,或者其他线程提交到该 eventloop 的任务

每个 Worker NioEventLoop 循环执行的任务包含 3 步:

1) 轮询 Read、Write 事件。

2) 处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理。3) 处理任务队列中的任务 runAllTasks

1.4 Netty 的异步处理模式

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

Netty 中的 I/O 操作都是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture

调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。

当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作常见有如下操作:

  • isDone 方法:判断当前操作是否完成。

  • isSuccess 方法:判断已完成的当前操作是否成功。

  • getCause 方法:获取已完成的当前操作失败的原因。

  • isCancelled 方法:判断已完成的当前操作是否被取消。

  • addListener 方法:注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器,如果 Future 对象已完成,则立即通知指定的监听器。

下面的代码中绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑:

serverBootstrap.bind(port).addListener(future -> {       if(future.isSuccess()) {           System.out.println(newDate() + ": 端口["+ port + "]绑定成功!");       } else{           System.err.println("端口["+ port + "]绑定失败!");       }   });
复制代码

我们也可用 sync 方法把异步操作变为同步操作,具体代码如下:

   try {         ChannelFuture f = b.bind(12345).sync();         if(f.isSuccess()){            System.out.println("服务器启动成功");         }         f.channel().closeFuture().sync();     } catch (InterruptedException e) {         e.printStackTrace();     }
复制代码

2. Netty 的模块组件

2.1 Bootstrap、ServerBootstrap

Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类ServerBootstrap 类是服务端启动引导类


2.2 Future、ChannelFuture

正如前面介绍,在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。

但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

2.3 Channel

Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 是被注册到 EventLoop 上的。Channel 为用户提供:

  • 当前网络连接的通道的状态(例如是否打开?是否已连接?)

  • 网络连接的配置参数 (例如接收缓冲区大小)

  • 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。

  • 调用立即返回一个 ChannelFuture 实例,通过注册监听器(addListener)到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。

  • 支持关联 I/O 操作与对应的处理程序。

不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,下面是一些常用的 Channel 类型:

  • NioSocketChannel,异步的客户端 TCP Socket 连接。

  • NioServerSocketChannel,异步的服务器端 TCP Socket 连接,统一使用水平触发

  • EpollServerSocketChannel,异步的服务器端 TCP Socket 连接,统一使用边缘触发以获得最大性能,这个只能在 Linux 机器上使用

/** * {@link ServerSocketChannel} implementation that uses linux EPOLL Edge-Triggered Mode for * maximal performance. */
复制代码
  • NioDatagramChannel,异步的 UDP 连接。

  • NioSctpChannel,异步的客户端 Sctp 连接。

  • NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。

2.4 NioEventLoop 事件执行器

NioEventLoop 就是异步 IO 处理网络连接的生命周期中发生的各种事件,其中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:

  • I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。

  • 非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。

两种任务的执行时间比由变量 ioRatio 控制,默认为 50,则表示允许非 IO 任务执行的时间与 IO 任务的执行时间相等。

2.5 NioEventLoopGroup 事件循环组

NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个事件执行器的组(线程池),内部维护了一组循环线程,每个线程负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。每个线程执行都调用 next 函数把任务交给下一个线程执行,这样就可以获得 Future,保证了 Netty 的全异步执行。其实 next 函数就是使用一个 RoundRobin 算法,每次从 EventLoopGroup 里面轮询下一个 EventLoop 线程来异步处理提交的任务。

2.6 Channel、EventLoop(Group)和 ChannelFuture

  • Channel:代表一个 Socket 链接。

  • ChannelFuture:异步通知,ServerBootstrap bind 端口返回 ChannelFuture。

  • EventLoop:循环(RoundRobin 算法)处理网络连接的生命周期中发生的各种事件。

注意:EventLoop 就是单线程的事件循环执行器,EventLoop 组合成 EventLoopGroup,Channel 被创建后就注册在了一个 EventLoop 上,Channel 在整个生命周期内使用 EventLoop 处理 IO 事件。


关系说明:

  • 一个 EventLoopGroup 包含一个或者多个 EventLoop

  • 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定

  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理

  • 一个 Channel 在它的生命周期内只注册于一个 EventLoop

  • 一个 EventLoop 可能会被分配给一个或多个 Channel

2.7 ChannelHandler、ChannelPipline 和 ChannelHandlerContxt

  • ChannelHandler:应用程序开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的地方。Netty 以适配器类的形式(这里的适配器并不是设计模式中的适配器模式,更像是模版方法)提供了大量默认的 ChannelHandler 实现,帮我们简化应用程序处理逻辑的开发过程

ChannelHandler 的适配器(Adapter)


  • ChannelPipeline:提供了 ChannelHandler 链的容器(责任链模式),并定义了用于在该链上传播入站和出站事件流的 API。当 ChannelHandler 被添加到 ChannelPipeline 时,它将会被分配一个 ChannelHandlerContext,其代表了 ChannelHandler 和 ChannelPipeline 之间的绑定上下文数据流。


注意:Netty 会把出站 Handler 和入站 Handler 放到一个 Pipeline 中,物理视图上看是一个,从逻辑视图上看是两个。那么站在逻辑视图的角度,分属出站和入站不同的 Handler ,是无所谓顺序的。而同属一个方向的 Handler 则是有顺序的,因为上一个 Handler 处理的结果往往是下一个 Handler 的要求的输入。将图 中的处理器(ChannelHandler)从左到右进行编号,那么入站事件按顺序看到的 ChannelHandler 将是 1,2,4,而出站事件按顺序看到的 ChannelHandler 将是 5,3。

  • ChannelHandlerContext:创建 ChannelHandler 并绑定到 ChannelPipline 的时候 Netty 自动为该 ChannelHandler 生成相应 ChannelHandlerContext,他代表了 ChannelHandler 和 ChannelPipeline 之间绑定的上下文数据流,在 ChannelHandlerContext 中可以拿到相应的 Channel 和 ChannelPipeline。


注意:ChannelHandlerContext,Channel,Pipeline 都有 flush 方法,区别:

  1. Channel,pipeline 调用时将遍历所有 ChannelHandler 然后出站。

  2. ChannelHandlerContext 调用 flush 时将只调用他后面的 ChannelHandler,所以一般都调用 ChannelHandlerContext 的 WriteAndFlush 函数。

3. Netty 整体框架的总结

3.1 Netty 的特点

  • Netty 是一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持。

  • Netty 使用更高效的 socket 底层通信方式 epoll,对 JAVA 原生 NIO 空轮询引起的 CPU 占用飙升在内部进行了处理,避免了直接使用 NIO 的陷阱,简化了 NIO 的处理方式。

  • 采用多种 decoder/encoder 支持,对 TCP 粘包/半包问题进行自动化处理。

  • 可使用接受(bossGroup)/工作(workGroup)线程池,提高连接效率,对重连、心跳检测的简单支持。

  • 可配置 IO 线程数、TCP 参数, TCP 接收和发送缓冲区可以使用直接内存代替堆内存,实现零拷贝,通过内存池的方式循环利用 ByteBuf。 通过引用计数器及时申请释放不再引用的对象,降低了 GC 频率。

  • 使用单线程串行化的方式,高效的 Reactor 线程模型

  • 大量使用了 volitale、使用了 CAS 原子类、线程安全类的使用、读写锁的使用。

3.2 Netty 的优势有哪些

  • 使用简单:封装了 NIO 的很多细节,使用更简单。

  • 功能强大:预置了多种编解码功能,支持多种主流协议。

  • 定制能力强:可以通过 ChannelHandler 对通信框架进行灵活地扩展。

  • 性能高:通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优。

  • 稳定:Netty 修复了已经发现的所有 NIO 的 bug,让开发人员可以专注于业务本身。

  • 社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。

3.3 Netty 高性能表现在哪些方面

  • IO 线程模型:同步非阻塞,用最少的资源做更多的事。

  • 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。

  • 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。

  • 串形化处理读写:避免使用锁带来的性能开销。即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

  • 高性能序列化协议:支持 protobuf 等高性能序列化协议。

  • 高效并发编程的体现:volatile 的大量、正确使用;CAS 和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。

4. 交易框架对 Netty 框架可能的借鉴和应用

4.1 Netty 的 Reactor 线程模型对实时交易系统的借鉴意义

Netty 是一个对 JAVA NIO 通信进行了深度封装的通信框架,其实就我使用 JAVA NIO 和 Netty 两个框架的经验来看,JAVA NIO 的使用是更为简单的。大家可以在网上搜索一些示例代码来看看,其实 JAVA NIO 使用起来很简洁,只要使用好三大组件 Selector、SelectionKey、SocketChannel,就可以玩转多路复用的高性能通信代码。

既然 JAVA NIO 的使用很简洁,那为什么又会出现 Netty,这是因为大家在使用 JAVA NIO 的时候发现了很多问题:

  • 首先 JAVA NIO 只是个毛坯架子,不处理通信中的粘包/拆包问题,如果看过 Kafka 源码的同学可能会有体会,Kafka 的通信框架就是 JAVA NIO 写的,里面有很多复杂代码都是在处理粘包/拆包

  • 其次 JAVA NIO 只是对多路复用 selector 的一个封装,没有任何的线程,以及队列模型。

  • 最后 JAVA NIO 没有提供各种协议的转换以及编解码功能,这些都需要自己做处理。

就是因为这些原因,Netty 才会获得大家的青睐,首先他采用了一个可以处理高并发的高性能线程模型,Reactor 线程模型,其次对于通信底层的粘包/拆包他也做了底层封装,让开发者完全不需要关心,只要定义好报文的格式协议即可,最后 Netty 开发框架中用责任链模式,让开发者可以十分灵活的扩展与编排自己的处理模块。

我们作为交易系统的开发者,其实在研究过 Netty 高并发框架之后,很难不去把它引申到我们的交易系统。

那么 Netty 高并发框架中有哪些地方是我们开发一个高并发实时交易系统可以借鉴的呢,我认为有以下几个方面:

  • Reactor 线程模型可以用较少资源处理高并发请求,那么交易系统也可以参考 Reactor 模型,用一个线程监听和接收消息请求,然后把消息的实际处理丢给一个高性能队列。

  • Netty 使用责任链模式,灵活装配对消息的具体处理。在交易系统中我们也有很多对消息的处理,如果把各种处理进行分解和抽象成一个个处理的组件,然后再通过一定的灵活配置,就可以在不同场景下完成对消息的不同处理流程。

高并发的 Reactor 线程模型可以让交易系统提升实时处理消息的性能,把 Netty 的责任链模式加以抽象为一个流程编排引擎,就可以沉淀和共用交易系统的各种处理功能组件,然后针对这种交易场景,灵活组合各个功能组件,达到快速开发,灵活配置,降低代码量的目的。


总结

本文对于 Netty 这个高并发的通信框架进行了详细的介绍,通过对 Netty 框架的整体把握之后,还将其中的设计思想引申到了我们的交易系统,所以我们在设计交易系统的时候也参考了 Netty 框架的一些设计思想,下面两篇文章分别介绍了大象交易系统的核心架构:

  • 《大象交易系统专题系列:基于 State Machine 的流程编排引擎在大象交易系统中的应用》

  • 《大象交易系统专题系列:流式数据事件驱动框架在实时连续交易系统中的应用》大家看了 Netty 框架的介绍之后,再看大象交易系统架构的介绍文章,我相信会有更多的灵感和收获。


发布于: 刚刚阅读数: 4
用户头像

大象无形,大道至简 2020.12.01 加入

中国好码农~~

评论

发布
暂无评论
Netty高并发处理架构设计介绍_架构_孙大卫(华泰)_InfoQ写作社区