抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(四)
4. 啊哈!!Bug ! !
image.png
netty不论是在本文中处理接收客户端连接的场景还是在处理接收客户端连接上的网络数据场景都会在一个do{....}while(...)循环read loop中不断的处理。
同时也都会利用在上一小节中介绍的RecvByteBufAllocator.Handle来记录每次read loop接收到的连接个数和从连接上读取到的网络数据大小。
从而在read loop的末尾都会通过allocHandle.continueReading()方法判断是否应该退出read loop循环结束连接的接收流程或者是结束连接上数据的读取流程。
无论是用于接收客户端连接的main reactor也好还是用于接收客户端连接上的网络数据的sub reactor也好,它们的运行框架都是一样的,只不过是具体分工不同。
所以netty这里想用统一的RecvByteBufAllocator.Handle来处理以上两种场景。
而RecvByteBufAllocator.Handle中的totalBytesRead字段主要记录sub reactor线程在处理客户端NioSocketChannel中OP_READ事件活跃时,总共在read loop中读取到的网络数据,而这里是main reactor线程在接收客户端连接所以这个字段并不会被设置。totalBytesRead字段的值在本文中永远会是0。
所以无论同时有多少个客户端并发连接到服务端上,在接收连接的这个read loop中永远只会接受一个连接就会退出循环,因为allocHandle.continueReading()方法中的判断条件totalBytesRead > 0永远会返回false。
do {
//底层调用NioServerSocketChannel->doReadMessages 创建客户端SocketChannel
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
//统计在当前事件循环中已经读取到得Message数量(创建连接的个数)
allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());
而netty的本意是在这个read loop循环中尽可能多的去接收客户端的并发连接,同时又不影响main reactor线程执行异步任务。但是由于这个Bug,main reactor在这个循环中只执行一次就结束了。这也一定程度上就影响了netty的吞吐。
让我们想象下这样的一个场景,当有16个客户端同时并发连接到了服务端,这时NioServerSocketChannel上的OP_ACCEPT事件活跃,main reactor从Selector上被唤醒,随后执行OP_ACCEPT事件的处理。
public final class NioEventLoop extends SingleThreadEventLoop {
@Override
protected void run() {
int selectCnt = 0;
for (;;) {
try {
int strategy;
try {
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
............省略.........
case SelectStrategy.BUSY_WAIT:
............省略.........
case SelectStrategy.SELECT:
............监听轮询IO事件.........
default:
}
} catch (IOException e) {
............省略.........
}
............处理IO就绪事件.........
............执行异步任务.........
}
}
但是由于这个Bug的存在,main reactor在接收客户端连接的这个read loop中只接收了一个客户端连接就匆匆返回了。
private final class NioMessageUnsafe extends AbstractNioUnsafe {
do {
int localRead = doReadMessages(readBuf);
.........省略...........
} while (allocHandle.continueReading());
}
然后根据下图中这个Reactor的运行结构去执行异步任务,随后绕一大圈又会回到NioEventLoop#run方法中重新发起一轮OP_ACCEPT事件轮询。
Reactor线程运行时结构.png
由于现在还有15个客户端并发连接没有被接收,所以此时Main Reactor线程并不会在selector.select()上阻塞,最终绕一圈又会回到NioMessageUnsafe#read方法的do{.....}while()循环。在接收一个连接之后又退出循环。
本来我们可以在一次read loop中把这16个并发的客户端连接全部接收完毕的,因为这个Bug,main reactor需要不断的发起OP_ACCEPT事件的轮询,绕了很大一个圈子。同时也增加了许多不必要的selector.select()系统调用开销
issue讨论.png
这时大家在看这个?Issue#11708中的讨论是不是就清晰很多了~~
Issue#11708:https://github.com/netty/netty/issues/11708
4.1 Bug的修复
笔者在写这篇文章的时候,Netty最新版本是4.1.68.final,这个Bug在4.1.69.final中被修复。
image.png
由于该Bug产生的原因正是因为服务端NioServerSocketChannel(用于监听端口地址和接收客户端连接)和 客户端NioSocketChannel(用于通信)中的Config配置类混用了同一个ByteBuffer分配器AdaptiveRecvByteBufAllocator而导致的。
所以在新版本修复中专门为服务端ServerSocketChannel中的Config配置类引入了一个新的ByteBuffer分配器ServerChannelRecvByteBufAllocator,专门用于服务端ServerSocketChannel接收客户端连接的场景。
image.png
image.png
在ServerChannelRecvByteBufAllocator的父类DefaultMaxMessagesRecvByteBufAllocator中引入了一个新的字段ignoreBytesRead,用于表示是否忽略网络字节的读取,在创建服务端Channel配置类NioServerSocketChannelConfig的时候,这个字段会被赋值为true。
image.png
当main reactor线程在read loop循环中接收客户端连接的时候。
private final class NioMessageUnsafe extends AbstractNioUnsafe {
do {
int localRead = doReadMessages(readBuf);
.........省略...........
} while (allocHandle.continueReading());
}
在read loop循环的末尾就会采用从ServerChannelRecvByteBufAllocator中创建的MaxMessageHandle#continueReading方法来判断读取连接次数是否超过了16次。由于这里的ignoreBytesRead == true这回我们就会忽略totalBytesRead == 0的情况,从而使得接收连接的read loop得以继续地执行下去。在一个read loop中一次性把16个连接全部接收完毕。
image.png
以上就是对这个Bug产生的原因,以及发现的过程,最后修复的方案一个全面的介绍,因此笔者也出现在了netty 4.1.69.final版本发布公告里的thank-list中。哈哈,真是令人开心的一件事情~~~
image.png
通过以上对netty接收客户端连接的全流程分析和对这个Bug来龙去脉以及修复方案的介绍,大家现在一定已经理解了整个接收连接的流程框架。
接下来笔者就把这个流程中涉及到的一些核心模块在单独拎出来从细节入手,为大家各个击破~~~