Netty高并发高性能架构设计&NIO空轮训BUG

Netty高并发高性能架构设计

Netty线程模型

Netty是采用主从Reactor模型,简单来说就是两个线程组,每个线程组中的每一个循环的线程都是一个多路复用器selector,也就是Netty是采用NIO的底层IO通信模型,NIO本身是同步非阻塞的IO模型,但是Netty是异步非阻塞的,也就是AIO的一种模型,非阻塞的IO模型都是通过事件响应机制来完成的异步非阻塞,底层是采用EPOLL函数来处理的,NIO本身不是异步非阻塞的,但是Netty对NIO做了封装,完成了异步非阻塞,在Netty的线程组中每一个线程对象都是一个NioEventLoop,就是线程循环事件组,通过循环的线程组来完成事件的注册。
在这里插入图片描述
何谓循环,如上图,比如说我们的bossGroup只有1个线程,而workGroup有 10个,那么如果这时候来了20个连接,那么workGroup只有10个循环线程,那么10个selector注册好了以后,那么从第11个开始,又开始从workGroup中第一个开始注册selector。
在上图中可以很清晰的知道,Netty的服务端是通过NioServerSocketChannel来注册到bossGroup中的selector上的,并且监听了一个端口,那么它的工作主要是接受连接事件,将接受到的连接事件 注册到workgroup中的selector上,当有连接事件过来以后,bossGroup中注册的NioServerSocketChannel就能感知到并且这个时候会创建一个客户端的NioSocketChannel,然后从workGroup中选择一个NioEventLoop,然后将客户端的这个连接注册到这个选择出来的Selector上;当成功的注册到workGroup上的selctor过后,这个连接如果这个时候向服务端写入数据过后,那么这个时候就和bossGroup没有任何关系了,发送的数据直接会到workGroup的selector上,而workGroup的selector接受到读取数据的事件过后,将数据读取出来交给管道上的处理器进行依次处理。
在NioEventLoop中有三个主要的属性,selector、taskQueue和executor,其中selector和taskQueue是每个NioEventLoop单独的,而executor是共用的,就是每个NioEventLoop共用的,taskQueue的作用是将一些事件的任务放入 到taskQueue,然后在监听事件的同事,如果被唤醒,那么会执行添加 到taskQueue中的task任务,taskQueue是一个队列,而放入taskQueue中的task都是一个线程,每次取出了task过后,直接调用task.run就可以调用调用线程 中的业务逻辑,Netty中的多路复用器的注册,boss和work的注册都是通过添加task到taskQueue来执行的。

Netty主从Reactor模型设计的精髓

Reactor是什么呢?Reactor简单来说就是响应式编程,spring webflux就是采用的Reactor的模型,在webfulx中是叫Reactive编程,反正都是差不多的一个意思,响应式编程其实就是基于事件响应驱动模型来做的,在spring 的webflux中,服务器就是默认采用的Netty作为web服务器,所以 Netty 的设计已经被广泛使用到各大开源框架中了;Netty是采用的主从架构,现在已经了解了主从其实就是创建了两个线程组,bossGroup和workGroup,这个两个线程组的模型已经分析的非常透彻了,我们这里来分析下这种模型下的优势:举个例子,我们比喻公司里面的公司的老板就是bossGroup,而下面的员工都是workGroup,那么boss肯定是业务能力比员工强,那么它会很轻松的去拿到很多的项目,而这些项目每个项目都是比较复杂的,不是三两天就可以做好的,但是boss谈项目能力很强,当boss谈到了项目就会依次的分配给下面的员工去实施开发,因为项目的开发是很耗时的,所以就把开发项目比喻成workGroup接受了客户端的读写事件,所以当老板谈好了项目交给了员工过后了,那么老板就不再过问了,除非项目出现问题。所以这个例子就好比比如服务端突然来了几十万的连接,而bossGroup只需要将这几十万的连接依次注册派发到workGroup中的selector上,那么boss的工作就完成了,那么这些注册到workGroup上的连接如果进行读写事件,那么会直接和workGroup的selector进行交互,和boss就没关系了。而每个Selector都是通过线程池派发任务,一直在循环里面监听是否有事件发生,如果有事件发生就会断开阻塞,从而获取到事件key进行处理;所以 这就是Netty的主从架构带来的高性能的具体体现。

