目录

  1. 分布式系统的驱动力和挑战(Drivens and challenges)
  2. 分布式系统的抽象和实现工具(Abstraction and Implementation)
  3. 可扩展性(Scalability)
  4. 可用性(Availability)
  5. 一致性(Consistency)

分布式系统的驱动力和挑战(Drivens and challenges)

分布式系统的核心是通过网络来协调,共同完成一致任务的一些计算机集合。分布式计算之所以如此重要的原因是:许多重要的基础设施都是在它之上建立的,它们需要多台计算机或者本质上需要多台物理隔离的计算机。

⚠️ 使用分布式系统时的注意点:

如果在设计一个系统或者面对一个需要解决的问题时,如果可以在一台计算机上解决,而不需要分布式系统,就应该用一台计算机解决问题。(这是因为,选择使用分布式系统会让问题变得复杂)

🤔 那些原因迫使我们进行分布式系统的研究?

  • (parallelism)我们想要获取更高的计算性能。大量的计算机意味着大量的并行运算,大量的 CPU、大量内存、以及大量磁盘在并行的运行
  • (fault tolerance)分布式系统能够为我们提供容错(tolerate faults)的能力。比如,两台计算机运行完全相同的任务,其中一台发生故障,可以切换到另一台
  • (physical problem)现实中存在一些问题,在空间上本身就是分布式的。例如银行转账,我们假设银行 A 在纽约有一台服务器,银行 B 在伦敦有一台服务器,这就需要一种两者之间协调的方法。所以,有一些天然的原因导致系统是物理分布的。
  • (security/isolated)构建分布式系统来达成一些安全的目标。比如有一些代码并不被信任,但是你又需要和它进行交互,这些代码不会立即表现的恶意或者出现 bug。但是,你又不想信任这些代码,所以你或许可以将代码分散在多处运行,这样你的代码在另一台计算机运行,我的代码在我的计算机上运行,我们通过一些特定的网络协议通信。所以,我们可能会担心安全问题,我们把系统分成多个的计算机,这样可以限制出错域。

MIT 6.824 主要会讨论前两点的问题:计算机性能和容错,综上所述,分布式系统的挑战(challenge)在于:

  • 因为系统中存在很多部分,这些部分又在并发执行,所以会遇到并发编程和各种复杂交互所带来的问题,以及时间依赖的问题(比如同步、异步),这导致分布式系统问题很难
  • 分布式系统有多个组成部分,再加上计算机网络,所以我们会遇到一些意想不到的故障,多台计算机组成的分布式系统,可能会有一部分组建在工作,而另一部分组件停止运行,或者这些计算机都在正常运行,但是网络中断了或者不稳定。所以,局部错误也是分布式系统很难的原因
  • 人们设计分布式系统的根本原因通常是为了获得更高的性能,比如说增加一千台计算机最终想要将性能增强一千倍。但是实际上一千台机器到底有多少性能是一个棘手的问题,这里有很多难点。所以通常需要倍加小心地设计才能让系统实际达到你期望的性能。

分布式系统的抽象和实现工具(Abstraction and Implementation)

此课程主要介绍目前现存一些基础架构(存储、通信和计算)在分布式系统中的问题,其中存储是主要的关注点,这是因为:存储是一个定义明确且有用的抽象概念,并且通常比较直观,我们知道如何构建和使用存储系统,知道如何去构建一种多副本,容错的,高性能分布式存储实现。

对于存储和计算,我们的目标是为了能够设计一些简单的接口,让第三方应用能够使用这些分布式的存储和计算,这样才能简单的在这些基础架构之上,构建第三方应用程序。这里的意思是:我们希望通过这种抽象的姐扩,将分布式特性隐藏在整个系统内。这样从应用程序的角度来看,整个系统是一个非分布式的系统。

🎯 我们希望构建一个接口,它看起来就像一个非分布式存储和计算系统一样,但是实际上又是一个有极高的性能和容错性的分布式系统。

随着学习,我们也许会发现,很难找到一个抽象来描述分布式的存储或者计算,使得它们能够像非分布式系统一样有简单易懂的接口,但是,人们在这方面将会做的越来越好,我们将学习人们在构建这样的抽象时的原因以及最终得到的结果

