关于请求处理的具体逻辑:编码/解码->消息的派发->业务处理->返回响应,这些逻辑能否通过合理分配到具体线程上,设计合理的线程模型,是能否得到高性能的网络服务的关键
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
给服务使用,使用完成后会回收到内存池中。