无锁串行化设计思想

在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。NIO的多路复用就是一种无锁串行化的设计思想(理解下Redis和Netty的线程模型)为了尽可能提升性能,Netty采用了串行无锁化设计,在IO线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop调用到用户的Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。

零拷贝

直接内存

要明白Netty的零拷贝,首先要知道直接内存和堆内存的区别和含义;直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,某些情况下这部分内存也会被频繁地使用,而且也可能导致OutOfMemoryError异常出现。Java里用DirectByteBuffer可以分配一块直接内存(堆外内存),元空间对应的内存也叫作直接内存,它们对应的都是机器的物理内存。
在这里插入图片描述

如上图,java在运行的时候,在JVM中会创建一块内存区域,这块内存区域就是用来存放应用程序中所产生的各种对象,然后JVM主要对这块内存进行管理,包括创建和回收等操作,这是JVM维护的一块内存区域,在java中也叫堆内存,而在服务器上,除了堆内存以外的内存就可以叫做堆外内存,简单来说就是比如服务器内存为16G,设置的堆内存为4G,那么堆外内存就是12G,这就是一个简单区分,但是如果说我们要在java中使用堆外内存,怎么使用,比如说我们申请了一个buffer缓冲区,按正常来说,这个缓冲区中放入数据过后,这个缓冲区对象和缓冲区对象中所存放的数据都为在堆内存中分配空间,也就是对象本身和对象所存放的数据都是共用堆内存,而如果说在java中要使用堆内存的话,如上图所示,我们创建的对象,依然还是在堆内存中分配空间,但是如果你往这个对象中写入数据,那么这些数据就不在占用堆内存,而是将这些数据写入了堆外内存,而堆内存中的缓冲区对象与堆外内存的数据地址建立一个应用关系,这样的话,不管数据多大,对于堆内存是对象是不变的,只是堆外内存占用大一点。
在jvm启动的参数中可以直接设置直接内存的大小,需要注意的是如果没有设置直接内存的大小,那么默认的大小就是堆内存启动的大小,可以通过‐XX:MaxDirectMemorySize=进行设置;如果使用内存的时候如果判断空间是否够,如果空间不够,会触发一次full gc,回收堆内存对象,如果堆内存引用的堆外内存被回收了,那么堆外内存也将被回收,如果释放完还不够内存,那么会直接报OOM异常;堆外内存使用ByteBuffer申请ByteBuffer.allocateDirect(100)就可以直接申请堆外内存


import java.nio.ByteBuffer;

