bookstack

概览

任何计算机系统的两项基本任务是: - 存储 - 计算

分布式编程的艺术在于:使用多台计算机解决原本可以在单台计算机上完成的问题——通常是因为问题的规模超出了单台计算机的能力

实际上,并没有什么强制要求必须使用分布式系统。在理论上,如果拥有无限资金和研发时间,就不需要分布式系统。所有的存储和计算都可以交给一个“魔法盒”完成——一个速度极快且高度可靠的单一系统,而且还有人帮你设计好

然而,现实中很少有人拥有无限资源。因此我们需要在实际成本收益曲线中找到一个平衡点。在小规模下的情况下,升级软件是一种可行的策略。然而随着问题规模的增大,会达到瓶颈,此时,要么没有可以让你在单个节点上解决问题的硬件升级,要么这种升级变得成本过高。此时,欢迎你来到分布式系统的世界。

在现实场景中,中端通用硬件具有最佳性价比 – 只要能通过软件容错降低维护成本。

计算主要受益于高端硬件,前提是它们能将慢速网络访问替换为内部内存访问。在需要大量节点通信的任务中,高端硬件的性能优势是有限的。

如上图所示,来自Barroso, Clidaras & Hölzle的研究表明,假设所有节点都采用统一的内存访问模式,高端硬件与通用硬件之间的性能差距会随着集群规模的增加而减少。

理想情况下,添加一台机器应当能够线性地提高系统的性能和容量。然而,实际上这是不可能的,因为存在由于拥有独立计算机而产生的某些开销。数据需要在各个节点之间复制,计算任务需要协调等等。这就是为什么学习分布式算法是值得的 ———— 它们提供了针对特定问题的搞笑解决方案,以及关于什么是可能的、正确实现的最低成本是什么、以及什么是不可能的指导

本文的重点是分布式编程和系统,场景虽然普通,但在商业上具有重要意义:数据中心。例如,我不会讨论由于特殊的网络配置或共享内存环境而引发的特定问题。此外,本文着重于探索系统设计领域,而不是优化某个特定设计 ———— 后者属于更专业的主题。

我们要实现的目标:可扩展性以及其他优势

在我看来,这一切都要源自于规模大小

大多数事情在小规模时都很简单,但同样的问题一旦超过某个规模、体积或其他物理限制,就会变得更加简单。举个例子,搬动一块巧克力很容易,搬动一座山就很难。数清楚房间里有多少人很简单,但要数清楚一个国家有多少人就很难。

因此,一切的起点是规模 ———— 可扩展性。通俗地说,在一个可扩展的系统中,当我们从小规模扩展到大规模时,情况不应该逐步恶化。这里还有一个定义:

可扩展性

是指一个系统、网络或流程以高效的方式处理不断增加的工作量的能力,或其能够扩展以适应这种增长的能力。

那么,究竟是什么在增长?实际上,增长可以用几乎任何指标来衡量(如人数、电力消耗等)。但有三个特别值得关注的方面:

  • 规模可扩展性:增加更多节点应使系统性能线性提升;数据集的增长不应显著增加延迟。
  • 地理可扩展性:系统应能利用多个数据中心来减少响应用户查询所需的时间,同时以合理的方式处理跨数据中心的延迟。
  • 管理可扩展性:增加更多节点不应显著增加系统的管理成本(例如,管理员与机器的比例应保持平稳)。

当然,在实际系统中,增长通常会同时发生在多个不同的维度上;每个指标仅捕捉了增长的某个方面。

一个可扩展的系统是指在规模不断扩大时,仍能持续满足用户需求的系统。其中有两个特别重要的方面——性能和可用性,它们可以通过多种方式进行衡量。

性能(及时延)

性能

是指计算机系统完成的有用工作量与所使用的时间和资源的比较。

根据上下文,这可能涉及实现以下一个或多个目标:

  • 对于给定工作,短响应时间/低延迟
  • 高吞吐量(处理工作的速率)
  • 低计算资源利用率

在优化这些结果时,需要权衡。例如,系统可以通过处理更大批量的工作来实现更高的吞吐量,从而减少操作开销。权衡是由于批处理导致单个工作项的响应时间更长

我发现低延迟 —— 实现短响应时间 —— 是性能中最有趣的方面,因为它与物理(而非成本资源)限制有着密切的联系。使用成本资源来解决延迟问题比解决性能的其他方面要困难得多。

延迟有很多非常具体的定义,但我非常喜欢这个词的词源所唤起的概念:

延迟(Latency)

处于潜在的状态;延期(delay),某事的开始与发生之间的时间段。

那么“潜在”是什么意思呢?

潜在(Latent)

