服务器基本的架构如下:
图中的各个单元在单一服务器和集群中育有类似的功能:
模块单元 | 单一服务器 | 服务器集群 |
IO处理单元 | 处理客户连接,读写网络数据 | 提供接入服务,实现负载均衡 |
逻辑单元 | 业务进程或线程 | 逻辑服务器 |
网络存储单元 | 本地数据库、文件、缓存 | 数据库服务器 |
请求队列 | 各单元的通信方式 | 各服务器间的永久TCP连接 |
服务器需要处理三类事件:IO事件、信号处理、定时事件。相应的处理模式和设计模式一样被广泛使用
2.1 Reactor模式
Reactor模式要求主IO处理线程只负责监听文件描述符或socket上是否有事件发生,如果有立刻通知逻辑单元进行读写数据等指定的操作。IO处理线程不做其他工作,只监听指定事件。Reactor模式的工作流程(以epoll_wait IO 复用函数为例)如下:
Reactor模式的优点有:
Reactor模式的缺点有:
2.2 Proactor模式
Proactor模式将所有的IO操作都交给主线程和内核处理,包括监听和数据读写。工作线程仅负责业务逻辑,不关系IO操作何时进行和如何进行。 下图是Proactor模式的工作流程(以异步IO函数aio_read和aio_write函数为例):
Proactor模式的优点是:
Proactor模式的缺点有:
文章福利 Linux后端开发网络底层原理知识学习提升,后台私信(Linux)获取,完善技术栈,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。
我们知道,多进程和多线程是并发编程的两种方法,这里先讲并发模式,强调IO处理单元和逻辑单元间如何协调。
3.1 半同步/半异步模式
并发中同步是指按照代码顺序依次执行;异步是指通过中断、信号等系统事件跳跃执行代码。图示如下:
半同步半异步模式中:
半同步半异步并发模式 + Reactor事件处理模式可以得到一个高效的服务器处理模式:
3.2 领导者/追随者模式
任意时刻,程序只有一个领导者线程,负责监听IO事件;其他线程作为追随者,在线程池中休眠等待成为领导者。 领导者线程获得IO事件后,先选出新的领导者,然后处理IO事件,这样新的领导者监听IO事件,原来的领导者处理IO事件,之后变为追随者休眠。 领导者模式的状态变化图如下:
该模式的工作流程如下:
领导者线程自己监听并处理事件请求,所以不需要线程之间通信,也不需要请求队列。但缺点是仅支持handleSet中已有的事件,不能灵活的让工作线程独立管理其他事件。
4.1 池pool
提高服务器性能的一个很直接的方法就是以空间换时间,即“浪费”服务器的硬件资源,以换取其运行效率。这就是池(pool)的概念。 池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源分配。当服务器进入正式运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无须动态分配。很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的。 当服务器处理完一个客户连接后,可以把相关的资源放回池中,无须执行系统调用来释放资源。从最终的效果来看,池相当于服务器管理系统资源的应用层设施,它避免了服务器对内核的频繁访问。 池可以分为多种形式:
4.2 避免不必要的数据复制
高性能服务器应该避免不必要的数据复制,尤其是当数据复制发生在用户代码和内核之间的时候。如果内核可以直接处理从socket或者文件读入的数据,则应用程序就没必要将这些数据从内核缓冲区复制到应用程序缓冲区。
比如ftp服务器,当客户请求一个文件时,服务器只需要检测日标文件是否存在,以及客户是否有读取它的权限,而绝对不会关心文件的具体内容。这样的话,ftp服务器就无须把日标文件的内容完整地读人到应用程序缓冲区中并调用send函数来发送,而是可以使用“零拷贝”函数sendfile来直接将其发送给客户端。
用户代码内部(不访问内核)的数据复制也应该避免。举例来说,当两个进程之间要传递大量的数据时,我们就应该考虑使用共享内存来在它们之间直接共享这些数据,而不是使用管道或者消息队列来传递,因为这样既浪费空间,又效率低下。
4.3 考虑上下文切换和锁
并发程序必须考虑上下文切换(context switch)的问题,即进程切换或线程切换导致的的系统开销。即使是IO密集型的服务器,也不应该使用过多的工作线程,否则线程间的切换将占用大最的CPU时问,服务器真正用于处理业务逻辑的CPU时间的比重就显得不足了。
如果有更好的解决方案(比如半同步半异步模式),尽量避免使用锁。如果一定要使用锁,要考虑减小锁的粒度,做到影响最小。