从内核角度剖析Netty高性能的奥秘IO多路复用模型与Reactor模式

news2024/11/20 23:37:43

Netty 是Jboos 提供的java开源框架, 是基于非阻塞IO(NIO)的客户端/服务器编程框架, 它既能快速开发高并发、高可用、高可靠的网络服务器程序,也能开发高可用、高可靠的客户端程序。

Netty 作为异步框架, Netty 的所有IO操作都是异步非阻塞的, 通过feature-Listener机制,用户可以方便地主动获取或者通过通知机制获取IO操作结果。相比JDK原生NIO,Netty具有API使用简单、功能强大、定制能力强、性能高、成熟稳定等优点。

netty 主要用于两大场景:
场景一:web开发,在web应用中通讯协议基本都是http协议,web的后台基本使用的都是rest协议接口,rest协议采用的是http传输。 主要是集中在to C 的高并发应用中,主要是集中在基础性组件或 中台性质的组件,例如推送中台、服务总线。

场景二:非Http协议,典型的就是IM即时通讯。

IO 高并发底层原理

首先,我们来看下在java程序中进行IO操作的代码是如何写的? 面向流的IO模型代码如下:

byte[] input = new byte[NioDemoConfig.SERVER_BUFFER_SIZE];
// 读取数据 
socket.getInputStream().read(input);

Logger.info("收到:"+new String(input));

// 处理业务逻辑,获取处理结果 
byte[] output =input;
// 写入结果 
socket.getOutputStream().write(output);

面向缓冲区的NIO模型write代码如下:

   /**
     * 客户端
     */
    public static void startClient() throws IOException {
        InetSocketAddress address =
                new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP,
                        NioDemoConfig.SOCKET_SERVER_PORT);

        // 1、获取通道(channel)
        SocketChannel socketChannel = SocketChannel.open(address);
        // 2、切换成非阻塞模式
        socketChannel.configureBlocking(false);
        //不断的自旋、等待连接完成,或者做一些其他的事情
        while (!socketChannel.finishConnect()) {

        }

        Logger.info("客户端连接成功");
        // 3、分配指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("hello world".getBytes());
        byteBuffer.flip();
        socketChannel.write(byteBuffer);
        Logger.info("客户端写入成功");

        socketChannel.shutdownOutput();
        socketChannel.close();
    }

面向缓冲区的read代码如下:

     //若选择键的IO事件是“可读”事件,读取数据
    SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
    // 读取数据
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    int length = 0;
    while ((length = socketChannel.read(byteBuffer)) > 0) {

        byteBuffer.flip();

        Logger.info(new String(byteBuffer.array(), 0, length));

        byteBuffer.clear();

    }
    socketChannel.close();

以上代码是BIO /Nio的读写需要依赖操作系统的IO,java的IO操作属于应用程序,需要通过JNI调用C语言的库函数进行操作系统层面IO读写。如下图所示:
在这里插入图片描述

IO的本质是内核(CPU和内存)与磁盘、网卡等物理设备之间数据转移的过程。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内核空间,也有访问底层硬件设备的权限。为了避免用户进程直接操作内核空间,保证内核安全;操作系统将虚拟内存分为用户空间和内核空间,它们需要不同的执行权限。内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方。当进程运行在内核空间时处于内核态,当进程运行在用户空间时就处于用户态。

用户态、内核态的指令最终都是由CPU执行的,CPU指令会根据其重要程度划分了不同的权限,有些指令执行失败了没事,但是有些指令失败会导致操作系统的崩溃,甚至会重启系统。如果将这些指令随意开放给应用程序,就会让整个系统崩溃的概率大大增加。

应用程序是不允许直接在内核空间区域进行读写,也不允许直接调用内核代码定义的函数的。当用户态的程序需要向操作系统申请更高权限的服务时,就需要通过系统调用把用户态切换到内核态才可以向内核发起请求。

内核空间和用户空间所占的虚拟内存比例是1:3。如下图所示:
在这里插入图片描述

内核空间是驻留在内核中,它是为操作系统的内核保留的,按照访问权限,内核空间可以分为进程私有和进程共享两块区域。

每个用户进程都有一个单独的用户空间,用户空间的内存区域包括 运行时栈、运行时堆、代码段、未初始化的数据段、已初始化的数据段。

