分布式系统概述-爱代码爱编程
分布式系统的驱动力和挑战
- 分布式系统的核心:通过网络来协调,共同完成一致任务的一些计算机。
- 分布式计算如此重要的原因:许多重要的基础设施都是在它之上建立的,它们需要多台计算机或者说本质上需要多台物理隔离的计算机。
- 使用大量的相互协作的计算机驱动力是:
- 更高的计算性能:大量的计算机意味着大量的并行运算,大量CPU、大量内存、以及大量磁盘在并行运行。
- 容错(tolerate faults):比如两台计算机运行完全相同的任务,其中一台发生故障,可以切换到另一台。
- 天然的原因导致系统是物理分布的:例如银行转账,我们假设银行A在纽约有一台服务器,银行B在伦敦有一台服务器,这就需要一种两者之间协调的方法。
- 安全的目标:比如有一些代码并不被信任,但又需要和它进行交互。所以想要将代码分散在多处运行,这样你的代码在另一台计算机运行,我的代码在我的计算机上运行,通过一些特定的网络协议通信。所以我们把系统分成多个计算机,这样可以限制出错域。
所有的这些分布式系统的问题(挑战)在于:
- 系统中存在很多部分,这些部分又在并发执行,会遇到并发编程和各种复杂交互所带来的问题,以及时间依赖的问题(如同步,异步)。这让分布式系统变得很难。
- 分布式系统有多个组成部分,再加上计算机网络,会遇到一些意想不到的故障。例如可能会有一部分组件在工作,而另一部分组件停止运行,或者这些计算机都在正常运行,但是网络中断了或者不稳定。所以,局部错误也是分布式系统很难的原因。
- 人们设计分布式系统的原因通常是为了获得更高的性能,但是实际上一千台机器到底有多少性能是一个棘手的问题。所以通常需要小心地设计才能让系统实际达到你期望的性能。
在MIT6.824有四次编程实验:
- 根据在论文中读到的来实现MapReduce。
- 实现Raft算法,这是一个理论上通过复制来让系统容错的算法,具体是通过复制和出现故障时自动切换来实现。
- 使用Raft算法实现来建立一个可以容错的KV服务。
- 把KV服务器分发到一系列的独立集群中,切分KV服务,并通过运行这些独立的副本集群进行加速。同时也要负责将不同的数据块在不同的服务器之间搬迁,并确保数据完整。这里我们通常称之为分片式KV服务。分片是指我们将数据在多个服务器上做了分区,来实现并行的加速。
分布式系统的抽象和实现工具
基础架构的类型主要是存储,通信(网络)和计算。
- 实际上我们最关注的是存储,因为这是一个定义明确且有用的抽象概念,并且通常比较直观。人们知道如何构建和使用储存系统,知道如何去构建一种多副本,容错的,高性能分布式存储实现。
- 计算系统,比如MapReduce。
- 通信是我们建立分布式系统所用的工具。比如计算机可能需要通过网络相互通信,需要保证一定的可靠性。
对于存储和计算,目标是设计一些简单接口,让第三方应用能够使用这些分布式的存储和计算,这样才能简单的在这些基础架构之上,构建第三方应用程序。我们希望通过这种抽象的接口,将分布式特性隐藏在整个系统内。这样从应用程序的角度来看,整个系统是一个非分布式的系统,就像一个文件系统或者一个大家知道如何编程的普通系统,并且有一个非常简单的模型语句。
我们希望构建一个接口,它看起来就像一个非分布式存储和计算系统一样,但是实际上又是一个有极高的性能和容错性的分布式系统。
当我们在考虑这些抽象的时候,第一个出现的话题就是实现。人们在构建分布系统时,使用了很多的工具,例如:
- RPC(Remote Procedure Call)。RPC的目标就是掩盖我们正在不可靠网络上通信的事实。
- 线程。这是一种编程技术,使得我们可以利用多核心计算机。更重要的是,线程提供了一种结构化的并发操作方式,这样,从程序员角度来说可以简化并发操作。
- 因为会经常用到线程,需要在实现的层面上,花费一定的时间来考虑并发控制,比如锁。
可扩展性(Scalability)
构建分布式系统的目的:获得可扩展的加速。这里说的可扩展或者可扩展性指的是,如果我用一台计算机解决了一些问题,当我买了第二台计算机,我只需要一半的时间就可以解决这些问题,或者说每分钟可以解决两倍数量的问题。两台计算机构成的系统如果有两倍性能或者吞吐,就是我说的可扩展性。
所以,当人们使用一整个机房的计算机来构建大型网站的时候,为了获取对应的性能,必须要时刻考虑可扩展性。你需要仔细设计系统,才能获得与计算机数量匹配的性能。
- 但很少有可以通过无限增加计算机来获取完整的可扩展性的场景。因为在某个临界点,你在系统中添加计算机的位置将不再是瓶颈了。在我们的例子中,如果你有了很多的web服务器,那么瓶颈就会转移到了别的地方,比如从web服务器移到了数据库。
- 这时,你几乎是必然要做一些重构工作。但是只有一个数据库时,很难重构它。而虽然可以将一个数据库拆分成多个数据库(进而提升性能),但是这需要大量的工作。
扩展性:我们希望可以通过增加机器的方式来实现扩展,但是现实中这很难实现,需要一些架构设计来将这个可扩展性无限推进下去。
可用性(Availability)
另一个重要的话题是容错。
因为错误总会发生,必须要在设计时就考虑,系统能够屏蔽错误,或者说能够在出错时继续运行。同时,因为我们需要为第三方应用开发人员提供方便的抽象接口,我们的确也需要构建这样一种基础架构,它能够尽可能多的对应用开发人员屏蔽和掩盖错误。这样,应用开发人员就不需要处理各种各样的可能发生的错误。
有以下容错特性:
-
可用性(Availability):某些系统经过精心的设计,这样在特定的错误类型下,系统仍然能够正常运行,仍然可以像没有出现错误一样,为你提供完整的服务。
可用系统:在特定的故障范围内,系统仍然能够提供服务,系统仍然是可用的。如果出现了更多的故障,系统将不再可用。
-
自我可恢复性(recoverability):如果出现了问题,服务会停止工作,不再响应请求,之后有人来修复,并且在修复之后系统仍然可以正常运行,就像没有出现过问题一样。
可恢复性是一个重要的需求。
对于一个具备可用性的系统,为了让系统在实际中具备应用意义,也需要具备可恢复性。所以,一个好的可用的系统,某种程度上应该也是可恢复的。当出现太多故障时,系统会停止响应,但是修复之后依然能正确运行。这是我们期望看到的。
为了实现这些特性,有很多工具。其中最重要的有两个:
-
非易失存储(non-volatile storage,类似于硬盘)。这样当出现类似电源故障,甚至整个机房的电源都故障时,我们可以使用非易失存储,比如硬盘,闪存,SSD之类的。我们可以存放一些checkpoint或者系统状态的log在这些存储中,这样当备用电源恢复或者某人修好了电力供给,我们还是可以从硬盘中读出系统最新的状态,并从那个状态继续运行。
因为更新非易失存储是代价很高的操作,所以出现了很多非易失存储的管理工具。同时构建一个高性能,容错的系统,聪明的做法是避免频繁的写入非易失存储。
-
复制(replication),不过,管理复制的多副本系统会有些棘手。任何一个多副本系统中,都会有一个关键的问题,比如说,我们有两台服务器,它们本来应该是有着相同的系统状态,现在的关键问题在于,这两个副本总是会意外的偏离同步的状态,而不再互为副本。
一致性(Consistency)
一致性就是用来定义操作行为的概念
- 在一个非分布式系统中,你通常只有一个服务器,一个表单。通常来说对于put/get的行为不会有歧义。
- 在一个分布式系统中,从性能和容错的角度来说,由于复制或者缓存,数据可能存在于多个副本当中,于是就有了多个不同版本的key-value对。
对于一致性有很多不同的定义。
- 强一致(Strong Consistency):get请求可以得到最近一次完成的put请求写入的值。
- 弱一致:不保证get请求可以得到最近一次完成的put请求写入的值。
强一致可以保证get得到的是put写入的最新的数据;而很多的弱一致系统不会做出类似的保证。
人们对于弱一致感兴趣的原因是,虽然强一致可以确保get获取的是最新的数据,但是实现这一点的代价非常高。几乎可以确定的是,分布式系统的各个组件需要做大量的通信,才能实现强一致性。所以,为了尽可能的避免通信,尤其当副本相隔的很远的时候,人们会构建弱一致系统,并允许读取出旧的数据。当然,为了让弱一致更有实际意义,人们还会定义更多的规则。