《Linux高性能服务器编程》学习笔记-----服务器程序架构

服务器程序架构

服务器编程基本框架

《Linux高性能服务器编程》学习笔记-----服务器程序架构_第1张图片

服务器程序的基本框架如上图所示,上图既能表示一台服务器,也能表示一个服务器集群。其中各模块的含义和功能如下表所示。

模块 单个服务器程序 服务器集群
I/O 处理单元 处理客户连接,读写网络数据 作为接入服务器,实现负载均衡
逻辑单元 业务进程或线程 逻辑服务器
请求队列 各单元之间的通信方式 各服务器之间的永久TCP连接
网络存储单元 本地数据库、文件或缓存 数据库服务器

I/O模型

I/O 模型指的是程序处理 I/O 的模式。例如,对于阻塞 I/O,程序只有等到上一个 I/O 事件完成后,才能继续往下处理其他任务。

几种 I/O 模型的对比如下表所示:

I/O模型 读写操作和阻塞阶段
阻塞 I/O 会在读写操作时发生阻塞
I/O 复用 对 I/O 本身的读写操作是非阻塞的;会在 I/O 复用系统调用时发生阻塞,但可以同时监听多个 I/O 事件
SIGIO 信号 程序没有阻塞阶段;信号触发读写就绪事件,应用程序来处理读写操作;
异步 I/O 程序没有阻塞阶段;内核执行读写操作并触发读写完成事件;

阻塞 I/O

阻塞 I/O 是指,执行可能会发生阻塞的系统调用后,系统调用不能立即完成并返回,因此操作系统会将其挂起,直到等待的事件(写完成、读完成等)发生为止。

非阻塞 I/O

非阻塞 I/O 执行的系统调用总是立即返回,不管事件是否已经发生。如果事件没有立即发生,这些系统调用返回 -1,然后设置 errno,应用程序需要根据返回的 errno 进行相应的处理。

I/O 复用

I/O 复用是指,应用程序通过 I/O 复用函数(select、poll、epoll)向内核注册一组事件,内核通过 I/O 复用函数把其中就绪的事件通知(发送)给应用程序,应用程序接收并处理这些就绪事件。

I/O复用函数本身是阻塞的,它能提高程序效率的原因是 I/O 复用函数具有同时监听多个 I/O 事件的能力。

SIGIO 信号(待补充)

SIGIO 是指通过信号来捕获 I/O 事件,

异步 I/O

阻塞I/O、I/O复用和信号驱动I/O都是同步I/O模型,在这三种 I/O 模型中,I/O 的读写操作都是由应用程序完成的。而异步 I/O 模型是指,I/O 的读写操作由内核完成。

对于异步 I/O,应用程序将用户读写缓冲区的位置以及 I/O 操作完成之后内核通知应用程序的方式告知内核。即,同步 I/O 模型需要用户程序自行将数据从内核缓冲区读入到用户缓冲区;异步 I/O 模型由内核将数据从内核缓冲区移动到用户缓冲区。从事件通知的角度来看,同步 I/O 向应用程序通知的是 I/O 就绪事件(你快来将数据取走吧),异步 I/O 向应用程序通知的是 I/O 完成事件(我已经将数据发送给你了,你可以直接使用了)。

注意:在 I/O模型中,“同步”和“异步”的概念是指,内核向应用程序通知的是何种 I/O 事件(就绪事件 还是 完成事件)。

事件处理模式

服务器程序通常要处理三类事件:I/O 事件、信号及定时事件。

事件处理模式是指,各种事件之间的协同处理关系。例如,对于服务端监听 socket 的主进程/主线程,当监听到一个客户端发来的连接请求时,是在主线程处理连接请求,还是使用一个子进程/子线程处理连接请求。可以根据不同的处理连接请求的方式,划分不同的事件处理模式。

下面介绍两种事件处理模式,Reactor模式和Proactor模式。Reactor模式通常使用同步模型实现,Proactor模式通常使用异步模型实现。

Reactor(反应堆)模式