在TCP/IP网络分层模型中,整个协议栈被分成物理层、网络链路层、网络层、传输层和应用层。应用层属于用户空间, 对用的是我们常见的nginx、FTP等各种应用,以及我们写的各种服务端程序。 Linux 内核空间以及网卡驱动主要实现网络链路层、网络层和传输层的功能,内核为用户空间提供socket接口来支持用户进程的访问。 以linux 的视角看到的TCP/IP 网络分层模型如下图:

在这里插入图片描述

基于TCP/IP 为基础,网络数据收包和发包的路径示意图如下:
在这里插入图片描述

上图左半部分是读取网络数据包的过程,右半部分是发送数据网络的过程。

应用程序读取网络数据包的过程如下:
1、网卡收到数据以后,以DMA的方式把网卡数据写到内存的RingBuffer中;
2、向CPU发起一个中断,以通知CPU有数据到达;
3、当CPU收到中断请求后,简单处理后发出软中断请求。内核线程ksoftirqd发现有软中断请求到来,先关闭硬中断,尽快释放CPU资源。
4、将RingBuffer中的数据帧skb取下来放到内核申请一个内核态的skb内存中。
5、当数据帧skb 到达协议栈的网络层,取出数据帧的ip头,判断该数据帧该去哪里。去除IP头后,将数据包交给传输层处理。
6.当我们采用的是TCP协议时,数据包到达传输层后,取出TCP头后,根据四元组(源ip、源接口、目的IP、目的端口)查找对应的socket。
7、找到对应的socket后,将数据帧中的传输数据复制到socket的接收缓冲区中。
8.socket 接收缓冲区有数据后,CPU将socket 接收缓存冲的传输数据复制到用户空间的缓冲区。 最后系统调用read返回。应用程序读取数据。

应用程序发送网络数据包的过程如下:
1、当应用程序调用write系统调用发送数据时,用户线程从用户空间切换成内核态。
2、CPU把用户空间缓冲区的数据复制到Socket的发送缓冲区;
3、数据通过发送流程进入内核协议栈的传输层设置TCP头;
4、设置TCP头完成后后;通过内核函数进入内核协议栈的网络层进行IP头设置。
5、经过内核一些列的设置封装好一个完整的数据帧,将数据帧skb添加到RingBuffer中,然后通过DMA方式将数据帧通过物理网卡发送出去。
6、数据发送完成后,网卡设备向CPU发送一个硬中断,CPU简单处理完后,发出软中断对RingBuffer清理解除.

网路数据包读取和发送的过程中,涉及的性能开销主要有:
1、应用程序通过系统调用从用户态与内核态相互转换的开销;
2、内核缓存区与用户缓存区的数据相互复制的开销;
3、内核线程ksoftirqd响应软中断的开销;
4、CPU 响应硬中断的开销;
5、DMA拷贝网络数据包到内存缓冲区的开销;

也许你会问为什么不直接从网卡读写数据,我们 先来看下内存、机械硬盘、固态硬盘、网卡的IO性能对比情况,如下所示:
在这里插入图片描述

从上图中可以看出 ,网卡的读取速率是内存的百分之一,相比内存来说是很慢的,主要是从速率上考虑。

(阻塞、非阻塞 )与(异步、同步)

再了解网络数据包接收和发送的过程后,我们接下来谈谈阻塞、非阻塞和异步、同步的问题。

首先网络数据包的接收和发送过程可以分为两个阶段,如下图所示:
在这里插入图片描述

第一阶段是数据准备,此阶段是网卡与socket 之间通过DMA方式复制数据帧,CPU不参与其中,
第二阶段是数据复制, 内核缓冲区和用户缓冲区的数据相互复制的过程,同步与异步主要是这个阶段;同步是指用户空间主动发起IO请求,系统内核是被动接受方; 异步是指内核主动发起IO请求,用户空间是被通知的一方,属于被动接受方。

阻塞与非阻塞指的是用户进程的执行状态。阻塞是指用户进程一直等待,不能干别的事情;非阻塞是指用户进程拿到内核返回的状态就返回用户空间,可以去干其他事情。

IO 模型

