高性能IO模型与线程模型

IO模型

linux系统实现IO通信,主要是依靠几个系统调用函数实现的:

  • recvFrom实现非阻塞和阻塞IO,该系统调用会阻塞当前线程,直到内核缓存区文件描述符就绪,便停止阻塞,将数据从内核缓存区读取到用户缓存区进行处理,这边是阻塞IO;如果recvFrom系统调用时,如果内核缓存区文件描述符没有就绪,立即返回一个EWOULDLOCK错误,便会中断阻塞,通过应用程序不断以该种方式进行内核数据访问,便实现了NIO:


    BlockingIO.png
NonBlockingIO.png
  • select/poll实现多路复用IO,该系统调用同样会阻塞当前线程,直到内核有文件描述符准备就绪,但是该方法和recvFrom不一样的地方在于select/poll系统调用可以注册多个文件描述符,监听多个感兴趣事件,从而可以实现单一线程阻塞监听事件,下发通道事件,多个线程并发处理事件任务,极大地提高了程序性能;netty中的eventloop事件处理机制就是依靠这实现的;


    MultiPlexIO.png
  • SIGIO实现信号/事件驱动IO,该系统调用会在系统内核安装一个事件处理器后立即返回,当文件描述符准备就绪时,会以SIGIO信号通知应用程序,这时应用程序调用recvFrom系统调用到内核缓存区拷贝数据,在拷贝数据这一过程仍然是同步的;


    SIGIO.png
  • int aio_read( struct aiocb *aiocbp )实现异步IO,该系统调用和事件处理IO并没什大的区别,主要区别在于事件驱动IO是在文件描述符在内核准备就绪时通知处理,而aio_read是在数据在内核拷贝到应用程序缓存区完成时便通过应用程序在aio_read()方法注册回调函数进行数据处理,这才是真正的异步IO,解决了IO线程读取数据阻塞以及应用程序无需重新通过类似线程池的方式实现异步;


    AIO_READ.png

阻塞和非阻塞,异步和同步的区别
阻塞指的是一个对象必须等条件满足时才会去做其他事,例如recvfrom系统调用必须等到有数据响应式,才会中断等待,去做其他事,非阻塞就是条件满足就处理该事件,不满足则处理其他事件;
同步指的是两个对象处理相关事件时必须保证事件的有序性,例如外卖小哥送外卖,只有当外卖送来时,客户才会去取快递,客户取完外卖,外卖小哥才能离开,而异步则是两个对象处理相关事件时无需保证有序性,例如快递员送快递,快递到了放到快递柜里面,然后通知客户取快递,然后可以自行做其他事情,同时客户可以选择时间取快递;

线程模型

上面介绍了服务器如何基于IO模型管理连接,获取数据,接下来介绍下应用程序如何IO模型来处理请求

传统IO模型

传统IO模型会为每个请求都分配一个线程用来处理该请求,关于该请求的read,handle,和send都放在一个线程中进行处理,如下图所示:


传统IO阻塞模型.png

特点:

  • 采用阻塞式IO模型获取输入数据
  • 为每个请求都创建一个独立的线程来完成读取数据,业务处理,发送数据等操作;

缺点:

  • 当存在大量连接时,会造成系统创建大量处理线程从而占用大量系统资源;
  • 当连接建立后,如果没有数据可读时,那么连接会进入阻塞状态,从而浪费该线程资源;

基于线程池的伪异步IO模型

针对传统IO模型中会造成线程资源极大浪费的缺点,通过线程池来复用线程处理客户端连接和数据处理,
如下图所示:


基于线程池复用的IO模型.png

在该模型中,accptor用来处理socket连接的,如果有socket连接请求则将该客户端socket包装成一个Task,交给线程池处理
特点:

  • 会有一个阻塞线程负责socket连接,即acceptor;
  • 会有一个线程池维护n个活跃线程和一个消息队列,来处理socketTask,所以资源是可控的,所以无论客户端多少并发连接,都会导致系统资源耗尽和宕机;

缺点:

  • 无法解决通信阻塞的问题,因为socket.read()方法是流式数据读取,因此只能读取完所有数据后才能正确处理,如果一个socket发送数据需要60秒那么该线程处理数据至少要60秒,那么这段时间内的io事件,该线程是无法及时处理的,如果这样的io事件出现多次,很可能造成消息队列阻塞;
  • 只有一个acceptor负责socket连接,如果线程池阻塞队列阻塞之后,那么所有新的客户端连接也将会被拒绝;如果大量连接拒绝,就可能会认定为系统故障;

Reactor模型(实时响应)

