响应式宣言

宣言词汇表

异步的

牛津词典把“异步的”定义为“不在同一时刻存在或发生的”。在本宣言的上下文中,我们想表达的意思是:对一项请求的处理过程可以发生在任意时间点,有时候请求已经从客户端发送到服务端了,其处理过程还未开始。这时候,客户端无法直接观察到这项请求在服务端的执行过程,也无法与执行过程保持同步。作为异步的反义词,同步是指客户端暂停执行,直到服务端已经把请求处理完毕,才继续自己的执行过程

背压

当一个组件需要挣扎着保持正常运行时,系统作为整体也需要以某种可感知的方式做出响应。让一个承受压力的组件灾难性地停止工作,或在失控的状态下丢弃消息,是不可接受的。因为它既不能应对,又不能停止工作,所以就该与上游的组件沟通,说明自己正在承受压力的事实,并使上游组件减少负载,这就是背压。背压是一种很重要的反馈机制,它允许系统在面对负载时优雅地做出响应,而不是被负载压垮。背压可以层层升级,直至到达用户面前,这种情况下系统的响应能力可能会降级,但这种机制可确保系统在承受负载时表现出一定的可回复性,并能对外提供信息,使系统有机会申请更多有助于分散负载的资源,参见可扩展性

批处理

当今的电脑被优化为重复处理同一项任务:指令缓存和分支预测增加了每秒钟内可以被处理的指令数,但时钟频率却保持不变。这就意味着把不同的任务一个接一个地快速提交给同一个 CPU 内核并不能受益于 CPU 的最大性能,而另一种方式就能达到目的:只要有可能我们就该对程序的结构做出调整,使其执行过程在不同任务之间交替的动作不那么频繁。这意味着成批次地处理一系列数据元素,或者在专属的硬件线程上执行不同的处理步骤。

同样的推理也可应用于那些需要同步和协调的外部资源的使用。当命令来自一个单独的线程(因此也是同一个 CPU 内核)而不是争夺来自多个内核的带宽时,持久性存储设备的输入/输出带宽将能得到显著的提升。使用单一的入口具有额外优势:对 CPU 的操作可以被重新排列,以更好地适应针对这类设备的优化的访问模式(当前的存储设备的线性访问性能优于随机访问)。

更进一步,批处理还带来了分摊高消费操作成本的机会,输入/输出或昂贵的计算过程都属于这样的高消费操作。举例来说,把多个数据项打包进同一个网络包或者磁盘块,就能提高效率并减少高消费操作。

组件

在使用组件这个术语的时候,我们是在描述一种模块化的软件架构,模块化是一种非常旧的观念,可以参考 Parnas(1972) 中的例子。我们使用“组件”这个术语是因为它与隔间很近似,暗示每个“组件”都是自包含的,被封装的,并且与其它组件相互隔离(以及包含)的。此概念首先适用于描述系统的运行时特征,但它也通常也会在源代码的模块结构中得到反映。正如不同的组件可能会利用同一个软件模块执行常见任务,定义每个模块顶层行为的程序代码本身也是一个模块。组件的边界常常与问题领域的有界上下文相匹配。这意味着系统设计倾向于反映问题领域,并且因此能在保持隔离的情况下,相对容易地进行演化。消息协议在不同的有界上下文(组件)之间提供了一种自然的映射和沟通层次。

委派

把一项任务异步地委派给另一个组件处理,意味着这项任务的执行过程将会在那个组件的上下文中发生。这个被委派的上下文将会继续运行在另一个错误处理上下文中,或另一个线程、进程、网络节点中,这里仅列出几种可能性。委派的目的是把一项任务的处理责任移交给另一个组件,这样发起委派的组件就可以转而执行其它处理任务;或者,当它需要在委派之后做进一步善后处理——如故障处理或进度汇报时,转而观察被委派的任务的进展状况。