IO模型的选择是构建一个高性能网络框架的基础,使用什么IO模型来读写数据将在很大程度上决定了网络框架的IO性能。IO模型有五种:同步阻塞IO(Blocking IO ,BIO)、同步非阻塞IO(Non-Blocking IO,NIO)、IO多路复用(IO Multiplexing)、信号驱动IO模型、异步IO(Asynchronous IO)

同步阻塞IO(BIO)

从用户进程发起创建Socket 到一个网络包到达网卡被用户进程接收,同步阻塞IO总体流程如下图:
在这里插入图片描述

从上图可以看出, 用户进程发起创建socket指令进入系统调用后,用户进程从用户态切换成了内核态,用户进程会先到socket对象的接收队列中查看是否有数据,没有数据的话就把自己添加到socket对应的等待队列中。当网络数据到达网卡后,通过DMA复制把数据复制到socket的缓存区,当socket 缓存区有数就,ksoftirqd 内核线程会通知等待队列上的用户进程。

在整个过程中, 由java应用程序(用户空间)主动发起IO请求,系统内核是被动接受方, 所以是同步IO; 在整个获取网络数据包的过程中,用户进程主动发起IO请求后,需要一直等待系统调用返回,在整个过程主动发起IO请求的用户进程状态是阻塞的。

同步阻塞IO的开销主要有以下:

(1)进程通过系统调用接收一个socket 上的数据时,如果数据没有达到进程就被从CPU撒花姑娘拿下来,然后再换上另一个进程,这导致一次进程上下文切换的开销;

(2)当连接上的数据就绪的时候,睡眠的进程又会被唤醒,导致进程切换的开销;

(3)一个进程同时只能等待一条连接,如果有很多并发,则需要很多进程。

同步阻塞IO 的特点是在内核进行IO执行的两个阶段,发起IO请求的用户进程被阻塞了;

同步阻塞Io的优点是应用的程序开发非常简单,在阻塞等待数据期间,用户线程挂起,用户线程基本不占用CPU资源。

阻塞IO 的缺点是:一般情况下,会为每个连接配备一个独立的线程,一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。在高并发应用场景中,阻塞 IO 模型是性能很低的,基本上是不可用的。

同步非阻塞IO(NIO)

从用户进程发起创建Socket 到一个网络包到达网卡被用户进程接收,同步非阻塞IO总体流程如下图:
在这里插入图片描述

与同步阻塞IO不同的时候, 没有等待队列,用户进程发起系统调用后,不管用没有数据会立即返回。

在NIO模型中,用户空间主动发起IO请求进行系统调用会出现以下两种情况:
(1 )在socket缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的
信息。
(2 )在socket缓冲区中有数据的情况下,socket缓存区的数据复制到用户空间缓存区的过程是阻塞的,复制完成后,系统调用返回成功,用户进程可以开始处理用户空间的缓存数据。

同步非阻塞I并不是全程不阻塞,只是相对于与同步阻塞来说的。同步非阻塞IO 的特点:应用程序的线程需要不断地进行 IO 系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成 IO 系统调用为止。

同步非阻塞IO 的优点:每次发起的 IO 系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。
同步非阻塞IO 的缺点:不断地轮询内核,这将占用大量的 CPU 时间,效率低下。

在高并发应用场景中,同步非阻塞IO 是性能很低的,也是基本不可用的,一般 Web 服务器都不使用这种 IO 模型。在 Java 的实际开发中,也不会涉及这种 IO 模型。但是NIO的作用是为IO多路复用模型提供了基础。

IO多路复用模式

IO 多路复用模型的出现主要是为了解决同步非阻塞IO模型中轮询等待的问题。大多数的高并发服务端的程序,一般都是基于linux系统的, 大多采用IO多路复用模型,包括netty框架也是采用的IO多路复用模型。

首先,我们来明确一下什么是多路?什么是复用?
多路:多路指的是需要处理的众多连接。
复用: 用有限的资源处理众多连接上的读写事件。即多个连接可以复用一个独立的线程去专门处理多个连接上的读写。

现在问题的关键是如何去实现复用,在同步非阻塞IO 模型中,是通过不断的轮询众多连接的socket缓存区看是是否有数据,如果有则处理,如果没有则继续轮询下一个Socket。这样就达到了用一个线程去处理众多连接上的读写事件了。但是频繁的系统调用也会带来大量的上下文切换开销,当并发量足够大的时候,就会导致性能下降。

