深入研究Netty之线程模型详解

https://my.oschina.net/7001/blog/1480153

本文主要介绍Netty线程模型及其实现,介绍Netty线程模型前,首先会介绍下经典的Reactor线程模型,目前大多数网络框架都是基于Reactor模式进行设计和开发,Reactor模式基于事件驱动,非常适合处理海量的I/O事件。下面简单介绍下Reactor模式及其线程模型。

Reactor模式

Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。下面先回顾下Reactor线程模型。

单线程模型

单线程模型下,所有的IO操作都由同一个Reactor线程来完成,其主要职责如下:

  • 作为服务端,接收客户端的TCP连接;
  • 作为客户端,向服务端发起TCP连接;
  • 读取通信对端的请求或者应答消息;
  • 向通信对端发送消息请求或者应答消息。

Reactor单线程模型原理图如下:

如图所示,由于Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会导致阻塞。通常Reactor线程中聚合了多路复用器负责监听网络事件,当有新连接到来时,触发连接事件,Disdatcher负责使用Acceptor接受客户端连接,建立通信链路;当I/O事件就绪后,Disdatcher负责将事件分发到对应的event handler上负责处理。

该模型的缺点很明显,不适用于高负载、高并发的应用场景;由于只有一个Reactor线程,一旦挂彩,整个系统通信模块将不可用。

多线程模型

先看原理图:

该模型的特点:

扫描二维码关注公众号,回复: 4714553 查看本文章
  • 专门由一个Reactor线程-Acceptor线程用于监听服务端,接收客户端连接请求;
  • 网络I/O操作读、写等由Reactor线程池负责处理;
  • 一个Reactor线程可同时处理多条链路,但一条链路只能对应一个Reactor线程,这样可避免并发操作问题。

绝大多数场景下,Reactor多线程模型都可以满足性能需求,但是,在极个别特殊场景中,一个Reactor线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。因此,诞生了第三种线程模型。

主从多线程模型

先看原理图:

该模型的特点:

  • 服务端使用一个独立的主Reactor线程池来处理客户端连接,当服务端收到连接请求时,从主线程池中随机选择一个Reactor线程作为Acceptor线程处理连接;
  • 链路建立成功后,将新创建的SocketChannel注册到sub reactor线程池的某个Reactor线程上,由它处理后续的I/O操作。

Netty线程模型

Netty同时支持Reactor单线程模型 、Reactor多线程模型和Reactor主从多线程模型,用户可根据启动参数配置在这三种模型之间切换。Netty线程模型原理图如下:

服务端启动时,通常会创建两个NioEventLoopGroup实例,对应了两个独立的Reactor线程池。常见服务端启动代码实现如下:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
     .channel(NioServerSocketChannel.class)
     .option(ChannelOption.SO_BACKLOG, 100)
     .handler(new LoggingHandler(LogLevel.INFO))
     .childHandler(new ChannelInitializer<SocketChannel>() {
         @Override
         public void initChannel(SocketChannel ch) throws Exception {
               ......

bossGroup负责处理客户端的连接请求,workerGroup负责处理I/O相关的操作,执行系统Task、定时任务Task等。

用户可根据服务端引导类ServerBootstrap配置参数选择Reactor线程模型,进而最大限度地满足用户的定制化需求;同时,为了最大限度地提升性能,netty很多地方采用了无锁化设计,如为每个Channel绑定唯一的EventLoop,这意味着同一个Channel生命周期内的所有事件都将由同一个Reactor线程来完成,这种串行化处理方式有效地避免了多线程操作之间锁的竞争和上下文切换带来的开销。此外,每个Reactor线程配备了一个task队列和Delay task队列,分别用于存放系统Task和周期性Task,也就是说每个Reactor线程不仅要处理I/O事件,还会处理一些系统任务和调度任务。

EventLoop家族

EventLoop又叫事件循环,旨在通过运行任务来处理在连接的生命周期内发生的事件。Netty的EventLoop是协同设计的一部分,主要采用了两个基本的API:并发和网络编程。首先,io.netty.util.concurrent包构建在JDK的java.util.concurrent包上,用来提供线程执行器。其次,io.netty.channel包中的类,为了与Channel的事件进行交互,扩展了io.netty.util.concurrent包中的接口和类。下面通过类图来说明:

类图

成员简介

首先从io.netty.util.concurrent开始:

EventExecutorGroup:字面含义是事件执行器组,管理着一组EventExecutor,负责通过其next()方法提供EventExecutor的使用,并负责管理这些EventExecutor的生命周期。EventExecutorGroup扩展了JDK的ScheduledExecutorService,使得其子类具有提交执行调度任务的能力;同时还扩展了Iterable,说明EventExecutorGroup是可迭代的。此外,EventExecutorGroup还定义了优雅退出的方法。

AbstractEventExecutorGroup:EventExecutorGroup的抽象实现,并没有提供新的API,只是简单的为EventExecutorGroup中定义的方法提供了默认实现。EventExecutorGroup本身并不能执行任务,它首先通过next()选择一个EventExecutor对象,然后将执行任务的工作都是委托给这个对象。换句话说,具体实现由EventExecutor的子类来完成。

MultithreadEventExecutorGroup:EventExecutorGroup的抽象实现,内部组合了多个EventExecutor用于对外提供服务,并负责管理一组EventExecutor实例。它还提供了抽象方法newChild用于构造EventExecutor实例,由子类提供实现,便于构造定制化的EventExecutor。它还聚合了一个EventExecutorChooser对象,用于定制通过next从数组中选择EventExecutor对象的规则。

DefaultEventExecutorGroup:EventExecutorGroup的默认实现,当需要使用DefaultEventExecutor来执行任务时,可使用该实现,比较少用。

EventExecutor:事件执行器,它是一个只使用一个线程来执行任务的特殊线程池,其扩展了EventExecutorGroup,主要是为了方便代理EventExecutorGroup中的方法;此外,EventExecutor中也定义一些自己的API,如:用于识别线程身份的方法inEventLoop,创建各种通知器Promise的方法。

AbstractEventExecutor:EventExecutor的抽象实现,其实现了EventExecutorGroup中的抽象方法,提交任务的方法委托给父类AbstractExecutorService来完成,但不支持提交调度任务,调用schedule相关方法都会抛出UnsupportedOperationException。此外,为了实现iterator,其内部定义了一个只能包含一个元素的Collection,且这个元素就是当前EventExecutor实例,因此迭代AbstractEventExecutor只会返回自身实例。用户可直接调用next方法,默认也是返回当前EventExecutor实例,更方便快捷。

AbstractScheduledEventExecutor:EventExecutor的抽象实现,主要为了支持调度任务的执行。其持有一个PriorityQueue用于存放调度任务,并实现了ScheduledExecutorService中定义的提交调度任务的方法。

OrderedEventExecutor:它是一个标识接口,没有任何方法和属性,仅仅表明实现该接口的类拥有按顺序串行执行任务的能力。

SingleThreadEventExecutor:一个可执行普通任务和调度任务的单线程执行器,也就是说,提交到该线程池的所有任务都有同一个线程来完成。内部具有一个阻塞队列用于保存所有提交到该执行器的任务。同时,其扩展了OrderedEventExecutor,表明需要按顺序串行执行所有提交的任务,这里体现了Netty无锁化设计。该接口实现了执行任务内部运作逻辑,后续会对其源码进行深入分析。即使如此,由于它还没有和特定的Selector绑定,因此不能执行I/O相关的操作。

io.netty.channel:

EventLoopGroup:Event Loop线程组,用于管理一组EventLoop对外提供服务,其扩展了EventExecutorGroup,同时定义了注册Channel的方法,用于将一个EventLoop与Channel绑定。

MultithreadEventLoopGroup:EventLoopGroup的抽象实现,初始化时会确认用于IO操作的EventLoop线程数量,默认值是处理器个数的2倍。从EventLoopGroup继承的register方法主要委托给next返回的EventLoop来完成。同时还扩展了MultithreadEventExecutorGroup,这样从EventExecutorGroup继承的方法都得到默认实现,但从MultithreadEventExecutorGroup中继承的newChild没有默认实现,它需要由由最终子类来实现。

DefaultEventLoopGroup:用于只能用于本地传输的EventLoop实现。

EventLoop:一旦与Channel绑定,将处理该Channel上的所有I/O操作。EventLoop可同时处理多个Channel中I/O操作,也可以只处理一个Channel上的I/O操作,具体由不同网络I/O确定。如Oio只能处理单个Channel的I/O操作,NIO则可以处理多个Channel的I/O操作。EventLoop所有子类都将顺序串行执行任务,因为其扩展了OrderedEventExecutor。

SingleThreadEventLoop:EventLoop的抽象实现,同时扩展了SingleThreadEventExecutor,负责用单个线程来执行所有提交到当前EventLoop的任务。SingleThreadEventLoop内部持有一个tailTasks队列,不知道干嘛用,目前内部也没有任何地方调用。SingleThreadEventLoop中主要实现了register相关方法。不同网络I/O类型通过扩展该类来完成底层实现。

ThreadPerChannelEventLoop:主要用于Oio的EventLoop实现,一个EventLoop只处理一个Channel的I/O操作。

io.netty.channel.nio:

NioEventLoopGroup:扩展自MultithreadEventLoopGroup,定义NIO的独特实现,主要实现了newChild方法。对于用户,代码中使用较多的也就NioEventLoopGroup了。

NioEventLoop:NIO实现,内部聚合了Java Selector,使得EventLoop成为一个真正意义的Reactor线程。内部除了实现Selector相关的一些操作,同时实现了执行任务的核心逻辑run方法。后续会详细分析它的源码。

线程管理

Netty线程模型的卓越性能取决于它对当前执行的Thread的身份确定,也就是说,确定他是否是分配给当前Channel以及它的EventLoop的那个线程(通过调用inEventLoop(Thread))。

如果当前调用线程正是支撑EventLoop的线程,那么所提交的代码块都将被直接执行。否则,EventLoop将调度该任务以便以后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的任务、事件。这也解释了任何的Thread是如何与Channel直接交互而无需在ChannelHandler中进行额外同步的。不过,这仅对Netty4或更高的版本有效,在Netty3中只保证了入站事件在EventLoop对应的线程中执行,所有的出站事件都由调用线程处理,调用线程可能是EventLoop线程也可能是别的线程,因此,需要在ChannelHandler中对出站事件的处理进行同步,保证线程安全。

每个EventLoop都有它自己的任务队列,独立于其他的EventLoop。实际开发过程中,绝不应该阻塞当前I/O线程,或是将一个长时间运行的任务放入任务队列,因为它将阻塞需要在同一个线程上执行的任何其他任务。

此外,需注意EventLoop的分配方式对ThreadLocal使用的影响。由于NIO实现中EventLoop通常用于支持多个Channel,所以对于所有相关联的Channel来说,ThreadLocal都是一样的。

线程模型选择

下面以服务端的配置为例,说明如何选择不同的线程模型。

单线程模型

 EventLoopGroup bossGroup = new NioEventLoopGroup(1);
 try {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup)
         .channel(NioServerSocketChannel.class)
        ......

以上示例中实例化了一个NIOEventLoopGroup,并传入线程数量为1,然后调用ServerBootstrap的group方法绑定线程组,看实现:

    @Override
    public ServerBootstrap group(EventLoopGroup group) {
        return group(group, group);
    }
    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
        super.group(parentGroup);
        if (childGroup == null) {
            throw new NullPointerException("childGroup");
        }
        if (this.childGroup != null) {
            throw new IllegalStateException("childGroup set already");
        }
        this.childGroup = childGroup;
        return this;
    }

从源码可知,实际仍然绑定了 bossGroup 和 workerGroup,只是都是同一个NioEventLoopGroup实例而已,这样Netty中的acceptor和后续的所有客户端连接的IO操作都是在一个线程中处理,这就相当于Reactor的单线程模型。

多线程模型

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
      ServerBootstrap b = new ServerBootstrap();
      b.group(bossGroup, workerGroup)
       .channel(NioServerSocketChannel.class)
       ......

创建1个线程的bossGroup线程组,这个线程负责处理客户端的连接请求,而workerGroup默认使用处理器个数*2的线程数量来处理I/O操作。这就相当于Reactor的多线程模型。

主从多线程模型

EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
      ServerBootstrap b = new ServerBootstrap();
      b.group(bossGroup, workerGroup)
       .channel(NioServerSocketChannel.class)
       ......

猜你喜欢

转载自blog.csdn.net/xxxcyzyy/article/details/85160570