public class DirectMemoryTest {
    public static void heapAccess() {
        long startTime = System.currentTimeMillis();
        //分配堆内存
        ByteBuffer buffer = ByteBuffer.allocate(1000);
        for (int i = 0; i < 100000; i++) {
            for (int j = 0; j < 200; j++) {
                buffer.putInt(j);
            }
            buffer.flip();
            for (int j = 0; j < 200; j++) {
                buffer.getInt();
            }
            buffer.clear();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("堆内存访问:" + (endTime - startTime) + "ms");
    }

    public static void directAccess() {
        long startTime = System.currentTimeMillis();
        //分配直接内存
        ByteBuffer buffer = ByteBuffer.allocateDirect(1000);
        for (int i = 0; i < 100000; i++) {
            for (int j = 0; j < 200; j++) {
                buffer.putInt(j);
            }
            buffer.flip();
            for (int j = 0; j < 200; j++) {
                buffer.getInt();
            }
            buffer.clear();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("直接内存访问:" + (endTime - startTime) + "ms");
    }

    public static void heapAllocate() {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            ByteBuffer.allocate(100);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("堆内存申请:" + (endTime - startTime) + "ms");
    }

    public static void directAllocate() {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            ByteBuffer.allocateDirect(100);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("直接内存申请:" + (endTime - startTime) + "ms");
    }

    public static void main(String args[]) {
        for (int i = 0; i < 10; i++) {
            heapAccess();
            directAccess();
        }
        System.out.println();
        for (int i = 0; i < 10; i++) {
            heapAllocate();
            directAllocate();
        }
    }
}

输出如下:

堆内存访问:68ms
直接内存访问:74ms
堆内存访问:35ms
直接内存访问:51ms
堆内存访问:76ms
直接内存访问:83ms
堆内存访问:35ms
直接内存访问:50ms
堆内存访问:52ms
直接内存访问:47ms
堆内存访问:43ms
直接内存访问:54ms
堆内存访问:51ms
直接内存访问:52ms
堆内存访问:41ms
直接内存访问:31ms
堆内存访问:31ms
直接内存访问:32ms
堆内存访问:32ms
直接内存访问:35ms

堆内存申请:28ms
直接内存申请:145ms
堆内存申请:9ms
直接内存申请:37ms
堆内存申请:0ms
直接内存申请:32ms
堆内存申请:0ms
直接内存申请:29ms
堆内存申请:0ms
直接内存申请:34ms
堆内存申请:0ms
直接内存申请:32ms
堆内存申请:0ms
直接内存申请:205ms
堆内存申请:0ms
直接内存申请:36ms
堆内存申请:0ms
直接内存申请:158ms
堆内存申请:0ms
直接内存申请:33ms

可以看出堆外内存在申请的时候性能不如堆内存的申请,而在进行访问的时候是直接内存访问较快
申请内存:堆内存 > 堆外内存
数据访问:堆外 > 堆内
在java虚拟机实现上,本地IO一般会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)。
上面分析了如果创建的缓冲区对象是堆内存的话,数据是存放在堆中的,如果是直接内存的话,那么数据是存放在直接内存中的,这里通过debug来看下

堆内存的buffer数据存放:
在这里插入图片描述
直接内存:
在这里插入图片描述
从上面可以看出,堆内存是存放数据到堆中的,所以在buffer中有一个数组,存放了数据,作为数据的缓冲区,而直接内存的数据是存放在直接内存中在,在buffer中只有一个address,就是直接内存数据的内存地址,就是堆内的buffer对象与直接内存的数据建立了一种引用关系,这样访问数据的时候是可以直接通过这个引用关系找到直接内存中的数据,从而操作数据。
直接内存分配源码分析

public static ByteBuffer allocateDirect(int capacity) {   
 return new DirectByteBuffer(capacity);
 }
 DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap, null);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    //判断是否有足够的直接内存空间分配,可通过-XX:MaxDirectMemorySize=<size>参数指定直接内存最大可分配空间,
    //如果不指定默认为最大堆内存大小,    
    //在分配直接内存时如果发现空间不够会显示调用System.gc()触发一次full gc
    //回收掉一部分无用的直接内存的引用对象,同时直接内存也会被释放掉    
    //如果释放完分配空间还是不够会抛出异常java.lang.OutOfMemoryError
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        // 调用unsafe本地方法分配直接内存
        base = UNSAFE.allocateMemory(size);
    } catch (OutOfMemoryError x) {
          // 分配失败,释放内存
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    UNSAFE.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
      // 使用Cleaner机制注册内存回收处理函数,当直接内存引用对象被GC清理掉时,   
       // 会提前调用这里注册的释放直接内存的Deallocator线程对象的run方法
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;




}
// 申请一块本地内存。内存空间是未初始化的,其内容是无法预期的。
// 使用freeMemory释放内存,使用reallocateMemory修改内存大小
public native long allocateMemory(long bytes);
// openjdk8/hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size)) 
 UnsafeWrapper("Unsafe_AllocateMemory");  
 size_t sz = (size_t)size;  
 if (sz != (julong)size || size < 0) {  
 THROW_0(vmSymbols::java_lang_IllegalArgumentException()); 
 }  if (sz == 0) {    return 0;  }  
 sz = round_to(sz, HeapWordSize);  
 // 调用os::malloc申请内存,内部使用malloc这个C标准库的函数申请内存 
 void* x = os::malloc(sz, mtInternal);
 if (x == NULL) {  
 THROW_0(vmSymbols::java_lang_OutOfMemoryError());  
 }  
 //Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);    
 return addr_to_java(x);
 UNSAFE_END