其实我们可以把频繁的轮询操作交给操作系统来完成,从而避免用户空间频繁的系统调用带来的所带来的性能开销。目前支持IO多路复用的系统调用有select、poll和epoll。select 系统调用,几乎在所有的操作系统上都有支持,具有良好的跨平台特性。poll相当于改进版的select,工作原理基本上和select没有本质区别,只是解决了select 的1024个文件描述符fd的限制。epoll 是在 Linux 2.6 内核中提出的,是 select系统调用的 Linux 增强版本。

IO多路复用模型的特点: IO 多路复用模型的 IO 涉及两种系统调用,一种是 IO 操作的系 统调用,另一种是 select/epoll 就绪查询系统调用。 IO 多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用 select/epoll 。

和 NIO 模型相似,多路复用 IO 也需要轮询。负责 select/epoll 状态查询调用的线程,需要 不断地进行 select/epoll 轮询,查找出达到 IO 操作就绪的 socket 连接。

IO 多路复用模型的优点:一个选择器查询线程,可以同时处理成千上万的网络连接, 所以,用户程序不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。 这是一个线程维护一个连接的阻塞 IO 模式相 比,使用多路 IO 复用模型的最大优势。

IO多路复用模型的缺点:本质上, select/epoll 系统调用是阻塞式的,属于同步 阻塞 IO 。 都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个事件的查询过程是 阻塞的。

epoll

epoll在select、poll、epoll三个中的性能表现是优秀的,能支持的并发量最大。所以在高性能的框架中都是使用epoll, 比如redis和netty。epoll 高性能最根本的原因是极大程度地减少了无用的进程上下文切换,让进程更专注地处理网络请求。

我们先来看下epoll的整个工作流程如下:
在这里插入图片描述

epoll相关的函数在内核环境中,可以分为两部分:

第一部分:用户进程内核态

在内核的硬、软中断上下文中,网络数据包到达网卡后,通过DMA复制到socket的缓存区,然后再找到socket关联的epitem,并把它添加到epoll对象的就绪链表中。

在用户进程调用epoll_create创建一个struct eventpoll的内核对象,eventpoll 有3个成员,分别是wq、rbr、rdllist。wq是等待队列链表,软中断数据就绪的时候会通过wq来找到阻塞在epoll对象上的用户进程。 rbr 是一个颗红黑树,为支持海量连接的高效查找、插入和删除,通过红黑树来管理用户进程下添加进来的socket连接。rdllist 是就绪的描述符的链表,当有连接就绪的时候,内核会把就绪的连接放到rdllist链表中。 这样应用进程只需要判断链表就能找出就绪连接。

通过epollctl中首先根据传入fd找到eventpoll、socket相关的内核对象。对于每个socket,调用epoll_ctl的时候,都会为socket分配一个epitem,epitem创建并初始化完成后,就会设置socket对象上的等待任务队列和数据就绪时的回调函数。分配完epitem对象后,紧接着把epitem插入红黑树。

在用户进程中,通过调用epollwait 来查看就绪链表中是否有事件到达,如果有,直接取走进行处理。处理完毕再次调用epollwait。在高并发的实战中,只要活足够多,epollwait根本不会让用户进程阻塞。用户进程会一直不同的干活,直到epoll_wait 中没活可干了才会主动让出CPU,这是epoll高效的核心。

第二部分:硬、软中断上下文

当网络数据包到达网卡的时候,将数据接收到socket的缓存区,在数据接收完成后,先查找等待队列上注册的回调函数,执行socket就绪回调函数,执行epoll就绪通知唤醒在socket上等待的用户进程返回事件。

epoll 有水平触发和边缘触发两站模式,这两种模式的关键区别在于当socket中的缓存区中还有数据可读时, epoll_wait 是否会情况rdllist。

水平触发:用户线程调用epoll_wait获取到IO就绪的socket后,对Socket进行系统IO调用读取数据,假设socket中的数据只读了一部分没有全部读完,这时再次调用epoll_wait,epoll_wait会检查这些Socket中的接收缓冲区是否还有数据可读,如果还有数据可读,就将socket重新放回rdllist。所以当socket上的IO没有被处理完时,再次调用epoll_wait依然可以获得这些socket,用户进程可以接着处理socket上的IO事件。JDK的NIO默认是水平触发模式。

