《分布式系统:为了乐趣和利益》是一本广受欢迎的资源,用于理解和学习分布式系统。该书由作者 Mikito Takada 撰写,介绍了构建分布式系统的基本概念、原则和挑战。

这本书涵盖了与分布式系统相关的广泛主题,包括网络、容错性、一致性模型、分布式算法、可扩展性等等。它旨在以清晰易懂的方式解释复杂的概念,适合初学者和有经验的分布式系统从业者阅读。

在整本书中,作者提供了各种实际案例和案例研究,以说明分布式系统的实际应用和实践方面。它还强调了构建分布式系统涉及的权衡和设计考虑,帮助读者全面理解这个主题。

《分布式系统:为了乐趣和利益》作为开源资源,可以免费在线获取,非常适合任何对学习分布式系统感兴趣的人。

原文链接:Distributed systems: for fun and profit

1. 高层分布式系统

分布式编程是利用多台计算机解决在单台计算机上可以解决的相同问题的一种技术。

任何计算机系统都需要完成两个基本任务:

  • 存储
  • 计算

分布式编程是一种艺术,通过利用多台计算机解决在单台计算机上可以解决的相同问题,通常是因为该问题已经超出了单台计算机的处理能力。

实际上,并没有强制要求我们使用分布式系统。如果拥有无限的资金和无限的研发时间,我们就不需要分布式系统。所有的计算和存储可以在一个魔盒上完成,这是一台单一的、极其快速和可靠的系统,你可以支付给别人来为你设计。

然而,很少有人拥有无限的资源。因此,他们必须在现实世界的成本效益曲线上找到合适的位置。在小规模情况下,升级硬件是一种可行的策略。然而,随着问题规模的增加,你会达到一个阶段,在这个阶段,要么不存在可以让你在单个节点上解决问题的硬件升级,要么成本过高。在这一点上,我欢迎你进入分布式系统的世界。

当前的现实是,只要通过容错软件将维护成本控制在较低水平,中档、大众化硬件提供了最佳的性价比。

计算主要受益于高端硬件,尤其是在能够通过内部内存访问取代缓慢的网络访问时。在需要节点间大量通信的任务中,高端硬件的性能优势有限。

cost-efficiency

正如 Barroso、Clidaras 和 Hölzle 的上图所示,假设所有节点都采用统一的内存访问模式,高端硬件和商用硬件之间的性能差距会随着集群规模的扩大而缩小。

理想情况下,添加一台新的机器将线性增加系统的性能和容量。但是,现实情况并非如此,因为由于存在独立的计算机,会产生一些开销。数据需要在计算机之间进行复制,计算任务需要进行协调等等。 这就是为什么值得研究分布式算法的原因——它们提供了针对特定问题的高效解决方案,以及关于可能性、正确实现的最低成本以及不可能性的指导。

这段文字的重点是在一个平凡但商业相关的环境中,即数据中心的分布式编程和系统。例如,我不会讨论由于具有异乎寻常的网络配置或在共享内存设置中出现的专门问题。此外,重点是探索系统设计空间,而不是优化任何特定设计——后者是一个更专门的文本主题。

我们想要实现的目标:可扩展性和其他好的东西

从我看来,一切都始于处理规模的需求

在小规模下,大多数事情都是微不足道的,而同样的问题一旦超过一定的大小、容量或其他物理限制,就会变得更加困难。举起一块巧克力很容易,但举起一座山就很困难。数一下房间里有多少人很容易,但数一下国家里有多少人就很难。

所以一切都始于规模——可扩展性。非正式地说,在一个可扩展的系统中,当我们从小规模向大规模过渡时,事情不应该逐渐变得更糟。以下是另一种定义:

可扩展性是指系统、网络或进程处理不断增长的工作负载的能力,或者说它能够被扩大以适应这种增长的能力。

什么是在增长呢?嗯,你可以用几乎任何方式来衡量增长(人数、用电量等)。但有三个特别有趣的方面值得关注:

  • 规模可扩展性:增加更多节点应该使系统线性加快;增加数据集的大小不应增加延迟。
  • 地理可扩展性:应该可以利用多个数据中心来缩短响应用户查询所需的时间,同时以某种合理的方式处理跨数据中心的延迟。
  • 管理可扩展性:添加更多节点不应增加系统的管理成本(例如管理员与机器的比率)。

当然,在真实的系统中,增长同时发生在多个不同的轴上;每个指标仅反映增长的某些方面。

可扩展的系统是一种随着规模的增加而持续满足用户需求的系统。有两个特别相关的方面——性能和可用性——可以通过多种方式来衡量。

性能(和延迟)

性能是指计算机系统在使用的时间和资源相对于所完成的有用工作量来衡量的特征。

根据具体情况,这可能涉及实现以下一项或多项:

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

针对任何这些结果进行优化都需要权衡。例如,系统可以通过处理更大批量的工作来实现更高的吞吐量,从而减少操作开销。由于批处理,权衡将是个别工作的响应时间更长。

我发现低延迟(实现较短的响应时间)是性能中最有趣的方面,因为它与物理(而不是财务)限制密切相关。使用财务资源来解决延迟问题比性能的其他方面更难。

对于延迟有很多非常具体的定义,但我真的很喜欢这个词的词源所唤起的想法:

延迟是指潜伏状态,延迟的或在某事物开始和发生之间的一段时间。

“潜在的”是什么意思?

