抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(一)
对于一个高性能网络通讯框架来说,最最重要也是最核心的工作就是如何高效的接收客户端连接,这就好比我们开了一个饭店,那么迎接客人就是饭店最重要的工作,我们要先把客人迎接进来,不能让客人一看人多就走掉,只要客人进来了,哪怕菜做的慢一点也没关系。
本文笔者就来为大家介绍下netty这块最核心的内容,看看netty是如何高效的接收客户端连接的。
下图为笔者在一个月黑风高天空显得那么深邃遥远的夜晚,闲来无事,于是捧起Netty关于如何接收连接这部分源码细细品读的时候,意外的发现了一个影响Netty接收连接吞吐的一个Bug。
issue讨论.png
于是笔者就在Github提了一个?Issue#11708,阐述了下这个Bug产生的原因以及导致的结果并和Netty的作者一起讨论了下修复措施。如上图所示。
Issue#11708:https://github.com/netty/netty/issues/11708
这里先不详细解释这个Issue,也不建议大家现在就打开这个Issue查看,笔者会在本文的介绍中随着源码深入的解读慢慢的为大家一层一层地拨开迷雾。
之所以在文章的开头把这个拎出来,笔者是想让大家带着怀疑,审视,欣赏,崇敬,敬畏的态度来一起品读世界顶级程序员编写的代码。由衷的感谢他们在这一领域做出的贡献。
好了,问题抛出来后,我们就带着这个疑问来开始本文的内容吧~~~
前文回顾
按照老规矩,再开始本文的内容之前,我们先来回顾下前边几篇文章的概要内容帮助大家梳理一个框架全貌出来。
笔者这里再次想和读者朋友们强调的是本文可以独立观看,并不依赖前边系列文章的内容,只是大家如果对相关细节部分感兴趣的话,可以在阅读完本文之后在去回看相关文章。
在前边的系列文章中,笔者为大家介绍了驱动Netty整个框架运转的核心引擎Reactor的创建,启动,运行的全流程。从现在开始Netty的整个核心框架就开始运转起来开始工作了,本文要介绍的主要内容就是Netty在启动之后要做的第一件事件:监听端口地址,高效接收客户端连接。
在?《聊聊Netty那些事儿之从内核角度看IO模型》一文中,我们是从整个网络框架的基石IO模型的角度整体阐述了下Netty的IO线程模型。
而Netty中的Reactor正是IO线程在Netty中的模型定义。Reactor在Netty中是以Group的形式出现的,分为:
• 主Reactor线程组也就是我们在启动代码中配置的EventLoopGroup bossGroup,main reactor group中的reactor主要负责监听客户端连接事件,高效的处理客户端连接。也是本文我们要介绍的重点。
• 从Reactor线程组也就是我们在启动代码中配置的EventLoopGroup workerGroup,sub reactor group中的reactor主要负责处理客户端连接上的IO事件,以及异步任务的执行。
最后我们得出Netty的整个IO模型如下:
netty中的reactor.png
本文我们讨论的重点就是MainReactorGroup的核心工作上图中所示的步骤1,步骤2,步骤3。
在从整体上介绍完Netty的IO模型之后,我们又在?《Reactor在Netty中的实现(创建篇)》中完整的介绍了Netty框架的骨架主从Reactor组的搭建过程,阐述了Reactor是如何被创建出来的,并介绍了它的核心组件如下图所示:
image.png
• thread即为Reactor中的IO线程,主要负责监听IO事件,处理IO任务,执行异步任务。
• selector则是JDK NIO对操作系统底层IO多路复用技术实现的封装。用于监听IO就绪事件。
• taskQueue用于保存Reactor需要执行的异步任务,这些异步任务可以由用户在业务线程中向Reactor提交,也可以是Netty框架提交的一些自身核心的任务。
• scheduledTaskQueue则是保存Reactor中执行的定时任务。代替了原有的时间轮来执行延时任务。
• tailQueue保存了在Reactor需要执行的一些尾部收尾任务,在普通任务执行完后 Reactor线程会执行尾部任务,比如对Netty 的运行状态做一些统计数据,例如任务循环的耗时、占用物理内存的大小等等
在骨架搭建完毕之后,我们随后又在在?《详细图解Netty Reactor启动全流程》》一文中介绍了本文的主角服务端NioServerSocketChannel的创建,初始化,绑定端口地址,向main reactor注册监听OP_ACCEPT事件的完整过程。
Reactor启动后的结构.png
main reactor如何处理OP_ACCEPT事件将会是本文的主要内容。
自此Netty框架的main reactor group已经启动完毕,开始准备监听OP_accept事件,当客户端连接上来之后,OP_ACCEPT事件活跃,main reactor开始处理OP_ACCEPT事件接收客户端连接了。
而netty中的IO事件分为:OP_ACCEPT事件,OP_READ事件,OP_WRITE事件和OP_CONNECT事件,netty对于IO事件的监听和处理统一封装在Reactor模型中,这四个IO事件的处理过程也是我们后续文章中要单独拿出来介绍的,本文我们聚焦OP_ACCEPT事件的处理。
而为了让大家能够对IO事件的处理有一个完整性的认识,笔者写了?《一文聊透Netty核心引擎Reactor的运转架构》这篇文章,在文章中详细介绍了Reactor线程的整体运行框架。
Reactor线程运行时结构.png
Reactor线程会在一个死循环中996不停的运转,在循环中会不断的轮询监听Selector上的IO事件,当IO事件活跃后,Reactor从Selector上被唤醒转去执行IO就绪事件的处理,在这个过程中我们引出了上述四种IO事件的处理入口函数。
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
//获取Channel的底层操作类Unsafe
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
......如果SelectionKey已经失效则关闭对应的Channel......
}
try {
//获取IO就绪事件
int readyOps = k.readyOps();
//处理Connect事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
//移除对Connect事件的监听,否则Selector会一直通知
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
//触发channelActive事件处理Connect事件
unsafe.finishConnect();
}
//处理Write事件
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
//处理Read事件或者Accept事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
本文笔者将会为大家重点介绍OP_ACCEPT事件的处理入口函数unsafe.read()的整个源码实现。
当客户端连接完成三次握手之后,main reactor中的selector产生OP_ACCEPT事件活跃,main reactor随即被唤醒,来到了OP_ACCEPT事件的处理入口函数开始接收客户端连接。