边缘触发:epoll_wait就会直接清空rdllist,不管socket上是否还有数据可读。所以在边缘触发模式下,当你没有来得及处理socket接收缓冲区的剩下可读数据时,再次调用epoll_wait,因为这时rdlist已经被清空了,socket不会再次从epoll_wait中返回,所以用户进程就不会再次获得这个socket了,也就无法在对它进行IO处理了。除非,这个socket上有新的IO数据到达,根据epoll的工作过程,该socket会被再次放入rdllist中。在Netty中实现的EpollSocketChannel默认的就是边缘触发模式。

用户空间的IO线程模型

阻塞IO 模型、非阻塞IO模型、多路复用IO模型有个共同的特点都是同步的,三个IO模型都是基于用户空间与内核空间的交互进行划分的,此小节从用户空间的角度介绍一下同步IO线程模型Reactor线程模型,异步IO线程模型是Proactor 。

Reactor 模式也叫作反应器设计模式,Reactor模型是处理并发I/O比较常见的一种模式,中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。简单概括就是将消息放到了一个队列中,通过异步线程池对其进行消费。简单来说Reactor模式的核心思想是IO多路复用与线程池相结合。

Reactor是一种事件驱动机制,和普通函数调用的不同之处在于:应用程序不是主动的调用某个API完成处理,而是恰恰相反,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。

Reacotr模型主要以下角色:

(1)Reactor:把IO事件分配给对应的handler处理 ,是通过调度响应IO事件

(2)Handler:处理非阻塞的任务;完成真正的连接建立、通道的读取、处理业务逻辑、负责将结果写出到通道等。

(3)Acceptor:请求连接器,Reactor 接收到 client 连接事件后,会将其转发给 Acceptor,Acceptor 则会接受 Client 的连接,建立对应的Handler,并向 Reactor注册此Handler

Reactor 模型大致如下:
在这里插入图片描述

根据Reactor的数量和处理资源的线程数量的不同,分为三类:

(1)单Reactor单线程模型

(2)单Reactor多线程模型

(3)多Reactor多线程模型

单线程Reactor反应器

Reactor 反应器和 Handers 处理器处于一个线程中执行。它是最简单的反应器模型,如下图所示:
在这里插入图片描述

单线程Reactor 反应器模式,是基于 Java 的 NIO 实现的。相对于传统的多线程 OIO ,反应 器模式不再需要启动成千上万条线程,避免了线程上下文的频繁切换,服务端的效率自然是 大大提升了。 Reactor 线程通过select/epoll(IO多路复用接口)监听事件,收到事件后通过Dispatch 来分发事件,事件会分发给Acceptor和Handler两个组件,具体是哪个组件要看事件的类型。如果事件类型为建立连接,则将事件分发给Acceptor,Acceptor会通过 accept 方法 获取连接,并创建一个 Handler对象来处理后续的响应事件。如果时间类型不是建立连接,则将该事件交由当前连接的Handler来处理。

在单线程反应器模式中,Reactor反应器和 Handler处理器都执行在同一条线程上。这样, 带来了一个问题:当其中某个 Handler 阻塞时,会导致其他所有的 Handler 都得不到执行。在 这种场景下,被阻塞的 Handler 不仅仅负责输入和输出处理的传输处理器,还包括负责新连 接监听的 AcceptorHandler 处理器,这就可能导致服务器无响应。这个是非常严重的问题。因 为这个缺陷,因此单线程反应器模型在生产场景中使用得比较少。

单Reactor多线程模型

单Reactor多线程模型升级了Handler处理器, 从高效率的角度考虑加入了线程池,单reactor多线程反应器如下图所示:

在这里插入图片描述

Reactor 接受请求后,根据请求类型来进行分发,分发逻辑与 单Reactor单线程 模型一样,不同之处在于Handler不在进行业务处理了,它只负责接受和发送,Handler接受数据后,会将数据发送给 Worker 线程池中的线程处理,该线程才是处理业务的真正线程,线程将业务处理完成后,将数据发送给Handler,然后Handler 再send出去。

优点
由于Handler使用了多线程模式,则可以利用充分利用CPU的性能。

