高性能网络框架笔记四(IO线程模型)

上一文介绍中,我们详述了网络数据包的接收和发送过程,并通过介绍5中IO模型了解了内核是如何读取网络数据并通知给用户线程的。

前面的内容都是以内核空间的视角来剖析网络数据的收发模型,本小节我们站在用户空间的视角来看一下如何对网络数据进行收发。

相对内核来讲,用户空间的IO线程模型相对简单一些。这些用户空间的IO线程模型都是在讨论当前多线程一起配合工作时谁负责接收连接,谁负责响应IO读写,谁负责计算,谁负责发送和接收,仅仅是用户IO线程的不同分工模式。

1、Reactor

Reactor是利用NIO对IO线程进行不同的分工:

  • 使用前面我提到的IO多路复用模型,比如select、poll、epoll,kqueue进行IO事件的注册和监听。
  • 将监听到就绪的IO事件分发dispatch到各个具体的处理Handler中进行相应的IO事件处理。

通过IO多路复用技术就可以不断的监听IO事件,不断的分发dispatch,就像一个反应堆一样,看起来像不断的产生IO事件,因此我们称这种模式为Reactor模式。

Reactor模型分三类:

1.1 单Reactor单线程

Reactor是依赖IO多路复用技术实现监听IO事件,从而源源不断的产生IO就绪事件,在Linux系统下我们使用epoll来进行IO多路复用,我们以Linux系统为例:

  • 单Reactor意味着只有一个epoll对象,用来监听所有的事件,比如连接事件,读写事件。
  • 单线程意味着只有一个线程来执行epoll_wait获取IO就绪的socket,然后对这些就绪的socket执行读写,以及以后的业务处理也依然是这个线程。

单Reactor单线程模型就好比我们开了个很小的饭馆,作为老板的我们需要一个人看所有的事情,包括:迎接顾客(accept事件),为顾客介绍菜单并等待顾客点菜(IO请求),做菜(业务处理),上菜(IO响应),送客(断开连接)

1.2单Reactor多线程

随着客人的增多(并发请求),显然饭馆只有一个人(单线程)干活肯定忙不过来,这时招聘一些员工(多线程)来帮忙干上述事情。

于是就有了单Reactor多线程模型:

  • 这种模式下,也是只有一个epoll对象来监听所有的IO事件,一个线程来调用epoll_wait获取IO就绪的socket。
  • 但是当IO就绪事件产生时,这些IO事件对应处理的业务Handler,我们是通过线程池来执行。这样相比单Reactor单线程模型提高了执行效率,充分发挥了多核CPU的优势。

1.3主从Reactor多线程

做任何事情都要区分事件的优先级,我们应该优先高效的去做优先级更高的事情,而不是一股脑儿不分优先级的全部去做。

当我们的小饭馆客人越来越多(并发量越来越大),我们就需要扩大饭店的规模,在这个过程中我们发现,迎接客人是饭店最重要的工作,我们要把客人迎进来,不能让客人一看人多就走掉,只要客人进来了,哪怕菜做的慢一点也没关系。

  • 我们由原来单Reactor变为了多Reactor。主Reactor用来优先专门做优先级最高的事情,也就是迎接客人(处理连接事件)。
  • 创建好连接,简历对应的socket后,在acceptor中将要监听的read事件注册到从Reactor中,由从Reactor来监听socket上的读写事件。
  • 最终将读写的业务逻辑交给线程池处理。

注意:这里想从Reactor注册的只是read事件,并没有注册write事件,因为read事件是有epoll内核触发的,而write事件则是由用户业务线程触发的(什么时候发送数据是由具体业务线程决定的),所以write事件理应由用户业务线程去注册。

用户线程注册write事件的时机是只有当用户发送的数据无法一次性全部写入buffer时,才会去注册write事件,等待buffer重新科协时,继续写入剩下的发送数据,如果用哪个好线程一股脑的将发送数据全部写入buffer,那么也就无需注册write事件到从Reactor中。

主从Reactor多线程模型是目前大部分主流网络框架中采用的一种IO线程模型。Netty就是用的这种模型。

2、Preactor

Proactor是基于AIO对IO线程进行分工的一种模型。前面我们介绍了异步IO模型,它是操作系统内核支持的一种全异步编程模型,在数据准备阶段和数据拷贝阶段全程无阻塞。

Proactor线程模型将IO事件的监听,IO操作的执行,IO结果的dispatch统统交给内核来做。

Proactor模型组件介绍:

  • completion handler 为用户程序定义了异步IO操作回调函数,在异步IO操作完成时被内核回调通知IO结果。
  • completion Event Queue 异步IO操作完成后,会产生对应的IO完成事件,将IO完成事件放到该队列中。
  • Asynchronous Operation Processor负责异步IO的执行。执行完成后产生IO完成事件放入completion Event Queue队列中。
  • proactor是一个事件循环派发器,负责从completion Event Queue中获取IO完成事件,并回调与IO完成事件关联的completion handler。
  • Initiator初始化异步操作(asynchronous operation)并通过asynchronous Operation Processor将completion handler和proactor注册到内核。

