Linux高性能服务器-第八章-高性能服务器程序框架

99 阅读10分钟

按照服务器程序的一般原理,可将服务器解构分为三个主要模块:

  • I/O处理单元,本章介绍四种I/O模型和两种高效事件处理模式;
  • 逻辑单元,本章介绍两种高效并发模式以及高效逻辑处理方式——有限状态机;
  • 存储单元,暂不讨论

8.2 服务器编程框架

服务器编程框架.png

8.3 I/O模型

IO模型-1709192929540.png

以read为例,一次read操作要经历两个阶段:1、等待数据就绪;2、将数据从内核拷贝到用户进程或线程中。

不同I/O模型在上面两个阶段情况各有不同。

8.3.1 阻塞I/O

在 linux 中,默认情况下所有的 socket 都是 blocking,一个典型的读操作流程为:

blockIO.png

当用户进程调用了 read,kernel 开始IO的第一个阶段:准备数据。等待足够的数据到来之前,用户进程会被阻塞。直到数据准备好了,数据从 kernel 中拷贝到用户内存,然后 kernel 返回结果,用户进程才解除 block 的状态,重新运行起来。所以, blocking IO 的特点就是在 IO 执行的两个阶段(等待数据和拷贝数据两个阶段)都被 block 。

listen()、send()、recv() 等接口都是阻塞型。实际上,除非特别指定,几乎所有的 IO 接口 ( 包括 socket 接口 ) 都是阻塞型的。

下面是一个简单的问答服务器模型: AQServer.png

上述服务器性能极低,若某个调用被阻塞,线程将无法执行任何运算或响应。假设对上述的服务器 / 客户机模型,提出更高的要求,即让服务器同时为多个客户机提供一问一答的服务。于是有了如下的模型:

AQServer2.png

在上述图例中,主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供为前例同样的问答服务。 这似乎已经解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率。

8.3.2 非阻塞I/O

nonBlocKIO.png

在非阻塞状态下, recv() 接口在被调用后立即返回,返回值代表了不同的含义。如在本例中,

  • recv() 返回值大于 0,表示接受数据完毕,返回值即是接受到的字节数;
  • recv() 返回 0,表示连接已经正常断开;
  • recv() 返回 -1,且 errno 等于 EAGAIN,表示 recv 操作还没执行完成;
  • recv() 返回 -1,且 errno 不等于 EAGAIN,表示 recv 操作遇到系统错误 errno。

非阻塞状态下,使用一个线程也能够同时从多个连接中检测数据是否送达,并且接受数据:

nonBlocKServer.png

8.3.3 多路复用I/O

多路复用的好处就在于单个 线程就可以同时处理多个网络连接的 IO。它的基本原理就是 select/epoll 这个function会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。

使用多路复用改进的问答服务器模型如下图:

MultiIOServer.png

这种模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。我们可以将这种模型归类为“事件驱动模型”。

相比其他模型,使用 select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。

但这个模型依旧有着很多问题。首先 select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时, select()接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如linux提供了epoll, BSD提供了kqueue, Solaris提供了/dev/poll, …。如果需要实现更高效的服务器程序,类似 epoll 这样的接口更被推荐。遗憾的是不同的操作系统特供的 epoll 接口有很大差异,所以使用类似于 epoll 的接口实现具有较好跨平台能力的服务器会比较困难。

其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。如下例,庞大的执行体 1 的将直接导致响应事件 2 的执行体迟迟得不到执行,并在很大程度上降低了事件探测的及时性。

幸运的是,有很多高效的事件驱动库可以屏蔽上述的困难,常见的事件驱动库有libevent 库,还有作为 libevent 替代者的 libev 库。这些库会根据操作系统的特点选择最合适的事件探测接口,并且加入了信号(signal) 等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择。

8.3.4 异步I/O

AIO.png

用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从 kernel的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。然后, kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后, kernel 会给用户进程发送一个 signal,告诉它 read 操作完成了。

区分异步IO和非阻塞IO:

non-blocking IO 在执行 read 这个系统调用的时候,如果 kernel 的数据没有准备好,这时候不会 block 进程。但是当 kernel 中数据准备好的时候, read 会将数据从 kernel 拷贝到用户内存中,这个时候进程是被 block 了,在这段时间内进程是被 block的。 而 asynchronous IO 则不一样,当进程发起 IO 操作之后,就直接返回再也不理睬了,直到 kernel 发送一个信号,告诉进程说 IO 完成。在这整个过程中,进程完全没有被 block。

8.3.5 信号驱动I/O

SIGIO.png

首先我们允许套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。当数据报准备好读取时,内核就为该进程产生一个 SIGIO 信号。我们随后既可以在信号处理函数中调用 read 读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它来读取数据报。无论如何处理 SIGIO 信号,这种模型的优势在于等待数 据报到达(第一阶段)期间,进程可以继续执行,不被阻塞。免去了 select 的阻塞与轮询,当有活跃套接字时,由注册的 handler 处理。

8.4 高效事件处理模式

服务器通常需要处理三类事件:I/O事件、信号及定时事件。此节先从整体上介绍两种高效的事件处理模式:Reactor和Proactor。同步I/O模型通常用于实现Reactor模式,异步I/O模型通常用于实现Proactor模式。

8.4.1 Reactor

Reactor模式要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)。除此之外,主线程不做任何其他实质性工作。读写数据,接受新连接,以及处理客户请求均在工作线程中完成。

Reactor流程.png

Reactor流程2-1709194031283.png

工作线程从请求队列中取出事件后,将根据事件类型来决定处理方式。

对于可读事件,执行读数据和处理请求。对于可写事件,执行写数据操作。因此,在Reactor模式中,没必要区分读写工作线程。

目的: 服务器高效服务多个客户端

演进:

  1. 为每条连接创建一个线程处理。

    问题: 处理完业务后,随着连接关闭线程也要销毁,不停创建销毁浪费资源;上万连接创建上万线程处理不现实。

    解决方式:线程池

  2. 创建线程池,将连接分配给线程,每个线程可处理多个连接。

    问题:处理时仍是一线程对应一连接,线程一般采用【read读请求--处理业务--send发响应】的流程来处理,若当前线程连接没有数据可读,那线程就会阻塞在read操作上。

    解决方式:异步IO

  3. 采用异步IO,线程不断轮询调用read来判断有无数据可读。

    问题:大幅提高CPU占用率

    解决方式:异步IO的问题在于线程无法获知当前连接是否有数据可读,只能不断轮询。IO复用可实现只有当连接上有数据的时候,线程才发起读请求。

8.4.2 Proactor

8.5 高效并发模式

服务器的并发模式是指IO处理单元和多个逻辑单元之间协调完成任务的方法。服务器主要有两种并发编程模式:半同步/半异步模式、领导者/追随者模式。

8.5.1 半同步/半异步模式

这里的同步、异步与前面IO模型中讨论的同步、异步是完全不同的概念。

在IO模型中,同步与异步区分的是内核向应用程序通知的是何种IO事件(就绪还是完成事件),以及该由谁来完成IO读写(应用还是内核)。在并发模式中,同步指程序完成按代码顺序执行,异步指程序运行需系统事件来驱动。

同步异步模式:

同步异步模式.png

异步执行效率高,实时性强,但难以调试和扩展,同步效率较低,实时性差,但逻辑简单。对服务器来说,既要求高性能,又要求能同时处理多个客户请求,可使用半同步/半异步模式。

半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程用于处理IO事件。异步线程监听到客户请求后,就将其封装为请求对象并插入到请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象,具体选择哪个工作线程,取决于请求队列的设计。

8.5.2 领导者/追随者模式

领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。

在任意时刻,程序都仅有一个领导者线程,它负责监听IO事件,其他线程则都是追随者,它们休眠在线程池中等待成为新的领导者。

当前的领导者若检测到IO事件,首先要从线程池中推选出新的领导者,然后处理IO事件。此时,新领导者线程继续监听IO事件,原领导者则处理刚才的IO事件,二者并发。

领导者线程自己监听IO事件并处理客户请求,因而领导者/追随者模式不需要再线程之间传递任何额外的数据,也无须像半同步/半反应堆模式那样在线程之间同步对请求队列的访问,但领导者/追随者的一个明显缺点是仅支持一个事件源集合。