缺点
Handler使用多线程模式,则会涉及到数据共享的问题,需要考虑互斥,实现肯定比 单Reactor单线程模式复杂一些;单Reactor,一个线程处理事件监听、分发、响应,对于高并发场景,容易造成性能瓶颈

多Reactor多线程模型

单Reactor多线程模式解决了Handler单线程的性能问题,但是Reactor还是单线程的,对于高并发场景还是会有性能瓶颈,所以需要对Reactor调整为多线程模式。多Reactor多线程模型如下图所示:

在这里插入图片描述

主线程中的MainReactor对象通过select监听事件,接收到事件后通过Dispatch进行分发,如果事件类型为建立连接则将事件分发给Acceptor进行连接建立;如果收到的事件不是连接,则他将事件分发个某个SubReactor,SubrReactor将连接加入到连接队列进行监听,并创建Handler进行各种事件处理;如果有新的事件发生,SubReactor 则会调用当前连接的Handler来进行处理。Handler 通过read 读取数据后,将数据发送给Worker线程进行处理,Worker线程池则会分配线程进行业务处理,处理完成后返回结果,Handler接受结果后,通过send发送给客户端。

优点
该模式主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理,同时主线程和子线程的交互也很简单,子线程接收主线程的连接后,只管业务处理即可,无须关注主线程。

缺点
模型复杂。

这种模式适用于高并发场景,广泛运用于各种项目中,如大名鼎鼎的Netty。

Reactor线程模型运行机制

一个IO事件从操作系统底层产生后,在Reactor反应器模式中的处理流程如下图所示:

在这里插入图片描述

步骤一:通道注册。IO事件源于通道(Channel),IO是和通道(对应于底层连接而言)
强相关的。一个IO事件一定属于某个通道。但是,如果要查询通道的事件,首先要将通道
注册到选择器。

步骤二:事件轮询;轮询Selector选择器中已注册的Channel的I/O事件;

步骤三:事件分发。如果查询到 IO 事件,则分发给与 IO 事件有绑定关系的 Handler 业务处
理器。

步骤四:任务处理;Reactor线程负责任务队列中的I/O任务,每个Worker线程从各自维护的任务队列中取出任务异步执行;

Netty的Reactor线程模式实现

Netty高性能主要在于Reactor线程模型, EventLoop 是Netty Reactor 线程模型的核心处理引擎。
EventLoop是一种事件等待和处理的程序模型,可以解决多线程资源消耗高的问题。EventLoop运行模式:
在这里插入图片描述

1、应用程序将产生的事件放入事件队列;
2、EventLoop轮询队列中的事件,然后取出事件执行或者将事件分发给相应的事件监听者;

在Netty中,EventLoop可以理解为Reactor线程模型的事件处理引擎,每个EventLoop线程维护者一个Selector选择器和任务队列taskQueue。它主要负责处理I/O事件、普通任务和定时任务。

Netty主推NioEventLoop实现类,所以Netty的心脏就是NioEventLoop#run,每次循环的处理流程包含事件轮询select、事件处理processSelectedKeys、任务处理runAllTasks几个步骤,典型的Reactor线程模型的运行机制。

Netty EventLoop事件处理机制

NioEventLoop采用的是无锁串行化的设计思路,其模型如下
在这里插入图片描述

根据Reactor的模型,NioEventLoop具体思路如下:

1、BoosEventLoopGroup和WorkerEventLoopGroup包含一个或者多个NioEventLoop。BootEventLoopGroup负责监听客户端的Accept事件,当事件触发时,将事件注册至WorkerEventLoopGroup中的一个NioEventLoop上。每新建一个Channel,就将Channel与NioEventLoop绑定。从而实现CHannel生命周期中的所有事件都有一个NioEventLoop处理。一个NioEventLoop可以处理多个Channel的事件,但一个Channel的事件只能被一个NioEventLoop处理。这就实现了不同的NioEventLoop线程之间不发生任何交集,从而不会发生线程切换,提高性能。

2、NioEventLoop完成数据读取后,会调用绑定的ChannelPipeline进行事件传播,ChannelPipeline也是线程安全的,数据会被传递到ChannelPipeline中第一个CHannelHandler中,执行完成后会传递给下一个CHannelHandler,从而实现串行化执行,不会发生线程上下文切换,提高性能。、