使用直接内存的优缺点:
优点:

1.不占用堆内存空间,减少了发生GC的可能
2.java虚拟机实现上,本地IO会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)。
缺点:
1.初始分配较慢
2.没有JVM直接帮助管理内存,容易发生内存溢出。为了避免一直没有FULL GC,最终导致直接内存把物理内存耗完。我们可以指定直接内存的最大值,通过-XX:MaxDirectMemorySize来指定,当达到阈值的时候,调用system.gc来进行一次FULL GC,间接把那些没有被使用的直接内存回收掉。

Netty零拷贝

理解了直接和内存和堆内存的概念和原理过后,我们再来分析下Netty的零拷贝,Netty的零拷贝其实就是采用了直接内存来设计的,原理和直接内存的原理一样,就是减少了数据的拷贝次数,不是说零拷贝,只是在堆内是零拷贝,但是在整个socket的交易过程中还是有拷贝的。
在这里插入图片描述
在网络中客户端向服务端发送数据,一般是网卡接受到数据放入socket缓冲区,然后向cpu发出硬中断,然后通知cpu调度,整个时候需要将socket缓冲区的数据拷贝到内核空间内存,然后应用程序从直接内存中读取缓冲区数据到堆内的缓冲区,处理过后也是要原来拷贝回去,整个过程就有4次拷贝,在上面的的直接内存的分析中,我们已经知道了直接内存在分配的时候性能不如堆内块,而数据的操作肯定是直接内存的块,原因是因为在网络数据交换过程中,用户空间不能直接操作数据,需要将接受到的数据从内核拷贝到用户线程空间中,然后才能操作,最后再回写过去,这期间经历了多次拷贝,所以性能不如直接内存的操作的块,因为直接内存中,用户空间只是建立了直接内存的一个引用(数据的内存地址),那么这样在用户空间就可直接操作直接内存中的数据,所以性能要好很多,所以对于使用直接内存来说,就减少了拷贝的次数,但是如果从用户空间来说的话,使用直接内存的方式是没有拷贝的次数的,所以Netty的零拷贝就是使用直接内存来操作接受到的数据从而达到零拷贝,注意的是Netty的零拷贝只是在我们的jvm中是不存在拷贝的,而整个交易链条上还是存在拷贝的,所以Netty的零考虑是针对于jvm,也就是用户空间来说的。

了解了零拷贝就是采用的直接内存的方式,而在应用中建立的缓冲区对象中的只是存放了数据到直接内存中的内存地址,那么我们这里来看下Netty是如何实现的,对于的数据的零拷贝主要是对数据的读写进行实现零拷贝,所以这里大概看下netty的读写源码,比如read源码NioByteUnsafe.read()
在这里插入图片描述
这个read方法就是channel接受到了数据,红框框里面就是将channel接受到的数据读取到bytebuf中,零拷贝的实现就在
allocHandle.allocate这行代码
io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator.MaxMessageHandle#allocate

public ByteBuf allocate(ByteBufAllocator alloc) {
    //调用ioBuffer从缓冲区读取数据
    return alloc.ioBuffer(guess());
}

io.netty.buffer.AbstractByteBufAllocator#ioBuffer(int)

@Override
public ByteBuf ioBuffer(int initialCapacity) {
    if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
        //这里实现的零拷贝,说白了就是建立bytebuff与直接内存中缓冲区数据的引用关系
        return directBuffer(initialCapacity);
    }
    return heapBuffer(initialCapacity);
}