可伸缩性(与可扩展性相对照

可伸缩性的意思是,系统能自动做出吞吐量的升级或降级动作,以便在资源被加入或移除时,自动地满足变化的需求。系统需要满足可扩展性,才能允许运行期间动态地加入或移除资源,实现可伸缩性带来的效益。由此可见,可伸缩性建立在可扩展性之上,并且在此基础上增减了动态资源管理的概念。

故障(与错误相对照

故障是一个发生在当前服务中的意外事件,它会阻止服务继续正常工作。故障通常会阻止当前的客户请求的响应,并且很可能也会阻止所有后续的响应。这与错误形成了对照,错误是一种可预料并可用代码去应对的情况——例如在输入检查阶段发现的错误,作为常规的消息处理的一部分,系统可以与客户沟通说明这类错误。与错误不同,故障是意外发生的,而且需要对系统做出干预,才能恢复到故障前相同级别的运行状态。这么说并不意味着故障总是致命的,而是意味着系统的某些能力在故障发生后可能会下降。错误是正常操作过程中预料之内的,可以立刻得到处理,而且系统在错误发生后能够以原来的能力水平继续运作。

故障的例子包括硬件失灵,因致命的资源耗尽导致处理过程中止,因程序缺陷导致崩溃的内部状态等。

隔离(以及围控)

隔离可以用解耦的概念来定义,这里所谓的解耦是时间和空间双重意义上的。在时间上解耦意味着发送者与接受者可拥有独立的生命周期——两者不必同时出现,沟通照样可以进行。这种效果是通过在组件之间增加异步的界限,并通过消息驱动(与事件驱动相对照)进行沟通来实现的。在空间上解耦(又称位置透明性)是指发送者与接受者不必在同一个进程中运行,而是交给运营部门或运行时本身来做判断——它们认为在哪里运行最高效,就在哪里运行。因此,实际的运行位置可能在应用程序的整个生命周期中发生改变。

真正意义的隔离超越了大多数面向对象编程语言中能够找到的封装概念,它为我们提供了对下列问题的区隔与围控(译者注:即包围加控制):

  • 状态与行为:真正的隔离使得“零分享”设计得以实现,这种设计可以把由竞争性与相干性导致的成本降到最低水平(正如通用可扩展性法则所定义的那样);
  • 故障:真正的隔离允许错误事件被捕捉到,以信号传递出来并且在一种细粒度的等级上得到管理,而不是放任它们级联式地影响其它组件。

组件之间强隔离的实现是有前提的:组件之间的沟通遵循完善定义的协议,这样才能实现松耦合,并且导向更容易理解、扩展、测试和演化的系统。

位置透明性

可扩展性的系统需要有适应性,并且能持续地针对需求变化做出反应,他们需要优雅且高效地增加和缩减规模。有一个很关键的洞见可以极大地简化这个问题:那就是意识到我们所有人都在进行分布式计算。无论我们在一个单独的节点上运行一个系统(其中有多个互相独立的 CPU 通过 QPI 链路进行通信),还是在一系列节点组成的集群上运行一个系统(其中有多个独立的机器通过网络进行通信),都能看到分布式计算的影子。充分接受这个事实,就意味着基于多核的垂直式扩展与基于集群的水平式扩展并无概念上的区别。

如果我们的所有组件都支持移动性,仅仅是把本地通信看做一种优化的手段,那么我们就无需事先定义一个静态的系统拓扑结构,也不必定义一个静态的部署模型。我们可以把这个决策留给运营人员在系统运行时考虑,让他们根据系统的实际使用情况做出调整与优化。

这种通过异步的消息驱动(与事件驱动相对照)以及运行时实例与其引用解耦而实现的空间上的解耦(参考隔离(以及包含)的定义),就是我们所谓的位置透明性。位置透明性经常被错误理解为“透明的分布式计算”,但实际情况正相反:我们拥抱网络和它的所有局限——比如局部故障,网络割裂,消息的丢失,以及其异步性和基于消息的天然特性——通过把这些特性当做编程模型中的一等公民,而不是尽力基于网络去模拟进程内方法调度(如 RPC,XA 等方式那样)。关于位置透明性,我们的观点与 Waldo 等人所发表的 A Note On Distributed Computing 中的观点完全一致。

消息驱动(与事件驱动相对照)

消息是一个数据项,它需要被发送到一个特定目的地。事件是一个信号,当组件在运行中达到某个给定状态时,就会发出信号。在一个消息驱动的系统中,可访问的接收者等待消息的到来并做出反应,无消息时它将处于休眠状态。在一个事件驱动的系统中,通知的监听者被附着在事件源之上,以便在事件出现时被调用。这意味着事件驱动的系统的关注焦点是可访问的事件源,而消息驱动的系统则尽力关注可访问的接收者。一条消息中可以包含一个已编码的事件作为它的载荷。

在一个事件驱动的系统中,因为事件消费链天然的短暂性,很难实现较高的可回复性:当处理过程已经开始,且监听者也已附着在事件源上以便对执行结果做出反应和变换时,这些监听者通常会以报告给原始客户端的方式直接处理成功运行状态和故障 状态。另一方面,要想对组件的故障做出响应使其正常工作,需要处理的并不是那些与转瞬即逝的与客户端请求直接挂钩的故障,而是那些因为组件的整体健康状况出现问题才导致的故障。

非阻塞的

在并发式编程中,当一个算法的各个线程始终在争夺资源,并且这些资源没有被互斥性的保护机制限制,不会出现线程的执行被不确定地延后的情况时,我们就说这个算法是非阻塞的。在实践中这种特性通常表现为 API 的特性,当资源可用时它允许算法访问这项资源;当资源当前不可用时,它会立刻返回消息告知调用者这项资源当前不可用,或者调用者对 API 的操作已经初始化但还未完成。针对一项资源的非阻塞 API 允许调用者去做其它的工作,而不是阻塞它们,让它们一直等待到这项资源变为可用状态。作为补充,还可以允许请求资源的客户端在服务端注册,以便当资源变为可用状态或操作已完成时,自动向客户端发送通知。

协议

协议定义了在组件之间交换或传输消息的处理方法和礼节性规范。协议是一种公式化的描述,它界定了消息交换的参与者,协议的累积状态与允许发送的消息集合之间的关系。这意味着一个协议可以描述这样的:在任意给定时间点上,一个参与者可能会向另一个参与者发送怎样的消息。可以用消息交换的形式对协议做分类,一些通用的协议类别包括:请求-回复,重复的请求-回复(比如 HTTP 协议),发布-订阅以及流的形式(既有推的动作,又有拉的动作)。

与本地编程接口相对比,协议更加泛化,因为它可以包含两个以上的参与者,并且它预见到一系列消息交换;一个接口一次只能指定调用者和接受者之间的一种交互。

需要注意的是,这里定义的协议仅仅指定了需要发送哪些消息,而并没有说明如何发送这些消息:编码,解码(即编码解码器)以及运输机制属于实现细节,它们对于组件间如何使用这些协议而言是透明的。

复制

同时从不同的位置执行同一个组件的动作,被称为复制。复制可以在不同的线程或线程池中,不同的进程中,不同的网络节点或不同的计算中心执行。当进入的工作负载被分发到某个组件的多个实例上时,复制提供了可扩展性;当进入的工作负载被复制到多个实例,每个实例都在并行地处理同一个请求时,复制提供了可伸缩性。这些方式可以被混合使用,例如确保所有与某个特定用户相关的交易所属的组件都被两个实例执行,而实例的总数则随着进入负载的变化而变化(参见可扩展性)。

资源

一个组件为了实现其功能而依赖的任何东西都可以称作资源,资源的特点是,必须根据组件的需要把它们预先提供出来。典型的资源包括 CPU、主内存、持久存储和网络带宽,主内存带宽,CPU 缓存,套接字之间的 CPU 链接,可靠的计时器和任务排程服务,其它输入与输出设备,外部服务如数据库或网络文件系统等。所有这些资源的可扩展性与可回复性都需要被考虑到,因为任何一项所需资源的缺失都将导致这个组件在被要求发挥作用时无法正常工作。

可扩展性

可扩展性是指一个系统能够利用更多的计算资源提升其自身性能的能力,度量可扩展性的指标是吞吐量增量与资源增加的比值。一个完美可扩展的系统具有这样的特性,它的吞吐量增量与资源增加量是成正比的:当分配给系统的资源翻倍时,它的吞吐量也翻了一倍。可扩展性通常会因为引入了瓶颈或系统内的同步点而受到限制,导致受约束的可扩展性,参见 Amdahl 法则与 Gunther 通用可扩展性模型

系统

系统的作用是向用户或客户提供服务。系统可大可小,大的系统由较多组件组成,小的系统由有限的几个组件组成。系统的全部组件需要通力协作才能提供这些服务。在很多情况下,这些组件在整个系统中形成客户端-服务端关系(好比前端组件依赖后端组件)。一个系统共享同一个可回复性模型,这句话的意思是任何一个组件的故障 都将在系统内部被处理,从一个组件委派给另一个组件。如果在某个系统中,一组组件相对于系统的其他部分而言,在功能、资源或故障模式等方面比较隔离(以及包含),那么不妨把它们视为一个子系统。

用户

我们用这个术语表示使用一项服务的消费者,无论它是一个人还是另一项服务。

↑ 回到网页顶部