Reactor模式是指,一个主线程(也即上面服务器框架图中的I/O处理单元)只负责监听文件描述符上的是否有事件发生,而不处理事件;若有事件发生,则将发生的事件通知(转交给)工作线程(也即上面服务器框架图中的逻辑单元)处理。

同步I/O模型(epoll_wait)实现的Reactor模式

《Linux高性能服务器编程》学习笔记-----服务器程序架构_第2张图片

工作流程如下:

  1. 主线程向 epoll 内核事件表中注册 socket(监视 socket 和连接 socket) 上的读就绪事件;
  2. 主线程调用 epoll_wait 等待 socket 上有数据可读;
  3. socket 上有数据可读时,epoll_wait 通知主线程,于是主线程将 socket 可读事件放入请求队列;
  4. 睡眠在请求队列的某个工作线程被唤醒,该工作线程从 socket 上读取数据,然后进行相应的处理;然后该工作线程往 epoll 内核事件表中注册 socket(连接 socket,此时应该是已经完成了TCP连接的建立) 上的写就绪事件。例如,如果是监听 socket 上有数据可读,则处理客户端的连接请求;如果是连接 socket 上有数据可读,则根据数据内容处理客户端相应的请求。
  5. 主线程调用 epoll_wait 等待 socket 可写;epoll_wait 调用都是由主线程完成,因为 Reactor 模式下事件的发生都是由主线程来监视的;
  6. socket 可写时,epoll_wait 通知主线程,主线程将 socket 可写事件放入请求队列;
  7. 睡眠在请求队列上的某个工作线程被唤醒,然后该工作线程将客户的请求结果写入 socket

注意:

  • 上述方案中,工作线程从请求队列中取出事件后,根据事件的类型来决定如何处理。对于读事件,执行读数据或处理请求;对于写事件,执行写数据的操作。因此,Reactor模式中没有读/写工作线程之分。
  • 上述是《Linux高性能服务器编程》中给出的方案是,事件的通知方式是,以请求队列的方式把读写事件从主线程传递给工作线程。思考主线程和工作线程之间传递读写事件的方案,并对比分析优缺点。

Proactor 模式

Proactor模式是指,所有的 I/O 操作都交给主线程和内核来处理,工作线程只负责业务逻辑处理。

异步 I/O 模型(aio_read和aio_write)实现的 Proactor 模式

《Linux高性能服务器编程》学习笔记-----服务器程序架构_第3张图片

工作流程如下:

  1. 主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核,用户读缓冲区的位置,以及读操作完成后如何通知应用程序(这里以信号为例,参考 sigevent),然后主线程可以继续处理其他逻辑操作。
  2. 当 socket 中的数据已经读入用户的缓冲区,内核将向应用程序发送一个信号,通知应用程序数据已经可用;
  3. 应用程序根据预先定义好的信号处理函数选择一个工作线程来处理缓冲区中的数据;工作线程根据缓冲区中的数据进行相应的处理,处理完毕后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成后如何通知应用程序(以信号为例);然后主线程可以继续处理其他逻辑操作。
  4. 当用户缓冲区中的数据被写入 socket 后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
  5. 应用程序根据预先定义好的信号处理函数选择一个工作线程来处理接下来的操作,例如是否决定关闭 socket。

注意:

内核通过信号向应用程序报告的是连接 socket 上的读写事件。

使用同步I/O模拟Proactor模式

游双的《Linux高性能服务器编程》中给出的案例如下。

使用同步 I/O 模拟Proactor模式的原理是,主线程执行数据的读写操作,读写完成之后,主线程向工作线程通知“读写完成”事件。因此工作线程就可以直接获得完成读写后的数据,直接对数据进行相应的逻辑操作。

《Linux高性能服务器编程》学习笔记-----服务器程序架构_第4张图片