ByteBuf内存池设计

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer(相当于一个内存块),情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty提供了基于ByteBuf内存池的缓冲区重用机制。需要的时候直接从池子里获取ByteBuf使用即可,使用完毕之后就重新放回到池子里去。下面我们一起看下Netty ByteBuf的实现:
在这里插入图片描述
可以看下netty的读写源码里面用到的ByteBuf内存池,比如read源码NioByteUnsafe.read()
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
继续看newDirectBuffer方法,我们发现它是一个抽象方法,由AbstractByteBufAllocator的子类负责具体实现,代码如下:
在这里插入图片描述
代码跳转到PooledByteBufAllocator的newDirectBuffer方法,从Cache中获取内存区域PoolArena,调用它的allocate方法进行内存分配:
在这里插入图片描述
PoolArena的allocate方法如下:
在这里插入图片描述
我们重点分析newByteBuf的实现,它同样是个抽象方法,由子类DirectArena和HeapArena来实现不同类型的缓冲区分配
在这里插入图片描述
我们这里使用的是直接内存,因此重点分析DirectArena的实现
在这里插入图片描述
最终执行了PooledUnsafeDirectByteBuf的newInstance方法,代码如下:
在这里插入图片描述
通过RECYCLER的get方法循环使用ByteBuf对象,如果是非内存池实现,则直接创建一个新的ByteBuf对象。
简单来说就是比如数据过来了,数据有4M,那么我们都知道内存中碎片化非常严重的,而分配空间需要连续的空间,如果说每次来都是去内存中找空间分配,那么是非常耗时的,所以Netty这里设计的缓冲区bytebuff就是提前将bytebuff的空间分配好,有数据来了直接取出来进行分配,用完还回去,减少了数据分配空间带来的性能损耗,其实还有一个点就是我们知道直接内存的分配是比较耗时的,没有堆内存分配块,所以Netty这里提前分配好,类似数据库连接池和线程池一个概念。

灵活的TCP参数配置能力

合理设置TCP参数在某些场景下对于性能的提升可以起到显著的效果,例如接收缓冲区SO_RCVBUF和发送缓冲区SO_SNDBUF。如果设置不当,对性能的影响是非常大的。通常建议值为128K或者256K。Netty在启动辅助类ChannelOption中可以灵活的配置TCP参数,满足不同的用户场景。
在这里插入图片描述

ByteBuf扩容机制

如果我们需要了解ByteBuf的扩容,我们需要先了解ByteBuf中定义的几个成员变量,再从源码的角度来分析扩容。
在这里插入图片描述
minNewCapacity:表用户需要写入的值大小
threshold:阈值,为Bytebuf内部设定容量的最大值
maxCapacity:Netty最大能接受的容量大小,一般为int的最大值
ByteBuf核心扩容方法
进入ByteBuf源码中,深入分析其扩容方法: idea源码进入:ByteBuf.writeByte()->AbstractByteBuf->calculateNewCapacity
1.判断目标值与阈值threshold(4MB)的大小关系,等于直接返回阈值
在这里插入图片描述
2.采用步进4MB的方式完成扩容
在这里插入图片描述
3.采用64为基数,做倍增的方式完成扩容
在这里插入图片描述
总结:Netty的ByteBuf需要动态扩容来满足需要,扩容过程: 默认门限阈值为4MB(这个阈值是一个经验值,不同场景,可能取值不同),当需要的容量等于门限阈值,使用阈值作为新的缓存区容量 目标容量,如果大于阈值,采用每次步进4MB的方式进行内存扩张((需要扩容值/4MB)*4MB),扩张后需要和最大内存(maxCapacity)进行比较,大于maxCapacity的话就用maxCapacity,否则使用扩容值 目标容量,如果小于阈值,采用倍增的方式,以64(字节)作为基本数值,每次翻倍增长64 -->128 --> 256,直到倍增后的结果大于或等于需要的容量值。

handler的生命周期回调接口调用顺序