✨ 另一方面,一旦我们讨论了抽象,就不得不考虑抽象对应的实现。人们在构建分布式系统时,使用了很多的工具,例如:

  • RPC(Remote Procedure Call),RPC 的目标就是掩盖我们正在不可靠网络上通信的事实
  • 另一个经常看到的实现内容就是「 线程 」,这是一种编程技术,使得我们可以利用多核芯计算机,对于分布式系统而言,线程提供了一种结构化的并发操作方法,这样,从开发人员角度来说就可以简化并发操作
  • 由于经常用到线程进行并发操作,所以需要在实现的层面上,话费一定的时间来考虑并发控制,这种并发控制的实现就是 「 锁 」

可扩展性(Scalability)

性能是在分布式系统论文中出现非常多的一个话题。

通常来说,构建分布式系统的目的是为了获取人们常常提到的可扩展的加速。所以,我们追求性能的本质其实是追求系统的 「 可扩展性(Scalability) 」。而这里说的可扩展或者可扩展性指的是:如果我用一台计算机解决了一些问题,那么当我买了第二台计算机,我只需要一半的时间就可以解决这些问题,或者说每分钟可以解决两倍数量的问题。两台计算机构成的系统如果有两倍性能或者吞吐,就是这里表述的可扩展性。

可用性(Availability)

分布式系统另一个重要的话题就是容错。

如果你只使用一台计算机构建你的系统,那么你的系统大概率是可靠的。因为一台计算机通常可以很好的运行很多年而没有故障,计算机是可靠的,操作系统是可靠的,所以一台计算机正常工程很长时间是常见的,然而如果你通过数千台计算机构建你的系统,那么即使每台计算机可以稳定运行一年,在 0.3%失败率的情况下,1000 台计算机也意味着平均每天会有 3 台计算机出现故障

所以,大型分布式系统中有一个大问题,那就是一些很罕见的问题就会被放大,例如在我们的 1000 台计算机的集群中,总是有故障,要么是机器故障,要么是运行出错,要么是运行缓慢,要么是执行错误的任务。一个更常见的问题是网络,在一个有 1000 台计算机的网络中,会有大量的网络电缆和网络交换机,所以总是会有人踩着网线导致网线从接口掉出,或者交换机风扇故障导致交换机过热而不工作。在一个大规模分布式系统中,各个地方总是有一些小问题出现。所以大规模系统会将一些几乎不可能并且你不需要考虑的问题,变成一个持续不断的问题。

所以,分布式系统中错误总是会发生的,必须要在设计时就考虑,让系统具有「 屏蔽错误 」的能力,或者说能够在出错时继续运行。同时,因为我们需要为第三方应用开发人员提供方便的抽象接口,通过构建这样一种基础架构,就能够尽可能多的对开发人员屏蔽和掩盖错误。这样,应用开发人员就不需要处理各种各样可能发生的错误。

对于容错,有很多不同的概念可以表述。这些表述中,有一个共同的思想就是可用性(Availability)。某些系统经过精心的设计,这样在特定的错误类型下,系统仍然能够正常运行,仍然可以像没有出现错误一样,为你提供完整的服务。比如,你构建了一个有两个拷贝的多副本系统,其中一个故障了,另一个还能运行。当然如果两个副本都故障了,你的系统就不再有可用性。所以,可用系统通常是指,在特定的故障范围内,系统仍然能够提供服务,系统仍然是可用的。如果出现了更多的故障,系统将不再可用。系统可用性存在边界条件。

除了可用性之外,另一种容错特性是自我可恢复性(recoverability)。这里的意思是,如果出现了问题,服务会停止工作,不再响应请求,之后有人来修复,并且在修复之后系统仍然可以正常运行,就像没有出现过问题一样。这是一个比可用性更弱的需求,因为在出现故障到故障组件被修复期间,系统将会完全停止工作。但是修复之后,系统又可以完全正确的重新运行,所以可恢复性是一个重要的需求。