3、NIoEventLoop的优势:无锁串行化提高了系统吞吐量,降低用户开发业务逻辑难度,无须关注线程安全问题;劣势:不能执行时间过长的I/O操作,一旦某个事件I/O发生阻塞,后续的所有I/O事件都无法执行,造成事件积压。所以用Netty开发程序时,一定要对ChannelHandler的逻辑有充分的风险意识。

Netty是规避JDK epollo的空轮询Bug方法

JDK中epoll的实现却是有漏洞的,其中最有名的就是NIO空轮询bug(该bug只存在于Linux,因为Linux中NIO底层是使用epoll实现的)。
理论上无客户端连接时Selector.select()方法会阻塞,但空轮询bug导致:即使无客户端连接,NIO照样不断的从select本应该阻塞的Selector.select()中wake up出来,导致CPU100%问题。

netty提供一种检测机制判断线程是否可能陷入空轮询,具体实现方式如下;

1、selector.select(timeoutMillis),调用了select方法,并默认设置1秒超时时间,同时记录轮询次数:selectCnt ++;

2、获取当前时间,计算select方法的操作时间是否真的阻塞了timeoutMillis,如果是就证明是一次正常的select(),重置selectCnt =1;如果不是,就可能触发了JDK的空轮询BUG,然后判断selectCnt 轮询次数是否大于默认的512,然后进行rebuildSelector()。

3、rebuildSelector()方法重新打开一个Selector;然后遍历oldSelector,将所有的key重新注册到新的Selector;然后重新赋值selector,selectCnt = 1;这时候已经规避了空轮询。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/553533.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面试:从输入URL到页面加载的全过程

首先在浏览器中输入URL 查找缓存:浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有则显示页面内容。如果没有则进行下一步。 浏览器缓存:浏览器会记录DNS一段时间,因此,只是第一个地方解析DNS请求…

【算法排序】动态规划

目录 一、动态规划思想二、动态规划与分治法的区别1、共同点2、不同点 三、动态规划特征1、最优子结构2、重叠子问题 四、动态规划求解问题的基本步骤五、斐波那契数分析六、实现思路七、代码实现 一、动态规划思想 将待求问题划分为若干个子问题,按划分的顺序求解…

为什么袁隆平的英语这么好?这才是学到老的典范!

文 / 冰雪(微信公众号:王不留) 2021年5月22日13时07分,“共和国勋章”获得者、中国工程院院士、国家杂交水稻工程技术研究中心主任、湖南省政协原副主席袁隆平,因病逝世,享年91岁。 一晃两年过去了。袁隆平…

iTOP-RK3568开发板编译瑞芯微原厂源码

1 输入以下命令设置 java 版本为 1.8 版本,确认 java 版本是 1.8 版本之后,才可以进行下一步编译,如下图所示: source javaenv.sh java -version 2 输入命令配置 Android 分支 source build/envsetup.sh lunch rk3568_r-user…

【软考】系统集成项目管理工程师 第3章 信息系统集成专业技术知识

文章目录 3.1 信息系统建设3.1.1 信息系统的生命周期3.1.2信息系统开发方法 3.3 软件工程3.3.1软件需求分析与定义3.3.2软件设计、测试与维护3.3.3软件质量保证及质量评价3.3.4软件配置管理3.3.5软件过程管理3.3.6软件开发工具3.3.7软件复用 3.4 面向对象系统分析与设计3.4.1面…

ESP32-WROOM-32 TCP通讯AT指令例程

ESP32-WROOM-32 AT指令配置TCP通讯 ESP32-WROOM-32前言固件烧录测试AT指令TCP通讯\透传ESP32配置SoftAPESP32作TCP Client连接TCP Server通讯/透传普通传输模式演示WIFI透传演示 ESP32做TCP Server连接TCP Client通讯演示 ESP32-WROOM-32 前言 软硬件准备: ESP32-W…

自动驾驶行业迎来大规模发展,“高精度定位”重要性凸显

5月16日,2023 中国 (亦庄) 智能网联汽车科技周暨第十届国际智能网联汽车技术年会在北京隆重召开。工信部装备工业一司一级巡视员苗长兴在在会上表示:2022 年我国搭载辅助自动驾驶系统的智能网联乘用车新车销售量达 700 万辆,同比增长45.6%&am…

计算机操作系统(慕课版)第三章课后题答案

