文章目录
对于一个服务器而言,主要有如下三部分构成:
- I/O处理单元。主要有四种I/O模型和两种高效的事件处理模式。
- 逻辑单元。主要有两种高效的并发模式。
- 存储单元。
1. 服务器模型
C/S模型
- 服务端创建一个或者多个监听socket->bind->listen
- 客户端 connect。
由于客户连接请求是随机到达的异步事件,服务器需要使用某种I/O模型来监听到连接请求->accept->分配逻辑单元为新的连接服务。
逻辑单元就可以是新创建的子进程,子线程或者其他。
C/S模型非常适合资源相对集中的场合,并且它的实现也很简单,但其缺点也很明显:服务器是通信中心,当访问量过大时,可能所有客户都将得到很慢的响应。P2P可以解决这个问题。
P2P 模型
占坑-》可衍生到 Dynamo(分布式键值系统)
2. 服务器编程框架
虽然服务器程序种类繁多,但其基本框架都一样,不同之处在于逻辑处理。其基本框架如下所示:
模块 | 单个服务器程序 | 服务器集群 |
---|---|---|
I/O处理模块 | 处理客户端连接,读写网络数据 | 作为接入服务器,实现负载均衡 |
逻辑单元 | 业务进程或者线程 | 逻辑服务器 |
网络存储单元 | 本地数据库,文件或者缓存 | 数据库服务器 |
请求队列 | 各个单元之间的通信方式 | 各个服务器之间的永久TCP连接 |
I/O处理单元:服务器用来管理客户连接的模块
。通常就是等待并接受新的客户连接,数据的收发(这个会取决于具体的事件处理模式)。对于集群而言,就是作为一个接入服务器,实现负载均衡等。逻辑单元:通常是一个进程/线程等
。分析处理客户数据,然后将结果传递给I/O单元或者直接发送给客户端(取决于具体的事件处理模式)。对于集群而言,就是一台逻辑服务器的某个部分。网络存储单元:DB,缓存,文件
请求队列:是各个单元之间通信的抽象
。I/O单元接收到请求时,需要以某种方式通知逻辑单元。逻辑单元访问存储单元时同理。对于集群,就是在各个服务器之间建立的永久TCP连接。这样可以节省一定的开销。
3. I/O处理单元之几大I/O模型
阻塞与非阻塞见:阻塞-非阻塞-异步-同步-的理解
所以我们只有在事件已经发生的情况下去操作非阻塞 I/O,才是有效的 。因此,非阻塞I/O一般会与其他的I/O通知机制一起使用。比如:I/O复用,SIGNO信号等。
在处理 IO 的时候,阻塞和非阻塞都是同步 IO。
只有使用了特殊的 API 才是异步 IO。
-
对于I/O模型而言,同步是:
I/O 的读写操作,都是在I/O事件发生之后,由应用程序来完成的,由用户代码自行执行I/O操作(将数据从内核缓冲区读人用户缓冲区,或将数据从用户缓冲区写人内核缓冲区) -
异步是:
用户可以直接对I/O执行读写操作,这些操作告诉内核用户读写缓冲区的位置,以及I/O操作完成之后内核通知应用程序的方式
。异步I/O的读写操作总是立即返回,而不论I/O是否是阻塞的,因为真正的读写操作已经由内核接管。由内核来执行I/O操作(数据在内核缓冲区和用户缓冲区之间的移动是由内核在“后台”完成的)。
3. I/O处理单元之两大高效的事件处理模式
Reactor 模式(同步)
Reactor 是这样一种模式,它要求主线程只负责监听文件描述上是否由事件发生(相当于只监听就绪事件)
,有的话就立即将该事件通知工作线程,除此之外,主线程不做任何其他实质的工作。
读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步I/O模型(以epoll_wait为例子)实现的Reactor模式的工作流程如下所示:
- 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
- 主线程调用epoll_wait等待socket上有数据可读。
- 当socket上有数据可读时,epoll_wait通知主线程。主线程将socket可读事件放入请求队列中。
- 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪时间。
- 主线程用epoll_wait等待socket可写。
- 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它网socket上写入服务器处理客户请求的结果。
Proactor 模式(异步)
Proactor 模式将所有 I/O 操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。
因此,Proactor模式更符合服务器框架。
使用异步I/O模型(以aio_read和aio_write为例)实现的Proactor模式的工作流程如下:
- 1)主线程调用aio_read函数向内核注册socket上的读完成时间,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序。
- 2)主线程继续处理其他逻辑。
- 3)当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
- 4)应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序。
- 5)主线程继续处理其他逻辑。
- 6)当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
- 7)应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket。
注意:epoll_wait 不能在此处检测连接socket上的读写事件。只能用来检测监听socket上的连接请求事件。
aio_read/aio_write见:只说一件事之 Linux 异步 I/O-AIO
逻辑单元之两种高效的并发模式
并发编程的目的是让程序“同时”执行多个任务。
- 如果程序是
计算密集型的(尽量保证不切换CPU)
,并发编程并没有优势,反而由于任务的切换使效率降低。 - 如果程序是I/O密集型的,比如经常读写文件,访问数据库等,则情况就不同了。
由于I/O操作的速度远没有CPU的计算速度快,所以让程序阻塞于1/O操作将浪费大量的CPU时间
。如果程序有多个执行线程,则当前被I/O操作所阻塞的执行线程可主动放弃CPU(或由操作系统来调度),并将执行权转移到其他线程。这样一来,CPU就可以用来做更加有意义的事情
(除非所有线程都同时被I/O操作所阻塞),而不是等待I/O 操作完成,因此CPU的利用率显著提升。
并发模式是指I/O处理单元和多个逻辑单元之间协调完成任务的方法。服务器主要有两种并发编程模式:
- 半同步/半异步(half-synce/half-async) 模式
- 领导者/追随者(Leader/Followers) 模式。
半同步/半异步(half-synce/half-async) 模式
在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行:“异步”指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。(那么协程就是通过自行切换来实现异步通知喽~~)
半同步/半异步模式中,同步线程用于处理客户逻辑,相当于图8-4中的逻辑单元:异步线程用于处理I/O事件,相当于图8-4中的I/O处理单元。异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象
。具体选择哪个工作线程来为新的客户请求服务,则取决于请求队列的设计。比如最简单的轮流选取工作线程的RoundRobin算法,也可以通过条件变量(见第14章)或信号量(见第14章)来随机地选择-一个工作线程。图8-9总结了半同步/半异步模式的工作流程。
其变体:半同步/半反应堆模式
线程插入请求队列中的任务是就绪的连接socket。这说明该图所示的半同步/半反应堆模式采用的事件处理模式是Reactor 模式:它要求工作线程自己从socket上读取客户请求和往socket写入服务器应答。这就是该模式的名称中"half-reactive"的含义。
实际上,半同步/半反应堆模式也可以使用模拟的Proactor事件处理模式,即由主线程来完成数据的读写。在这种情况下,主线程一般会将应用程序数据、任务类型等信息封装为-一个任务对象,然后将其(或者指向该任务对象的- . 个指针)插人请求队列。工作线程从请求队列中取得任务对象之后,即可直接处理之,而无须执行读写操作了。如下图:
半同步/半反应堆模式存在如下缺点:
- 主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从请求队列中取出任务,都需要对请求队列加锁保护,从而白白耗费CPU时间。
- 每个工作线程在同一时间只能处理- .个客 户请求。如果客户数量较多,而工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这- ~问题,则工作线程的切换也将耗费大量CPU时间。
再变
当有新的连接到来时,主线程就接受之并将新返回的连接socket派发给某个工作线程, 此后该新socket上的任何I/O操作都由被选中的工作线程来处理,直到客户关闭连接
。主线程向工作线程派发socket的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道上有数据可读时,就分析是否是-一个新的客户连接请求到来。如果是,则把该新socket,上的读写事件注册到自已的 epoll 内核事件表中。
OK,如果工作线程有I/O 操作,那么就交给逻辑单元处理,逻辑单元一般就是进程或者线程
。
还有我觉得需要注意的就是:需不需要将连接全部发到一个工作线程上,如果需要,那就是多个请求复用一个TCP连接(即长连接),如果不需要就是短连接 。
领导者/追随者(Leader/Followers) 模式。
领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。
在任意时间点,程序都仅有一个领导者线程,它负责监听I/O事件。而其他线程则都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到IO事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件。此时,新的领导者等待新的I/O事件,而原来的领导者则处理I/O事件,二者实现了并发。
领导者/追随者模式包含如下几个组件:
- 句柄集(HandleSet):文件描述符
- 线程集(ThreadSet)
- 事件处理器( EventHandler)
- 具体的事件处理器(ConcreteEventHandler)。
详情见:Linux高性能
- 优劣:
由于领导者线程自己监听IO事件并处理客户请求,因而领导者/追随者模式不需要在线程之间传递任何额外的数据,也无须像半同步/半反应堆模式那样在线程之间同步对请求队列的访问。但领导者/追随者的一个明 显缺点是仅支持一个事件源集合,因此也无法像图8-11所示的那样,让每个工作线程独立地管理多个客户连接。
Redis 的事件循环模式
占坑------