在channel的pipeline里如下handler:ch.pipeline().addLast(new LifeCycleInBoundHandler());
handler的生命周期回调接口调用顺序: handlerAdded -> channelRegistered -> channelActive -> channelRead -> channelReadComplete -> channelInactive -> channelUnRegistered -> handlerRemoved
handlerAdded: 新建立的连接会按照初始化策略,把handler添加到该channel的pipeline里面,也就是channel.pipeline.addLast(new LifeCycleInBoundHandler)执行完成后的回调;
channelRegistered: 当该连接分配到具体的worker线程后,该回调会被调用。
channelActive:channel的准备工作已经完成,所有的pipeline添加完成,并分配到具体的线上上,说明该channel准备就绪,可以使用了。
channelRead:客户端向服务端发来数据,每次都会回调此方法,表示有数据可读;
channelReadComplete:服务端每次读完一次完整的数据之后,回调该方法,表示数据读取完毕;
channelInactive:当连接断开时,该回调会被调用,说明这时候底层的TCP连接已经被断开了。
channelUnRegistered: 对应channelRegistered,当连接关闭后,释放绑定的workder线程;
handlerRemoved: 对应handlerAdded,将handler从该channel的pipeline移除后的回调方法。

NIO的空轮训BUG

Nio的空轮训bug是什么呢?如果说对于NIO使用比较多的时候可能会出现这个问题,具体来说还是linxu底层的一个epoll函数的问题,但是这个问题到目前都还是没有解决,不管是linxu层面的epoll函数,还是JDK中的NIO的使用也没有解决这个问题,这个空轮训的bug就是说在在调用 selector.select()的时候,我们都知道当有事件发生的时候,select()调用的就是linux底层epoll_wait,如果有事件,比如连接,数据的读写,那么select()退出阻塞,将被唤醒,被唤醒过后执行selectKeys就可以得到具体的事件,但是如果出现了空轮训的bug过后,就是select()突出阻塞过后,其实根本没有事件过来,就是selectKyes也是空的,而且出现这个bug过后,每次循环的select()方法都不会阻塞,就一直在那里循环,根本不会阻塞,一直循环,导致cpu达到100%,就是说出现了这个bug过后,那么这个线程就一直占用着cpu,一会进行空轮训,而且不能消除,所以这就是NIO的空轮训BUG。但是出现这个bug过后,除非干掉这个select多路复用器或者重启才能解决。

Netty如何解决NIO空轮训BUG

针对NIO的空轮训bug,Netty是对其进行解决的,至少在我的这个版本中是解决了的,之前分析源码的时候也看到了,只是没有分析,它的具体解决思路就是每次select返回过后弄了一个计数器,每次+1,如果select()返回过后,如果没有任何事件过来,也不是由于select超时而导致的返回的话,那么如果这个计数器累加到一定的时候,那么Netty就认为出现了空轮训的bug,那么这个时候Netty就重新创建一个select,将出现空轮训bug的select上的事件全部移植到新创建的select上,最后关闭之前空循环的select;如果说select阻塞返回的时候有事件返回的或者由于超时返回的,那么这个计数器重新设置为0,意思重新开始计数。Netty中的空轮训bug是在NioEventLoop中的run方法解决的,源码分析已经在上篇笔记分析了,这里不过多的解释为什么在NioEventLoop的run方法中,因为run方法就是Netty多路复用器事件发生的聚集地

/**
 * 这里的run方法才是真正的将NIOServerSocketChannel注册到多路复用器上
 */