一、简答题 1.高级调度与低级调度的主要任务是什么?为什么要引入中级调度? 1)高级调度的主要任务是将外存的作业调入内存,又称作业调度; 低级调度的主要任务数为内存中处于就绪态的作业分配处理机。 2)为了…

小航助学2023年3月GESP_C++二级试卷(含题库答题软件账号)

GESP在线模拟训练系统请点击 电子学会-全国青少年编程等级考试真题Scratch一级(2019年3月)在线答题_程序猿下山的博客-CSDN博客_小航答题助手 答案:D 第1题以下存储器中的数据不会受到附近强磁场干扰的是( )。 A、硬盘B、U 盘C…

共享单车之数据存储-获取工作簿中的数据

第1关:获取工作簿中的数据 任务描述 本关任务:获取data.xls文件中的数据。 相关知识 获取工作簿中的信息,我们可以使用Java POI(POI是一个提供API给Java程序对Microsoft Office格式档案读和写的功能)提供的Workboo…

二十三种设计模式第七篇--适配器模式

适配器模式 适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于 结构型模式,它结合了两个独立接口的功能。这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。 我们生活中…

注意!ChatGPT的Plus账号也会被封禁

文 / 高扬(微信公众号:量子论) 最近经常看到有人丢个截图,然后悲伤地说,ChatGPT账号被封了。 这不是账号被封,而是所用的代理节点被OpenAI封了,换个节点即可。 这个截图才是账号真正被封的提示&…

LeetCode 1080. 根到叶路径上的不足节点

【LetMeFly】1080.根到叶路径上的不足节点 力扣题目链接:https://leetcode.cn/problems/insufficient-nodes-in-root-to-leaf-paths/ 给定一棵二叉树的根 root,请你考虑它所有 从根到叶的路径:从根到任何叶的路径。(所谓一个叶子…

使用GPT-4.0编写量化交易策略:方法、案例与参数优化

量化策略开发,高质量社群,交易思路分享等相关内容 『正文』 ˇ 随着人工智能的发展,GPT-4.0已经成为量化交易策略编写的强大工具。在这篇文章中,我们将探讨如何使用GPT-4.0编写量化交易策略,并提供一个实际的案例。我…

Maven基础学习---5、其他核心概念

1、生命周期 1、作用 为了让构建过程自动化完成,Maven设定了三个生命周期。生命周期中的每一个环节对应构建过程中的一个操作。 2、三个生命周期 3、特点 前面三个生命周期彼此都是独立的在任何一个生命周期内部,执行任何一个具体环节的操作&#xff…

ElasticSearch 的DSL查询文档

一、DSL查询文档 本章目标 文本检索:match_all、match、multi_match精确查询:term、range地理坐标查询:geo_distance复合查询:function_score、bool 1. 说明 查询语法: GET /索引库名/_search {"query": {}…

基于C++实现房贷计算器的设计

访问【WRITE-BUG数字空间】_[内附完整源码和文档] 本次项目的要求是完成一个房贷计算器的设计,实现商业贷款、公积金贷款和组合贷款的利息计算三种功能。并且使用Qt或其他的界面库设计人机交互界面,要求界面友好方便使用。并且必须使用面向对象的思想进…

NSS周常刷密码(1)

感觉没怎么做密码,就每周上nss随便做点 稍微难点的会分析 [SWPU 2020]happy import gmpy2 from Crypto.Util.number import * import z3c 0x7a7e031f14f6b6c3292d11a41161d2491ce8bcdc67ef1baa9e e 0x872a335 #q q*p^3 128536731745208998078944182958039785532190189…

ChatGPT 4.0大升级,能替代留学中介吗?

ChatGPT4.0发布了! 在之前轰动世界版本的基础上又有了大幅度升级,根据官方介绍,它的输入可以是文字(上限2.5万字)还可以是图像,是一个超大的多模态模型。 越来越多的人开始好奇这个东西到底能用来做什么&…

CMake Practice 学习笔记三---建立动静态库

任务: 建立一个静态库和动态库,提供HelloFunc函数供其他程序编程使用,HelloFunc向终端输出Hello World字符串。安装头文件与共享库。 1、准备工作 在/backup/cmake目录建立t3目录 mkdir t3建立共享库 cd t3 mkdir lib在t3目录下建立CMak…