使用同步 I/O 模型(以epoll_wait 为例)模拟的Proactor模式的工作流程如下:

  1. 主线程向 epoll 内核事件表中注册 socket(监听 / 连接) 上的读就绪事件;
  2. 主线程调用 epoll_wait 等待 socket 上有数据可读;
  3. 当 socket 上有数据可读时,epoll_wait 通知主线程,主线程从 socket 上循环读取数据,直到没有更多的数据可读(即完成一次数据的读操作),然后将读取到的数据封装成一个请求对象并插入到请求队列中;
  4. 睡眠在请求队列上的某个工作线程被唤醒,获得请求对象,根据请求对象中的数据内容进行相应的数据处理,然后往 epoll 内核事件表中注册 socket 上的写就绪事件;
  5. 主线程调用 epoll_wait 等待 socket 可写;
  6. 当 socket 可写,epoll_wait 通知主线程,主线程往 socket 上写入服务器处理客户请求的结果;

思考

使用同步 I/O 模拟Proactor模式时,首先考虑是使用阻塞的 I/O 还是非阻塞的 I/O;考虑并发,应该首选的是使用非阻塞的 I/O。

其次,对于使用同步 I/O 模拟Proactor模式,主线程要负责所有的 I/O 读写,让并发量上来,主线程的处理能力会受到很大的影响,思考当并发量变大,在该模式下,如何提高主线程的并发处理能力。

个人的一个思路:使用多个主线程来处理数据的读写操作(这里就要考虑线程间的竞争关系了),然后使用一个类似于负载均衡的线程来管理这些主线程,即选择使用当前工作负荷小的主线程来处理读写事件。

并发模式

并发模式是指,I/O 处理单元和多个逻辑单元之间协调完成任务的方法,以使得CPU能够被充分利用。

并发模式中,“同步”是指程序完全按照代码的逻辑顺序进行执行,“异步”是指程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。在 I/O 模型中,“同步”和“异步”的区分是内核向应用程序通知的是何种 I/O 事件(就绪事件还是完成事件),以及由谁来负责完成 I/O 的读写(应用程序还是内核)。

半同步/半异步(half-sync/half-async)模式

按照同步方式运行的线程称为同步线程,按照异步方式运行的线程称为异步线程。

对于半同步、半异步模式,服务端程序同时使用同步线程和异步线程来实现。在该模式中,同步线程用于处理客户逻辑,即服务器基本框架中的逻辑处理单元;异步线程用于处理 I/O 事件,即服务器基本框架中的 I/O 处理单元。

半同步、半异步模式的基本逻辑如下:

  • 异步线程负责监听客户端发来的请求,当监听到客户端发来的请求后,将请求数据封装成一个请求对象插入到请求队列中;
  • 请求队列将通知某个工作在同步模式下的工作线程来读取并处理该请求对象;具体选择哪个工作线程来为新的客户请求服务,取决于请求队列的设计,比如轮流选择工作线程的 Round Robin 算法,也可以通过条件变量或信号量来随机地选择一个工作线程。

半同步/半反应堆模式

考虑事件处理模式和 I/O 模型,一种半同步/半异步模式为 半同步/半反应堆(half-sync / half-reactive),如下图所示。

《Linux高性能服务器编程》学习笔记-----服务器程序架构_第5张图片

半同步 / 半反应堆模式的工作流程如下图所示:

  1. 异步线程只有一个(异步的线程可以有多个吗?),由主线程充当;主线程负责监听所有 socket (监听 socket 和 连接 socket)上的读写事件;
  2. 当有新的连接到来,即监听 socket 上有读事件发生,主线程就接收该连接 socket,然后向 epoll 内核事件表中注册该连接 socket 上的读写事件;
  3. 当连接 socket 上有读写事件发生时,主线程将该连接 socket 插入到请求队列中;当请求队列中有新任务到来,睡眠在请求队列中的工作线程通过竞争(例如互斥锁)获得任务的处理权(通过竞争机制使得只有空闲的工作线程才有机会处理新任务)。对于读事件,工作线程需要自己从连接 socket 上读取数据;对于写事件,工作线程需要自己往连接 socket 上写入数据。因为读写操作都是由工作线程自己完成的,因此称为 half-reactive。

对于上述的半同步/半反应堆模式,事件处理采用的是反应堆模式(Reactor),若事件处理模式采用的 Proactor 模式,区别在于,主线程负责完成 socket 上数据的读写,这种情况下,主线程一般将应用程序数据、任务类型等信息封装为一个任务对象,然后将其(或者指向该任务的一个指针)插入请求队列,工作线程从请求队列中取得任务对象之后,即可直接处理。