@Override
protected void run() {
    int selectCnt = 0;
    for (;;) {
        try {
            int strategy;
            try {
                /**
                 *这里就是判断现在是那种事件,如果Netty在启动的时候,这里的case都不满足,但是如果启动完成了
                 *那么再来调用就会调用SELECT,所以根据不同的情况来的
                 */
                strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
                switch (strategy) {
                case SelectStrategy.CONTINUE:
                    continue;

                case SelectStrategy.BUSY_WAIT:
                    // fall-through to SELECT since the busy-wait is not supported with NIO

                case SelectStrategy.SELECT:
                    //这里就是调用的多路复用器的select方法,就是在nio中的selector.select()方法
                    /**
                     * curDeadlineNanos这个是计算调用select的时候阻塞的时间的,因为netty这里 还有很多事情没有做
                     * 注册多路复用器都还没有做,所以这里要计算出一个时间,如果这个时间过了,还没有连接过来,那么也会唤醒,
                     * 不会一直阻塞
                     */
                    long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                    if (curDeadlineNanos == -1L) {
                        curDeadlineNanos = NONE; // nothing on the calendar
                    }
                    nextWakeupNanos.set(curDeadlineNanos);
                    try {
                        /**
                         * 如果是第一次启动的注册多路复用器是不会进入这个 if的,因为
                         * hasTasks就是判断当前的多路复用器注册队列中是是否有任务,如果有任务,
                         * 那么返回true,没有就返回 false的,netty的意思就是服务端刚启动要注册的是NioServerSocketChannel
                         * 所以不阻塞,否则阻塞了没意义,因为多路复用器都还没有注册的有channel,根本没有办法监听
                         */
                        if (!hasTasks()) {
                            strategy = select(curDeadlineNanos);
                        }
                    } finally {
                        // This update is just to help block unnecessary selector wakeups
                        // so use of lazySet is ok (no race condition)
                        nextWakeupNanos.lazySet(AWAKE);
                    }
                    // fall through
                default:
                }
            } catch (IOException e) {
                // If we receive an IOException here its because the Selector is messed up. Let's rebuild
                // the selector and retry. https://github.com/netty/netty/issues/8566
                rebuildSelector0();
                selectCnt = 0;
                handleLoopException(e);
                continue;
            }

            /**
             * strategy在Netty启动的时候是0
             */
             //select事件发生的计数器
            selectCnt++;
            cancelledKeys = 0;
            needsToSelectAgain = false;
            final int ioRatio = this.ioRatio;
            boolean ranTasks;
            if (ioRatio == 100) {
                try {
                    if (strategy > 0) {
                        //调用select有事件发生的时候会来调用,这里面就是NIO的那个获取selectKeys然后处理的逻辑,netty封装了
                        processSelectedKeys();
                    }
                } finally {
                    // Ensure we always run tasks.
                    ranTasks = runAllTasks();
                }
            } else if (strategy > 0) {
                final long ioStartTime = System.nanoTime();
                try {
                    //调用select有事件发生的时候会来调用
                    processSelectedKeys();
                } finally {
                    // Ensure we always run tasks.
                    final long ioTime = System.nanoTime() - ioStartTime;
                    ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }
            } else {
                //Netty启动的时候调用,这里才是真正的多路复用器的注册,就是调用最开始的那个execute(Runable task)中的那个task
                //就是从taskQueue中取出这个线程任务然后执行run方法
                ranTasks = runAllTasks(0); // This will run the minimum number of tasks
            }

            if (ranTasks || strategy > 0) {
                if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
                    logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                            selectCnt - 1, selector);
                }
                selectCnt = 0;
                //unexpectedSelectorWakeup select多路复用器空轮训的修复
            } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
                 //空循环bug修复完成过后,将计数器重置为0
                selectCnt = 0;
            }
        } catch (CancelledKeyException e) {
            // Harmless exception - log anyway
            if (logger.isDebugEnabled()) {
                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                        selector, e);
            }
        } catch (Error e) {
            throw (Error) e;
        } catch (Throwable t) {
            handleLoopException(t);
        } finally {
            // Always handle shutdown even if the loop processing threw an exception.
            try {
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Error e) {
                throw (Error) e;
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }
}

io.netty.channel.nio.NioEventLoop#unexpectedSelectorWakeup

