文章目录
- 两种服务器模型及三个模块
- C/S模型
- P2P模型
- I/O处理单元、逻辑单元、存储单元
- 并发
- 同步与异步
- 半同步/半异步模式
- 变体:半同步/半反应堆模式
- 改进:高效的半同步/半异步模式
- 领导者/追随者模式
- 组件 :句柄集、线程集、事件处理器
- 工作流程
两种服务器模型及三个模块
C/S模型
即常说的 客户端/服务器 模型,将资源(视频、文本、图片、软件等)提供者视作服务器,资源请求者视为客户端。
由于客户端连接请求(connect函数
)是随机到达的异步事件,服务器需要使用某种 I/O模型 来监听这一事件。例如 I/O复用技术之一的 select系统调用:当监听到连接请求后,服务器就调用 accept函数
接收它,并分配一个 逻辑单元(新创建的子进程、子线程等) 管理这个新连接。
工作流程如下图所示:
服务器在处理一个客户请求的同时还要继续监听其他客户请求,否则就变成了效率低下的串行服务器了(必须先处理完前一个客户的请求,才能继续处理下一个客户请求)。这一点上图中是通过 select系统调用
实现的。
- 优点: 实现简单、适合资源相对集中的场合。
- 缺点: 服务器是通信的中心,当访问量过大时,可能所有客户都将得到很慢的响应。
P2P模型
为了解决 C/S模型 的缺点而诞生,P2P(Peer to Peer,点对点)模型 比 C/S模型 更符合网络通信的实际情况。摒弃了以服务器为中心的格局,让网络上所有主机重新回归对等的地位。
- 优点: 每台机器在消耗服务的同时给别人提供服务,这样资源能够充分、自由地共享。(P2P模型的典范:云计算机群)
- 缺点: 用户之间传输的请求过多时,网络的负载将加重。
P2P模型的实现: 主机之间很难互相发现,所以实际使用时通常带有一个专门的发现服务器,其还提供查找服务(甚至还可以提供内容服务),使每个客户都能尽快地找到自己需要的资源。
I/O处理单元、逻辑单元、存储单元
可以将服务器解构为三个主要模块:
模块 | 单个服务器程序 | 服务器机群 |
---|---|---|
I/O处理单元 | 等待并接受新的客户连接、读写网络数据,将服务器响应数据返回给客户端。 | 作为接入服务器,实现负载均衡,从所有逻辑服务器中选取负荷最小的一台来为新客户服务。 |
逻辑单元 | 通常是一个进程或线程,处理客户数据并将结果传递给 I/O 处理单元或者直接发送给客户端(取决于事件处理模式) | 一台逻辑服务器 |
网络存储单元 | 本地数据库、文件或缓存 | 数据库服务器 |
请求队列 | 各单元间的通信方式 | 各服务器间的永久的、静态的TCP连接,避免了动态TCP连接导致的额外系统开销。 |
实际编程中,I/O处理单元常被称作主线程,逻辑单元常被称为工作线程。
值得注意的是:
- 服务器通常拥有多个逻辑单元,以实现对多个客户任务的并行处理。
- 网络存储单元不是必须的,如
ssh
、telnet
等登陆服务就不需要这个单元。 - 请求队列通产被实现为池的一部分,是各个单元之间的通信方式的抽象。
并发
缺点与优点:
- 缺点: 如果程序是计算密集型,并发编程引起的任务切换反而使得效率降低。(任务切换耗时大于计算耗时)
- 优点: 如果程序是I/O密集型,由于I/O操作耗时远大于CPU计算耗时,因此如果程序阻塞于I/O操作将浪费大量CPU时间,解决方法是:当前被I/O操作阻塞的执行线程可以主动放弃CPU(或由操作系统调度),将执行权转移到其他线程。此时并发引起的任务切换可以大大提高CPU利用率。
并发模式: I/O处理单元和多个逻辑单元之间协调完成任务的方法。
服务器主要有两种并发编程模式:半同步/半异步模式(half-sync/half-async
)、领导者/追随者模式(Leader/Followers
)。
同步与异步
- I/O模型中,同步 or 异步 区分的是内核向应用程序通知的是 I/O就绪事件 or I/O完成事件,以及由 应用程序 还是 内核 来完成I/O读写。(详见两种高效事件处理模式一文)
- 并发模式中,同步指程序完全按照代码序列的顺序执行;异步指程序的执行需要由系统事件(中断、信号等)来驱动。
半同步/半异步模式
按照同步/异步方式运行的线程被称为同步线程/异步线程:
- 同步线程: 效率低、实时性差,但逻辑简单。
- 异步线程: 效率高、实时性强,但编程复杂且难于调试、扩展。
服务器同时使用同步线程和异步线程实现,即半同步/半异步模式。
工作流程:
- 同步线程用于处理客户逻辑(类似于工作线程)、异步线程用于处理注册的I/O事件(类似于主线程)。
- 异步线程监听到客户请求后,就将其封装成一个请求对象并插入请求队列中。
- 请求队列通知某个同步模式的工作线程来读取并处理该请求对象。
变体:半同步/半反应堆模式
结合两种事件处理模式和几种I/O模型的话,半同步/半异步模式就存在多种变体,其中一种称为半同步/半反应堆(half-sync/half-reactive
)模式。
工作流程
- 异步线程只有一个,由主线程充当,负责监听所有
socket
上的事件。 - 如果 监听
socket
上有可读事件发生时(即有新的连接请求到来),主线程就接受新的socket
连接,然后往epoll
内核事件表中注册该socket
上的读写事件。 - 如果接受的 连接
socket
上有读写事件发生(上一步注册的),即 有新的客户请求到了 or 有数据要发送到客户端,主线程就将该socket
连接 插入请求队列中。 - 所有的工作线程都睡眠在请求队列上,当有任务到来时(就绪的
socket
连接被插入请求队列中,这说明半同步/半反应堆模式采用的事件处理模式是Reactor
模式),所有空闲的工作线程通过竞争(比如申请互斥锁)获取任务的接管权。
事件处理模式的选择
- 采用
Reactor
模式意味着 工作线程 要负责 读写工作:既要 从socket
上读取客户请求 ,还要 往socket
写入服务器应答 。这也是名称中 半反应堆 的含义。 - 当然,半同步/半反应堆模式也可以使用模拟的
Proactor
事件处理模式,即由主线程来完成数据的读写:- 此时,主线程会将应用程序数据、任务类型等信息封装为一个任务对象,然后将任务对象(或者是指向该任务对象的一个指针)插入请求队列。
- 工作线程从请求队列中取得任务对象之后,即可直接处理客户请求,无须执行读写操作了。
缺点
- 主线程和工作线程共享请求队列。 主线程往请求队列中添加任务,或者工作线程从请求队列中取出任务,都需要对请求队列加锁保护,从而白白耗费CPU时间。
- 每个工作线程在同一时间只能处理一个客户请求。 如果客户数量较多,而工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度越来越慢。如果增加工作线程,则又会耗费大量CPU时间。
总而言之,耗费CPU时间。
改进:高效的半同步/半异步模式
针对上面提到的第二个缺点,可以让每个工作线程都能同时处理多个客户连接:
工作流程:
- 主线程 只管理 监听
socket
,连接socket
由 工作线程 来负责:当有新的连接socket
到来时,主线程就接受之并将其派发给某个工作线程,此后该新socket
上的任何IO操作都由被选中的工作线程来处理,直到客户关闭连接。 - 主线程向工作线程派发
socket
的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新socket
上的读写事件注册到自己的epoll
内核事件表中。(注册这件事本来是主线程在做)
PS: 事实上,每个线程(主线程和工作线程)都维持自己的事件循环,它们各自独立地监听不同的事件,每个线程都工作在异步模式,所以它并非严格意义上的半同步/半异步模式。
领导者/追随者模式
领导者/追随者模式是:多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。
- 领导者: 在任意时间点中,程序都只会有一个领导者线程,它负责进行I/O事件的监听。
- 追随者: 而其他的线程则为追随者线程,他们休眠在线程池中,等待成为新的领导者。
- 工作流程: 如果当前的领导者检测到I/O事件,首先要从线程池中推选出新的领导者线程等待新的I/O事件的到来,然后旧的领导者处理I/O事件,以此实现并发。
用通俗点的方法来讲就像是一群在营地中轮流放哨的哨兵,每次都会有一个人在值班,而其他人去休息。当值班者发现有什么特殊情况的时候就会去让领班叫醒一个哨兵来继续放哨,然后自己去探查情况。如果探查情况完后没人值班,则自己继续盯梢,否则就去休息。
组件 :句柄集、线程集、事件处理器
领导者/追随者模式包含的组件有:句柄集(HandleSet
)、线程集(ThreadSet
)、事件处理器(EventHandler
),之间的关系如图所示:
句柄集
- 句柄(
Handle
) 用于表示I/O资源,在Linux
下通常就是一个文件描述符。 - 句柄集 其实就是句柄的监控管理集合,通过调用
wait_for_event
方法来监听这些句柄上的I/O事件,并将其中的就绪事件通知给领导者线程,而领导者线程则调用绑定到Handle
上的事件处理器来处理事件。绑定是通过调用句柄集中的register_handle
方法实现的。
线程集
线程集是所有工作线程(包括领导者和追随者)的管理者。它负责各线程之间的同步,以及新领导者线程的推选。
线程集中的线程在任意时间都必然处于下面三种状态之一:
- 领导者(Leader): 线程处于领导者身份,负责监听句柄集上的I/O事件
- 事件处理中(Processing): 线程正在处理事件。领导者检测到I/O事件后,转移到
Processing
状态进行事件的处理,并且调用promote_new_leader
推选新的领导者。如果不想让出领导者的地位,也可以指定其他的追随者来处理事件(Event Handoff
)。当处于Processing
状态的线程处理完事件之后,如果当前线程集中没有领导者,他就会成为新的领导者,否则就直接变为追随者。 - 追随者(Follower): 线程此时处于追随者身份,此时处于休眠状态,通过调用线程集的
join
等待被推选为新的领导者,也可能被当前的领导者指定处理新的任务。
状态转移图:
PS
领导者推选新的领导者 和 追随者等待成为新的领导者 这两个操作都将修改线程集,因此线程集提供一个成员 Synchronizer
来同步这两个操作,以避免竟态条件。
事件处理器
- 事件处理器通常包含一个或者多个回调函数
handle_event
,用于处理事件对应的业务逻辑。 - 事件处理器在使用前首先需要被绑定到某个句柄之上,每当该句柄上有事件发生的时候,领导者就执行与之绑定的事件处理器中的回调函数。
- 具体的事件处理器需要重新实现基类的
handle_event
方法,以处理特定任务。
工作流程
PS
- 由于领导者线程自己监听I/O事件并且处理客户请求,所以在本模式中不需要在线程之间传递任何额外的数据,也不需要像半同步/半反应堆模式那样在线程之间同步对请求队列的访问。(CPU耗时低)
- 但是也有一个明显的缺点就是只能支持一个事件源集合,因此无法像高效的半同步/半异步模式那样让每个工作线程独立地管理多个客户连接。