当前位置: 首页 > 科技观察

深入Netty逻辑架构,从Reactor线程模型入手

时间:2023-03-22 16:13:50 科技观察

本文是Netty系列文章的第六篇。我们通过一个Nettydemo了解了使用Netty构建Server服务器应用程序的基本方法。并从这个Demo出发,简单描述了Netty的逻辑架构。今天主要深入学习逻辑架构中的EventLoop和EventLoopGroup,掌握Netty的线程模型,这是Netty最精要的知识点之一。本文预计阅读时间约为“15分钟”,将重点关注以下问题:1.什么是Reactor线程模型?2、EventLoopGroup和EventLoop是如何实现Reactor线程模型的?3.深入Netty的线程模型优化Netty3和Netty4线程模型的变化什么是Netty4线程模型的无锁序列化4.从线程模型看最佳实践先简单回顾一下上一篇文章的逻辑架构图查看EventLoop和EventLoopGroup在哪里。1.什么是Reactor线程模型?首先回顾一下我们在Netty系列第2章介绍的I/O线程模型,包括BIO、NIO、I/O多路复用、信号驱动IO、AIO。IO多路复用在Java中有一个专门的NIO包,封装了相关的方法。之前的文章也说过,使用Netty而不是直接使用JavaNIO包是因为Netty帮我们封装了很多NIO包的使用细节,做了很多优化。最著名的之一是Netty的“Reactor线程模型”。前提知识不清楚的话,可以回头看看之前的几篇文章:《没搞清楚网络I/O模型?那怎么入门Netty》《从网络I/O模型到Netty,先深入了解下I/O多路复用》《从I/O多路复用到Netty,还要跨过Java NIO包》Reactor模式是一种“事件驱动”的模式。“Reactor线程模型”是使用单线程使用JavaNIO包中的Selector的select()方法进行监控。当获取到一个事件(如accept、read等)时,将分配(dispatch)该事件进行相应的事件处理(handle)。如果要对Reactor线程模型给出一个更清晰的定义,应该是:Reactor线程模式=Reactor(I/O多路复用)+线程池其中Reactor负责监听和分发事件,线程池负责处理事件。那么根据Reactor的个数和线程池的个数,Reactor可以分为三种模型:SingleReactor单线程模型(线程池大小固定为1)SingleReactor多线程模型Multi-Reactor多线程threadedmodel(一般是master-slave2Reactors)1.1SingleReactor单线程模型Reactor内部通过selector监听连接事件,收到事件后通过dispatch进行分发。如果是连接建立事件,则通过accept接受连接,并创建一个Handler来处理连接后续的各种事件。如果是读或者写事件,直接调用连接对应的Handler来处理。Handler完成read=>(decode=>compute=>encode)=>send的整个过程。在这个过程中,无论是事件监听,事件分发,还是事件处理,始终只有一个线程在做所有事情。缺点:当请求过多时,不支持。因为只有一个线程,无法发挥多核CPU的性能。而一旦一个Handler被阻塞,服务器就根本无法处理其他的连接事件。1.2SingleReactor多线程模型为了提高性能,我们可以将复杂的事件处理handler交给线程池,进而演变成“单Reactor多线程模型”。这种模式与第一种模式的主要区别在于,业务处理脱离了之前的单线程,取而代之的是线程池处理。1)Reactor线程通过select监听客户端请求。如果是连接建立事件,则通过accept接受连接,并创建一个Handler来处理连接后续的读写事件。这里的Handler只负责响应事件,读写事件,会将具体的业务处理交给Worker线程池处理。只处理连接事件,读写事件。2)Worker线程池处理所有业务事件,包括(decode=>compute=>encode)过程。充分利用多核机器的资源,提升性能。缺点:在极少数特殊场景下,一个Reactor线程负责监听和处理所有的客户端连接,可能会有性能问题。例如百万级并发客户端连接(双十一、春运购票)1.3Multi-Reactor多线程模型为了充分利用多核能力,可以搭建两个Reactor,演化为“主控”-slaveReactor线程模型”。1)主Reactor主Reactor单独监听serversocket,接受新的连接,然后将建立的SocketChannel注册到指定的slaveReactor,2)slaveReactor将连接添加到连接队列中进行监听,并创建handler用于事件处理。执行事件的读写分发,将业务处理丢给工作线程池完成。3)Worker线程池处理所有业务事件,充分利用多核机器的资源,提高性能。轻松应对百万级并发。缺点:实现比较复杂。但是有了Netty,一切都变得更容易了。Netty已经为我们包装好了一切,可以快速使用主从Reactor线程模型(Netty4的实现加入了无锁序列化设计)。具体代码这里就不贴了。大家可以看看上一篇文章中的demo。2、EventLoop和EventLoopGroup是如何实现Reactor线程模型的?上面我们已经了解了Reactor线程模型,其核心是:Reactor线程模式=Reactor(I/O多路复用)+线程池。其运行模式包括四种第一步:连接注册:连接建立后,将通道注册到选择器上事件轮询:在选择器上轮询(select()函数),获取注册通道的所有I/O事件(多路复用)事件分发:将就绪的I/O事件分配给相应的线程进行事件处理:每个工作线程执行事件任务。这样的模型如何在Netty中实现呢?这就需要我们了解EventLoop和EventLoopGroup。2.1什么是EventLoopEventLoop并不是Netty独有的,它是一种事件等待和处理的通用程序模型。主要用于解决多线程资源消耗高的问题。例如,Node.js使用了EventLoop运行机制。那么,在Netty中,什么是EventLoop呢?Reactor模型的事件处理程序。单线程。一个EventLoop内部维护了一个selector和一个“taskQueue任务队列”,分别负责处理“I/O事件”和“任务”。“taskQueue任务队列”是一个多生产者单消费者队列,可以保证多线程并发添加任务时的线程安全。“I/O事件”是指selectionKey中的事件,如accept、connect、read、write等;“任务”包括普通任务、定时任务等。普通任务:通过NioEventLoop的execute()方法向任务队列taskQueue添加任务。比如Netty写数据的时候,会封装WriteAndFlushTask,提交给taskQueue。定时任务:通过调用NioEventLoop的schedule()方法在scheduledTaskQueue中添加一个定时任务,用于定时执行任务(如发送心跳消息等)。定时任务队列中的任务会在执行时间到时,合并到普通任务队列中去真正执行。一图胜千言:EventLoop运行在单线程上,循环执行三个动作:selectoreventpollingI/Oeventprocessingtaskprocessing2.2什么是EventLoopGroupEventLoopGroup比较简单,可以简单理解为一个“EventLoop线程”水池”。Tips:监听一个端口只会绑定BossEventLoopGroup中的一个Eventloop,所以在BossEventLoopGroup中配置多个线程是没有用的,除非同时监听多个端口。2.3具体实现Netty通过简单的配置可以支持单反应器单线程模型、单反应器多线程模型、多反应器多线程模型。下面以“多反应堆多线程模型”为例,看看Netty是如何通过EventLoop来实现的。还是一图胜千言:下面梳理一下Reactor线程模型的四个步骤:1)在连接注册masterEventLoopGroup中有一个EventLoop,绑定到特定的端口进行监听。一旦有新的连接进来触发accept类型的事件,在当前EventLoop的I/O事件处理阶段,会将该连接分配给slaveEventLoopGroup中的一个EventLoop,以监听后续事件。2)事件轮询slaveEventLoopGroup中的EventLoop会通过selector轮询自身绑定的channel,获取注册channel的所有I/O事件(多路复用)。当然,EventLoopGroup中会运行多个EventLoop,每个EventLoop循环处理。EventLoops的具体数量是用户指定的线程数或者默认是核心数的两倍。3)事件分发当从EventLoopGroup中的EventLoop获取到I/O事件后,会在EventLoop的I/O事件处理(processSelectedKeys)阶段分发到对应的ChannelPipeline中进行处理。注意串行处理仍然在当前线程中进行4)事件处理I/O事件在ChannelPipeline中进行处理。I/O事件处理完成后,EventLoop在任务处理(runAllTask??s)阶段消费并处理队列中的任务。至此,我们就可以完全理清EventLoopGroup/EventLoop与Reactor线程模型的关系了。咦,好像有什么不对?是的,细心的朋友可能会发现,slave的EventLoopGroup并不是一个selector+线程池,而是一个multi-selector+多个EventLoop组成的单线程。为什么?那么有必要继续了解Netty4的线程模型的优化。3.Netty的线程模型的深度优化上面说过,对于每一个EventLoop来说,都是一个单线程操作,循环执行三个动作:selectoreventpollingI/Oeventprocessing任务在slaveEventLoopGroup中进行处理,它不是“一个选择器+线程池”的模型,而是多个EventLoop组成的“多个选择器+多个单线程”的模型。为什么是这样?这主要是因为我们在分析Netty4的线程模型,它与Netty3的线程模型类似,与传统的Reactor模型相比,有所不同。3.1Netty3和Netty4线程模型的变化在Netty3的线程模型中,分为读事件处理模型和写事件处理模型。读取事件的ChannelHandler由Netty的I/O线程执行(对应Netty4中的EventLoop)。I/O线程调度执行ChannelPipeline中Handler链的相应方法,直到业务实现的EndHandler。EndHandler将消息封装成Runnable,放入业务线程池执行,I/O线程返回,继续进行读写等I/O操作。写入事件由调用线程处理,调用线程可以是I/O线程或业务线程。如果是业务线程,业务线程会执行ChannelPipeline中的ChannelHandler。执行到系统最后一个ChannelHandler,将编码后的消息推送到发送队列,业务线程返回。Netty的I/O线程从发送消息队列中取出消息,调用SocketChannel的write方法发送消息。从上面可以看出,在Netty3的线程模型中,采用了“选择器+业务线程池”的模型。注意在这个模型下,读写模型是不一致的。特别是,读取事件和写入事件的“执行线程”是不同的。但是在Netty4的线程模型中,采用的是“多选择器+多单线程”的模型。Read事件:I/O线程NioEventLoop从SocketChannel中读取数据,将ByteBuf传递给ChannelPipeline,触发ChannelRead事件;I/O线程NioEventLoop调用ChannelHandler链,直到消息传递给业务线程,然后I/O线程返回并继续后续操作。Write事件:业务线程调用ChannelHandlerContext.write(Objectmsg)方法发送消息。ChannelHandlerInvoker将发送消息封装成一个任务,放入EventLoop的Mpsc任务队列中,业务线程返回。后续由EventLoop统一调度在循环中执行。I/O线程EventLoop在执行任务处理时,从Mpsc任务队列中获取任务,调用ChannelPipeline进行处理,处理Outbound事件,直到消息放入发送队列,然后唤醒Selector进行写操作.在Netty4中,无论读写,都统一通过I/O线程(即EventLoop)进行处理。为什么Netty4的线程模型会做出这样的改变呢?答案是无锁序列化设计。3.2什么是Netty4线程模型的无锁序列化?先来看Netty3线程模型存在的问题:读写线程模型不一致,带来额外的开发心理负担。当业务线程发起写??操作时,通常业务会使用线程池并发执行某个业务流程,所以在某个时刻,多个业务线程会同时操作ChannelHandler。我们需要并发保护ChannelHandler,大大降低了开发效率。.频繁的线程上下文切换会带来额外的性能损失。Netty4线程模型的“无锁序列化”设计很好地解决了这些问题。一图抵千言:事件轮询、消息读取、编码,以及后续的Handler执行,始终由I/O线程NioEventLoop串行操作,也就是说整个过程不会切换线程上下文。避免多线程竞争带来的性能下降,数据不会面临被并发修改的风险。从表面上看,串行设计似乎CPU利用率低,并发不足。但是通过调整slaveEventLoopGroup的线程参数,可以同时启动多个NioEventLoop,串行化的线程并行运行。这种部分无锁的串行线程设计比“一个队列-多个工作线程模型”具有更好的性能。总结Netty4无锁序列化设计的优点:一个EventLoop会处理一个channel整个生命周期的所有事件。I/O线程NioEventLoop始终负责消息的读取、编码和后续的Handler执行。每个EventLoop都会有自己独立的任务队列。整个过程不会切换线程上下文,数据不会面临并发修改的风险。对于用户来说,统一的读写线程模型也减轻了使用的心理负担。4.从线程模型看最佳实践。NioEventLoop的无锁序列化设计这么好,完美吗?不!在某些场景下,Netty3的线程模型可能会有更高的性能。比如编码等写操作是非常耗时的,由多个业务线程并发执行,性能肯定比单个EventLoop线程串行执行要高。因此,单线程执行虽然避免了线程切换,但它的缺陷是无法执行耗时过长的I/O操作。一旦某个I/O事件被阻塞,所有后续的I/O事件都无法执行,甚至造成事件的积压。因此Netty4的线程模型的最佳实践需要注意以下两点:无论读/写,不要在自定义的ChannelHandler中做耗时操作。不要将耗时操作放入任务队列。本文从Reactor线程模型入手,介绍Netty如何用EventLoop实现Reactor线程模型。然后详细介绍了Netty4的线程模型优化,尤其是“无锁序列化设计”。最后从EventLoop线程模型入手,阐述在日常开发中使用Netty4的最佳实践。希望大家能够对EventLoop有一个全面的了解。