// returns true if selectCnt should be reset
//官方对这个的解释是返回true,如果计数器被重置了,也是就是如果返回true,那么计数器重新开始计数
//返回true的意思就是空轮训已经修复,返回false表示没有出现空轮训或者空轮训次数没有达到上限
private boolean unexpectedSelectorWakeup(int selectCnt) {
    if (Thread.interrupted()) {
        // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
        // As this is most likely a bug in the handler of the user or it's client library we will
        // also log it.
        //
        // See https://github.com/netty/netty/issues/2426
        if (logger.isDebugEnabled()) {
            logger.debug("Selector.select() returned prematurely because " +
                    "Thread.currentThread().interrupt() was called. Use " +
                    "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
        }
        return true;
    }
    //这里的意思就是空轮训次数达到了512次过后,进行空轮训bug的修复
    if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
            selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
        // The selector returned prematurely many times in a row.
        // Rebuild the selector to work around the problem.
        logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
                selectCnt, selector);
       //空循环bug的修复,重新构建select多路复用器
        rebuildSelector();
        return true;
    }
    return false;
}

//这是全局变量的代码片段,在static代码区域中,表示空轮训出现了,那么轮训多少次开始认定为多路复用器出现了空轮训
//如果没有配置的话,默认是512次
int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
if (selectorAutoRebuildThreshold < MIN_PREMATURE_SELECTOR_RETURNS) {
    selectorAutoRebuildThreshold = 0;
}

SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;
/**
 * Replaces the current {@link Selector} of this event loop with newly created {@link Selector}s to work
 * around the infamous epoll 100% CPU bug.
 *这个方法官方解释额意思就是由于epoll的问题导致了空轮训以至于cpu暴增到100%,这个方法就是解决这种情况的
 */
public void rebuildSelector() {
    //根据当前执行的线程是否是Eventloop的内部线程而决定是异步调用还是同步调用
    if (!inEventLoop()) {
        execute(new Runnable() {
            @Override
            public void run() {
                rebuildSelector0();
            }
        });
        return;
    }
    rebuildSelector0();
}
//重新构建selector
private void rebuildSelector0() {
    //将老的select赋值给oldSelector 
    final Selector oldSelector = selector;
    //定义一个新的Selector
    final SelectorTuple newSelectorTuple;

    if (oldSelector == null) {
        return;
    }

    try {
        //调用JDK的NIO api创建一个新的selector,赋值给newSelectorTuple 
        newSelectorTuple = openSelector();
    } catch (Exception e) {
        logger.warn("Failed to create a new Selector.", e);
        return;
    }

    // Register all channels to the new Selector.
    int nChannels = 0;
    //这个for循环的意思就是循环老的selector上的所有事件,将老selector上的所有事件全部注册到新的selector上
    for (SelectionKey key: oldSelector.keys()) {
        Object a = key.attachment();
        try {
            if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
                continue;
            }

            int interestOps = key.interestOps();
            key.cancel();
            //将channel注册到新的selector上unwrappedSelector,interestOps表示事件类型,连接、读写事件
            //这里调用的api是JDK的NIO API
            SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
            if (a instanceof AbstractNioChannel) {
                // Update SelectionKey
                ((AbstractNioChannel) a).selectionKey = newKey;
            }
            nChannels ++;
        } catch (Exception e) {
            logger.warn("Failed to re-register a Channel to the new Selector.", e);
            if (a instanceof AbstractNioChannel) {
                AbstractNioChannel ch = (AbstractNioChannel) a;
                ch.unsafe().close(ch.unsafe().voidPromise());
            } else {
                @SuppressWarnings("unchecked")
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                invokeChannelUnregistered(task, key, e);
            }
        }
    }

   //将本NioEventLoop中的selector替换成新创建的selector
    selector = newSelectorTuple.selector;
    unwrappedSelector = newSelectorTuple.unwrappedSelector;

    try {
        // time to close the old selector as everything else is registered to the new one
        //将老的selector进行关闭完成了空轮训bug的修复
        oldSelector.close();
    } catch (Throwable t) {
        if (logger.isWarnEnabled()) {
            logger.warn("Failed to close the old Selector.", t);
        }
    }

    if (logger.isInfoEnabled()) {
        logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值