netty线程模型

作者: 孤独的死锁 | 来源:发表于2021-09-18 15:24 被阅读0次

    1. 背景

    1.1 传统线程模型

    0

    特点:

    1. 基于阻塞式 I/O 模型;
    2. 每个连接都需要独立的线程完成数据输入,业务处理,数据返回的完整操作。

    存在问题:

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

    1.2 Reactor模型

    针对传统阻塞 I/O 服务模型的缺点,我们一般基于 I/O 复用模型来进行改进:所有连接的事件都由IO多路复用器(使用select/poll/epoll操作系统函数实现)管理,线程只需要在复用器上等待,不需要阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,等待在复用器上的线程就会从阻塞状态返回,开始进行业务处理。通过这种方式,可以实现一个线程处理多个连接。

    Reactor模型就是基于多路复用IO的。

    维基百科上对reactor的描述是,“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.”。从这个描述中,我们知道Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers,这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。所以reactor模式也叫dispatcher模式。如下图所示:

    0

    从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理,而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会主动的根据不同的Event类型将其分发给对应的EventHandler来处理。

    Reactor 模式中有 2 个关键组成:

    1. Reactor:即上图中的ServiceHandler,处于单独的线程中,封装selector(多路复用器),负责监听和分发事件,分发给适当的处理器来对 IO 事件做出反应。
    2. Handlers:即上图中的eventHandler,handler执行 I/O 事件对应的要完成的实际操作。

    Reactor 模型有 3 种典型的实现,分别为:

    1)Reactor 单线程;2)Reactor 多线程;3)主从 Reactor 多线程。

    1.2.1 单线程模型

    0

    reactor、accpet/read/write及业务处理共用一个线程。

    方案说明:

    1. Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发;
    2. 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,获取到连接对象(socket),然后创建一个 Handler 对象与连接进行绑定;
    3. 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应;
    4. Handler 会完成 Read→业务处理→Send 的完整业务流程。

    优点:模型简单,没有多线程、线程通信、竞争的问题,全部都在一个线程中完成。

    缺点:性能问题,只有一个线程,无法发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。

    使用场景:客户端的数量有限,业务处理非常快速。比如 Redis,纯内存操作,数据结构时间复杂度低。

    1.2.2 多线程模型

    0

    抽出单独的线程池进行业务的处理。

    方案说明:

    1. Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发;
    2. 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后续的各种事件;
    3. 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应;
    4. Handler 只负责响应事件,不做具体业务处理,通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理;
    5. Worker 线程池会分配独立的线程完成真正的业务处理,然后将响应结果发给 Handler 进行处理;
    6. Handler 收到响应结果后通过 Send 将响应结果返回给 Client。

    优点:可以充分利用多核 CPU 的处理能力。业务操作不会影响IO事件的响应。

    缺点:Reactor 承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈。

    1.2.2 主从多线程模型

    0

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

    方案说明:

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

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

    父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。

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

    2. Netty线程模型

    其实Netty的默认线程模型是Reactor模型的变种,就是去掉工作线程池的第三种形式(主从Reactor模型)的变种。

    0

    Netty中Reactor模式的参与者主要有下面2大组件:

    NioEventLoopGroup/NioEventLoop

    ChannelPipeline

    2.1 NioEventLoopGroup / NioEventLoop

    0

    EventLoop 本质是一个单线程执行器(同时维护了一个 Selector)。

    它的继承关系比较复杂,重要的是:

    • 一条线是继承自 j.u.c.ScheduledExecutorService 因此包含了线程池中所有的方法
    • 另一条线是继承自 netty 自己的 EventExecutor,
    1. 提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
    2. 提供了 parent 方法来看看自己属于哪个 EventLoopGroup
    0

    EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定到其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理,每个NioEventLoop负责可以处理多个Channel上的事件,而一个Channel只对应于一个EventLoop。这是一种串行化的设计理念,每条连接从消息的读取、编码以及后续Handler的执行,默认情况下始终都由IO线程EventLoop负责,这就意味着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险。

    • 继承自 netty 自己的 EventExecutorGroup
    1. 实现了 Iterable 接口提供遍历 EventLoop 的能力
    2. 另有 next 方法获取集合中下一个 EventLoop

    NioEventLoop 肩负着两种任务, 第一个是作为 IO 线程, 执行与 Channel 相关的 IO 操作,包括 调用 select 等待就绪的 IO 事件、读写数据与数据的处理等,由processSelectedKeys方法触发。而第二个任务是作为任务执行者, 执行TaskQueue 中的任务, 例如用户调用 eventLoop.submit或eventLoop.schedule 提交的普通或定时任务也是这个线程执行的。两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。

    0

    Netty抽象了两组线程池BossGroup和WorkerGroup,其类型都是NioEventLoopGroup,BossGroup为MainReactor线程(只需设置一个线程),WorkerGroup为SubReactor线程(线程的个数默认为 2 * CPU Core)。

    Boss NioEventLoop线程的执行步骤:

    1. 处理accept事件与client建立连接, 生成NioSocketChannel。
    2. 将NioSocketChannel注册到某个worker NIOEventLoop上的selector
    3. 处理任务队列的任务 即runAllTasks。

    Worker NioEventLoop线程的执行步骤:

    1. 轮询注册到自己Selector上的所有NioSocketChannel的read和write事件。
    2. 处理read和write事件在对应NioSocketChannel处理业务。
    3. runAllTasks处理任务队列TaskQueue的任务,一些耗时的业务处理可以放入TaskQueue中慢慢处理这样不影响数据在pipeline中的流动处理。
    4. 每个worker NIOEventLoop处理NioSocketChannel业务时,会使用 pipeline (管道),管道中维护了很多 handler处理器用来处理 channel 中的数据。

    2.1 ChannelPipeline

    ChannelPipeline是保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。可以方便的新增和删除ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持。

    ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。

    包含2个重要的子类:

    • ChannelInboundHandler 用于处理入站 I/O 事件。
    • ChannelOutboundHandler 用于处理出站 I/O 操作。
    0

    比如白名单校验,数据解码,业务处理等逻辑等需要对输入的数据进行处理的,可以分别封装为入站handler,实现ChannelInboundHandler 接口或者继承ChannelInboundHandlerAdapter。而数据编码等对输出的数据进行处理的,可以分别封装为出站handler,实现ChannelOutboundHandler 接口或者继承ChannelOutboundHandlerAdapter。

    入站事件由自下而上方向的入站处理程序处理,如下图左侧所示。 入站Handler处理程序通常处理由图底部的I / O线程生成的入站数据。 通常通过实际输入操作(例SocketChannel.read(ByteBuffer))从远程读取入站数据。出站事件由上下方向处理,如下图右侧所示。 出站Handler处理程序通常会生成或转换出站传输,例如write请求。 I/O线程通常执行实际的输出操作,例如SocketChannel.write(ByteBuffer)。

    0

    在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应, 它们的组成关系如下:

    0

    一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。

    默认情况下ChannelHandler 是在 IO 线程中执行,在将事件处理器添加到事件链时可以指定在哪个线程池中执行。

    0

    那么当不同的handler由不同的eventLoop执行的话,是如何在eventLoop中切换的?

    关键代码 io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead()

    static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); // 下一个 handler 的事件循环是否与当前的事件循环是同一个线程 EventExecutor executor = next.executor(); // 是,直接调用 if (executor.inEventLoop()) { next.invokeChannelRead(m); } // 不是,将要执行的代码作为任务提交给下一个事件循环处理(换人) else { executor.execute(new Runnable() { @Override public void run() { next.invokeChannelRead(m); } }); } }

    • 如果两个 handler 绑定的是同一个线程,那么就直接调用
    • 否则,把要调用的代码封装为一个任务对象,由下一个 handler 的线程来调用

    通常呢,我们还会专门开辟业务一个线程池来处理耗时的业务逻辑,那业务处理完成之后,如何将响应结果通过 IO 线程写入到网卡中呢?

    0

    业务线程调用 Channel 对象的 write 方法并不会立即写入网络,只是将数据放入一个待写入队列(缓冲区),然后IO线程每次执行事件选择后,会从待写入缓存区中获取写入任务,将数据真正写入到网络中,数据到达网卡之前会经过一系列的 Channel Handler(Netty事件传播机制),最终写入网卡。

    0 0

    总结

    • Netty的线程模型基于主从多Reactor模型。通常由一个线程负责处理OP_ACCEPT事件,拥有 CPU 核数的两倍的IO线程处理读写事件。

    • 一个通道的IO操作会绑定在一个IO线程中,而一个IO线程可以注册多个通道。

    • 在一个网络通信中通常会包含网络数据读写,编码、解码、业务处理。默认情况下这些操作会在IO线程中运行,但也可以指定其他线程池。

    • 通常业务处理会单独开启业务线程池,但也可以进一步细化,例如心跳包可以直接在IO线程中处理,而需要再转发给业务线程池,避免线程切换。

    • 在一个IO线程中所有通道的事件是串行处理的。

    相关文章

      网友评论

        本文标题:netty线程模型

        本文链接:https://www.haomeiwen.com/subject/xuqygltx.html