目录
多路复用机制
I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。
什么是fd:在linux中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个fd(文件描述符)。而对于一个socket的读写也会有相应的文件描述符,成为socketfd。
常见的IO多路复用方式有【select、poll、epoll】,都是Linux API提供的IO复用方式,那么接下来重点讲一下select、和epoll这两个模型。
select:进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点:
- 由于它能够同时监听多个文件描述符,假如说有1000个,这个时候如果其中一个fd 处于就绪状态了,那么当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大。
- 同时,select在单个进程中能打开的fd是有限制的,默认是1024,对于那些需要支持单机上万的TCP连接来说确实有点少。
epoll:linux还提供了epoll的系统调用,epoll是基于事件驱动方式来代替顺序扫描,因此性能相对来说更高,主要原理是,当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那么当前进程只需要去从指定的fd上读取数据即可,另外,epoll所能支持的fd上线是操作系统的最大文件句柄,这个数字要远远大于1024。
【由于epoll能够通过事件告知应用进程哪个fd是可读的,所以我们也称这种IO为异步非阻塞IO,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行】
I/O多路复用的好处是可以通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线程,降低了系统的资源开销,它的整体实现思想如图1所示。
客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻塞,服务端会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可。也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行相关的处理即可。
图1 I/O多路复用整体实现思想
Reactor模型
了解了NIO多路复用后,就有必要再和大家说一下Reactor多路复用高性能I/O设计模式,Reactor本质上就是基于NIO多路复用机制提出的一个高性能IO设计模式,它的核心思想是把响应IO事件和业务处理进行分离,通过一个或者多个线程来处理IO事件,然后将就绪的事件分发到业务处理handlers线程去异步非阻塞处理,如图2所示。
Reactor模型有三个重要的组件:
- **Reactor :**将I/O事件发派给对应的Handler
- **Acceptor :**处理客户端连接请求
- **Handlers :**执行非阻塞读/写

图2 Reactor模型
这是最基本的单Reactor单线程模型(整体的I/O操作是由同一个线程完成的)。其中Reactor线程,负责多路分离套接字,有新连接到来触发connect 事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于racotor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,由Reactor分发)
多线程单Reactor模型
单线程Reactor这种实现方式有存在着缺点,从上图中可以看出,handler的执行是串行的,如果其中一个handler处理线程阻塞将导致其他的业务处理阻塞。由于handler和reactor在同一个线程中的执行,这也将导致新的无法接收新的请求,比如:打开多个客户端窗口连接到Reactor Server端,其中一个窗口发送一个信息后被阻塞,另外一个窗口再发信息时由于前面的请求阻塞导致后续请求无法被处理。
为了解决这种问题,有人提出使用多线程的方式来处理业务,也就是在业务处理的地方加入线程池异步处理,将reactor和handler在不同的线程来执行,如图3所示。
图3 多线程单Reactor模型
多线程多Reactor模型
在多线程单Reactor模型中,我们发现所有的I/O操作是由一个Reactor来完成,而Reactor运行在单个线程中,它需要处理包括Accept/read/write/connect操作,对于小容量的场景,影响不大。但是对于高负载、大并发或大数据量的应用场景时,容易成为瓶颈,主要原因如下:
- 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的读取和发送;
- 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
所以,我们还可以更进一步优化,引入多Reactor多线程模式,如图4所示,Main Reactor负责接收客户端的连接请求,然后把接收到的请求传递给SubReactor(其中subReactor可以有多个),具体的业务IO处理由SubReactor完成。
Multiple Reactors 模式通常也可以等同于 Master-Workers 模式,比如 Nginx 和 Memcached 等就是采用这种多线程模型,虽然不同的项目实现细节略有区别,但总体来说模式是一致的。
图4 多线程多Reactor模型
- Acceptor,请求接收者,在实践时其职责类似服务器,并不真正负责连接请求的建立,而只将其请求委托 Main Reactor 线程池来实现,起到一个转发的作用。
- Main Reactor,主 Reactor 线程组,主要负责连接事件,并将IO读写请求转发到 SubReactor 线程池。
- Sub Reactor,Main Reactor 通常监听客户端连接后会将通道的读写转发到 Sub Reactor 线程池中一个线程(负载均衡),负责数据的读写。在 NIO 中 通常注册通道的读(OP_READ)、写事件(OP_WRITE)。
高性能通信框架之Netty-整体工作机制
Netty的整体工作机制如下,整体设计就是前面我们讲过的多线程Reactor模型,分离请求监听和请求处理,通过多线程分别执行具体的handler。

