8 高性能服务器程序框架

8.1 服务器模型

c/s模型

p2p模型

实际使用的P2P模型通常带有一个专门的发现服务器,提供查找服务

 

8.2 服务器编程框架

I/O处理单元是服务器管理客户连接的模块

一个逻辑单元通常是一个进程或线程,服务器通常由多个逻辑单元,实现对多个客户任务的并行处理

8.3 I/O模型

socket在创建的时候是默认阻塞的,可以通过给socket系统调用的第二个参数传递SOCK_NONBLOCK标志,或者通过fcntl系统调用的F_SETEL命令,将其设置为非阻塞的

针对阻塞I/O执行的系统调用可能因为无法立即完成而被操作系统挂起,可能被阻塞的系统调用包括accept send recv connect

针对非阻塞I/O执行的系统调用总是立即返回

非阻塞I/O通常要和其他I/O通知机制一起使用,比如I/O复用和SIGIO信号

Linux上最常用的I/O复用函数是select poll epoll_wait

以上都是同步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完成事件

8.4 两种高效的事件处理模式

服务器通常处理三类事件:I/O事件  信号事件  定时事件

同步I/O模型通常用于实现reactor模式  异步I/O模型通常用于实现proactor模式 

Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.

reactor模式 

同步IO模型(以epoll_wait为例)实现的Reactor模式的工作流程:

1. 主线程往epoll内核事件表中注册socket上的读就绪事件

2. 主线程调用epoll_wait等待socket上有数据可读。

3. 当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列。

4. 睡眠在请求队列上的工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件

5. 主线程调用epoll_wait等待socket可写。

6. 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。

7. 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。

工作线程从队列中取出事件后,将根据事件是可读或可写执行读写数据和处理请求的操作。因此,在Reactor模式中,没必要区分所谓的“读工作线程”和“写工作线程”。

 

proactor模式

与Reactor模式不同,Proactor模式将所有IO操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。

(以aio_read和aio_write为例)工作流程:
1. 主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例,详情sigevent的man手册)
2. 主线程继续处理其他逻辑。
3. 当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
4. 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区位置,以及写操作完成时如何通知应用程序(仍以信号为例)
5. 主线程继续处理其他逻辑。
6. 当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
7. 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket.

模拟proactor模式

主线程执行数据读写,主线程想工作线程通知完成事件

1. 主线程往epoll内核事件表中注册socket上的读就绪事件。
2. 主线程调用epoll_wait等待socket上有数据可读。
3. 当socket上有数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据,知道没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入到请求队列。
4. 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后网epoll内核事件表中注册socket上的写就绪事件。
5. 主线程调用epoll_wait等待socket可写。
6.0当socket可写时,epoll_wait通知主线程。主线程网往socket上写入服务器处理客户端请求的结果。

 

8.5 两种高效的并发模式

并发编程的目的是让程序同时执行多个任务,适合I/O密集型,不适合计算密集型

半同步半异步模式

这里的“同步”和“异步”和前面的IO的“同步”“异步”是完全不同的概念。在IO模型中,“同步”和“异步”区分的是内核向应用程序通知的是何种IO事件(是就绪事件还是完成事件),以及该由谁来完成IO读写(是应用程序还是内核)。

在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行;“异步”指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。

显然异步线程的执行效率高,实时性强,是很多嵌入式系统采用的模型。但编写异步方式执行的程序相对复杂,难于调试和扩展,而且不适合于大量的并发。而同步线程则相反,它虽然效率相对较低,实时性较差,但逻辑简单。

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

缺点:

1. 主线程和工作线程on共享请求队列,主线程往请求队列添加任务和工作线程从请求队列取出任务都需要给队列加锁

2. 每个工作线程同一时间只能处理一个客户请求,工作线程的切换将耗费大量CPU时间

一种高效的半同步/半异步模式(nginx应该是这种模式)

它每个工作线程能同时处理多个客户连接,主线程只管监听socket,连接socket由工作线程来管理

 

领导者/追随者模式

领导者追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。在任意时间点,程序都仅有一个领导者线程,它负责监听IO事件。而其他线程都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到IO事件,首先要从线程池中推选出新的领导者线程,然后处理IO事件。此时,新的领导者等待新的IO事件,而原来的领导者则处理IO事件,二者实现了并发。

句柄集(HandleSet):用于表示I/O资源,在linux下通常就是一个文件描述符

线程集(ThreadSet):所有工作线程的管理者,负责各线程之间的同步,以及新领导线程的推选

事件处理器(EventHandler)

具体的事件处理器(ConcreteEventHandler)

 

8.7 提高服务器性能的其他建议

服务器硬件资源相对“充裕”,那么提高服务器性能的一个很直接的方法就是以空间换时间,即“浪费”服务器的硬件资源,以换取其运行效率。这就是池(pool)的概念。池是一种资源的集合,这组资源在服务器启动之初就被完全创建并初始化,这称为静态资源分配。速度要快得多,因为分配系统资源的系统调用都是很耗时的。当服务器处理完一个客户连接后,可以把相关的资源放回池中,无须执行系统调用来释放资源。从最终效果来看,池相当于服务器管理系统资源的应用层设施,它避免了服务器对内核的频繁访问。

按照资源类型分类:
内存池:通常用于socket的接收缓存和发送缓存。
进程池、线程池:并发编程常用“伎俩”。
连接池:常用于服务器或服务器集群的内部永久连接。

数据复制  用零拷贝 sendfile

应该避免不必要的数据复制,尤其当数据复制发生在用户代码和内核之间的时候。如果内核可以直接处理从socket或者文件读入的数据,则应用程序就没有必要将这些数据从内核缓冲区复制到应用程序缓冲区。如ftp服务器,服务器只需检测目标文件是否存在,以及客户是否有读取权限,而不用关心文件具体内容。就可以使用“零拷贝”sendfile来直接将其发送给客户。

此外,用户代码内部(不访问内核)的数据复制也是应该避免的。如两个工作进程之间要传递大量的数据时,我们就应该考虑使用共享内存来在它们之间直接共享这些数据,而不是使用管道或者消息队列来传递。

http://www.linuxidc.com/Linux/2014-05/102321.htm

  

 

 

上下文切换和锁

并发程序必须考虑上下文切换(context switch)的问题,即进程线程切换导致的系统开销。即使是IO密集型的服务器,也不应该使用过多的工作线程(或进程,下同),否则切换将占用大量CPU时间,服务器真正用于业务逻辑的CPU时间比重就显得不足了。因此为每个客户连接都建立一个服务器线程的模型不可取。之前描述的半同步半异步模型是一个比较合理的解决方案,它允许一个线程同时处理多个客户连接。此外,多线程服务器的一个优点是不同的线程可以同时运行在不同的cpu上。当线程数量不大于cpu的数目时,上下文切换就不是问题了。

并发程序需要考虑的另一个问题是共享资源的加锁保护。锁通常被认为是导致服务器效率低下的一个因素,因为由它引入的代码不仅不处理任何业务逻辑,而且需要访问内核资源。因此,服务器如果有更好的解决方案,就应该避免使用锁。如果服务器必须使用锁,则可以考虑减小锁的粒度,比如使用读写锁。当所有工作线程都只读取一块共享内存的内容时,读写锁并不会增加系统的额外开销。只有当其中一个工作线程需要写这块内存时,系统才必须去锁住这块区域。

 

8.6 有限状态机

 一个http请求读取分析的例子

posted on 2015-12-09 16:57  已停更  阅读(995)  评论(0编辑  收藏  举报