半同步/半反应堆模式存在的缺点主要如下:

  • 主线程和工作线程是共享请求队列的,因此主线程往共享队列中插入任务,或者工作线程从共享队列中获取任务,都需要对请求队列加锁,会消耗浪费CPU时间;
  • **每个工作线程同一时间只能处理一个任务,即一个客户请求。**若客户数量增多,而工作线程较少,请求队列中会存在大量任务得不到即使处理,使得客户端响应速度变慢;若增加工作线程的数量,CPU之间的切换开销也会随之增大。

一种相对高效的半同步/半异步模式是,工作线程可以同时处理多个客户连接,工作流程如下:

  1. 主线程只负责处理监听 socket,即主线程中的epoll内核事件表中(以 epoll_wait 为例)只注册监听socket上的读事件;
  2. 当监听socket上有读事件发生,即有新的连接请求到来,主线程就负责接收该连接socket并将该socket派发给某个工作线程,此后该连接socket上的任何 I/O 都有该工作线程负责,直至客户关闭连接。主线程向工作线程派发连接 socket 的一种简单方式是,主线程往它和工作线程之间的管道里写数据,当工作线程检测到管道中有读数据可读时,就可以判断是否为一个新的客户的连接请求,如果是,则把该连接socket上的读写事件注册到该工作线程的epoll内核事件表中;注意是注册到工作线程的epoll内核事件中
  3. 每个工作线程负责维护自己的事件,因此每个工作线程同时处理多个客户连接,即每个工作线程通过管理自己的 epoll_event 数组来管理多个客户连接。

该模式相对高效的原因在于,每个工作线程负责独立管理各自的内核事件,因此每个工作线程可以独立的处理多个连接socket。需要注意的是,主线程和工作线程都是以异步模式进行工作的(事件驱动),因此并非严格意义上的半同步/半异步模式。

领导者/追随者模式

领导者/追随者模式的思想是,多个线程轮流监听、分发处理事件的一种模式。

更具体地,在线程池中,任意时刻都仅有一个线程作为领导者线程,负责监听 I/O 事件,而其他线程作为追随者线程;当领导者线程监听到 I/O 事件,领导者线程首先从其他线程中推选出一个线程作为新的领导者线程,然后新的领导者线程负责继续监听 I/O 事件,而原来的领导者变成追随者,并处理该 I/O 事件。

领导者线程监听到 I/O 事件后,也可以继续充当领导者,而指定某一个追随者线程来处理监听到的 I/O 事件,这样就有点退化为了上述的半同步半异步模式了。

《Linux高性能服务器编程》中给出的领导者/追随者模式中,主要包含三个组件:句柄集(HandleSet)、线程集(ThreadSet)和事件处理器(EventHandler、包括具体的事件处理器(ConcreteEventHandler))。

句柄集(HandleSet)

句柄集负责管理一组 I/O 资源,一个句柄(Handler)通常为 Linux 下的一个文件描述符。句柄集负责监听其所管理的文件描述符上的 I/O 事件(调用某个方法让领导者线程负责监听文件描述符上的 I/O 事件)和文件描述符上对应的事件处理器的绑定和解绑操作。

线程集

线程集负责管理所有工作线程(领导者线程和追随者线程),包括新的领导者线程的推选和线程之间的同步。

事件处理器(具体的事件处理器)

事件处理器通常包含一个或多个回调函数,用于处理对应事件的具体业务逻辑。事件处理器需要事先绑定在句柄上(某个文件描述符上),当某个文件描述符上的某个事件发生后,就执行与之绑定的事件处理器中的回调函数(由线程集决定是由原来的领导者执行还是指定一个追随者来执行)。

优缺点

  • 领导者线程负责监听并处理 I/O 事件,因此不需要在线程之间传递额外的数据,也即不需要对请求队列进行同步操作。
  • 只支持一个事件源集合(对比相对高效的半同步/半异步模式),因此每个线程无法同时处理多个客户的连接请求。(如何设计,解决该问题)

你可能感兴趣的:(网络编程,cpp,服务器,linux,开发语言,c++)