Proactor执行过程:

  • 用户线程发起aio_read,并告诉内核用户空间的都读缓存地址,以便内核完成IO操作将结果放入用户空间的读缓冲区,用户线程直接可以读取结果(无任何阻塞)。
  • Initiator初始化aio_read异步读取操作(asynchronous operation),并将completion handler注册到内核。

在Proactor中我们关系的IO完成事件:内核已经帮助我们读好数据并放入我们指定的读缓冲区,用户线程可以直接读取。在Reactor中我们中我们关系的是IO就绪事件:数据已经到来,但是需要用户线程自己去内核读取。

  • 此时用户线程就可以做其他的事情了,无需等待IO结果。而内核与此同事开始异步执行IO操作。当IO操作完成时会产生一个completion event事件,将这个IO完成事件放入completion event queue中。
  • Proactor从completion event queue中取出completion event,并回调与IO完成事件关联的completion handler。
  • 在completion handler中完成业务逻辑处理。

3、Reactor与Proactor对比

  • Reactor是基于NIO实现的一种IO线性模型,Proactor是基于AIO实现的IO线程模型。
  • Reactor关心的是IO就绪事件,Proactor关心的是IO完成事件。
  • 在Proactor中,用户程序需要向内核传递用户空间的读缓冲区地址。Reactor则不需要。这也就导致了Proactor中没有并发操作都要求有独立的缓冲区,在内存上有一定的开销。
  • Proactor的实现逻辑复杂,编码成本较Reactor要高的多。
  • Proactor在处理高耗时IO时的性能要过于Reactor,但对于低耗时IO的执行效率提升并不明显。

4、Netty的IO模型

介绍完网络数据包在内核中的收发过程以及五种IO模型和两种线程模型后,线程我们来看下Netty中的IO模型是什么样的。

在我们介绍Reactor IO线程模型的时候提到有三种Reactor模型:单Reactor单线程,单Reactor多线程,主从Reactor多线程。

这三种Reactor模型在netty中都是支持的,但是我们常用的是主从Reactor多线程模型。

而我们之前介绍的三种Reactor只是一种模型,是一种设计思想。实际上各种网络框架在实现中并不是严格按照模型来实现的,会有一些小的不同,但大体设计思想上是一样的。

下面我们来看下netty中的主从Reactor多线程模型是什么样子:

  • Reactor在netty中是以group的形式出现的。netty中将Reactor分为两组,一组是MainReactorGroup也就是我们在编码中常常看到的EventLoopGroup bossGroup,另一组是SubReactorGroup也就是我们在编码中常常看到的EventLoopGroup workerGroup。
  • MainReactorGroup通常只有一个Reactor,专门负责做重要的事情,也就是监听连接accept事件。当有连接事件产生时,在对应的处理handler acceptor中创建初始化相应的NioSocketChannel(代表一个Socket连接)。然后以负载均衡的方式在SubReactorGroup中选取一个Reactor,注册上去,监听Read事件。

MainReactorGroup中只有一个Reactor的原因是,通常我们的服务端程序只会绑定监听一个端口,如果绑定监听多个端口,就会配置多个Reactor。

  • SubReactorGroup中有多个Reactor,具体Reactor的个数可以由系统参数 -D io.netty.evnetLoopThreads指定。默认的Reactor的个数为CPU核数*2。SubReactorGroup中的Reactor主要负责监听读写事件,每个Reactor负责监听一组socket连接。将全部的连接分摊在多个Reactor中。
  • 一个Reactor分配一个IO线程,这个IO线程负责从Reactor中获取IO就绪事件,执行IO调用获取IO数据,执行PipeLine。

socket连接在创建后就被固定的分配给一个Reactor,所以一个socket连接也只会被一个固定的IO线程执行,每个socket连接分配一个独立的PipeLine实例,用来编排这个socket连接上的IO处理逻辑。这种无锁串行化的设计目的是为了防止线程并发执行同一个socket连接上的IO逻辑处理,防止出现线程安全问题。同时使系统吞吐量达到最大化

由于每个Reactor中只有一个IO线程,这个IO线程叫执行IO活跃的socket连接对应的Pipeline中的ChannelHandler,又要从Reactor中获取IO就绪事件,执行IO调用。所以Pipeline中channelhandler中执行的逻辑不能耗时太长,尽量将耗时的业务逻辑处理放入单独的业务线程池中处理,否则会影响其它连接的IO读写,从而近一步影响到整个服务程序的IO吞吐。

当IO请求在业务线程中完成相应的业务逻辑处理后,在业务线程中利用持有的ChannelHandlerContext引用将响应数据在Pipeline中反向传播,最终写回给客户端。

netty支持的三种Reactor模型:

配置单Reactor单线程

EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap(); 
serverBootstrap.group(eventGroup);

配置单Reactor多线程

EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap(); 
serverBootstrap.group(eventGroup);

配置主从Reactor多线程

EventLoopGroup bossGroup = new NioEventLoopGroup(1); 
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap(); 
serverBootstrap.group(bossGroup, workerGroup);

 

https://mp.weixin.qq.com/s/Wx8_LeqYPnqSKGQKDhrstg

posted @ 2022-03-20 16:49  钟齐峰  阅读(179)  评论(0编辑  收藏  举报