来自拉丁语latens,latentis,lateo(“隐藏”)的现在分词。存在或出现,但被隐藏或不活跃。

这个定义非常有趣,因为它突出了时延实际上是某事发生与其产生影响或变得可见之间的时间。

例如,想象一下你感染了一种空气传播的病毒,这种病毒会把人变成僵尸。延迟是指你感染的时间和你变成僵尸的时间之间的时间。这就是延迟:在某些事情已经发生但仍然被隐藏在视野之外的时间。

让我们假设我们的分布式系统只执行一个高层次的任务:给定一个查询,它会获取系统中的所有数据并计算出一个单一的结果。换句话说,可以将分布式系统视为一个数据存储,能够对其当前内容运行一个单一的确定性计算(函数):

result = query(all data in the system)

那么,影响延迟的不是旧数据的数量,而是新数据在系统中“生效”的速度。例如,延迟可以通过写入变得对读者可见所需的时间来衡量。

基于这个定义的另一个关键点是,如果没有任何事情发生,就没有“延迟期(latent period)”。在数据不发生变化的系统中,不会(或不应该存在)延迟问题。

在分布式系统中,有一个无法克服的最小延迟:光速限制了信息传播的速度,硬件组件在每次操作中都有一个最低延迟的成本(考虑内存、硬盘和CPU)

最小延迟对查询的影响程度取决于查询的性质以及信息需要传播的物理距离。

可用性(和容错性)

可扩展系统的第二个方面是可用性。

可用性

系统处于正常运行状态的时间比例。如果用户无法访问系统,则称其为不可用。

分布式系统使我们能够实现一些在单一系统上难以实现的理想特性。例如,单台机器无法容忍任何故障,因为它要么失败,要么正常工作。

分布式系统可以将一组不可靠的组件组合在一起,并在其上构建一个可靠的系统。

没有冗余的系统只能与其底层组件一样可用。构建有冗余的系统可以容忍部分故障,从而提高可用性。值得注意的是,“冗余”在不同的方面可能意味着不同的东西 —— 组件、服务器、数据中心等。

可用性公式是:Availability = uptime / (uptime + downtime).

从技术角度来看,可用性主要与容错性有关。由于故障发生的概率随着组件数量的增加而增加,因此系统应该能够进行补偿,以便在组件数量增加时不会变得不可靠。

例如:

可用性 % 每年允许故障时间?
90% (“一个9”) 超过1个月
99% (“两个9”) 小于4天
99.9% (“三个9”) 小于9个小时
99.99% (“四个9”) 小于1个小时
99.999% (“五个9”) 约为5分钟
99.9999% (“六个9”) 约为31秒

可用性在某种意义上是一个比正常运行时间更广泛的概念,因为服务的可用性也可能收到网络故障或拥有该服务的公司破产等因素影响(这些因素与容错性并不直接相关,但仍会影响系统的可用性)。但在不了解系统的每一个具体方面的情况下,我们能做的最好的设计就是容错性设计。

什么是容错性?

容错性

系统在发生故障以后,以良好定义的方式运行的能力。

容错性归结位:定义你预期的故障,然后设计一个能够容忍这些故障的系统或算法。你无法容忍那些你没考虑到的故障。

是什么阻止了我们的实现?

分布式系统收到两个物理因素的限制:

  • 节点数量(随着所需存储和计算能力的增加而增加)
  • 节点之间的距离(信息传播的速度,最大速度是光速)

在这些限制条件下:

  • 独立节点数量的增加会增加系统故障的概率(降低可用性并增加管理成本)
  • 独立节点数量的增加可能会增加节点之间的通信需求(随着规模的增加而降低性能)
  • 地理距离的增加会增加远程节点之间通信的最小延迟(降低某些操作的性能)

超越这些限制 —— 这是物理限制的结果 —— 是系统设计选项的世界。

性能和可用性都由系统所做的外部保证定义。从高层次来看,可以将这些保证视为系统的SLA(服务层协议):如果我写入数据,我可以多快在其他地方访问它?数据写入后,我对持久性有什么保证?如果我要求系统运行一个计算,它会多快返回结果?当组件故障或被停用时,这将对系统产生什么影响?

还有一个标准,虽然没有明确提到,但隐含在其中:可理解性。这些保证有多容易理解?当然,对于什么是可理解性,没有简单的度量标准。

我有点想把“可理解性”归入物理限制。毕竟,对于人类而言,理解涉及more moving things than we have fingers是一个硬件限制。这就是错误和异常之间的区别 —— 错误是错误的行为,而异常是意外的行为。如果你更聪明,你会预期异常的发生。

抽象和模型

