关于请求处理的具体逻辑:编码/解码->消息的派发->业务处理->返回响应,这些逻辑能否通过合理分配到具体线程上,设计合理的线程模型,是能否得到高性能的网络服务的关键
1. 说说什么是Reactor模型
Reactor模型是基于IO多路复用+线程池开发出来的一种模型,它的核心设计是事件驱动、能够处理一个或者多个输入源、多路复用,能够分发事件给对应的处理器
什么叫多路复用
分用和复用是比较重要的概念,比如说在传输层中就存在这种设计,比如说在当TCP连接为上层的应用程序交付数据的时候,根据TCP报文的首部的目的端口完成分用,这里之所以说是分用,是因为说这些数据在底层是基于同一个运输层协议TCP协议进行传输的,而最终却能够交付到不同的进程,产生了分叉,这种就叫做复用
至于复用,就是说从应用层传递下来的报文,能够通过相同的传输层协议传输到对等端完成分用。
而这里的I/O多路复用,它指的是当多个I/O输入源来的时候,都会通过一个或者多个请求处理器完成。
复用体现在:一段时间内,多个请求复用了同一个进程/线程,而不是说像BIO那样,一个线程就对应于一个请求,从而节约了线程的创建与销毁甚至是上下文切换的开销。
那么Reactor模型就是这样的一种模型
一般来说,Reactor模型具有两种角色,第一种是Reactor,它负责解析请求,并且将这个请求分发给具体的处理线程,第二种是Handler模型角色,它负责处理具体的任务,对于任务而言,可以分为连接请求的任务和读写请求的任务,因此Handler又可以细分为:
Handler:负责处理具体的读写任务Acceptor:负责处理连接请求,负责将连接队列中的socket进行accpet()
2. 什么是单Reactor单线程模型
如图所示,在这个单Reactor单线程模型中,可以这样进行解读
首先它的模型非常简单,一开始有一个连接请求到来的时候,它的调用链是:先经过Reactor,然后Reactor判断它是一个连接请求,然后就会将这个请求分发给具体的Acceptor,然后Acceptor通过调用accpet()方法来接收这个请求,将这个socket注册到底层的epoll上,进行fd之间的数据传输。
如果是一个读写请求来的时候,那么就会将fd取出来,然后在内核中调用I/O函数,这个过程实际上就是将读写事件分发给提前注册好的Handler进行处理,由于是单线程模型,因此Handler也是单线程的,这个线程需要完成读取数据、业务处理、发送响应,其中读取数据、发送响应都可能涉及到I/O,因此这时候,如果有线程因为I/O操作而阻塞,这就会导致线程资源的浪费,而且单线程模型无法充分利用多核CPU的特点。
CPU利用率为什么低?首先以常规的
select/poll来说,这些读写请求都需要内核利用CPU对这些FD进行轮询,这就会导致CPU空转,从而利用率就低了,还有一个问题就是,一个线程只能跑在一个CPU上,一旦某个线程获取了CPU,但是耗时长,就会导致无法接收其他请求。这也就是说,
Reactor和Handler和Acceptor都是同一个线程在执行操作,一个环节的操作耗时将会影响其他环节。
3. 什么是单Reactor和多线程模型
在单Reactor和单线程模型中,我们说到了关于线程只有一个的问题,最主要的问题是无法充分利用多核CPU以及在请求到来的时候,只能串行执行请求,当因为IO时间阻塞的时候,无法工作,CPU无法被利用,而通常来说,最为耗时的是执行业务逻辑的时候,所以如果可以在这时候就可以开启多个线程,这样的话就解耦了Reactor和部分Handler线程的操作,当Handler阻塞或者耗时长的时候,可以将线程换下CPU,从而达到一个调度的平衡,提高系统的吞吐量
有什么缺点?在这个模型中,
Reactor的工作依然是由一个线程来完成的,也就是说无论是读写请求还是建立连接的请求,都会打到同一个线程上,这是啥意思呢?比如说现在有很多个连接同时到来,那么这些连接的建立面临着和请求分发的竞争的问题假设一个场景,连接同时到来,然后Reactor忙着给这些读写请求分发
Handler,但是这时候大批量的连接请求涌进来,那么这些请求就得不到响应了,会被直接丢弃。从而出现了服务拒绝的现象。
4. 什么是主从 Reactor和多线程模型
为了解决单Reactor多线程中,一个Reactor同时负责接收多种请求的问题,因此可以采取一种方案,再将读写请求和读写请求的线程进行分离,当他们进行分离之后,可以达到一种效果:
一个线程专门负责分发请求,一个线程专门负责分发连接建立请求,这样的话两个线程同时工作,可以显著地提高系统的并发量
Reactor主线程接收到连接事件,然后给Acceptor处理,网络连接建立之后,主的Reactor会将连接分配给子Reactor进行后续的监听,子Reactor分配到连接之后,负责监听该连接上的读写事件,读写事件到来的时候,会分发给Worker线程池的Handler进行处理。
5. 说说Netty的线程模型
Netty针对服务端的设计是在主从Reactor多线程模型的基础上进行的修改。
基于主从Reactor模型,它将主Reactor和Acceptor单独抽象出来成了一个BossGroup
将从Reactor和Handler(不包含Acceptor)抽象成了WorkerGroup
BossGroup里的线程会监听连接事件,与客户端建立了网络连接之后,会构造出一个连接描述对象,称为NioSocketChannel,表示这是一条连接对象,之后就会将这个连接对象注册到WorkerGroup中的某个线程上负责监听WorkerGroup里的线程会监听这条连接上的读写事件,当监听到读写事件的时候,会通过pipeline添加的多个Handler进行处理。
总结:Netty是基于
主从Reactor进行开发的,其中主Reactor和Acceptor被抽象为了BossGroup,这个对象的任务主要是描述连接要怎么建立,建立完毕之后要如何处理,要分发到哪里去,具体的可以表现为将建立连接的请求到来的时候,将这个请求分发到其中的一个处理线程Acceptor,然后当处理当这个连接处理完毕后,就可以建立一个连接描述对象,叫做NioEventChannel,然后将这个对象注册到WorkerGroup中的某个线程上,这个线程负责具体的逻辑处理。
6. Netty的核心组件有哪些?怎么用的?
EventLoopGroup / EventLoop:一个EventGroup当中会包含一个或者多个EventLoop,EventLoop提供了next()方法,通过此方法可以获取某一个可用的线程,它从表面上看是一个不断循环的线程
EventLoop最常见的实现类是NioEventLoop,一个NioEventLoop包含了一个selector对象,可以支持多个Channel注册在其上,该NioEventLoop可以同时服务多个Channel,这多个Channel同时被这一个NioEventLoop实现了多路复用,它不是一个纯粹的IO线程,除了负责IO的读写之外,还需要兼顾下面的任务
- 系统任务:通过调用 NioEventLoop 的
execute(Runnable task)方法实现,Netty 有很多系统任务,当 I/O 线程和用户线程同时操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成任务放入消息队列中,由 I/O 线程负责执行,这样就实现了局部无锁化。 - 定时任务:通过调用 NioEventLoop 的
schedule(Runnable command, long delay, TimeUnit unit)方法实现。
Channel是对网络连接的抽象,其核心功能就是执行网络I/O操作,是服务端和客户端进行I/O数据交互的媒介
- 当客户端连接成功后,将新建一个
Channel Channel从EventLoopGroup获得一个EventLoop,并且注册上去Channel与客户端进行网络连接,关闭和读写,生成相对应的event,改变了selectinKey信息,触发EventLoop调度线程进行执行- 如果是读事件,那么就执行线程调度
pieline来处理逻辑
事件是怎么触发的,在
Channel注册到上面的时候,会产生一个selectionKey,当有事件到来的时候,这个selectionKey会被修改,当它被selector查询到的时候,就会触发响应的回调函数
什么是pipeline
pipeline是一组事件处理的逻辑链表,一个PipeLine由多个Handler串成一个有序的链表,一个Handler处理完了之后,调用next()即可进行下一步的处理
它的调用链既可以是正向的,也可以是反向的,处理入站的事件,Handler按照正向的顺序执行,处理出站事件的时候,Handler就可以按照反向的顺序执行
什么是ByteBuf
当需要以字节的形式发送和接收数据的时候,发送端和消费端都需要一个高效的数据容器来存储字节数据
ByteBuf简单来说就是一个字节数组,它维护了一个读索引和写索引,分别用来控制对ByteBuf中数据的读写操作,还有一个capacity用来记录缓冲区的总长度,当写数据超过capacity的时候,ByteBuf会自动扩容,直到达到maxCapacity
关于ByteBuf的分类
- 什么是堆缓冲区?
堆缓冲区其实就是在JVM的堆空间中分配一个字节数组,堆缓冲可以实现快速分配,由GC收集器管理
- 什么是直接缓冲区?
直接缓冲区使用的是直接内存,不会占用JVM的空间,在完成Socket数据传输的时候有较好的读写性能,因为不需要从内核缓冲区拷贝到用户进程缓冲区,但是在分配内存空间和释放内存的时候,会比较复杂,可能造成内存泄漏等问题
- 什么是复合缓冲区?
我们可以创建多个不同的 ByteBuf,然后提供一个这些 ByteBuf 组合的视图,也就是 CompositeByteBuf。它就像一个列表,可以动态添加和删除其中的 ByteBuf。
例如:一条消息由 header 和 body 两部分组成,将 header 和 body 组装成一条消息发送出去,可能 body 相同,只是 header 不同,使用CompositeByteBuf 就不用每次都重新分配一个新的缓冲区。
7. 什么是内存的池化技术?
Unpolled:非池化的内存管理方式,每次分配的时候都直接调用系统API向操作系统申请ByteBuf,在使用完成之后,通过系统调用进行释放,Unpooled是将内存管理直接交给系统,不做任何的特殊处理,使用起来会比较方便,对于申请和释放操作不频繁,操作成本比较低的ByteBuf来说,是比较好的选择
Pooled:是池化的内存管理方式,这个方式来预先申请一大块的内存形成内存池,在需要申请ByteBuf的时候,会将一部分合理的空间封装成ByteBuf给服务使用,使用完成后会回收到内存池中。