IO复用结合线程池复用就是Reactor模型设计的基本思想;
IO复用技术有select/poll,epoll等相关系统调用来实现,关于其具体区别此处暂时不介绍;IO复用主要是利用一个阻塞线程负责监听多个连接通道,当某个连接通道有新数据需要处理,则由操作系统通知应用程序,该阻塞线程则从阻塞状态返回,进行业务处理;
Reactor模式就是基于多个服务请求的事件驱动处理模型,通过复用一个阻塞的请求事件监听对象,来同步分发多个请求到指定的线程handle,进行业务处理,因此Reactor模式也叫做Dispatcher模式;
其基本设计模型图如下:


Reactor基本模型.png

Reactor模式有两个关键组成:

  • Reactor即Dispatcher,在一个单独的线程中运行,负责监听请求和分发事件,就相当于电话的接线员,它接听来自客户的电话并转交给指定的客户进行通信;
  • Handlers,处理实际要完成的IO事件的业务处理器;

Reactor模型可以分为:单Reactor单线程模型,单Reactor多线程模型和主从Reactor多线程模型;

单Reactor单线程模型

该模型监听连接请求和分发连接,以及处理连接都在一个线程中完成,该模型图如下:


单Reactor单线程.png

该模型优点:

  • 单一线程,所有操作都在一个线程中完成,模型简单,没有线程竞争;
    该模型缺点:
  • 单一线程,无法发挥多核cpu的优点,一次性只能执行一个连接,性能效率比较低;
  • 可靠性问题,线程意外跑飞或者陷入死循环,会造成整个通信模块瘫痪,从而无法接收和处理数据,出现节点故障
    使用场景:
    比较适合客户端连接较少,业务场景比较简单,处理速度比较快(时间复杂度为o(1)),这时单Reactor单线程模型可以有非常好的性能表现,如redis;
单Reactor线程池模型

单Reactor线程池模型是针对使用场景较复杂,客户端连接比较多时采用的一种模型,其模型图如下:


单Reactor线程池模型.png

具体说明:

    1. Reactor通过单一线程负责监听客户端请求,如果是连接事件,则交给Acceptor进行处理,如果是数据处理,则交给Handler用来读取数据(如进行协议头校验,序列化body数据),然后将处理后的数据包装成task交给线程池处理;
    1. 线程池负责具体的业务处理,处理完成后通过handler,将处理结果返回给客户端(该过程可以通过回调实现)

优点:

  • 充分发挥多核cpu的优点,可以同时处理多个task

缺点:

  • 如果存在超大并发连接时,单Reactor处理所有事件的监听和数据读写处理可能会导致系统出现瓶颈;
主从Reactor线程池模型

针对单Reactor线程池模型中,高并发场景可能导致Reactor出现性能瓶颈,则可以让Reactor在多线程中执行,具体模型图如下:


主从Reactor线程池模型.png

具体说明:

  • Main Reactor线程负责监听连接事件并建立连接,连接建立后交给Sub Reactor进行数据传输事件处理;
  • Sub Reactor线程负责监听传输数据事件,其内部会为每个连接建立一个连接处理队列,每当一个连接有可读数据到达,便为该连接调用相应Handler进行数据读取,并将处理后的数据,交给线程池进行业务处理,会后再通过回调,将业务处理后数据通过Handler发送给客户端;

优点:

  • 该模型有两个Reactor可以分别处理连接事件和数据传输事件,从而避免某一类事件出现故障而导致其他事件不可用,在高并发场景下非常实用;

缺点:

  • 对操作系统性能要求比较高,需要多核cpu

Proactor

由Reactor模型是基于IO复用技术实现的,而select/poll系统调用需要等socket数据准备好了才能进行数据读取,而数据没准备后则会进入阻塞状态,因此Reactor模型是非阻塞同步模型;
如果把IO操作改为异步,交给操作系统完成,在完成IO操作时,回调对应事件处理器,那么可以进一步提升性能,这是Proactor模式;
Proactor模式读事件处理流程如下:

  • 应用程序会初始化一个异步操作;
  • 异步操作中注册读事件和事件完成时需要回调的事件处理器(这是与Reactor模式较大的区别),事件分离器等待异步读操作完成;
  • 异步操作处理器(kernel中的aio_read())执行该异步操作,然后将读取的数据发送的用户空间分配的缓存上;
  • 事件分离器捕捉到异步读取操作完成,则回调事件处理器,事件处理器就会到缓存上读取数据;

Proactor模式理论上效率会高于Reactor, 但是其也有如下缺点:

  • 编程复杂性,由于异步操作流程的事件的初始化和事件完成在时间和空间上都是相互分离的,因此开发异步应用程序更加复杂。应用程序还可能因为反向流程控制,从而使得debug更加困难;
  • 内存使用,缓存区在读写操作的时间段必须保持住,可能造成持续的不确定性,并且每个并发操作都要求有独立的缓存,相比Reactor模式,在Socket已经准备好读或者写前,是不需要开辟缓存的;
  • 操作系统支持,windows下通过IOCP实现了真正的异步,而在Linux系统下,linux2.6才引入异步,目前异步IO还不完善;

你可能感兴趣的:(高性能IO模型与线程模型)