对于一个可恢复的系统,通常需要做一些操作,例如将最新的数据存放在磁盘中,这样在供电恢复之后(假设故障就是断电),才能将这些数据取回来。甚至说对于一个具备可用性的系统,为了让系统在实际中具备应用意义,也需要具备可恢复性。因为可用的系统仅仅是在一定的故障范围内才可用,如果故障太多,可用系统也会停止工作,停止一切响应。但是当足够的故障被修复之后,系统还是需要能继续工作。所以,一个好的可用的系统,某种程度上应该也是可恢复的。当出现太多故障时,系统会停止响应,但是修复之后依然能正确运行。这是我们期望看到的。

为了实现这些特性,最重要的两个工具(非易失存储、复制):

  • 实现容错的一个工具是非易失存储(non-volatile storage,类似于硬盘)。这样当出现类似电源故障,甚至整个机房的电源都故障时,我们可以使用非易失存储,比如硬盘,闪存,SSD 之类的。我们可以存放一些 checkpoint 或者系统状态的 log 在这些存储中,这样当备用电源恢复或者某人修好了电力供给,我们还是可以从硬盘中读出系统最新的状态,并从那个状态继续运行。所以,这里的一个工具是非易失存储。因为更新非易失存储是代价很高的操作,所以相应的出现了很多非易失存储的管理工具。同时构建一个高性能,容错的系统,聪明的做法是避免频繁的写入非易失存储。在过去,甚至对于今天的一个 3GHZ 的处理器,写入一个非易失存储意味着移动磁盘臂并等待磁碟旋转,这两个过程都非常缓慢。有了闪存会好很多,但是为了获取好的性能,仍然需要许多思考
  • 对于容错的另一个重要工具是复制(replication),不过,管理复制的多副本系统会有些棘手。任何一个多副本系统中,都会有一个关键的问题,比如说,我们有两台服务器,它们本来应该是有着相同的系统状态,现在的关键问题在于,这两个副本总是会意外的偏离同步的状态,而不再互为副本。对于任何一种使用复制实现容错的系统,我们都面临这个问题。

一致性(Consistency)

最后一个很重要的话题是一致性(Consistency)

要理解一致性,这里有个例子,假设我们在构建一个分布式存储系统,并且这是一个 KV 服务。这个 KV 服务只支持两种操作,其中一个是 put 操作会将一个 value 存入一个 key;另一个是 get 操作会取出 key 对应的 value。

需要对一个分布式系统举例时,总是会想到 KV 服务,因为它们也很基础,可以算是某种基础简单版本的存储系统。

实际上,对于一致性有很多不同的定义。有一些非常直观,比如说 get 请求可以得到最近一次完成的 put 请求写入的值。这种一般也被称为强一致(Strong Consistency)。但是,事实上,构建一个弱一致的系统也是非常有用的。弱一致是指,不保证 get 请求可以得到最近一次完成的 put 请求写入的值。尽管有很多细节的工作要处理,强一致可以保证 get 得到的是 put 写入的最新的数据;而很多的弱一致系统不会做出类似的保证。所以在一个弱一致系统中,某人通过 put 请求写入了一个数据,但是你通过 get 看到的可能仍然是一个旧数据,而这个旧数据可能是很久之前写入的。

对于弱一致性感兴趣的原因是,虽然强一致性可以确保 get 获取的是最新的数据,但是实现这一点点代价非常高。几乎可以确定的是,分布式系统的各个组件需要做大量的通信,才能实现强一致性。如果你有多个副本,那么不管 get 还是 put 都需要询问每一个副本。在之前的例子中,客户端在更新的过程中故障了,导致一个副本更新了,而另一个副本没有更新。如果我们要实现强一致性,简单的方法就是同时读两个副本,如果有多个副本就读取所有的副本,并使用最近一次写入的数据。但是这样的代价很高,因为需要大量的通信才能得到一个数据。所以,为了尽可能的避免通信,尤其当副本相隔很远的时候,人们会构建弱一致性系统,并允许读取出旧的数据。当然,为了让弱一致性更有实际的意义,人们还会定义更多的规则。

强一致带来的昂贵的通信问题,会让我们在实际应用中非常棘手。所以,人们常常会使用弱一致性系统,只需要更新最近的数据副本,并且只需要从最近的副本获取数据。将弱一致性应用在应用程序中非常有用,并且它可以用来获取高的性能。