Netty流程细讲之精彩拾遗
引言
在前面的文章中,我们详细的学习了Netty处理流程中的方方面面。不过仍然有一些部分细节处的知识点为了避免影响主流程的学习而延后,这篇文章,我们就来好好分析一下Netty当中这些精彩的细节处理。
JDK 空轮训Bug
在JDK的NIO接口中有一个流传很广,影响很恶劣的bug,被称为NIO 100% CPU,也被称为NIO空轮训。来看下如下代码
while(true)
{
selector.select();
Set < SelectionKey > keys = selector.selectedKeys();
//省略代码:处理keys的内容
}
正常情况下,如果没有关注事件发生,selector.select()是阻塞的,不返回的。但是当bug触发时,没有关注事件发生,select操作也会返回。由于没有事件发生,导致对处理keys的代码也不会运行到,此时就就会在while循环中反复的执行select,每次都不阻塞直接返回。导致cpu100%。
Netty在一个EventLoop线程中也是使用类似的循环方式在selector上执行select等待,来看下代码
private void select(boolean oldWakenUp) throws IOException
{
Selector selector = this.selector;
try
{
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for(;;)
{
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000 L) / 1000000 L;
//省略代码:判断当前是否已经超时以及是否存在队列,存在此种情况,则跳出循环
int selectedKeys = selector.select(timeoutMillis);
selectCnt++;
//省略代码:selectedKeys不为0或者出现队列有了新任务,则离开循环
else if(SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD)
{
selector = selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
if(selectCnt > MIN_PREMATURE_SELECTOR_RETURNS)
{
//省略代码:输出debug日志
}
}
catch(CancelledKeyException e)
{
//省略代码:输出debug日志
}
}
当bug出现时,selector.select方法不再阻塞,没有就绪事件也会返回,此时就会在for循环体内部快速的循环。而Netty使用一个局部变量selectCnt来计算已经执行过的selector.select次数。如果次数超过了阀值SELECTOR_AUTO_REBUILD_THRESHOLD,默认值为512,Netty会抛弃当前的Selector,重新建立一个。其重建Selector的方法为NioEventLoop#selectRebuildSelector,整体的实现思路也不复杂,就不贴代码了,来看整体实现的流程图,如下

重建selector对象完毕后,将选择计数selectCnt复位为1,重新开始选择流程。
从Netty的实际应用中,通过这种方式,对于线上出现CPU100%的情况得到有效的解决。目前这个思路也被Jetty,UnderTow等其他网络IO框架所采用。
自适应大小ByteBuf分配算法
在源码篇《Netty流程细讲之数据读取与连接远端》我们提到,在EventLoop线程进行数据读取时,申请的ByteBuf容量不够的话,在socket缓冲区中的数据就需要多次读取才能读取完毕,降低了程序性能;申请的ByteBuf容量太大的话,虽然减少了读取次数,但是浪费了内存空间,对应用的整体内存压力又上去了。找到一个合适的平衡点并不是容易的事情。
在Netty中,Netty采用了策略模式来处理这个问题,其将ByteBuf的分配工作交给接口RecvByteBufAllocator.Handle来处理。首先来看下这个接口定义的几个方法
allocate(ByteBufAllocator alloc):根据内建的策略,使用分配器分配一个合适大小的ByteBuf。lastBytesRead(int bytes):记录本次读取到的字节数,统计数据内部应用于分配策略。continueReading:是否应该继续下一轮从通道读取数据。
这个RecvByteBufAllocator.Handle对象实例是由RecvByteBufAllocator#newHandle方法生成,也就是具体采用什么策略,实际上由RecvByteBufAllocator接口实现类决定。让我们来看下这个接口的相关类图,如下

其区别和联系如下:
DefaultMaxBytesRecvByteBufAllocator:限定一个读取循环(可能执行多次读取操作)读取字节上限maxBytesPerRead和每次读取操作读取字节上限individualReadMax。分配ByteBuf大小时,取individualReadMax和剩余可读字节数(maxBytesPerRead减去已经读取的总数)的较小值分配。这种策略下,一轮读取的上限是被固定的。DefaultMaxMessagesRecvByteBufAllocator:限定一个读取循环中最大读取的消息数。每执行一次读取操作,读取消息数+1。这种策略下,实际相当于限定了一个读取循环中最多有几个读取操作。FixedRecvByteBufAllocator:继承于DefaultMaxMessagesRecvByteBufAllocator,使用固定大小来执行每一次的ByteBuf分配。AdaptiveRecvByteBufAllocator:继承于DefaultMaxMessagesRecvByteBufAllocator,通过自适应方式来计算最合适的ByteBuf分配大小。
默认情况下,Netty使用作为在上的分
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> 通过本专刊的学习,对网络开发所需掌握的基础理论知识会更加牢固,对网络应用涉及的线程模型,设计模式,高性能架构等更加明确。通过对Netty的源码深入讲解,使得读者对Netty达到“知其然更之所以然”的程度。在遇到一些线上的问题时,具备了扎实理论功底的情况,可以有的放矢而不会显得盲目。 本专刊购买后即可解锁所有章节,故不可以退换哦~ </p> <p> <br /> </p>