这就是抽象和模型发挥作用的地方。抽象通过去除与解决问题无关的现实世界因素,使事务变得更易于管理。模型以精确的方式描述分布式系统的关键属性。在下一章中,我将讨论多种模型,例如:

  • 系统模型(异步/同步)
  • 故障模型(崩溃故障、分区、拜占庭故障(Byzantine Failure))
  • 一致性模型(强一致性、最终一致性)

一个好的抽象使得与系统的工作更易于理解,同时捕捉与特定目的相关的因素。

现实中存在许多节点,而我们希望系统“像单一系统一样工作”之间存在一种紧张关系。通常,最熟悉的模型(例如,在分布式系统上实现共享内存抽象)成本过高。

一个提供较弱保证的系统具有更大的行动自由,因此可能具有更高的性能——但它也可能更难以推理。人们更擅长推理像单一系统那样工作的系统,而不是一组节点的集合。

通过暴露系统内部的更多细节,通常可以获得更好的性能。例如,在columnar storage中,用户可以(在某种程度上)推理系统内键值对的局部性,从而做出影响典型查询性能的决策。隐藏这些细节的系统更容易理解(因为它们更像一个单一的单元,思考的细节更少),而暴露更多现实世界细节的系统可能性能更好(因为它们与现实更紧密相关)。

几种类型的故障使得编写像单一系统那样工作的分布式系统变得困难。网络延迟和网络分区(例如,某些节点之间的完全网络故障)意味着系统有时需要在保持可用性但失去一些无法强制执行的关键保证,或在发生这些类型的故障时采取保守措施拒绝客户端之间做出艰难的选择。

CAP定理——我将在下一章中讨论——捕捉了这些紧张关系。最终,理想的系统同时满足程序员的需求(清晰的语义)和业务需求(可用性/一致性/延迟)。

设计技术:分区和复制

数据集在多个节点之间的分布方式非常重要。为了进行任何计算,我们需要定位数据并对其进行操作。

可以对数据集应用两种基本技术。可以将其分割到多个节点上(分区),以允许更多的并行处理。也可以将其复制或缓存到不同的节点上,以减少客户端与服务器之间的距离,并提高容错能力(复制)。

分而治之——我的意思是,分区和复制。

下图说明了这两者之间的区别:分区数据(下方的A和B)被划分为独立的集合,而复制数据(下方的C)被复制到多个位置。

这是解决任何涉及分布式计算的问题的双重策略。当然,关键在于为你的具体实现选择合适的技术;有许多实现复制和分区的算法,每种算法都有不同的限制和优点,需要根据你的设计目标进行评估。

分区

分区是将数据集划分为较小的独立集合;这用于减少数据集增长的影响,因为每个分区都是数据的一个子集。

  • 分区通过限制需要检查的数据量和将相关数据放在同一分区中来提高性能
  • 分区通过允许分区独立故障来提高可用性,从而增加在可用性受到影响之前需要故障的节点数量

分区也非常依赖于应用,因此在不知道具体情况的情况下很难说太多。这就是为什么大多数文献,包括本书,主要关注复制的原因。

分区主要是根据你认为主要访问模式来定义你的分区,并处理来自独立分区的限制(例如,跨分区的低效访问、不同的增长速率等)。

复制

复制是在多个机器上制作相同数据的副本;这允许更多的服务器参与计算。

让我不准确地引用Homer J. Simpson:

To replication! The cause of, and solution to all of life’s problems.

复制——复制或再现某物——是我们对抗延迟的主要方式。

  • 复制通过使额外的计算能力和带宽适用于数据的新副本来提高性能
  • 复制通过创建数据的额外副本来提高可用性,从而增加在可用性受到影响之前需要故障的节点数量

复制是提供额外带宽和在关键位置进行缓存。它还涉及根据某种一致性模型以某种方式维护一致性。

复制使我们能够实现可扩展性、性能和容错性。担心可用性丧失或性能降低?复制数据以避免瓶颈或单点故障。计算速度慢?在多个系统上复制计算。I/O速度慢?将数据复制到本地缓存以减少延迟,或复制到多个机器以提高吞吐量。

然而,复制也是许多问题的根源,因为现在有独立的数据副本需要在多个机器上保持同步——这意味着必须确保复制遵循一致性模型。

一致性模型的选择至关重要:一个好的一致性模型为程序员提供了清晰的语义(换句话说,它保证的属性易于推理),并满足高可用性或强一致性等业务/设计目标。

只有一种复制的一致性模型——强一致性——允许你像对待未复制的底层数据一样进行编程。其他一致性模型则向程序员暴露了一些复制的内部细节。然而,较弱的一致性模型可以提供更低的延迟和更高的可用性——并不一定更难理解,只是不同而已。


推荐阅读