1. 高层次的分布式系统
分布式编程是使用多台计算机解决你可以在单台计算机上解决的同一问题的艺术。
任何计算机系统都需要完成两项基本任务:
- 存储和
- 计算
分布式编程是使用多台计算机解决你可以在单台计算机上解决的同一问题的艺术——通常是因为问题不再适合单台计算机。
没有什么真正要求你使用分布式系统。如果有无限的资金和无限的研发时间,我们就不需要分布式系统。所有计算和存储都可以在一个魔法盒子上完成——一个单一的、极其快速和极其可靠的系统,你付钱让别人为你设计。
然而,很少有人拥有无限的资源。因此,他们必须在现实世界的成本效益曲线上找到合适的位置。在小规模上,升级硬件是一个可行的策略。然而,随着问题规模的增加,你将达到这样一个点:要么允许你在单个节点上解决问题的硬件升级不存在,要么成本过高。在那一刻,欢迎你来到分布式系统的世界。
当前的现实是,最佳价值在于中端商品硬件——只要可以通过容错软件保持维护成本降低。
计算主要从高端硬件中受益,程度在于它们可以用内部内存访问替换缓慢的网络访问。对于需要在节点之间进行大量通信的任务,高端硬件的性能优势是有限的。

正如 Barroso, Clidaras & Hölzle 的上图所示,假设所有节点之间具有统一的内存访问模式,高端和商品硬件之间的性能差距随着集群规模的增加而减小。
理想情况下,添加新机器将线性提高系统的性能和容量。但这当然是不可能的,因为由于拥有独立的计算机会产生一些开销。数据需要被复制,计算任务需要协调等等。这就是为什么值得研究分布式算法——它们为特定问题提供有效的解决方案,以及关于什么是可能的、正确实现的最小成本是什么以及什么是不可能的指导。
本文的重点是分布式编程和系统,处于一个平凡但商业相关的环境中:数据中心。例如,我不会讨论由拥有奇特的网络配置或在共享内存环境中产生的特殊问题。此外,重点是探索系统设计空间,而不是优化任何特定设计——后者是一个更专业文本的主题。
我们想要实现什么:可扩展性和其他好东西
在我看来,一切都始于处理规模的需求。
大多数事情在小规模上都是微不足道的——一旦你超过某个规模、体积或其他物理限制的事情,同样的问题就会变得困难得多。举起一块巧克力很容易,举起一座山很难。计算房间里有多少人很容易,计算国家里有多少人很难。
所以一切都从规模开始——可扩展性。非正式地说,在可扩展系统中,当我们从小到大移动时,事情不应该逐渐变得更糟。这是另一个定义:
可扩展性 是系统、网络或进程以有能力的方式处理不断增长的工作量的能力,或其能力被扩大以适应这种增长的能力。
什么是增长的?嗯,你几乎可以用任何术语来衡量增长(人数、用电量等)。但有三个特别有趣的事情值得关注:
- 规模可扩展性:添加更多节点应该使系统线性更快;增加数据集不应该增加延迟
- 地理可扩展性:应该可以使用多个数据中心来减少响应用户查询所需的时间,同时以某种合理的方式处理跨数据中心延迟。
- 管理可扩展性:添加更多节点不应该增加系统的管理成本(例如,管理员与机器的比率)。
当然,在真实系统中,增长同时发生在多个不同的轴上;每个指标只捕捉增长的某些方面。
可扩展系统是一个随着规模增加继续满足用户需求的系统。有两个特别相关的方面——性能和可用性——可以通过各种方式衡量。
性能(和延迟)
性能 的特征是计算机系统完成的有用工作量与使用的时间和资源的比率。
根据上下文,这可能涉及实现以下一项或多项:
- 给定工作的短响应时间/低延迟
- 高吞吐量(工作处理率)
- 计算资源利用率低
优化任何这些结果都涉及权衡。例如,系统可以通过处理更大的工作批次来实现更高的吞吐量,从而减少操作开销。权衡将是由于批处理导致单个工作的响应时间更长。
我发现低延迟——实现短响应时间——是性能中最有趣的方面,因为它与物理(而不是财务)限制有很强的联系。使用财务资源解决延迟比性能的其他方面更困难。
有很多非常具体的延迟定义,但我真的很喜欢这个词的词源所唤起的想法:
延迟 处于潜伏状态;延迟,某事物开始和发生之间的时期。
“潜伏”是什么意思?
Latent 来自拉丁语 latens,latentis,lateo(“隐藏”)的现在分词。存在或出现但被隐藏或不活跃。
这个定义很酷,因为它强调了延迟实际上是某事发生和它产生影响或变得可见之间的时间。
例如,想象你感染了一种空气传播的病毒,将人变成僵尸。潜伏期是你被感染和变成僵尸之间的时间。这就是延迟:某事已经发生但对视线隐藏的时期。
让我们暂时假设我们的分布式系统只执行一项高级任务:给定一个查询,它获取系统中的所有数据并计算单个结果。换句话说,将分布式系统视为一个数据存储,具有对其当前内容运行单个确定性计算(函数)的能力:
result = query(all data in the system)那么,对于延迟来说,重要的不是旧数据的数量,而是新数据在系统中”生效”的速度。例如,可以根据写入对读者可见所需的时间来衡量延迟。
基于这个定义的另一个关键点是,如果没有发生任何事情,就没有”潜伏期”。数据不变化的系统不(也不应该)有延迟问题。
在分布式系统中,存在无法克服的最小延迟:光速限制了信息传播的速度,硬件组件每个操作都会产生最小延迟成本(想想 RAM 和硬盘,还有 CPU)。
这个最小延迟对你的查询有多大影响取决于这些查询的性质和信息需要传播的物理距离。
可用性(和容错)
可扩展系统的第二个方面是可用性。
可用性 系统处于工作状态的时间比例。如果用户无法访问系统,则称其不可用。
分布式系统允许我们实现单系统难以实现的理想特性。例如,单台机器不能容忍任何故障,因为它要么故障要么不故障。
分布式系统可以采用一堆不可靠的组件,并在其上构建一个可靠的系统。
没有冗余的系统只能与其底层组件一样可用。具有冗余的系统可以容忍部分故障,因此更加可用。值得注意的是,“冗余”的含义可能因你所看的内容而异——组件、服务器、数据中心等等。
公式上,可用性是:可用性 = 正常运行时间 / (正常运行时间 + 停机时间)。
从技术角度来看,可用性主要是关于容错。因为故障发生的概率随着组件数量的增加而增加,系统应该能够补偿,以便随着组件数量的增加而不会变得不可靠。
例如:
| 可用性 % | 每年允许多少停机时间? |
|---|---|
| 90% (“一个九”) | 超过一个月 |
| 99% (“两个九”) | 少于 4 天 |
| 99.9% (“三个九”) | 少于 9 小时 |
| 99.99% (“四个九”) | 少于一个小时 |
| 99.999% (“五个九”) | ~ 5 分钟 |
| 99.9999% (“六个九”) | ~ 31 秒 |
从某种意义上说,可用性是一个比正常运行时间更广泛的概念,因为服务的可用性也可能受到网络中断或拥有服务的公司倒闭(这将是一个与容错无关但仍然会影响系统可用性的因素)等因素的影响。但在不了解系统的每一个具体方面的情况下,我们所能做的就是为容错而设计。
容错意味着什么?
容错 系统在发生故障后以明确定义的方式行为的能力
容错归结为这一点:定义你期望的故障,然后设计一个对它们具有容忍度的系统或算法。你不能容忍你没有考虑过的故障。
什么阻止我们实现好东西?
分布式系统受到两个物理因素的限制:
- 节点数量(随着所需存储和计算能力的增加而增加)
- 节点之间的距离(信息最多以光速传播)
在这些约束内工作:
- 独立节点数量的增加会增加系统发生故障的概率(降低可用性并增加管理成本)
- 独立节点数量的增加可能会增加节点之间通信的需求(随着规模扩大降低性能)
- 地理距离的增加会增加远距离节点之间通信的最小延迟(降低某些操作的性能)
除了这些趋势——它们是物理约束的结果——之外,是系统设计选项的世界。
性能和可用性都由系统做出的外部保证定义。在高层次上,你可以将保证视为系统的 SLA(服务级别协议):如果我写入数据,我多快可以在其他地方访问它?写入数据后,我对持久性有什么保证?如果我要求系统运行计算,它多快会返回结果?当组件故障或被停止运行时,这将对系统产生什么影响?
还有另一个标准,没有明确提及但暗示了:可理解性。做出的保证有多容易理解?当然,对于什么是可理解的没有简单的指标。
我有点想把”可理解性”放在物理限制下。毕竟,这是人们的硬件限制,我们很难理解任何涉及比我们手指更多的移动事物的东西。这就是错误和异常之间的区别——错误是不正确的行为,而异常是意外的行为。如果你更聪明,你会期望异常发生。
抽象和模型
这就是抽象和模型发挥作用的地方。抽象通过移除与解决问题无关的现实世界方面使事情更易于管理。模型以精确的方式描述分布式系统的关键属性。我将在下一章讨论许多种模型,例如:
- 系统模型(异步/同步)
- 故障模型(崩溃 - 故障、分区、拜占庭)
- 一致性模型(强、最终)
一个好的抽象使系统更易于理解,同时捕捉与特定目的相关的因素。
存在一个紧张关系:现实是有许多节点,而我们希望系统”像单系统一样工作”。通常,最熟悉的模型(例如,在分布式系统上实现共享内存抽象)太昂贵了。
做出较弱保证的系统有更多的行动自由,因此可能有更大的性能——但也可能更难推理。人们更擅长推理像单系统一样工作的系统,而不是节点集合。
人们通常可以通过暴露更多关于系统内部的细节来获得性能。例如,在 列式存储 中,用户可以在一定程度上推理系统内键值对的局部性,从而做出影响典型查询性能的决定。隐藏这些细节的系统更易于理解(因为它们更像单个单元,需要思考的细节更少),而暴露更多现实世界细节的系统可能性能更高(因为它们更紧密地对应于现实)。
几种类型的故障使得编写像单系统一样工作的分布式系统变得困难。网络延迟和网络分区(例如,某些节点之间的完全网络故障)意味着系统有时需要做出艰难的选择:是保持可用但失去一些无法强制执行的关键保证,还是安全行事并在发生这些类型的故障时拒绝客户端。
CAP 定理——我将在下一章讨论——捕捉了其中一些紧张关系。最终,理想的系统满足程序员需求(清晰的语义)和业务需求(可用性/一致性/延迟)。
设计技术:分区和复制
数据集在多个节点之间的分布方式非常重要。为了进行任何计算,我们需要定位数据然后对其执行操作。
有两种基本技术可以应用于数据集。它可以跨多个节点拆分(分区)以允许更多并行处理。它也可以在不同节点上复制或缓存,以减少客户端和服务器之间的距离并获得更大的容错性(复制)。
分而治之——我的意思是,分区和复制。
下图说明了这两者之间的区别:分区数据(下面的 A 和 B)被分成独立的集合,而复制数据(下面的 C)被复制到多个位置。

