高性能服务器程序框架
服务器模型
C/S模型
TCP/IP协议在设计和实现上并没有客户端和服务器的概念,在通信过程中所有机器都是对等的
C/S(客户端/服务器)模型:所有客户都通过访问服务器来获取所需的资源
适合资源相对集中的场合,并且其实现较为简单
服务器是通信的中心,当访问量过大时,可能所有的客户都将得到很慢的响应
P2P模型
P2P(Peer to Peer,点对点)模型
摒弃了以服务器为中心的格局,让网络上所有主机重新回归到对等的地位
P2P模型使得每台机器在消耗服务的同时也给别人提供服务,这样资源能够充分、自由地共享
当用户之间传输的请求过多时,网络的负载将加重
主机之间很难相互发现,实际使用的P2P模型通常带有一个专门的发现服务器
发现服务器还提供查找服务(甚至还可以提供内容服务),使每个客户都可以尽快找到自己需要的资源
服务器编程框架
模块 | 单个服务器程序 | 服务器机群 |
---|---|---|
I/O处理单元 | 处理客户连接,读取网络数据 | 作为接入服务器,实现负载均衡 |
逻辑单元 | 业务进程或线程 | 逻辑服务器 |
网络存储单元 | 本地数据库、文件或缓存 | 数据库服务器 |
请求队列 | 各单元间通信方式 | 各服务器间的永久TCP连接 |
I/O模型
阻塞I/O
程序阻塞于读写函数
只有在时间已经发生的情况下操作非阻塞I/O才能提高程序的效率。因此非阻塞I/O通常要和其他I/O通知机制一起使用,比如I/O复用和SIGIO信号
I/O复用
程序阻塞于I/O复用系统调用,但可以同时监听多个I/O事件,对I/O本身的读写操作是非阻塞的
SIGIO信号
信号触发读写就绪事件,用户程序执行读写操作,程序没有阻塞阶段
异步I/O
内核执行读写操作并触发读写完成事件,程序没有阻塞阶段
两种高效的事件处理模式
服务器程序通常要处理三类事件:I/O事件、信号及定时事件
Reactor模式
要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)
主线程不做任何其他实质性工作
读写数据,接收新连接以及处理客户请求均在工作线程中完成
使用同步I/O模型(以epoll_wait为例)实现的Reactor模式的工作流程
Proactor模式
将所有I/O操作都交给主线程和内核处理,工作线程只负责业务逻辑
使用异步I/O模型(以aio_read和aio_write为例)实现的Proactor模式的工作流程
连接socket上的读写事件是通过aio_read/aio_write
像内核注册,内核通过信号来向应用程序报告连接socket上的读写事件,所有主线程中的epoll_wait
调用只能用于检测监听socket上的连接请求事件,而不能用来检测连接socket上的读写事件
两种高效的并发模式
半同步/半异步模式
在I/O模型中,同步和异步区分的是内核向应用程序通知的是何种I/O事件(就绪事件还是完成事件),以及该由谁来完成I/O读写(应用程序还是内核)
在并发模式中,同步指程序完全按照代码序列的顺序执行,异步指程序的执行需要由系统事件来驱动
异步线程的执行效率高,实时性强,但编写以异步方式执行的程序相对复杂,难以调试和扩展,不适合大量的并发
同步线程效率相对较低,实时性较差,但逻辑简单
对于服务器需要较好的实时性且要求能同时处理多个客户请求的应用程序,应该同时使用同步线程和异步线程实现,即使用半同步/半异步模式
同步线程用于处理客户逻辑,相当于逻辑单元;异步线程用于处理I/O事件,相当于I/O处理单元
异步线程监听到客户请求后,将其封装为请求对象并插入请求队列中,请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象
半同步/半反应堆
异步线程只有一个,由主线程充当,负责监听所有socket上的事件,如果监听socket上有可读事件发生,即有新的连接请求到来,主线程九接受以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件。如果连接socket上有读写事件发生,即有新的客户请求到来或有数据要发送到客户端,主线程就将该连接socket插入请求队列中,所有的工作线程都睡眠在请求队列上,当有任务来时,它们将通过竞争(比如申请互斥锁)获取任务的接管权
缺点:
- 主线程和工作线程共享请求队列,主线程往请求队列中添加任务,或工作线程从请求队列中取出任务,都需要堆请求队列加锁保护,从而耗费CPU时间
- 每个工作线程在同一时间只能处理一个客户请求,如果客户数量较多,工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢,如果通过增加工作线程来解决,则工作线程的切换也将耗费大量CPU时间
高效的半同步/半异步模式
每个工作线程能够同时处理多个客户连接
主线程只管理监听socket,连接socket由工作线程管理。当有新的连接到来时,主线程就接受并将新返回的连接socket派发到某个工作线程,此后该新socket上的任何I/O操作都由选中的工作线程来处理,直到客户关闭连接
并非严格意义上的半同步/半异步模式
领导者/追随者模式
多个工作做线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式
在任意事件点,程序都仅有一个领导者线程,负责监听I/O事件,其他线程都是追随者,休眠在线程池中等待成为新的领导者
当前的领导者如果检测到I/O事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件
此时,新的领导者等待新的I/O事件,原来的领导者处理I/O事件,二者实现了并发
句柄集
句柄(Handle)用以表示I/O资源,在Linux下通常为一个文件描述符。句柄集管理众多句柄,使用wait_for_event()
监听句柄上的I/O事件,并将其中的就绪事件通知给领导者线程
领导者则调用绑定到Handle上的事件处理器来处理事件
线程集
所有工作线程的管理者,负责线程之间的同步以及新领导者线程的推选
线程在任意事件必处于如下三种状态之一
领导者线程推选新的领导者和追随者等待成为新领导者这两个操作都将修改线程集,线程集提供一个成员Synchronizer
来同步这两个操作,避免竞态条件
事件处理器和具体的事件处理器
事件处理器通常包含一个或多个回调函数handle_event
,这些回调函数用于处理事件对应的业务逻辑,事件处理器在使用前需要被绑定到某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器中的回调函数
具体的事件处理器是事件处理器的派生类,必须重新实现基类的handle_event
方法,以处理特定的任务
由于领导者线程自己监听I/O事件并处理客户请求,因此该模式不需要再线程之间传递额外的数据,也无须向半同步/半反应堆那样在线程之间同步对请求队列的访问
缺点:仅支持一个事件源集合,无法让每个工作线程独立管理多个客户连接
提高服务器性能建议
池
以空间换时间
池是一组资源的集合,这组资源在服务器启动之初就被完全创建号并初始化,这称为静态资源分配
当服务器进去正式运行阶段,即开始处理客户请求时,如果需要相关的资源,就可以直接从池中获取,无须动态分配
当服务器处理完一个客户连接后,可以将相关的资源返回池中,无须执行系统调用来释放资源
池相当于服务器管理系统资源的应用层设施,避免了服务器对内核的频繁访问