概述
任何计算机系统都需要完成两个基本任务:
- 存储
- 计算
分布式编程是一门艺术,可以使用多台计算机解决在单台计算机上解决的问题。通常是因为该问题不再适用于单台计算机。
并没有任务明确要求必须使用分布式系统。如果有无限的资金和无限的研发时间,我们就不需要分布式系统。所有的计算和存储都可以在一个魔盒——一个你付钱给其他人为你设计的单一的、非常快的、非常可靠的系统。
但是,很少有人拥有无限的资源。因此,他们必须在默写实际成本收益曲线上找到正确的位置。在较小的规模上,升级硬件上可行的策略。但是,随着问题规模的增加,你将无法实现可以在单个节点上解决问题的硬件升级,或者变得成本过高。到那时,欢迎进入分布式系统的世界。
当前的现实是,最好的解决方案是中端商用硬件——只要可以通过容错软件来降低维护成本。
计算主要受益于高端硬件,在某种程度上,它们可以通过内部内存访问代替缓慢的网络访问。高端硬件的性能优势受限于需要节点之间进行大量通信的任务。
正如上方来自 Barroso, Clidaras & Hölzle 的图所示,假设集群中所有节点的内存访问模式相同,高端硬件和商用硬件之间的性能差距会随着集群增大而减小。
理想情况下,添加新硬件后系统所增加的性能和容量都是线性的。但是这是不可能的,因为由于计算机之间是独立的,会产生一些额外开销,比如数据需要被复制,计算任务需要协调等等。这就是为什么值得研究分布式算法——它们为特定问题提供了有效的解决方案、可行的可能行、正确实施的最低成本和不可能的解决方案。
本文的重点是在普通但具有商业意义的环境中的分布式编程和系统:数据中心。例如,我不会讨论因非典型网络配置而引起的特殊问题,或者因为共享内存设置引起的特殊问题。此外,我们的重点在于探索系统设计领域,而不是针对任何特定设计的优化——后者是更专业的文章的主题。
我们想要实现的目标:可伸缩性和其他优点
我的看法是,一切始于处理规模的问题。
大多数事物在小规模上都是微不足道的——一旦超过一定大小、体积或者其他受物理限制的事物,相同的问题就会变得更加棘手。举起一块巧克力很容易,举起山峰很难。计算房间中有多少人很容易,计算一个国家中有多少人则很困难。
因此,一切始于规模—可扩展性。非正式地讲,在可伸缩系统中,从小到大,事情不会逐渐变得糟糕。这是另一个定义:
可伸缩性是系统、网络或过程处理不断增长的工作量的能力,或者是未来适应这种增长而进行扩展的能力。
增长是什么?好吧,你几乎可以用任何术语(人数、用电量等)衡量增长。但是有三件事值得关注:
- 大小可伸缩性:添加更多的节点将使系统性能线性增长;扩大数据集不应该增加延迟。
- 地理可伸缩性:应该可以使用多个数据中心来减少用户查询所需的响应时间,同时以某种合理的方式处理跨数据中心的延迟。
- 管理可伸缩性:添加更多的节点不应增加系统的管理成本(例如,管理员与机器的比例)。
当然,在实际系统中,增长会同时在多个不同的轴上发生;每个指标仅反映增长的某些方面。
可扩展的系统上随着规模的增长而不断满足其用户需求的系统。有两个特别相关的方面——性能和可用性,可以通过多种方式进行衡量。
性能(和延迟)
性能的特征是与所使用的时间和资源相比,计算机系统完成的有用任务量。
根据上下文,这可能涉及实现以下一项或多项:
- 给定任务的响应时间(等待时间)短
- 高吞吐量(处理任务率)
- 较低的计算资源使用量
优化这些结果的任何一个都需要权衡。例如,系统可以通过处理更大批量的任务来实现更高的吞吐量,从而减少操作开销。折衷方案是因为批量处理,导致单个任务的响应时间更长。
我发现低延迟(缩短响应时间)是性能中最有趣的方面,因为它与物理(而非财务)限制紧密相关。与性能的其他方面相比,使用财务资源解决延迟问题很困难。
让我们假设一下,我们的分布式系统仅执行一项高级任务:给定查询,获得系统中所有的数据并计算单个结果。换句话说,将分布式系统视为一个能够在其当前内容上运行单个确定性计算(函数)的数据存储:
\(结果=查询(系统中的所有数据)\)
然后,对于延迟而言,重要的不是旧数据的数量,而是新数据在系统中“生效”的速度。例如,可以根据写入对读者可见所需的时间来衡量延迟。
基于此定义的另一个关键点是,如果什么也没有发生,那么就没有“潜伏期”。数据不变的系统不会(也不应该)存在延迟问题。
在分布式系统中,又一个无法克服的最小延迟:光速限制了信息能够以多快的速度传播,和硬件组件的每次操作所产生的最小延迟成本(例如 RAM 和硬盘驱动器,以及 CPU)。
最小延迟对你的查询有多大影响,取决于这些查询的性质,以及信息传播的物理距离。
可用性(和容错)
可伸缩系统的第二个方面是可用性。
可用性是系统处于运行状态的时间比例。如果用户无法访问系统,则称该系统不可用。
分布式系统使我们能够获得理想的特性,而这些特性很难在单个系统上获得。例如,一台机器不能容忍任何故障,因为它要么失败要么不失败。
分布式系统可能会使用大量不可靠的组件,并在它们之上构建可靠的系统。
没有冗余的系统只能作为其基础组件使用。使用冗余构建的系统可以容忍部分故障,因此可以提供更好的可用性。值得注意的是,“冗余”可能意味着不同的含义,具体取决于你所关注的内容——组件、服务器、数据中心等等。
按照惯例:
从技术角度来看,可用性主要是关于容错的。因为发生故障的概率会随着组件数量的增加而提高,所以系统应该能够进行补偿,以使系统不会随着组件数量的增加而变得不那么可靠。
例如:
从某种意义上说,可用性是一个比正常运行时间更广泛的概念,因为服务的可用性还可能受到例如网络中断或拥有该服务的公司停业的影响(这与容错无关,但仍会影响系统的可用性)。但是,在不了解系统的每个特定方面的情况下,我们能做的最好的事情就是设计容错能力。
容错是什么意思?
容错是指故障发生后系统以明确定义的方式运行的能力。
容错可以总结为:确定你预期的故障,然后设计可以容忍该故障的系统或算法。你不能容忍从未考虑过的故障。
是什么阻止我们取得成功?
分布式系统受两个物理因素的限制:
- 节点个数(随所需的存储和计算能力而增加)
- 节点之间的距离(信息最多以光速传播)
在这些限制内工作:
- 独立节点数量的增加会增加系统发生故障的可能性(降低可用性并增加管理成本)
- 独立节点数量的增加可能会增加节点间通信的需求(随着规模的增加而降低性能)
- 地理距离的增加会增加远程节点之间通信的最小延迟(降低某些操作的性能)
除了这些趋势(物理限制的结果)之外就是系统设计选择的世界了。
系统的外部保证定义了性能和可用性。在较高的层次上,你可以将保证视为系统的 SLA(服务级别协议):
- 如果写入数据,可以在多久之后访问它?
- 数据写入后,有什么能保证耐用性?
- 如果要求系统运行计算,它将在多久后返回结果?
- 当组件发生故障或无法运行,这会对系统产生什么影响?
还有另一个没有明确提及但暗含的标准:可理解性。作出的保证有多可理解?当然,没有什么针对可理解性的简单指标。
我很想将“可理解性”置于物理限制之下。毕竟,对于人们来说,硬件上的局限在于,我们很难理解牵动比手指更多的东西。这就是错误和异常之间的区别——错误是不正确的行为,而异常是意外的行为。如果你非常聪明,你就可以预测会出现的异常情况。
抽象和模型
这是抽象和模型起作用的地方。抽象通过删除与解决问题无关的实际方面,使事情变得更易于管理。模型以精确的方式描述了分布式系统的关键特性。在下一章,我将讨论许多模型,例如:
- 系统模型(异步/同步)
- 失败模型(崩溃失败,分区,拜占庭)
- 一致性模型(强,最终)
良好的抽象使得系统更容易理解,同时捕获与特定目的相关的因素。
在存在许多节点的现实与我们对“像单个系统一样工作”的系统的渴望之间存在着一种张力。通常,最熟悉的模型(例如,在分布式系统上实现共享内存抽象)过于昂贵。
做出较弱保证的系统具有更大的行动自由度,因此可能具有更好的性能——但也可能更难推理结果。人们更擅长于对像单个系统那样工作的系统进行推理,而不是对节点的集合进行推理。
人们通常可以通过公开有关系统内部的更多细节来获得性能。例如,在列式存储中,用户可以(在某种程度上)推断键值对在系统中的位置,从而做出影响典型查询的性能的决策。隐藏这些细节的系统更易于理解(因为它们的行为更像是一个独立的单元,从而只有更少的细节需要被考虑),而暴露更多真实细节的系统可能具有更好的性能(因为它们与现实的关系更加紧密)。
几种类型的故障使编写像单个系统一样工作的分布式系统变得困难。网络延迟和网络分区(例如某些节点之间的整体网络故障)意味着系统有时需要做出艰难的选择,以便更好地保持可用性,但失去一些无法被强制执行的关键保证,或者当这些类型的故障发生时,以安全性为主拒绝客户端的请求。
我将在下一章中讨论的 CAP 定理涵盖了其中的一些对立关系。最后,理想的系统既可以满足程序员的需求(清晰的语义),又可以满足业务的需求(可用性/一致性/延迟)。
设计技巧:分区和复制
在多个节点之间分配数据集的方式非常重要。为了进行任何计算,我们需要定位数据,然后对其进行操作。
有两种基本技术可以应用于数据集:
- 可以将其拆分到多个节点,以进行更多并行处理(分区)。
- 可以将其复制或缓存在不同的节点上,以缩短客户端与服务器之间的距离,并提高容错能力(复制)。
下图说明了两者之间的区别:
- 分区数据(A 和 B)被分为独立的集合。
- 复制数据(C)被复制到多个位置。
这是解决分布式计算所面临的问题的两种方法。当然,诀窍在于为你的具体实现选择正确的技术。有许多实现复制和分区的算法,每种算法都有不同的限制和优点,需要针对你的设计目标进行评估。
分区
分区上将数据集分成较小的不同独立集。由于每个分区都是数据的一个子集,所以可用于减少数据集增长带来的影响。
- 分区通过限制要检查的数据量并在同一分区中定位相关数据来提高性能。
- 分区通过允许分区发生独立故障,增加牺牲可用性之前所能忍受的发生故障的节点数量,来提高可用性。
分区技术也是非常面向应用的,因此在不了解具体细节的情况下很难说太多。这就是为什么大多数文章(包括本文)都将重点放在复制上。
分区的依据更多是你所推断的主要访问模式,并解决由于独立分区而带来的限制(例如,跨区的低效访问,不同的增长率等)。
复制
复制上在多台机器上存储相同的数据。这使得更多的服务器可以参与到计算中。
让我不准确地引用 Homer J. Simpson:
复制是我们减少延迟到主要方法。
- 复制通过适用数据副本的额外计算能力和带宽来提高性能。
- 复制通过创建数据副本,增加牺牲可用性之前所能忍受的发生故障的节点数量,来提高可用性。
复制提供了额外的带宽,并在在需要的地方进行缓存。它根据某种一致性模型,以某种方式保持一致性。
复制使我们能够实现可伸缩性、性能和容错。
- 害怕失去可用性或者降低性能?复制数据以避免出现瓶颈或单点故障。
- 计算速度慢?将数据复制到多台机器上进行计算。
- I/O 速度慢?将数据复制到本地缓存以减少延迟,或者复制到多台机器上以提高吞吐量。
复制也是许多问题的根源,因为现在有独立的数据副本,而这些副本必须在多台计算机上保持同步——这意味着确保复制要遵循一致性模型。
一致性模型的选择至关重要:良好的一致性模型为程序员提供了清晰的语义(换句话说,它保证的特性易于被推断),并满足了诸如高可用性或强一致性之类的业务/设计目标。
只有一个一致性模型(强一致性)让你编程时如同没有复制数据。其他一致性模型向程序员公开了复制的某些内部信息。然而,弱一致性模型能提供较低的延迟和较高的可用性——而且不一定更难理解,只是有所不同。