聊聊 Netty 那些事儿之 Reactor 在 Netty 中的实现 (创建篇)

本系列 Netty 源码解析文章基于 4.1.56.Final 版本
在上篇文章《聊聊Netty那些事儿之从内核角度看IO模型》中我们花了大量的篇幅来从内核角度详细讲述了五种IO模型的演进过程以及ReactorIO线程模型的底层基石 IO 多路复用技术在内核中的实现原理。
最后我们引出了 netty 中使用的主从 Reactor IO 线程模型。
通过上篇文章的介绍,我们已经清楚了在 IO 调用的过程中内核帮我们搞了哪些事情,那么俗话说的好内核领进门,修行在netty,netty 在用户空间又帮我们搞了哪些事情?
那么从本文开始,笔者将从源码角度来带大家看下上图中的Reactor IO线程模型在 Netty 中是如何实现的。
本文作为 Reactor 在 Netty 中实现系列文章中的开篇文章,笔者先来为大家介绍 Reactor 的骨架是如何创建出来的。
在上篇文章中我们提到 Netty 采用的是主从Reactor多线程的模型,但是它在实现上又与 Doug Lea 在Scalable IO in Java论文中提到的经典主从Reactor多线程模型有所差异。
Netty 中的Reactor是以Group的形式出现的,主从Reactor在 Netty 中就是主从Reactor组,每个Reactor Group中会有多个Reactor用来执行具体的IO任务。当然在 netty 中Reactor不只用来执行IO任务,这个我们后面再说。
Main Reactor Group中的Reactor数量取决于服务端要监听的端口个数,通常我们的服务端程序只会监听一个端口,所以Main Reactor Group只会有一个Main Reactor线程来处理最重要的事情:绑定端口地址,接收客户端连接,为客户端创建对应的SocketChannel,将客户端SocketChannel分配给一个固定的Sub Reactor。也就是上篇文章笔者为大家举的例子,饭店最重要的工作就是先把客人迎接进来。“我家大门常打开,开放怀抱等你,拥抱过就有了默契你会爱上这里......”
Sub Reactor Group里有多个Reactor线程,Reactor线程的个数可以通过系统参数-D io.netty.eventLoopThreads指定。默认的Reactor的个数为CPU核数 * 2。Sub Reactor线程主要用来轮询客户端SocketChannel上的IO就绪事件,处理IO就绪事件,执行异步任务。Sub Reactor Group做的事情就是上篇饭店例子中服务员的工作,客人进来了要为客人分配座位,端茶送水,做菜上菜。“不管远近都是客人,请不用客气,相约好了在一起,我们欢迎您......”
一个
客户端SocketChannel只能分配给一个固定的Sub Reactor。一个Sub Reactor负责处理多个客户端SocketChannel,这样可以将服务端承载的全量客户端连接分摊到多个Sub Reactor中处理,同时也能保证客户端SocketChannel上的IO处理的线程安全性。
由于文章篇幅的关系,作为 Reactor 在 netty 中实现的第一篇我们主要来介绍主从Reactor Group的创建流程,骨架脉络先搭好。
下面我们来看一段 Netty 服务端代码的编写模板,从代码模板的流程中我们来解析下主从 Reactor 的创建流程以及在这个过程中所涉及到的 Netty 核心类。
Netty 服务端代码模板
首先我们要创建 Netty 最核心的部分 ->
创建主从Reactor Group,在 Netty 中EventLoopGroup就是Reactor Group的实现类。对应的EventLoop就是Reactor的实现类。
创建用于
IO处理的ChannelHandler,实现相应IO事件的回调函数,编写对应的IO处理逻辑。注意这里只是简单示例哈,详细的 IO 事件处理,笔者会单独开一篇文章专门讲述。
创建
ServerBootstrapNetty 服务端启动类,并在启动类中配置启动 Netty 服务端所需要的一些必备信息。通过
serverBootstrap.group(bossGroup, workerGroup)为 Netty 服务端配置主从Reactor Group实例。通过
serverBootstrap.channel(NioServerSocketChannel.class)配置 Netty 服务端的ServerSocketChannel用于绑定端口地址以及创建客户端SocketChannel。Netty 中的NioServerSocketChannel.class就是对 JDK NIO 中ServerSocketChannel的封装。而用于表示客户端连接的NioSocketChannel是对 JDK NIOSocketChannel封装。在上篇文章介绍
Socket内核结构小节中我们提到,在编写服务端网络程序时,我们首先要创建一个Socket用于listen和bind端口地址,我们把这个叫做监听Socket,这里对应的就是NioServerSocketChannel.class。当客户端连接完成三次握手,系统调用accept函数会基于监听Socket创建出来一个新的Socket专门用于与客户端之间的网络通信我们称为客户端连接Socket,这里对应的就是NioSocketChannel.classserverBootstrap.option(ChannelOption.SO_BACKLOG, 100)设置服务端ServerSocketChannel中的SocketOption。关于SocketOption的选项我们后边的文章再聊,本文主要聚焦在 NettyMain Reactor Group的创建及工作流程。serverBootstrap.handler(....)设置服务端NioServerSocketChannel中对应Pipieline中的ChannelHandler。netty 有两种
Channel类型:一种是服务端用于监听绑定端口地址的NioServerSocketChannel,一种是用于客户端通信的NioSocketChannel。每种Channel类型实例都会对应一个PipeLine用于编排对应channel实例上的 IO 事件处理逻辑。PipeLine中组织的就是ChannelHandler用于编写特定的 IO 处理逻辑。注意
serverBootstrap.handler设置的是服务端NioServerSocketChannel PipeLine中的ChannelHandler。serverBootstrap.childHandler(ChannelHandler childHandler)用于设置客户端NioSocketChannel中对应Pipieline中的ChannelHandler。我们通常配置的编码解码器就是在这里。ServerBootstrap启动类方法带有child前缀的均是设置客户端NioSocketChannel属性的。ChannelInitializer是用于当SocketChannel成功注册到绑定的Reactor上后,用于初始化该SocketChannel的Pipeline。它的initChannel方法会在注册成功后执行。这里只是捎带提一下,让大家有个初步印象,后面我会专门介绍。ChannelFuture f = serverBootstrap.bind(PORT).sync()这一步会是下篇文章要重点分析的主题Main Reactor Group的启动,绑定端口地址,开始监听客户端连接事件(OP_ACCEPT)。本文我们只关注创建流程。f.channel().closeFuture().sync()等待服务端NioServerSocketChannel关闭。Netty 服务端到这里正式启动,并准备好接受客户端连接的准备。shutdownGracefully优雅关闭主从Reactor线程组里的所有Reactor线程。
Netty 对 IO 模型的支持
在上篇文章中我们介绍了五种IO模型,Netty 中支持BIO,NIO,AIO以及多种操作系统下的IO多路复用技术实现。
在 Netty 中切换这几种IO模型也是非常的方便,下面我们来看下 Netty 如何对这几种 IO 模型进行支持的。
首先我们介绍下几个与IO模型相关的重要接口:
EventLoop
EventLoop就是 Netty 中的Reactor,可以说它就是 Netty 的引擎,负责 Channel 上IO就绪事件的监听,IO就绪事件的处理,异步任务的执行驱动着整个 Netty 的运转。
不同IO模型下,EventLoop有着不同的实现,我们只需要切换不同的实现类就可以完成对 NettyIO模型的切换。
在NIO模型下 Netty 会自动根据操作系统以及版本的不同选择对应的IO多路复用技术实现。比如 Linux 2.6 版本以上用的是Epoll,2.6 版本以下用的是Poll,Mac 下采用的是Kqueue。
其中 Linux kernel 在 5.1 版本引入的异步 IO 库 io_uring 正在 netty 中孵化。
EventLoopGroup
Netty 中的Reactor是以Group的形式出现的,EventLoopGroup正是Reactor组的接口定义,负责管理Reactor,Netty 中的Channel就是通过EventLoopGroup注册到具体的Reactor上的。
Netty 的 IO 线程模型是主从Reactor多线程模型,主从Reactor线程组在 Netty 源码中对应的其实就是两个EventLoopGroup实例。
不同的IO模型也有对应的实现:
ServerSocketChannel
用于 Netty 服务端使用的ServerSocketChannel,对应于上篇文章提到的监听Socket,负责绑定监听端口地址,接收客户端连接并创建用于与客户端通信的SocketChannel。
不同的IO模型下的实现:
SocketChannel
用于与客户端通信的SocketChannel,对应于上篇文章提到的客户端连接Socket,当客户端完成三次握手后,由系统调用accept函数根据监听Socket创建。
不同的IO模型下的实现:
我们看到在不同IO模型的实现中,Netty 这些围绕IO模型的核心类只是前缀的不同:
BIO 对应的前缀为
Oio表示old io,现在已经废弃不推荐使用。NIO 对应的前缀为
Nio,正是 Netty 推荐也是我们常用的非阻塞IO模型。AIO 对应的前缀为
Aio,由于 Linux 下的异步IO机制实现的并不成熟,性能提升表现上也不明显,现已被删除。
我们只需要将IO模型的这些核心接口对应的实现类前缀改为对应IO模型的前缀,就可以轻松在 Netty 中完成对IO模型的切换。
多种 NIO 的实现
我们通常在使用NIO模型的时候会使用Common列下的这些IO模型核心类,Common类也会根据操作系统的不同自动选择JDK在对应平台下的IO多路复用技术的实现。
而 Netty 自身也根据操作系统的不同提供了自己对IO多路复用技术的实现,比JDK的实现性能更优。比如:
JDK的 NIO默认实现是水平触发,Netty 是边缘触发(默认)和水平触发可切换。。Netty 实现的垃圾回收更少、性能更好。
我们编写 Netty 服务端程序的时候也可以根据操作系统的不同,采用 Netty 自身的实现来进一步优化程序。做法也很简单,直接将上图中红框里的实现类替换成 Netty 的自身实现类即可完成切换。
经过以上对 Netty 服务端代码编写模板以及IO模型相关核心类的简单介绍,我们对 Netty 的创建流程有了一个简单粗略的总体认识,下面我们来深入剖析下创建流程过程中的每一个步骤以及这个过程中涉及到的核心类实现。
以下源码解析部分我们均采用Common列下NIO相关的实现进行解析。
创建主从 Reactor 线程组
在 Netty 服务端程序编写模板的开始,我们首先会创建两个 Reactor 线程组:
一个是主 Reactor 线程组
bossGroup用于监听客户端连接,创建客户端连接NioSocketChannel,并将创建好的客户端连接NioSocketChannel注册到从 Reactor 线程组中一个固定的Reactor上。一个是从 Reactor 线程组
workerGroup,workerGroup中的Reactor负责监听绑定在其上的客户端连接NioSocketChannel上的IO就绪事件,并处理IO就绪事件,执行异步任务。
Netty 中 Reactor 线程组的实现类为NioEventLoopGroup,在创建bossGroup和workerGroup的时候用到了NioEventLoopGroup的两个构造函数:
带
nThreads参数的构造函数public NioEventLoopGroup(int nThreads)。不带
nThreads参数的默认构造函数public NioEventLoopGroup()
nThreads参数表示当前要创建的Reactor线程组内包含多少个Reactor线程。不指定nThreads参数的话采用默认的Reactor线程个数,用0表示。
最终会调用到构造函数
下面简单介绍下构造函数中这几个参数的作用,后面我们在讲解本文主线的过程中还会提及这几个参数,到时在详细介绍,这里只是让大家有个初步印象,不必做过多的纠缠。
Executor executor:负责启动Reactor线程进而 Reactor 才可以开始工作。
Reactor 线程组
NioEventLoopGroup负责创建Reactor线程,在创建的时候会将executor传入。
RejectedExecutionHandler:当向Reactor添加异步任务添加失败时,采用的拒绝策略。Reactor 的任务不只是监听 IO 活跃事件和 IO 任务的处理,还包括对异步任务的处理。这里大家只需有个这样的概念,后面笔者会专门详细介绍。SelectorProvider selectorProvider:Reactor 中的 IO 模型为IO多路复用模型,对应于 JDK NIO 中的实现为java.nio.channels.Selector(就是我们上篇文章中提到的select,poll,epoll),每个 Reator 中都包含一个Selector,用于轮询注册在该 Reactor 上的所有Channel上的IO事件。SelectorProvider就是用来创建Selector的。SelectStrategyFactory selectStrategyFactory:Reactor 最重要的事情就是轮询注册其上的Channel上的IO就绪事件,这里的SelectStrategyFactory用于指定轮询策略,默认为DefaultSelectStrategyFactory.INSTANCE。
最终会将这些参数交给NioEventLoopGroup 的父类构造器,下面我们来看下NioEventLoopGroup类的继承结构:
NioEventLoopGroup类的继承结构乍一看比较复杂,大家不要慌,笔者会随着主线的深入慢慢地介绍这些父类接口,我们现在重点关注Mutithread前缀的类。
我们知道NioEventLoopGroup是 Netty 中的Reactor线程组的实现,既然是线程组那么肯定是负责管理和创建多个Reactor线程的,所以Mutithread前缀的类定义的行为自然是对Reactor线程组内多个Reactor线程的创建和管理工作。
MultithreadEventLoopGroup
MultithreadEventLoopGroup类主要的功能就是用来确定Reactor线程组内Reactor的个数。
默认的Reactor的个数存放于字段DEFAULT_EVENT_LOOP_THREADS 中。
从static {}静态代码块中我们可以看出默认Reactor的个数的获取逻辑:
可以通过系统变量
-D io.netty.eventLoopThreads"指定。如果不指定,那么默认的就是
NettyRuntime.availableProcessors() * 2
当nThread参数设置为0采用默认设置时,Reactor线程组内的Reactor个数则设置为DEFAULT_EVENT_LOOP_THREADS。
MultithreadEventExecutorGroup
MultithreadEventExecutorGroup这里就是本小节的核心,主要用来定义创建和管理Reactor的行为。
首先介绍一个新的构造器参数EventExecutorChooserFactory chooserFactory。当客户端连接完成三次握手后,Main Reactor会创建客户端连接NioSocketChannel,并将其绑定到Sub Reactor Group中的一个固定Reactor,那么具体要绑定到哪个具体的Sub Reactor上呢?这个绑定策略就是由chooserFactory来创建的。默认为DefaultEventExecutorChooserFactory。
下面就是本小节的主题Reactor线程组的创建过程:
1. 创建用于启动 Reactor 线程的 executor
在 Netty Reactor Group 中的单个Reactor的IO线程模型为上篇文章提到的单Reactor单线程模型,一个Reactor线程负责轮询注册其上的所有Channel中的IO就绪事件,处理 IO 事件,执行 Netty 中的异步任务等工作。正是这个Reactor线程驱动着整个 Netty 的运转,可谓是 Netty 的核心引擎。
而这里的executor就是负责启动Reactor线程的,从创建源码中我们可以看到executor的类型为ThreadPerTaskExecutor 。
ThreadPerTaskExecutor
我们看到ThreadPerTaskExecutor 做的事情很简单,从它的命名前缀ThreadPerTask我们就可以猜出它的工作方式,就是来一个任务就创建一个线程执行。而创建的这个线程正是 netty 的核心引擎 Reactor 线程。
在Reactor线程启动的时候,Netty 会将Reactor线程要做的事情封装成Runnable,丢给exexutor启动。
而Reactor线程的核心就是一个死循环不停的轮询IO 就绪事件,处理 IO 事件,执行异步任务。一刻也不停歇,堪称996典范。
这里向大家先卖个关子,"Reactor线程是何时启动的呢??"
2. 创建 Reactor
Reactor线程组NioEventLoopGroup包含多个Reactor,存放于private final EventExecutor[] children数组中。
所以下面的事情就是创建nThread个Reactor,并存放于EventExecutor[] children字段中,
我们来看下用于创建Reactor的newChild(executor, args)方法:
newChild
newChild方法是MultithreadEventExecutorGroup中的一个抽象方法,提供给具体子类实现。
这里我们解析的是NioEventLoopGroup,我们来看下newChild在该类中的实现:
前边提到的众多构造器参数,这里会通过可变参数Object... args传入到 Reactor 类NioEventLoop的构造器中。
这里介绍下新的参数EventLoopTaskQueueFactory queueFactory,前边提到 Netty 中的Reactor主要工作是轮询注册其上的所有Channel上的IO就绪事件,处理IO就绪事件。除了这些主要的工作外,Netty 为了极致的压榨Reactor的性能,还会让它做一些异步任务的执行工作。既然要执行异步任务,那么Reactor中就需要一个队列来保存任务。
这里的EventLoopTaskQueueFactory就是用来创建这样的一个队列来保存Reactor中待执行的异步任务。
可以把Reactor理解成为一个单线程的线程池,类似于JDK中的SingleThreadExecutor,仅用一个线程来执行轮询IO就绪事件,处理IO就绪事件,执行异步任务。同时待执行的异步任务保存在Reactor里的taskQueue中。
NioEventLoop
这里就正式开始了Reactor的创建过程,我们知道Reactor的核心是采用的IO多路复用模型来对客户端连接上的IO事件进行监听,所以最重要的事情是创建Selector(JDK NIO 中IO多路复用技术的实现)。
可以把
Selector理解为我们上篇文章介绍的Select,poll,epoll,它是JDK NIO对操作系统内核提供的这些IO多路复用技术的封装。
openSelector
openSelector是NioEventLoop类中用于创建IO多路复用的Selector,并对创建出来的JDK NIO 原生的Selector进行性能优化。
首先会通过SelectorProvider#openSelector 创建 JDK NIO 原生的Selector。
SelectorProvider会根据操作系统的不同选择 JDK 在不同操作系统版本下的对应Selector的实现。Linux 下会选择Epoll,Mac 下会选择Kqueue。
下面我们就来看下SelectorProvider是如何做到自动适配不同操作系统下IO多路复用实现的
SelectorProvider
SelectorProvider是在前面介绍的NioEventLoopGroup类构造函数中通过调用SelectorProvider.provider()被加载,并通过NioEventLoopGroup#newChild方法中的可变长参数Object... args传递到NioEventLoop中的private final SelectorProvider provider字段中。
SelectorProvider 的加载过程:
从SelectorProvider加载源码中我们可以看出,SelectorProvider的加载方式有三种,优先级如下:
通过系统变量
-D java.nio.channels.spi.SelectorProvider指定SelectorProvider的自定义实现类全限定名。通过应用程序类加载器(Application Classloader)加载。
通过
SPI方式加载。在工程目录META-INF/services下定义名为java.nio.channels.spi.SelectorProvider的SPI文件,文件中第一个定义的SelectorProvider实现类全限定名就会被加载。
如果以上两种方式均未被定义,那么就采用
SelectorProvider系统默认实现sun.nio.ch.DefaultSelectorProvider。笔者当前使用的操作系统是MacOS,从源码中我们可以看到自动适配了KQueue实现。
不同操作系统中 JDK 对于
DefaultSelectorProvider会有所不同,Linux 内核版本 2.6 以上对应的Epoll,Linux 内核版本 2.6 以下对应的Poll,MacOS 对应的是KQueue。
下面我们接着回到io.netty.channel.nio.NioEventLoop#openSelector的主线上来。
Netty 对 JDK NIO 原生 Selector 的优化
首先在NioEventLoop中有一个 Selector 优化开关DISABLE_KEY_SET_OPTIMIZATION,通过系统变量-D io.netty.noKeySetOptimization指定,默认是开启的,表示需要对 JDK NIO 原生Selector进行优化。
如果优化开关DISABLE_KEY_SET_OPTIMIZATION 是关闭的,那么直接返回 JDK NIO 原生的Selector。
下面为 Netty 对 JDK NIO 原生的Selector的优化过程:
获取
JDK NIO原生Selector的抽象实现类sun.nio.ch.SelectorImpl。JDK NIO原生Selector的实现均继承于该抽象类。用于判断由SelectorProvider创建出来的Selector是否为JDK默认实现(SelectorProvider第三种加载方式)。因为SelectorProvider可以是自定义加载,所以它创建出来的Selector并不一定是 JDK NIO 原生的。
JDK NIO Selector 的抽象类sun.nio.ch.SelectorImpl
这里笔者来简单介绍下 JDK NIO 中的Selector中这几个字段的含义,我们可以和上篇文章讲到的 epoll 在内核中的结构做类比,方便大家后续的理解:
Set<SelectionKey> selectedKeys类似于我们上篇文章讲解Epoll时提到的就绪队列eventpoll->rdllist,Selector这里大家可以理解为Epoll。Selector会将自己监听到的IO就绪的Channel放到selectedKeys中。
这里的
SelectionKey暂且可以理解为Channel在Selector中的表示,类比上图中epitem结构里的epoll_event,封装 IO 就绪 Socket 的信息。其实SelectionKey里包含的信息不止是Channel还有很多 IO 相关的信息。后面我们在详细介绍。
HashSet<SelectionKey> keys:这里存放的是所有注册到该Selector上的Channel。类比epoll中的红黑树结构rb_root
SelectionKey在Channel注册到Selector中后生成。
Set<SelectionKey> publicSelectedKeys相当于是selectedKeys的视图,用于向外部线程返回IO就绪的SelectionKey。这个集合在外部线程中只能做删除操作不可增加元素,并且不是线程安全的。Set<SelectionKey> publicKeys相当于keys的不可变视图,用于向外部线程返回所有注册在该Selector上的SelectionKey
这里需要重点关注抽象类sun.nio.ch.SelectorImpl中的selectedKeys和publicSelectedKeys这两个字段,注意它们的类型都是HashSet ,一会优化的就是这里!!!!
判断由
SelectorProvider创建出来的Selector是否是 JDK NIO 原生的Selector实现。因为 Netty 优化针对的是 JDK NIO 原生Selector。判断标准为sun.nio.ch.SelectorImpl类是否为SelectorProvider创建出Selector的父类。如果不是则直接返回。不在继续下面的优化过程。
通过前面对SelectorProvider的介绍我们知道,这里通过provider.openSelector()创建出来的Selector实现类为KQueueSelectorImpl类,它继承实现了sun.nio.ch.SelectorImpl,所以它是 JDK NIO 原生的Selector实现
创建
SelectedSelectionKeySet通过反射替换掉sun.nio.ch.SelectorImpl类中selectedKeys和publicSelectedKeys的默认HashSet实现。
为什么要用SelectedSelectionKeySet替换掉原来的HashSet呢??
因为这里涉及到对HashSet类型的sun.nio.ch.SelectorImpl#selectedKeys集合的两种操作:
插入操作: 通过前边对
sun.nio.ch.SelectorImpl类中字段的介绍我们知道,在Selector监听到IO就绪的SelectionKey后,会将IO就绪的SelectionKey插入sun.nio.ch.SelectorImpl#selectedKeys集合中,这时Reactor线程会从java.nio.channels.Selector#select(long)阻塞调用中返回(类似上篇文章提到的epoll_wait)。遍历操作:
Reactor线程返回后,会从Selector中获取IO就绪的SelectionKey集合(也就是sun.nio.ch.SelectorImpl#selectedKeys),Reactor线程遍历selectedKeys,获取IO就绪的SocketChannel,并处理SocketChannel上的IO事件。
我们都知道HashSet底层数据结构是一个哈希表,由于Hash冲突这种情况的存在,所以导致对哈希表进行插入和遍历操作的性能不如对数组进行插入和遍历操作的性能好。
还有一个重要原因是,数组可以利用 CPU 缓存的优势来提高遍历的效率。后面笔者会有一篇专门的文章来讲述利用 CPU 缓存行如何为我们带来性能优势。
所以 Netty 为了优化对sun.nio.ch.SelectorImpl#selectedKeys集合的插入,遍历性能,自己用数组这种数据结构实现了SelectedSelectionKeySet ,用它来替换原来的HashSet实现。
SelectedSelectionKeySet
初始化
SelectionKey[] keys数组大小为1024,当数组容量不够时,扩容为原来的两倍大小。通过数组尾部指针
size,在向数组插入元素的时候可以直接定位到插入位置keys[size++]。操作一步到位,不用像哈希表那样还需要解决Hash冲突。对数组的遍历操作也是如丝般顺滑,CPU 直接可以在缓存行中遍历读取数组元素无需访问内存。比
HashSet的迭代器java.util.HashMap.KeyIterator遍历方式性能不知高到哪里去了。
看到这里不禁感叹,从各种小的细节可以看出 Netty 对性能的优化简直淋漓尽致,对性能的追求令人发指。细节真的是魔鬼。
Netty 通过反射的方式用
SelectedSelectionKeySet替换掉sun.nio.ch.SelectorImpl#selectedKeys,sun.nio.ch.SelectorImpl#publicSelectedKeys这两个集合中原来HashSet的实现。
反射获取
sun.nio.ch.SelectorImpl类中selectedKeys和publicSelectedKeys。
Java9版本以上通过sun.misc.Unsafe设置字段值的方式
通过反射的方式用
SelectedSelectionKeySet替换掉hashSet实现的sun.nio.ch.SelectorImpl#selectedKeys,sun.nio.ch.SelectorImpl#publicSelectedKeys。
将与
sun.nio.ch.SelectorImpl类中selectedKeys和publicSelectedKeys关联好的 Netty 优化实现SelectedSelectionKeySet,设置到io.netty.channel.nio.NioEventLoop#selectedKeys字段中保存。
后续
Reactor线程就会直接从io.netty.channel.nio.NioEventLoop#selectedKeys中获取IO就绪的SocketChannel
用
SelectorTuple封装unwrappedSelector和wrappedSelector返回给NioEventLoop构造函数。到此Reactor中的Selector就创建完毕了。
所谓的
unwrappedSelector是指被 Netty 优化过的 JDK NIO 原生 Selector。所谓的
wrappedSelector就是用SelectedSelectionKeySetSelector装饰类将unwrappedSelector和与sun.nio.ch.SelectorImpl类关联好的 Netty 优化实现SelectedSelectionKeySet封装装饰起来。
wrappedSelector会将所有对Selector的操作全部代理给unwrappedSelector,并在发起轮询IO事件的相关操作中,重置SelectedSelectionKeySet清空上一次的轮询结果。
到这里 Reactor 的核心 Selector 就创建好了,下面我们来看下用于保存异步任务的队列是如何创建出来的。
newTaskQueue
我们继续回到创建Reactor的主线上,到目前为止Reactor的核心Selector就创建好了,前边我们提到Reactor除了需要监听IO就绪事件以及处理IO就绪事件外,还需要执行一些异步任务,当外部线程向Reactor提交异步任务后,Reactor就需要一个队列来保存这些异步任务,等待Reactor线程执行。
下面我们来看下Reactor中任务队列的创建过程:
在
NioEventLoop的父类SingleThreadEventLoop中提供了一个静态变量DEFAULT_MAX_PENDING_TASKS用来指定Reactor任务队列的大小。可以通过系统变量-D io.netty.eventLoop.maxPendingTasks进行设置,默认为Integer.MAX_VALUE,表示任务队列默认为无界队列。根据
DEFAULT_MAX_PENDING_TASKS变量的设定,来决定创建无界任务队列还是有界任务队列。
Reactor内的异步任务队列的类型为MpscQueue,它是由JCTools提供的一个高性能无锁队列,从命名前缀Mpsc可以看出,它适用于多生产者单消费者的场景,它支持多个生产者线程安全的访问队列,同一时刻只允许一个消费者线程读取队列中的元素。
我们知道 Netty 中的
Reactor可以线程安全的处理注册其上的多个SocketChannel上的IO数据,保证Reactor线程安全的核心原因正是因为这个MpscQueue,它可以支持多个业务线程在处理完业务逻辑后,线程安全的向MpscQueue添加异步写任务,然后由单个Reactor线程来执行这些写任务。既然是单线程执行,那肯定是线程安全的了。
Reactor 对应的 NioEventLoop 类型继承结构
NioEventLoop的继承结构也是比较复杂,这里我们只关注在Reactor创建过程中涉及的到两个父类SingleThreadEventLoop,SingleThreadEventExecutor。
剩下的继承体系,我们在后边随着Netty源码的深入在慢慢介绍。
前边我们提到,其实Reactor我们可以看作是一个单线程的线程池,只有一个线程用来执行IO就绪事件的监听,IO事件的处理,异步任务的执行。用MpscQueue 来存储待执行的异步任务。
命名前缀为SingleThread的父类都是对Reactor这些行为的分层定义。也是本小节要介绍的对象
SingleThreadEventLoop
Reactor负责执行的异步任务分为三类:
普通任务:这是 Netty 最主要执行的异步任务,存放在普通任务队列taskQueue中。在NioEventLoop构造函数中创建。定时任务:存放在优先级队列中。后续我们介绍。尾部任务:存放于尾部任务队列tailTasks中,尾部任务一般不常用,在普通任务执行完后 Reactor 线程会执行尾部任务。**使用场景:**比如对 Netty 的运行状态做一些统计数据,例如任务循环的耗时、占用物理内存的大小等等都可以向尾部队列添加一个收尾任务完成统计数据的实时更新。
SingleThreadEventLoop 负责对尾部任务队列tailTasks进行管理。并且提供Channel向Reactor注册的行为。
SingleThreadEventExecutor
SingleThreadEventExecutor主要负责对普通任务队列的管理,以及异步任务的执行,Reactor线程的启停。
到现在为止,一个完整的Reactor架构就被创建出来了。
3. 创建 Channel 到 Reactor 的绑定策略
到这一步,Reactor 线程组NioEventLoopGroup里边的所有Reactor就已经全部创建完毕。
无论是 Netty 服务端NioServerSocketChannel关注的OP_ACCEPT事件也好,还是 Netty 客户端NioSocketChannel关注的OP_READ和OP_WRITE事件也好,都需要先注册到Reactor上,Reactor才能监听Channel上关注的IO事件实现IO多路复用。
NioEventLoopGroup(Reactor 线程组)里边有众多的Reactor,那么以上提到的这些Channel究竟应该注册到哪个Reactor上呢?这就需要一个绑定的策略来平均分配。
还记得我们前边介绍MultithreadEventExecutorGroup类的时候提到的构造器参数EventExecutorChooserFactory 吗?
这时候它就派上用场了,它主要用来创建Channel到Reactor的绑定策略。默认为DefaultEventExecutorChooserFactory.INSTANCE。
下面我们来看下具体的绑定策略:
DefaultEventExecutorChooserFactory
我们看到在newChooser 方法绑定策略有两个分支,不同之处在于需要判断 Reactor 线程组中的Reactor个数是否为2的次幂。
Netty 中的绑定策略就是采用round-robin轮询的方式来挨个选择Reactor进行绑定。
采用round-robin的方式进行负载均衡,我们一般会用round % reactor.length取余的方式来挨个平均的定位到对应的Reactor上。
如果Reactor的个数reactor.length恰好是2的次幂,那么就可以用位操作&运算round & reactor.length -1来代替%运算round % reactor.length,因为位运算的性能更高。具体为什么&运算能够代替%运算,笔者会在后面讲述时间轮的时候为大家详细证明,这里大家只需记住这个公式,我们还是聚焦本文的主线。
了解了优化原理,我们在看代码实现就很容易理解了。
利用%运算的方式Math.abs(idx.getAndIncrement() % executors.length)来进行绑定。
利用&运算的方式idx.getAndIncrement() & executors.length - 1来进行绑定。
又一次被 Netty 对性能的极致追求所折服~~~~
4. 向 Reactor 线程组中所有的 Reactor 注册 terminated 回调函数
当 Reactor 线程组NioEventLoopGroup中所有的Reactor已经创建完毕,Channel到Reactor的绑定策略也创建完毕后,我们就来到了创建NioEventGroup的最后一步。
俗话说的好,有创建就有启动,有启动就有关闭,这里会创建Reactor关闭的回调函数terminationListener,在Reactor关闭时回调。
terminationListener回调的逻辑很简单:
通过
AtomicInteger terminatedChildren变量记录已经关闭的Reactor个数,用来判断NioEventLoopGroup中的Reactor是否已经全部关闭。如果所有
Reactor均已关闭,设置NioEventLoopGroup中的terminationFuture为success。表示Reactor线程组关闭成功。
我们在回到文章开头Netty服务端代码模板
现在 Netty 的主从Reactor线程组就已经创建完毕,此时 Netty 服务端的骨架已经搭建完毕,骨架如下:
总结
本文介绍了首先介绍了 Netty 对各种IO模型的支持以及如何轻松切换各种IO模型。
还花了大量的篇幅介绍 Netty 服务端的核心引擎主从Reactor线程组的创建过程。在这个过程中,我们还提到了 Netty 对各种细节进行的优化,展现了 Netty 对性能极致的追求。
好了,Netty 服务端的骨架已经搭好,剩下的事情就该绑定端口地址然后接收连接了,我们下篇文章再见~~~
版权声明: 本文为 InfoQ 作者【bin的技术小屋】的原创文章。
原文链接:【http://xie.infoq.cn/article/c375714a034d72c2b66597607】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。










评论