网络通信层
网络通信层主要的职责是执行网络的IO操作,它支持多种网络通信协议和I/O模型的链接操作。当网络数据读取到内核缓冲区后,会触发读写事件,这些事件再分发给时间调度器来进行处理。
在Netty中,网络通信的核心组件以下三个组件:
- Bootstrap, 客户端启动api,用来连接远程netty server,只绑定一个EventLoopGroup
- ServerBootStrap,服务端监听api,用来监听指定端口,会绑定两个EventLoopGroup, bootstrap组件可以非常方便快捷的启动Netty应用程序
- Channel,Channel是网络通信的载体,Netty自己实现的Channel是以JDK NIO channel为基础,提供了更高层次的抽象,同时也屏蔽了底层Socket的复杂性,为Channel提供了更加强大的功能。
如图5所示,表示的是Channel的常用实现实现类关系图,AbstractChannel是整个Channel实现的基类,派生出了AbstractNioChannel(非阻塞io)、AbstractOioChannel(阻塞io),每个子类代表了不同的I/O模型和协议类型。

图5 Channel的类关系图
随着连接和数据的变化,Channel也会存在多种状态,比如连接建立、连接注册、连接读写、连接销毁。随着状态的变化,Channel也会处于不同的生命周期,每种状态会绑定一个相应的事件回调。以下是常见的时间回调方法。
- channelRegistered, channel创建后被注册到EventLoop上
- channelUnregistered,channel创建后未注册或者从EventLoop取消注册
- channelActive,channel处于就绪状态,可以被读写
- channelInactive,Channel处于非就绪状态
- channelRead,Channel可以从源端读取数据
- channelReadComplete,Channel读取数据完成
简单总结一下,Bootstrap和ServerBootStrap分别负责客户端和服务端的启动,Channel是网络通信的载体,它提供了与底层Socket交互的能力。而当Channel生命周期中的事件变化,就需要触发进一步处理,这个处理是由Netty的事件调度器来完成。
事件调度器
事件调度器是通过Reactor线程模型对各类事件进行聚合处理,通过Selector主循环线程集成多种事件(I/O时间、信号时间),当这些事件被触发后,具体针对该事件的处理需要给到服务编排层中相关的Handler来处理。事件调度器核心组件:
- EventLoopGroup 相当于线程池
- EventLoop 相当于线程池中的线程
EventLoopGroup本质上是一个线程池,主要负责接收I/O请求,并分配线程执行处理请求。为了更好的理解EventLoopGroup、EventLoop、Channel之间的关系,我们来看图6所示的流程。

图6 EventLoop的工作机制
从图中可知
- 一个EventLoopGroup可以包含多个EventLoop,EventLoop用来处理Channel生命周期内所有的I/O事件,比如accept、connect、read、write等
- EventLoop同一时间会与一个线程绑定,每个EventLoop负责处理多个Channel
- 每新建一个Channel,EventLoopGroup会选择一个EventLoop进行绑定,该Channel在生命周期内可以对EventLoop进行多次绑定和解绑。
图7表示的是EventLoopGroup的类关系图,可以看出Netty提供了EventLoopGroup的多种实现,如NioEventLoop、EpollEventLoop、NioEventLoopGroup等。从图中可以看到,EventLoop是EventLoopGroup的子接口,我们可以把EventLoop等价于EventLoopGroup,前提是EventLoopGroup中只包含一个EventLoop。

