我们按照服务器的一般原理,可将服务器解析为三个主要的模块:IO处理单元(主要用于处理客户连接,读写网络数据),逻辑单元(业务进程或者线程,用于分析客户数据并将结果传递给IO处理单元或者而直接发给客户端,具体是哪种方式取决于事件处理模式),存储单元(本地数据库或者缓存)。模块之间需要有可靠的通信方式,一般为请求队列,或者是池。
一、两种高效事件处理模式
服务器通常要处理三种事件:IO事件、信号以及定时事件,对应就需要事件的处理方式,同步IO模型通常用于实现Reactor模式,异步IO模型则用于实现Proactor模式。
(1)Reactor模式
Reactor是主动要求主线程(IO处理单元)只负责监听文件描述符上是否有事件发生,有的话就立刻将事件通知工作进程(逻辑单元),除此之外,主线程不做任何实质性工作,读写数据、接受新连接以及处理客户请求均在工作线程完成。
使用同步IO模型(以epoll为例)实现Reactor模式的流程:
- 主线程网epoll内核事件表中注册socket上的读就绪事件
- 主线程调用epoll_wait等待socket上有数据可读
- 当socket上有数据可读时候,epoll_wait通知主线程,主线程将socket可读事件放入请求队列
- 睡眠在请求队列上的某个工作线程被唤醒,其从socket读数据并处理客户请求,然后往epoll内核事件表中注册该socket的写就绪事件
- 主线程调用epoll_wait等待socket可写
- 当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列
- 睡眠在请求队列上的某个工作线程被唤醒,他往socket上写服务器处理客户请求的结果
(2)Proactor模式
Proacto模式将所有的IO操作都交给主线程和内核来完成,工作线程只负责业务逻辑。使用异步IO模型(以aio_read和aio_write
)实现Proactor模式的工作流程:
- 主线程调用aio_read函数往内核注册socket的读完成事件,并告诉内核用户读缓冲区的位置,以及操作完成时如何通知应用程序(如信号)
- 主线程继续处理其他逻辑
- 当socket上数据被读入用户缓冲区,内核应该向应用程序发一个信号,以通知应用程序数据可用
- 应用程序预先定义好信号处理函数选择一个工作线程来处理客户请求。工作线程处理完成后,调用aio_write函数向内核注册socket上写完成事件,并告诉内核写缓冲区的位置,以及写操作完成后如何通知应用程序(仍然以信号为例)
- 主线程继续处理其他逻辑
- 当用户缓冲区数据被写入socket,内核向应用程序发送一个信号,以通知应用程序数据以及发送完毕
- 应用程序预先定义好信号处理函数,并选择一个工作线程来善后处理,比如决定是否关闭socket
(3)模拟Proactor模式
可以用同步IO模拟Proactor模式的一种方法,主要原理:主线程执行数据的读写操作,读写完成后主线程向工作线程通知这一完成事件。
使用同步IO模型(epoll)模拟Proactor模式工作流程:
- 主线程网epoll内核事件表中注册socket上的读就绪事件
- 主线程调用epoll_wait等待socket上有数据可读
- 当socket上有数据可读时候,epoll_wait通知主线程。主线程从socket循环读取数据,直到没有更多数据,然后将读取道德数据封装成一个请求对象并插入请求队列
- 睡眠在请求队列上的某个工作线程被唤醒,其获取请求对象并处理客户请求,然后往epoll内核事件表中注册该socket的写就绪事件
- 主线程调用epoll_wait等待socket可写
- 当socket可写时,epoll_wait通知主线程,主线程往socket上写入服务器处理客户端的请求结果
三、两种高效并发模式
并发模式是指IO处理单元和多个逻辑单元之间协调完成任务的方法,服务器有两种并发编程模式:半同步/半异步模式和领导者/追随者模式。
(1)半同步/半异步模式
首先要注意,这里的”同步“、”异步“概念与之前的IO模型中的同步异步概念完全不同。在IO模型中,”同步“和”异步“是指内核向应用程序通知的是何种IO事件(就绪事件还是完成事件),以及由谁来完成IO读写(是应用程序还是内核)。在并发模式中,”同步“是指程序完全按照代码顺序执行;”异步“是指程序的执行需要由系统事件来驱动。异步编写程序相对复杂,难于调试和扩展,不适合大量的并发,而同步线程则相反,其虽然效率相对较低,实时性较差,但逻辑简单。
半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程用于处理IO事件。
如下是一种半同步/半反应堆模式,其采用的事件处理模式是Reactor模式,因为它要求工作线程自己从socket上读取客户请求和往socket上写入服务器应答。主要过程:异步线程只有一个,由主线程充当,他负责监听所有socket上事件,如果监听到socket上有可读事件发生,即有新的连接请求到来,主线程就接受之以得到新的连接socket,然后往epoll内核事件表中注册该socket读写事件,如果连接socket上由读写事件发生,即有新的客户端请求到来或数据要发送到客户端,主线程就将该连接socket插入请求队列,所有空闲工作线程都睡眠在请求队列中,当有任务到来,他们将通过竞争(如申请互斥锁)获得任务接管权。
半同步/半反应堆模式有以下缺点:
- 主线程和工作线程共享请求队列
- 每个工作线程在同一时间只能处理一个客户请求
如图是一种高效的半同步/半异步模式,他的每个工作线程都能同时处理多个客户连接。
主线程只管理监听socket,连接socket由工作线程处理,当由新的连接到来时,主线程就接受之并将新返回的连接socket派发给某个工作线程,以后该新socket上的任何IO操作都有这个被选中的工作线程处理,直到客户关闭连接。主线程向工作线程派发socket最简单的方式就是往它和工作线程之间的管道里写数据。工作线程利用epoll_wait可监听管道上读事件,然后分析是否是一个新客户连接请求,如果是则把新socket上的读写事件注册到自己的epoll内核事件表中。
高效的半同步/半异步模式中,每个线程都维护自己的事件循环,他么可以各自独立的监听不同的事件,这样每个线程都工作在异步模式。
(2)领导者/追随者模式
领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。在任意时间,程序只有一个领导者线程,他负责监听IO事件,而其他线程都是追随者,他们休眠在线程池中,等待成为新的领导者,当前领导者如果检测到IO事件,首先要从线程池中选取新的领导线程,然后处理IO事件,此时新的领导线程等到新的IO事件,旧的领导线程处理IO事件,如此实现并发。这种模式的缺点在于只支持一个事件源集合,因此无法让每个工作线程独立地管理多个客户连接。
领导者/追随者模式有四个组件:句柄集(handleset)、线程集(Threadset)、事件处理器(Eventhandler)、具体事件处理器(concreteEventhandler)。他们关系如下图:
- 句柄集用于表示IO资源,Linux下通常为文件描述符,使用wait_for_event方法监听这些句柄上的IO事件,并将其中的就绪事件通知领导线程,领导线程则调用绑定到handler上的事件处理器来处理事件,
- 线程集,这个组件是所有工作线程的管理者,负责各个线程之间的同步,以及新领导者线程的推选,任意时间的一个线程必处于三个状态之一:Leader(领导线程)、Processing(正在处理事件的线程)、Follower(追随者线程)
- 事件处理器和具体事件处理器 ,一般包括一个或多个回调函数handler_event,回调函数用于处理事件对应的业务逻辑。
四、服务器性能提高建议
- 池。池是一组资源的集合,这组资源在服务器启动之初就完全被创建好及初始化(即静态分配),当有客户请求相关资源时,直接从池中分配获取即可,无须动态分配。有个问题就在于静态分配的时候无法预计要多少资源,多了就会浪费,少了就会不够用。常见的池有进程池,线程池,内存池,连接池。
- 数据复制。高性能服务器应尽量避免发生在内核与用户代码之间的数据复制,如当内核可以直接处理从socket或文件读入的数据,则应用程序就不应该把这些数据从内核缓冲区复制到用户缓冲区。除此之外,两个进程之间如果有大量的数据需要交换,应该利用共享内存使其直接共享,而不是用管道或者消息队列来传递。
- 上下文切换和锁。并发程序需要考虑进程或者线程切换导致的系统开销。对共享资源的加锁保护也会导致服务器效率低下,如果必须用到”锁“,则最好选取粒度小的锁,如读写锁。
参考《Linux高性能服务器编程》