这是解决分布式计算发挥作用的任何问题的绝招。当然,技巧在于为你的具体实现选择正确的技术;有许多算法实现复制和分区,每种都有不同的限制和优势,需要根据你的设计目标进行评估。
分区
分区是将数据集分成更小的不同独立集合;这用于减少数据集增长的影响,因为每个分区都是数据的子集。
- 分区通过限制要检查的数据量并将相关数据定位在同一分区中来提高性能
- 分区通过允许分区独立故障来提高可用性,增加在牺牲可用性之前需要故障的节点数量
分区也非常特定于应用程序,因此如果不了解具体情况很难多说。这就是为什么大多数文本(包括本文)的重点是复制。
分区主要是关于根据你认为主要访问模式是什么来定义你的分区,并处理具有独立分区的限制(例如,跨分区低效访问、不同的增长率等)。
复制
复制是在多台机器上制作相同数据的副本;这允许更多服务器参与计算。
让我错误地引用 Homer J. Simpson:
为了复制!所有生活问题的原因和解决方案。
复制——复制或重现某物——是我们对抗延迟的主要方式。
- 复制通过使额外的计算能力和带宽适用于数据的新副本来提高性能
- 复制通过创建数据的额外副本来提高可用性,增加在牺牲可用性之前需要故障的节点数量
复制是关于提供额外的带宽,并在关键地方缓存。它也是关于以某种方式根据某种一致性模型维护一致性。
复制允许我们实现可扩展性、性能和容错。害怕可用性丧失或性能降低?复制数据以避免瓶颈或单点故障。计算缓慢?在多个系统上复制计算。I/O 缓慢?将数据复制到本地缓存以减少延迟,或复制到多台机器以增加吞吐量。
复制也是许多问题的来源,因为现在有必须在多台机器上保持同步的独立数据副本——这意味着确保复制遵循一致性模型。
一致性模型的选择至关重要:好的 consistency 模型为程序员提供清晰的语义(换句话说,它保证的属性易于推理),并满足业务/设计目标,如高可用性或强一致性。
只有一种复制一致性模型——强一致性——允许你像底层数据未被复制一样进行编程。其他一致性模型向程序员暴露复制的一些内部。然而,较弱的一致性模型可以提供更低的延迟和更高的可用性——并且不一定更难理解,只是不同。
进一步阅读
- 数据中心作为计算机——仓库级机器设计介绍 - Barroso & Hölzle, 2008
- 分布式计算的谬误
- 给年轻人的分布式系统笔记 - Hodges, 2013
本文为学习目的的个人翻译,译文仅供参考。
原文链接:Distributed systems for fun and profit - Chapter 1。
版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。