潜在的是指某物存在或出现,但被隐藏、隐蔽或处于不活动状态。它描述了一种存在却不容易察觉或可见的状态,但它仍以隐藏或潜在的形式存在。

这个定义非常酷,因为它强调了延迟是指某件事发生到它产生影响或变得可见之间的时间。

例如,假设您感染了一种空气传播的病毒,该病毒会将人变成僵尸。潜伏期是指从你被感染到变成僵尸之间的时间。这就是潜伏期:已经发生的事情被隐藏起来的时间。

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

1
result = query(all data in the system)

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

基于这个定义的另一个关键点是,如果什么都没有发生,就没有“潜伏期”。数据不改变的系统不会(或不应该)存在延迟问题。

在分布式系统中,存在一个无法克服的最小延迟:光速限制了信息传输的速度,而硬件组件每个操作都会产生一定的最小延迟成本(例如内存、硬盘以及 CPU)。

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

可用性(和容错)

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

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

分布式系统使我们能够实现在单一系统上很难实现的理想特性。例如,单个机器无法容忍任何故障,因为它要么发生故障,要么正常运行。

分布式系统可以采用一堆不可靠的组件,并在它们之上构建一个可靠的系统。

没有冗余的系统只能达到其底层组件的可用性。而具备冗余的系统可以容忍部分故障,从而提高可用性。 值得注意的是,“冗余”可以在不同层面上有不同的含义,比如组件、服务器、数据中心等。

从公式上讲,可用性为: Availability = uptime / (uptime + downtime)

从技术角度来看,可用性主要与容错性有关。因为故障发生的概率随着组件数量的增加而增加,系统应该能够进行补偿,以确保随着组件数量的增加,系统的可靠性不会降低。

例如:

可用性 %
90%(“一个九”)一个多月了
99%(“两个九”)少于 4 天
99.9%(“三个九”)不到 9 小时
99.99%(“四个九”)不到一个小时
99.999%(“五个九”)〜5 分钟
99.9999%(“六个九”)〜 31 秒

可用性在某种意义上比运行时间更广泛,因为服务的可用性还可能受到网络中断或公司破产等因素的影响(这些因素与容错性并不直接相关,但仍会影响系统的可用性)。但是,如果没有了解系统的每一个具体方面,我们能做的最好的就是设计容错性。

容错是什么意思?

容错性是指系统在发生故障时仍能以明确定义的方式继续运行的能力。

容错性归结为以下几点:首先定义您所预期的故障,然后设计一个能够容忍这些故障的系统或算法。您无法容忍您未考虑到的故障。

是什么阻碍我们取得美好的成果?

分布式系统受到两个物理因素的约束:

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

在这些限制下工作:

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

在系统设计选项中,除了物理约束之外,还存在与性能、可用性和可理解性相关的考虑因素。

性能和可用性取决于系统提供的外部保证。可以将这些保证视为系统的服务级别协议(SLA):如果写入数据,我能多快在其他地方访问它?数据写入后,对持久性有何保证?如果要求系统运行计算,结果将多快返回?当组件发生故障或停止运行时,对系统会产生什么影响?

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

在我很想将“可理解性”归类为物理限制之下。毕竟,对于我们人类来说,理解涉及比我们的手指数量更多的运动物体的任何事物都很困难。这就是错误和异常之间的区别-错误是不正确的行为,而异常是意外的行为。如果你更聪明,你会预料到异常的发生。

抽象和模型

这时候抽象和模型就发挥作用了。抽象通过去除与解决问题无关的现实世界方面,使事物更易于管理。模型以准确的方式描述了分布式系统的关键属性。在下一章中,我将讨论许多种类型的模型,例如:

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

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

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

一个系统提供的保证越低,它就有更大的自由度和潜在的性能优势,但同时也可能更难推理。人们更擅长推理像一个单一系统一样工作的系统,而不是一组节点。

通过暴露系统内部更多的细节,通常可以提高性能。例如,在列存储中,用户可以(在某种程度上)推理系统内的键值对的局部性,并因此做出影响典型查询性能的决策。隐藏这些细节的系统更易于理解(因为它们更像单个单元,需要考虑的细节更少),而暴露更多真实世界细节的系统可能更具性能(因为它们更接近真实情况)。

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

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

设计技术:分区和复制

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

“分而治之” - 我的意思是,分区和复制。

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

Partition and replicate

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

分区

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

  • 分区通过限制要检查的数据量并将相关数据定位在同一分区中来提高性能。
  • 分区通过允许分区单独失败,增加了需要失败的节点数量,从而提高了可用性,降低了牺牲可用性的风险。

分区也非常依赖于具体应用程序,所以在不了解具体情况的情况下很难做出详细说明。这就是为什么大多数文本,包括本文,都将重点放在复制上。

分区主要是根据您认为主要访问模式将是什么来定义分区,并处理由于存在独立分区而带来的限制(例如,跨分区的访问效率低下,增长速度不同等)的问题。

复制

复制是将相同的数据复制到多台机器上的过程;这样可以让更多的服务器参与计算。

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

为了复制!生活中所有问题的起因和解决之道。

复制 - 复制或再现某个东西 - 是我们对抗延迟的主要方式。

  • 复制通过使额外的计算能力和带宽适用于数据的新副本来提高性能。
  • 复制通过创建数据的额外副本,增加了需要失败的节点数量,从而提高了可用性。

复制是提供额外带宽和在关键位置进行缓存的方式。它还涉及按照某种一致性模型以某种方式保持一致性。

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

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

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

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


进一步阅读