图7 EventLoopGroup类关系图
EventLoopGroup是Netty的核心处理引擎,它和前面我们讲解的Reactor线程模型有什么关系呢?其实,我们可以简单的把EventLoopGroup当成是Netty中Reactor线程模型的具体实现,我们可以通过配置不同的EventLoopGroup使得Netty支持多种不同的Reactor模型。
- 单线程模型,EventLoopGroup只包含一个EventLoop,Boss和Worker使用同一个EventLoopGroup。
- 多线程模型:EventLoopGroup包含多个EventLoop,Boss和Worker使用同一个EventLoopGroup。
- 主从多线程模型:EventLoopGroup包含多个EventLoop,Boss是主Reactor,Worker是从Reactor模型。他们分别使用不同的EventLoopGroup,主Reactor负责新的网络连接Channel的创建(也就是连接的事件),主Reactor收到客户端的连接后,交给从Reactor来处理。
服务编排层
服务编排层的职责是负责组装各类的服务,简单来说,就是I/O事件触发后,需要有一个Handler来处理,所以服务编排层可以通过一个Handler处理链来实现网络事件的动态编排和有序的传播。
它包含三个组件:
-
ChannelPipeline,它采用了双向链表将多个Channelhandler链接在一起,当I/O事件触发时,ChannelPipeline会依次调用组装好的多个ChannelHandler,实现对Channel的数据处理。ChannelPipeline是线程安全的,因为每个新的Channel都会绑定一个新的ChannelPipeline。一个ChannelPipeline关联一个EventLoop,而一个EventLoop只会绑定一个线程,如图8所示表示ChannelPIpeline结构图。

图8 ChannelPipeline结构图
从图中可以看出,ChannelPipeline中包含入站ChannelInBoundHandler和出站ChannelOutboundHandler,前者是接收数据,后者是写出数据,其实就是InputStream和OutputStream,为了更好的理解,我们来看图9 InBound和OutBound的关系。

图9 InBound和OutBound的关系
-
ChannelHandler, 针对IO数据的处理器,数据接收后,通过指定的Handler进行处理。
-
ChannelHandlerContext,ChannelHandlerContext用来保存ChannelHandler的上下文信息,也就是说,当事件被触发后,多个handler之间的数据,是通过ChannelHandlerContext来进行传递的。ChannelHandler和ChannelHandlerContext之间的关系,如图10所示。
每个ChannelHandler都对应一个自己的ChannelHandlerContext,它保留了ChannelHandler所需要的上下文信息,多个ChannelHandler之间的数据传递,是通过ChannelHandlerContext来实现的。

图10 ChannelHandler和ChannelHandlerContext关系
以上就是Netty中核心的组件的特性和工作机制的介绍,可以看出,Netty的架构分层设计是非常合理的,它屏蔽了底层NIO以及框架层的实现细节,对于业务开发者来说,只需要关心业务逻辑的编排和实现即可。
组件关系及原理总结
如图11所示,表示Netty中关键的组件协调原理,具体的工作机制描述如下。
- 服务端启动初始化Boss和Worker线程组,Boss线程组负责监听网络连接事件,当有新的连接建立时,Boss线程会把该连接Channel注册绑定到Worker线程
- Worker线程组会分配一个EventLoop负责处理该Channel的读写事件,每个EventLoop相当于一个线程。通过Selector进行事件循环监听。
- 当客户端发起I/O事件时,服务端的EventLoop将就绪的Channel分发给Pipeline,进行数据的处理
- 数据传输到ChannelPipeline后,从第一个ChannelInBoundHandler进行处理,按照pipeline链逐个进行传递
- 服务端处理完成后要把数据写回到客户端,这个写回的数据会在ChannelOutboundHandler组成的链中传播,最后到达客户端。

图11 关键的组件协调原理图
3319

被折叠的 条评论
为什么被折叠?



