总结《Linux高性能服务器编程》第八章
第八章 高性能服务器程序框架
服务器模型
-
C/S(客户端/服务器)模型
- 实现简单,适合资源相对集中的场合;
- 缺点:服务器是通信的中心,当访问量过大时,可能所有客户都将得到很慢的响应;
-
P2P(Peer to Peer,点对点)模型
服务器编程框架
模块 | 单个服务器程序 | 服务器机群 |
---|
I/O处理单元 | 处理客户连接,读写网络数据 | 作为接入服务器,实现负载均衡 |
逻辑单元 | 进程或线程,分析并处理客户数据 | 逻辑服务器 |
网络存储单元 | 本地数据库、缓存和文件 | 数据库服务器 |
请求队列 | 各单元之间的通信方式的抽象 | 各服务器之间预先建立的永久TCP连接 |
I/O模型
- socket在创建的时候默认是阻塞的,socket的基础API中,可能被阻塞的系统调用包括
accept
、send
、recv
和connect
; - 阻塞的文件描述符为阻塞I/O,称非阻塞的文件描述符为非阻塞I/O;
- 针对阻塞I/O执行的系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发生为止;
- 针对非阻塞I/O执行的系统调用则总是立即返回,而不管事件是否已经发生,如果事件没有立即发生,这些系统调用就返回-1,并区分原因;
- 非阻塞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事件(是就绪事件还是完成事件);
同步,异步,阻塞,非阻塞这些概念,是从不同的层次看待问题的
-
阻塞和非阻塞是指函数内部遇到资源繁忙时,线程是否挂起;
-
同步和异步是指函数返回之时, 能不能保证任务完成;
a. 异步只有非阻塞,只有同步才区分阻塞和非阻塞;
b. 同步是在更大的任务层面整体看待的,而不是函数内部的细节
两种高效的事件处理模式
两种高效的并发模式
-
如果程序是计算密集型,并发编程并没有优势,反而由于任务的切换使效率降低;
-
如果程序是I/O密集型,让程序阻塞于I/O操作将浪费大量的CPU时间,因此当前被I/O操作所阻塞的执行线程可主动放弃CPU,并将执行权转移到其他线程;
-
并发编程主要有多进程和多线程两种方式;
-
服务器主要有两种并发编程模式:半同步/半异步(half-sync/half-async)模式和领导者/追随者(Leader/Followers)模式;
-
半同步/半异步模式
-
在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行,“异步”指的是程序的执行需要由系统事件(中断、信号等)来驱动;
- 同步线程的效率低,实时性差,但逻辑简单;
- 异步线程的执行效率高,实时性强,但程序复杂;
-
半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程用于处理I/O事件;
-
半同步/半反应堆(half-sync/half-reactive)模式
- 主线程插入请求队列中的任务是就绪的连接socket,说明事件处理模式是Reactor模
式; - 缺点:主线程和工作线程共享请求队列,需要加锁保护;每个工作线程在同一时间只能处理一个客户请求,如果客户数量多,而工作线程少,则请求队列中将堆积很多任务对象;
-
相对高效的半同步/半异步模式
- 主线程只管理监听socket,连接socket由工作线程来管理;
- 主线程通过管道向工作线程派发socket;
- 每个线程(主线程和工作线程)都维持自己的事件循环,每个线程都工作在异步模式,所以它并非严格意义上的半同步/半异步模式;
-
领导者/追随者模式
- 多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件;
- 在任意时间点,程序都仅有一个领导者线程负责监听I/O事件,其他线程则都是追随者,它们休眠在线程池中等待成为新的领导者;
- 当前的领导者如果检测到I/O事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件,新的领导者等待新的I/O事件,而原来的领导者则处理I/O事件,二者实现了并发;
- 领导者/追随者模式包含如下几个组件:句柄集、线程集、事件处理器和具体事件处理器;
有限状态机
提高服务器性能的其他建议
-
池:以空间换时间
- 池是一组资源的集合,在服务器启动之初就被完全创建好并初始化(这静态资源分配),当服务器开始处理客户请求时,需要相关的资源可以直接从池中获取,无须动态分配;
- 根据不同的资源类型,池分为多种
- **内存池:**用于socket的接收缓存和发送缓存;
- **进程池和线程池:**常用于并发编程,当我们需要一个工作进程或线程来处理请求时,可以直接从进程或线程池中取得一个执行实体;
- 连接池:服务器预先和数据库程序建立的一组连接的集合;
-
数据复制
- 高性能服务器应该避免不必要的数据复制:
- 用户代码和内核之间发生数据复制;
- 用户代码内部(不访问内核)的数据复制:考虑使用共享内存,而不是使用管道或者消息队列;
-
上下文切换和锁
- 并发程序必须考虑上下文切换的问题,即进程切换或线程切换导致的的系统开销;
- 即使是I/O密集型的服务器,也不应该使用过多的工作线程,否则线程间的切换将占用大量的CPU时间;
- 并发程序需要考虑的另外一个问题是共享资源的加锁保护:
- 应该尽量避免使用锁,若必须要使用,则可以考虑减小锁的粒度;