高性能服务器程序框架知识梳理
服务器编程框架
服务器程序种类有很多,但是基本框架都一样,核心不同点在于逻辑处理单元。基本框架包含:I/O处理单元、逻辑单元、网络存储单元以及请求队列。
- I/O处理单元(主线程):服务器用来管理客户连接的模块。通常需要等待并接受客户连接,接受客户数据,将服务器响应返回给客户端。但是,数据的收发也可能在逻辑单元(工作线程)中完成。(数据收发在I/O单元上一个典型的事件处理模式就是模拟Proactor)
- 逻辑单元:分析处理客户数据,将结果给I/O或者给客户端,服务器通常拥有多个逻辑单元实现对多个客户任务的并行处理
- 网络存储单元可以是数据库、缓存或者文件,甚至是一台独立的服务器,也不是必须的比如ssh
- 请求队列是各服务器之间预先建立的、静态的、永久的TCP连接,通常被设计为池的一部分,负责以上个单元的通信
I/O模型
- 同步I/O模型:比如阻塞I/O、I/O复用,即I/O的读写操作都是由应用程序来完成的,也就是说同步IO模型要求用户代码自动执行IO操作(即将数据从内核缓冲区读入或者写入用户缓冲区)
- 异步I/O模型:用户可以之间对IO执行读写操作,看起来是这样但是实际上用户只是处理了客户端的请求,没有真正执行IO操作,它实际上只告诉内核用户缓冲区的位置以及IO操作完成后如何通知应用程序进行剩下的逻辑处理,然后内核开始执行IO操作
- 综上,对于应用程序来说,同步IO模型告诉他的是事件是否就绪的信息,异步IO则是告诉他事件被完成的消息
高效的事件处理模式
Reactor模式(同步IO模型):
要求主线程(I/O处理单元)只负责监听fd上是否有事件发生,有的话立即将事件通知给工作线程(逻辑单元),逻辑单元进行读写数据(用户代码执行IO操作),接受新的连接以及处理客户请求,并将结果写入到socket上返回给客户。工作流程如下(以一次请求为例):
- 主线程往epoll注册socket上的读就绪事件
- 主线程调用epoll_wait监听,等待socket上有数据可读
- 当socket上有数据可读,epoll_wait通知主线程,主线程将事件放入请求队列唤醒工作线程处理
- 某个工作线程从socket读取数据,然后处理客户请求(协议解析 、处理业务逻辑比如数据库查询),并往epoll上注册socket上的写就绪事件
- 主线程调用epoll_wait监听写就绪事件
- epoll_wait通知主线程写就绪,主线程将写事件放入请求队列
- 请求队列上某个线程被唤醒,往socket写入客户请求的结果
Proactor(异步I/O模型):
所有的I/O操作交给内核处理,工作线程负责处理业务逻辑,工作流程如下(以aio_read/aio_read为例):
- 主线程调用aio_read向内核注册读完成事件,并且告诉内核用户缓冲区的位置以及读操作完成后如何通知应用程序
- 主线程继续处理其他逻辑
- 当socket上的数据被读入用户缓冲区(这里说明一下,从socket的上读数据,本质上是通过系统调用从内核缓冲区读取到用户缓冲区)后,内核通知应用程序告诉他可用
- 应用程序选择一个工作线程进行业务处理比如协议解析、查询数据库、业务判断等,处理完之后调用aio_read向内核注册一个写完成事件,并告诉内核用户缓冲区的位置,以及写操作完成后如何通知应用成
- 主线程继续执行其他逻辑
- 当用户缓冲区被写入socket后,内核通知应用程序,结果已经发送完毕,应用程序选择一个工作线程进行善后,决定是否关闭socket
综上,连接socket上的事件是由系统调用aio_read/aio_rea注册到内核的,主线程只调用epoll_wait只用来检测和监听socket上的连接请求事件
模拟Proator:
这就是使用同步I/O模拟出Proator的方法,主线程“充当”内核,工作线程依旧只处理业务逻辑。主线程往socket上注册事件,主线程调用epoll_wait()检测和监听事件,主线程处理读写事件。
两种高效的并发模式(针对I/O密集型,比如经常读写数据或访问数据库)
首先I/O模型中的同步和异步用来区分:
第一,内核向应用程序通知的是何种IO事件,是就绪事件还是完成事件
第二,由谁来完成IO读写操作,内核还是用户代码(应用程序)。
在并发模式中,同步指的是程序完全按照代码的顺序去执行;“异步”只程序的执行由系统事件驱动。
半同步/半异步:
同步线程用于处理客户逻辑,异步线程用于处理I/O事件。异步线程监听到客户请求后,将其封装插入请求队列,请求队列通知某个工作在同步模式的线程读取并处理该请求对象。
同属于该模式的还有:
- 半同步/半反应堆(结合了Reactor模式):工作过程见Reacor模式。 当然了也可以结合模拟proactor,主线程完成IO操作
- 主线程只负责管理监听socket,连接socket由工作线程来管理,并且该socket上的任何IO操作都由该工作线程负责,直到客户端关闭连接。简单来说,主线程监听到一个客户端的连接,就把一个客户端交给一个工作线程,就好像发任务一样,每个人对接一个客户,处理该客户的所有请求。所以,工作线程也是会维持自己的事件循环。
领导者/追随者
多个工作线程轮流获得事件源,轮流监听,分发并处理事件。在任意时间点,程序都有一个线程成为领导者,负责监听I/O事件。当前的领导者如果监听到I/O事件,首先从线程池中推选新的领导者,然后自己处理事件。此时,新的领导者在监听,旧领导者在执行实现了并发。
当然了,领导者在监听到事件时指定一个追随者来处理事件,此时领导者不变,继续监听。