学习MIT 6.824 部分笔记

学习MIT 6.824 部分笔记

有错请指正:hejiab@foxmail.com
可以看看git : https://github.com/mood321/Java

Lecture 01 - Introduction

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

分布式系统的核心是通过网络来协调,共同完成一致任务的一些计算机。

包括:大型网站的储存系统、大数据运算,如 MapReduce、以及一些更为奇妙的技术,比如点对点的文件共享。

人们使用大量的相互协作的计算机驱动力是:

  • 人们需要获得更高的计算性能。
  • 它可以提供容错(tolerate faults)
  • 一些问题天然在空间上是分布的。例如银行转账,我们假设银行A在纽约有一台服务器,银行B在伦敦有一台服务器,这就需要一种两者之间协调的方法。
  • 人们构建分布式系统来达成一些安全的目标, 系统间解耦,限制出错域。

分布式系统的问题(挑战)在于:

  • 并发执行,并发编程和各种复杂交互所带来的问题,以及时间依赖的问题(比如同步,异步)
  • 分布式系统有多个组成部分,再加上计算机网络,你会会遇到一些意想不到的故障。可能会有一部分组件在工作,而另一部分组件停止运行,或者这些计算机都在正常运行,但是网络中断了或者不稳定。所以,局部错误也是分布式系统很难的原因。
  • 如何设计能让多机器达到期望的性能

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

基础架构的类型主要是存储,通信(网络)和计算。

对于存储和计算,我们的目标是为了能够设计一些简单接口,让第三方应用能够使用这些分布式的存储和计算,这样才能简单的在这些基础架构之上,构建第三方应用程序

当我们在考虑这些抽象的时候,第一个出现的话题就是实现。人们在构建分布系统时,使用了很多的工具,例如:

  • RPC(Remote Procedure Call)。RPC的目标就是掩盖我们正在不可靠网络上通信的事实
  • 另一个我们会经常看到的实现相关的内容就是线程。这是一种编程技术
  • 因为我们会经常用到线程,我们需要在实现的层面上,花费一定的时间来考虑并发控制,比如锁。

1.4 可扩展性(Scalability)

重要的话题,就是性能。

构建分布式系统的目的是为了获取人们常常提到的可扩展的加速。

两台计算机构成的系统如果有两倍性能或者吞吐,就是可扩展性。

1.5 可用性(Availability)

另一个重要的话题是容错。

对于容错,有很多不同的概念可以表述。这些表述中,有一个共同的思想就是可用性(Availability)。在遇到各种错误时,系统能正常提供服务

某些系统通过这种方式提供可用性。比如,你构建了一个有两个拷贝的多副本系统,其中一个故障了,另一个还能运行。当然如果两个副本都故障了,你的系统就不再有可用性。

另一种容错特性是自我可恢复性(recoverability)。在出现故障到故障组件被修复期间,系统将会完全停止工作。但是修复之后,系统又可以完全正确的重新运行

对于一个可恢复的系统,通常需要做一些操作,例如将最新的数据存放在磁盘中,重启之后进行恢复

为了实现这些特性,有很多工具。其中最重要的有两个:

  • 一个是非易失存储(non-volatile storage,类似于硬盘)
  • 对于容错的另一个重要工具是复制(replication),不过,管理复制的多副本系统会有些棘手

1.6 一致性(Consistency)

对于一致性有很多不同的定义。有一些非常直观,比如说get请求可以得到最近一次完成的put请求写入的值。这种一般也被称为强一致(Strong Consistency)。

但是,事实上,构建一个弱一致的系统也是非常有用的。弱一致是指,不保证get请求可以得到最近一次完成的put请求写入的值。但最后一定能得到

强一致带来的昂贵的通信问题,需要很多次网络通信才可以强一致, 而弱一致性,大大的提高了性能

在学术界和现实世界(工业界),有大量关于构建弱一致性保证的研究。所以,弱一致对于应用程序来说很有用,并且它可以用来获取高的性能。

1.7 MapReduce基本工作方式

MapReduce的思想是,应用程序设计人员和分布式运算的使用者,只需要写简单的Map函数和Reduce函数,而不需要知道任何有关分布式的事情,MapReduce框架会处理剩下的事情。

抽象来看,MapReduce假设有一些输入,这些输入被分割成大量的不同的文件或者数据块。

MapReduce启动时,会查找Map函数。之后,MapReduce框架会为每个输入文件运行Map函数。

Map函数以文件作为输入,文件又是整个输入数据的一部分。

对所有的输入文件都运行了Map函数,并得到了论文中称之为中间输出(intermediate output),也就是每个Map函数输出的key-value对。

这就是一个典型的MapReduce Job。从整体来看,为了保证完整性,有一些术语要介绍一下:

  • Job。整个MapReduce计算称为Job。
  • Task。每一次MapReduce调用称为Task。

所以,对于一个完整的MapReduce Job,它由一些Map Task和一些Reduce Task组成。

1.8 Map函数和Reduce函数

Map函数使用一个key和一个value作为参数。

Reduce函数的入参是某个特定key的所有实例(Map输出中的key-value对中,出现了一次特定的key就可以算作一个实例)

MapReduce论文中,讨论了大量的避免使用网络的技巧。其中一个是将GFS和MapReduce混合运行在一组服务器上

默认情况下,读取本地文件,而不会涉及网络 虽然由于故障,负载或者其他原因,不能总是让Map函数都读取本地文件,但是几乎所有的Map函数都会运行在存储了数据的相同机器上,并因此节省了大量的时间,否则通过网络来读取输入数据将会耗费大量的时间。

上图 Reduce函数需要的是第一个Map函数的a=1和第三个Map函数的a=1

论文里称这种数据转换之为洗牌(shuffle)。所以,这里确实需要将每一份数据都通过网络从创建它的Map节点传输到需要它的Reduce节点。所以,这也是MapReduce中代价较大的一部分。

在MapReduce中,需要一直要等到所有的数据都获取到了才会进行Reduce处理,所以这是一种批量处理。现代系统通常会使用streaming并且效率会高一些。

有人提过,想将Reduce的输出传给另一个MapReduce job,而这也是人们常做的事情。在一些场景中,Reduce的输出可能会非常巨大,比如排序,比如网页索引器。10TB的输入对应的是10TB的输出。所以,Reduce的输出也会存储在GFS上。但是Reduce只会生成key-value对,MapReduce框架会收集这些数据,并将它们写入到GFS的大文件中。

一个典型的现代数据中心网络,会有很多的root交换机而不是一个交换机(spine-leaf架构)。每个机架交换机都与每个root交换机相连,网络流量在多个root交换机之间做负载分担。所以,现代数据中心网络的吞吐大多了。

Lecture 03 - GFS

存储是一种关键的抽象。你可以想象,在分布式系统中,可能有各种各样重要的抽象可以应用在分布式系统中,但是实际上,简单的存储接口往往非常有用且极其通用。

设计大型分布式系统或大型存储系统出发点通常是,他们想获取巨大的性能加成,进而利用数百台计算机的资源来同时完成大量工作。因此,性能问题就成为了最初的诉求

很自然的想法就是将数据分割放到大量的服务器上,这样就可以并行的从多台服务器读取数据。我们将这种方式称之为分片(Sharding)。

成百上千台服务器进行分片,每天甚至每个小时都可能会发生错误,我们需要自动化的方法而不是人工介入来修复错误。我们需要一个自动的容错系统,容错(fault tolerance)。

实现容错最有用的一种方法是使用复制,只需要维护2-3个数据的副本,当其中一个故障了,你就可以使用另一个。所以,如果想要容错能力,就得有复制(replication)。

如果有复制,那就有了两份数据的副本。可以确定的是,如果你不小心,它们就会不一致. 如果我们有了复制,我们就有不一致的问题(inconsistency)。

通过设计,你可以避免不一致的问题,并且让数据看起来也表现的符合预期,但你需要很多网络额外的交互,交互会降低性能。想要一致性,你的代价就是低性能。

但这并不是绝对的。你可以构建性能很高的系统,但是不可避免的,都会陷入到这里的循环来。现实中,如果你想要好的一致性,你就要付出相应的代价

3.2 错误的设计(Bad Design)

这是个错误的模型, 他没有做任何事情来保障两台服务器以相同的顺序处理这2个请求

假设我们尝试修复上面的问题,我们让客户端在S1还在线的时候,只从S1读取数据,S1不在线了再从S2读取数据

修复需要服务器之间更多的通信,并且复杂度也会提升。由于获取强一致会带来不可避免的复杂性的提升,有大量的方法可以在好的一致性和一些小瑕疵行为之间追求一个平衡。

3.3 GFS的设计目标

Google的目标是构建一个大型的,快速的文件系统。并且这个文件系统是全局有效的,这样各种不同的应用程序都可以从中读取数据。

为了获得大容量和高速的特性,每个包含了数据的文件会被GFS自动的分割并存放在多个服务器之上,这样读写操作自然就会变得很快。

GFS并没有花费过多的精力来降低延迟,它的关注点在于巨大的吞吐量上,所以单次操作都涉及到MB级别的数据。

也提出了一个当时非常异类的观点:存储系统具有弱一致性也是可以的。

在一些学术论文中,你或许可以看到一些容错的,多副本,自动修复的多个Master节点共同分担工作,但是GFS却宣称使用单个Master节点并能够很好的工作。

3.4 GFS Master 节点

尽管实际中可以拿多台机器作为Master节点,但是GFS中Master是Active-Standby模式,所以只有一个Master节点在工作。Master节点保存了文件名和存储位置的对应关系。除此之外,还有大量的Chunk服务器

Master节点用来管理文件和Chunk的信息,而Chunk服务器用来存储实际的数据

这些Chunk每个是64MB大小,它们共同构成了一个文件。

Master节点内保存的数据内容,这里我们关心的主要是两个表单:

  • 第一个是文件名到Chunk ID或者Chunk Handle数组的对应。这个表单告诉你,文件对应了哪些Chunk。但是只有Chunk ID是做不了太多事情的,所以有了第二个表单。
  • 第二个表单记录了Chunk ID到Chunk数据的对应关系。这里的数据又包括了:
    • 每个Chunk存储在哪些服务器上,所以这部分是Chunk服务器的列表
    • 每个Chunk当前的版本号,所以Master节点必须记住每个Chunk对应的版本号。
    • 所有对于Chunk的写操作都必须在主Chunk(Primary Chunk)上顺序处理,主Chunk是Chunk的多个副本之一。所以,Master节点必须记住哪个Chunk服务器持有主Chunk。
    • 并且,主Chunk只能在特定的租约时间内担任主Chunk,所以,Master节点要记住主Chunk的租约过期时间。

以上数据都存储在内存中,如果Master故障了,这些数据就都丢失了,也能恢复回来

Master 读数据都从内存读,但写数据都会追加Log ,并生成CheckPoint(类似于备份点)。

有些数据需要存在磁盘上,而有些不用。它们分别是:

  • Chunk Handle的数组(第一个表单)要保存在磁盘上。我给它标记成NV(non-volatile, 非易失),这个标记表示对应的数据会写入到磁盘上。
  • Chunk服务器列表不用保存到磁盘上。因为Master节点重启之后可以与所有的Chunk服务器通信,并查询每个Chunk服务器存储了哪些Chunk,所以我认为它不用写入磁盘。所以这里标记成V(volatile),
  • 版本号要不要写入磁盘取决于GFS是如何工作的,我认为它需要写入磁盘。这里先标记成NV。
  • 主Chunk的ID,几乎可以确定不用写入磁盘,因为Master节点重启之后会忘记谁是主Chunk,它只需要等待60秒租约到期,那么它知道对于这个Chunk来说没有主Chunk,这个时候,Master节点可以安全指定一个新的主Chunk。所以这里标记成V。
  • 类似的,租约过期时间也不用写入磁盘,所以这里标记成V。

Master节点向磁盘中的Log追加一条记录说,我刚刚向这个文件添加了一个新的Chunk或者我刚刚修改了Chunk的版本号。

这里在磁盘中维护log而不是数据库的原因是,数据库本质上来说是某种B树(b-tree)或者hash table,相比之下,追加log会非常的高效,因为你可以将最近的多个log记录一次性的写入磁盘

这里Log 的优势也就是LSM 的优势

当Master节点故障重启,并重建它的状态,你不会想要从log的最开始重建状态,因为log的最开始可能是几年之前,
所以Master节点会在磁盘中创建一些checkpoint点,这可能要花费几秒甚至一分钟。这样Master节点重启时,会从log中的最近一个checkpoint开始恢复
,再逐条执行从Checkpoint开始的log,最后恢复自己的状态。

3.5 GFS读文件(Read File)

对于读请求来说,意味着应用程序或者GFS客户端有一个文件名和它想从文件的某个位置读取的偏移量(offset),应用程序会将这些信息发送给Master节点

Master 查询自己的file表, 查询数据在那些Chunk ,都给客户端,客户端会就近选一个读取

客户端每次可能只读取1MB或者64KB数据,可能会连续多次读取同一个Chunk的不同位置所以,客户端会缓存Chunk和服务器的对应关系,而不用每次去Master 查询

3.6 GFS写文件(Write File)(1)

对于读文件来说,可以从任何最新的Chunk副本读取数据,但是对于写文件来说,必须要通过Chunk的主副本(Primary Chunk)来写入

如果Chunk的主副本不存在,Master会找出所有存有Chunk最新副本的Chunk服务器。 最新的副本是指,副本中保存的版本号与Master中记录的Chunk的版本号一致。

这里不能直接用Chunk 中最新的, 因为可能真的最新的在重启, 所以一定要和Mater 存的一致的

挑选一个作为Primary,其他的作为Secondary。Master 吧log和版本号 写入磁盘

Master节点会向Primary和Secondary副本对应的服务器发送消息并告诉它们,谁是Primary,谁是Secondary,Chunk的新版本是什么。Primary和Secondary服务器都会将版本号存储在本地的磁盘中。

有了一个Primary,它可以接收来自客户端的写请求,并将写请求应用在多个Chunk服务器中。

Master节点通知Primary和Secondary服务器,你们可以修改这个Chunk。它还给Primary一个租约,这个租约告诉Primary说,在接下来的60秒中,你将是Primary,60秒之后你必须停止成为Primary。这种机制可以确保我们不会同时有两个Primary

客户端会将要追加的数据发送给Primary和Secondary服务器,这些服务器会将数据写入到一个临时位置,并不会直接写到文件末尾,Primary和Secondary服务都有了文件,

Primary找到Chunk 末尾,检查磁盘空间是否足够 ,追加上去, 通知Secondary 追加 ,完成

如果客户端从Primary得到写入失败,那么客户端应该重新发起整个追加过程。客户端首先会重新与Master交互,找到文件末尾的Chunk;之后,客户端需要重新发起对于Primary和Secondary的数据追加操作。

3.7 GFS写文件(Write File)(2)

Primary告诉所有的副本去执行数据追加操作,某些成功了,某些没成功。

  • 实际上,部分副本还是成功将数据追加了。所以现在,一个Chunk的部分副本成功完成了数据追加,而另一部分没有成功,这种状态是可接受的,没有什么需要恢复,这就是GFS的工作方式。

写文件失败之后,读Chunk数据会有什么不同?

  • 写文件失败之后,一个客户端读取相同的Chunk,客户端可能可以读到追加的数据,也可能读不到,取决于客户端读的是Chunk的哪个副本。

可不可以通过版本号来判断副本是否有之前追加的数据?

  • 所有的Secondary都有相同的版本号。版本号只会在Master指定一个新Primary时才会改变。通常只有在原Primary发生故障了,才会指定一个新的Primary。

客户端将数据拷贝给多个副本会不会造成瓶颈?

  • 论文后面说客户端只会将数据发送给离它最近的副本,之后那个副本会将数据转发到另一个副本,以此类推形成一条链,直到所有的副本都有了数据。这样一条数据传输链可以在数据中心内减少跨交换机传输(否则,所有的数据吞吐都在客户端所在的交换机上)。

如果Master节点发现Primary挂了会怎么办?

  • 不会立即选一个Primary ,因为有可能网络分区,他一定是等租约时间用完,Mater 重新选一个

3.8 GFS的一致性

当我们追加数据时,面对Chunk的三个副本,当客户端发送了一个追加数据的请求,要将数据A追加到文件末尾

假设第二个客户端加入进来,想要追加数据B,但是由于网络问题发送给某个副本的消息丢失了,所以,现在我们有两个副本有数据B,另一个没有。

之后,第三个客户端想要追加数据C,并且第三个客户端记得下图中左边第一个副本是Primary。Primary选择了偏移量,并将偏移量告诉Secondary,将数据C写在Chunk的这个位置。三个副本都将数据C写在这个位置。

对于数据B来说,客户端会收到写入失败的回复,客户端会重发写入数据B的请求。所以,第二个客户端会再次请求追加数据B,或许这次数据没有在网络中丢包,并且所有的三个副本都成功追加了数据B。现在三个副本都在线,并且都有最新的版本号。

~~GFS这样设计的理由是足够的简单,但是同时也给应用程序暴露了一些奇怪的数据。这里希望为应用程序提供一个相对简单的写入接口,但应用程序需要容忍读取数据的乱序。

如果你想要将GFS升级成强一致系统,我可以为你列举一些你需要考虑的事情:

  • 你可能需要让Primary来探测重复的请求,这样第二个写入数据B的请求到达时,Primary就知道,我们之前看到过这个请求,可能执行了也可能没执行成功。
  • 对于Secondary来说,如果Primay要求Secondary执行一个操作,Secondary必须要执行而不是只返回一个错误给Primary。对于一个严格一致的系统来说,是不允许Secondary忽略Primary的请求而没有任何补偿措施的。如果有Secondary磁盘损坏,那就需要把他剔除
  • 当Primary要求Secondary追加数据时,直到Primary确信所有的Secondary都能执行数据追加之前,Secondary必须小心不要将数据暴露给读请求。
  • 另一个问题是,当Primary崩溃时,可能有一组操作由Primary发送给Secondary,Primary在确认所有的Secondary收到了请求之前就崩溃了,选一个新的Primary ,他必须处理所有节点的一致性,因为有些可能没收到第一个Primary的消息
  • Secondary之间可能会有差异,或者客户端从Master节点获取的是稍微过时的Secondary。需要类似Kafka的ISR 机制

所以GFS在Google内部广泛被应用。它最严重的局限可能在于,它只有一个Master节点,会带来以下问题:

  • Master节点必须为每个文件,每个Chunk维护表单,随着GFS的应用越来越多,这意味着涉及的文件也越来越多,最终Master会耗尽内存来存储文件表单。你可以增加内存,但是单台计算机的内存也是有上限的。所以,这是人们遇到的最早的问题。
  • 除此之外,单个Master节点要承载数千个客户端的请求,而Master节点的CPU每秒只能处理数百个请求,尤其Master还需要将部分数据写入磁盘,很快,客户端数量超过了单个Master的能力。
  • 另一个问题是,应用程序发现很难处理GFS奇怪的语义(本节最开始介绍的GFS的副本数据的同步,或者可以说不同步)。
  • 最后一个问题是,从我们读到的GFS论文中,Master节点的故障切换不是自动的。GFS需要人工干预来处理已经永久故障的Master节点,并更换新的服务器,这可能需要几十分钟甚至更长的而时间来处理。对于某些应用程序来说,这个时间太长了。

Lecture 04 - VMware FT

4.1 复制(Replication)

复制能处理的故障,那就是,单台计算机的fail-stop故障。

还有一些其他的限制。如果我们有两个副本,一个Primay和一个Backup节点,我们总是假设两个副本中的错误是相互独立的。

错误和复制的无关,不然复制吧错误又复制出来,那就无意义了

4.2 状态转移和复制状态机(State Transfer and Replicated State Machine)

状态转移传输的是可能是内存,每过一会,Primary就会对自身的内存做一大份拷贝,并通过网络将其发送到Backup。为了提升效率,你可以想到每次同步只发送上次同步之后变更了的内存。

复制状态机会将来自客户端的操作或者其他外部事件,事实上就是操作Log ,重放一样的日志,数据就能保持一致

复制状态机的原因是,通常来说,外部操作或者事件比服务的状态要小。Log 一般比数据小

但复制状态机 有个问题,就是随机操作, 经典的就是数据的 随机数,获取当前时间

4.3 VMware FT 工作原理

Primary和Backup,互为副本。某些我们服务的客户端,向Primary发送了一个请求,这个请求以网络数据包的形式发出。

这个网络数据包产生一个中断,之后这个中断送到了VMM。VMM可以发现这是一个发给我们的多副本服务的一个输入,所以这里VMM会做两件事情:

  • 在虚拟机的guest操作系统中,模拟网络数据包到达的中断,以将相应的数据送给应用程序的Primary副本。
  • 除此之外,因为这是一个多副本虚拟机的输入,VMM会将网络数据包拷贝一份,并通过网络送给Backup虚机所在的VMM。

Primary和Backup都有了这个网络数据包,它们有了相同的输入,再加上许多细节,它们将会以相同的方式处理这个输入,并保持同步。

虚机内的服务会回复客户端的请求。在Primary虚机里面,服务会生成一个回复报文,并通过VMM在虚机内模拟的虚拟网卡发出。之后VMM可以看到这个报文,它会实际的将这个报文发送给客户端。

由于Backup虚机运行了相同顺序的指令,它也会生成一个回复报文给客户端,并将这个报文通过它的VMM模拟出来的虚拟网卡发出。但是它的VMM知道这是Backup虚机,会丢弃这里的回复报文。所以这里,Primary和Backup都看见了相同的输入,但是只有Primary虚机实际生成了回复报文给客户端。

VMware FT论文中将Primary到Backup之间同步的数据流的通道称之为Log Channel。虽然都运行在一个网络上,但是这些从Primary发往Backup的事件被称为Log Channel上的Log Event/Entry。

4.4 非确定性事件(Non-Deterministic Events)

非确定性事件可以分成几类:

  • 客户端输入。假设有一个来自于客户端的输入,这个输入随时可能会送达,所以它是不可预期的
  • 另外,如其他同学指出的,有一些指令在不同的计算机上的行为是不一样的,这一类指令称为怪异指令,比如说:
    • 随机数生成器
    • 获取当前时间的指令,在不同时间调用会得到不同的结果
    • 获取计算机的唯一ID
  • 另外一个常见的非确定事件,在VMware FT论文中没有讨论,就是多CPU的并发。

所有的事件都需要通过Log Channel,从Primary同步到Backup。有关日志条目的格式在论文中没有怎么描述,猜日志条目中有三样东西:

  • 事件发生时的指令序号。因为如果要同步中断或者客户端输入数据,最好是Primary和Backup在相同的指令位置看到数据,所以我们需要知道指令序号
  • 日志条目的类型,可能是普通的网络数据输入,也可能是怪异指令。
  • 最后是数据。如果是一个网络数据包,那么数据就是网络数据包的内容。如果是一个怪异指令,数据将会是这些怪异指令在Primary上执行的结果

4.5 输出控制(Output Rule)

这节课 ,我的理解就是讲了, 复制架构中,主写,直接返回客户端和 等待从节点的ack之后响应的 两种同步,异步架构的性能取舍

4.6 重复输出(Duplicated Output)

还有一种可能的情况是,回复报文已经从VMM发往客户端了,所以客户端收到了回复,但是这时Primary虚机崩溃了。

客户端请求还没有真正发送到Backup虚机中。当Primary崩溃之后,Backup接管服务,Backup首先需要消费所有在等待缓冲区中的Log,以保持与Primay在相同的状态,这样Backup才能以与Primary相同的状态接管服务

然后执行完log ,给客户端相同的响应, 会丢弃

事实上,对于任何有主从切换的复制系统,基本上不可能将系统设计成不产生重复输出。为了避免重复输出,有一个选项是在两边都不生成输出,但这是一个非常糟糕的做法(因为对于客户端来说就是一次失败的请求)。当出现主从切换时,切换的两边都有可能生成重复的输出,这意味着,某种程度上来说,所有复制系统的客户端需要一种重复检测机制。

4.7 Test-and-Set 服务

一个非常常见的场景就是,Primary和Backup都在运行,但是它们之间的网络出现了问题,同时它们各自又能够与一些客户端通信。这时,它们都会以为对方挂了,自己需要上线并接管服务。

涉及到了计算机网络,那就可能出现上面的问题,而不仅仅是机器故障。如果我们同时让Primary和Backup都在线,那么我们现在就有了脑裂(Split Brain)。

这时就需要第三方来协调, VMware FT的Test-and-Set就是这样一个角色 ,类似zk 给一个标志位,都去拿,第一成功,吧0变成1 , 后面节点再去,就是1 拿不到了

Lecture 06 - Raft1

上面几个分布式系统的特点 :

  • MapReduce复制了计算,但是复制这个动作,或者说整个MapReduce被一个单主节点控制。
  • GFS以主备(primary-backup)的方式复制数据。它会实际的复制文件内容。
  • 它在一个Primary虚机和一个Backup虚机之间复制计算相关的指令。当其中一个虚机出现故障时,为了能够正确的恢复。需要一个Test-and-Set服务来确认,Primary虚机和Backup虚机只有一个能接管计算任务。

它们都是一个多副本系统(replication system),但是在背后,它们存在一个共性:它们需要一个单节点来决定,在多个副本中,谁是主(Primary)。

单主节点的好处 根本不会有网络分区这样的故障, 坏处 单节点故障

当时的人们在构建多副本系统时,需要排除脑裂的可能。这里有两种技术:

  • 第一种是构建一个不可能出现故障的网络。实际上,不可能出现故障的网络一直在我们的身边。你们电脑中,连接了CPU和内存的线路就是不可能出现故障的网络。
  • 另一种就是人工解决问题,不要引入任何自动完成的操作。默认情况下,客户端总是要等待两个服务器响应,如果只有一个服务器响应,永远不要执行任何操作

6.2 过半票决(Majority Vote)

在Raft 中提到解决网络分区的方案就是 过半机制,在任何时候为了完成任何操作,你必须凑够过半的服务器来批准相应的操作。

如果系统有 2 * F + 1 个服务器,那么系统最多可以接受F个服务器出现故障,仍然可以正常工作

也被称为多数投票(quorum)系统

6.3 Raft 初探

客户端将请求发送给Raft的Leader节点,在服务端程序的内部,

应用程序只会将来自客户端的请求对应的操作向下发送到Raft层,并且告知Raft层,请把这个操作提交到多副本的日志(Log)中,并在完成时通知客户端。

Raft节点之间相互交互,直到过半的Raft节点将这个新的操作加入到它们的日志中

6.4 Log 同步时序

客户端发一个请求 ,服务器1的Raft层会发送一个添加日志(AppendEntries)的RPC到其他两个副本(S2,S3),因为本身leader 就成功了,只需要等待一个节点返回成功就过半了,然后返回给客户端

在raft 中没有明确的告诉S2,S3 committed的消息,他是跟着下一条日志(AppendEntries) 里面的,在leader 发送心跳或者新的客户端请求的时候,就会发给S2 ,S3

6.5 日志(Raft Log)

Log的作用:

  • Log是Leader用来对操作排序的一种手段。这对于复制状态机而言至关重要,对复制状态机,必须要以相同的顺序执行相同的操作
  • 在一个(非Leader,也就是Follower)副本收到了操作,但是还没有执行操作时。该副本需要将这个操作存放在某处,直到收到了Leader发送的新的commit号才执行 ,维护了事务中间状态不可见
  • 另一个用途是用在Leader节点,Leader需要在它的Log中记录操作,因为这些操作可能需要重传给Follower。(网络之类的故障第一次传输失败)
  • 就是它可以帮助重启的服务器恢复状态。

6.6 应用层接口

应用层和Raft层之间的接口:

  • Start函数,这个函数只接收一个参数,就是客户端请求。key-value层说:我接到了这个请求,请把它存在Log中,并在committed之后告诉我。
  • 叫做applyCh的channel,通过它你可以发送ApplyMsg消息,以go channel中的一条消息的形式存在,Raft层会通知key-value层,处理已完成

Start函数的返回值包括,这个请求将会存放在Log中的位置(index),如果Commited了 还有当前的任期号(term number) 和其他内容

在ApplyMsg中,将会包含请求(command)和对应的Log位置(index)。

只有在ApplyMsg 返回值之后,k-v层才会响应客户端请求, 这是个两段提交的逻辑

对每个副本来说Log 是可能不一样的(参照7.1的图),所以Rafe 还有个重要功能 处理各个副本log的差异

6.7 Leader选举(Leader Election)

你可以不用Leader就构建一个类似的系统。实际上有可能不引入任何指定的Leader,通过一组服务器来共同认可Log的顺序,进而构建一个一致系统。

通常情况下,如果服务器不出现故障,有一个Leader的存在,会使得整个系统更加高效

对于一个无Leader的系统,通常需要一轮消息来确认一个临时的Leader,之后第二轮消息才能确认请求。

Raft生命周期中可能会有不同的Leader,它使用任期号(term number)来区分不同的Leader。

选举定时器:

  • 定时器时间耗尽之前,当前节点没有收到任何当前Leader的消息,这个节点会认为Leader已经下线,并开始一次选举。
  • 服务器会增加任期号(term number),在一个任期内只会有一个leader, 发消息给其他(N-1)个节点(Raft 候选者总是投给自己),选举一个新的Leader ,

Leader没有故障,也有可能发生重新选举,比如网络慢,丢了几个心跳 ,

当一个服务器赢得了一次选举,这个服务器会收到过半的认可投票,这个服务器会直接知道自己是新的Leader,因为它收到了过半的投票,

他会发心跳消息给其他服务器, 而且只有leader 可以AppendEntries消息

6.8 选举定时器(Election Timer)

任何一条AppendEntries消息都会重置所有Raft节点的选举定时器。 这样只有leader 还在线,或者不是太慢,都会重置掉他自己的选举定时器

假如候选人们几乎是同时参加竞选,它们分割了选票(Split Vote),候选者又都会投自己, 所以会选举失败

Raft不能完全避免分割选票(Split Vote),但是可以使得这个场景出现的概率大大降低。Raft通过为选举定时器随机的选择超时时间来达到这一点。

这有个问题,心跳时间一定要合理,不然太快影响业务,太慢又不能及时发现问题

另一个需要考虑的点是,不同节点的选举定时器的超时时间差(S2和S3之间)必须要足够长,使得第一个开始选举的节点能够完成一轮选举。

6.9 可能的异常情况

Log 不相同大体两个规则:

  • leader 在发AppendEntries消息的时候会带上,上一个Commit的任期号(term number)和Log位置(index),如果上一个不一样,一直向前找
  • 以当前leader的存储的任期号和位置为准 ,复写(因为他的选举机制,看7.2)

raft

7.1 日志恢复(Log Backup)

7.2 Raft选举约束

经典场景

节点只能向满足下面条件之一的候选人投出赞成票:

  • 候选人最后一条Log条目的任期号大于本地最后一条Log条目的任期号;

  • 或者,候选人最后一条Log条目的任期号等于本地最后一条Log条目的任期号,且候选人的Log记录长度大于等于本地Log记录的长度

7.3 快速恢复(Fast Backup)

在日志恢复的时候,如果每一次值恢复一条,一台机器长时间掉线是很耗时的,

可以让Follower在回复Leader的AppendEntries消息中,携带3个额外的信息,来加速日志的恢复。这里的回复是指,Follower因为Log信息不匹配,拒绝了Leader的AppendEntries之后的回复。这里的三个信息是指:

  • XTerm:这个是Follower中与Leader冲突的Log对应的任期号。在之前(7.1)有介绍Leader会在prevLogTerm中带上本地Log记录中,前一条Log的任期号。如果Follower在对应位置的任期号不匹配,它会拒绝Leader的AppendEntries消息,并将自己的任期号放在XTerm中。如果Follower在对应位置没有Log,那么这里会返回 -1。
  • XIndex:这个是Follower中,对应任期号为XTerm的第一条Log条目的槽位号。
  • XLen:如果Follower在对应位置没有Log,那么XTerm会返回-1,XLen表示空白的Log槽位数。

7.4 持久化(Persistence)

为了解决机器故障/断点等情况, 需要对一些数据持久化,便于重启或迁移

有且仅有三个数据是需要持久化存储的。它们分别是Log、currentTerm、votedFor

  • Log需要被持久化存储的原因是,这是唯一记录了应用程序状态的地方。所以当服务器重启时,唯一能用来重建应用程序状态的信息就是存储在Log中的一系列操作,所以Log必须要被持久化存储。
  • currentTerm和votedFor都是用来确保每个任期只有最多一个Leader。votedFor 保证投完票,重启完,知道这轮 自己是否投过票
  • currentTerm要更微妙一些,但是实际上还是为了实现一个任期内最多只有一个Leader,重启之后我们不知道任期号是什么,很难确保一个任期内只有一个Leader。

向磁盘写数据是一个代价很高的操作。一个机械硬盘,我们通过写文件的方式来持久化存储,向磁盘写入任何数据都需要花费大概10毫秒时间。

为了提高效率,这里有很多选择 ,使用ssd ,或者闪存,ssd 可以0.1毫秒完成一次写操作,提升了100倍 ,使用电池供电的DRAM的,在电池的可供电时间内重启都不会丢数据,如果资金充足,且不怕复杂的话,这种方式的优点是,你可以每秒写DRAM数百万次,那么持久化存储就不再会是一个性能瓶颈

另一个常见方法是,批量执行操作, 限定时间,数量批量持久化

为什么服务器重启时,commitIndex、lastApplied、nextIndex、matchIndex,可以被丢弃?

是因为Leader可以通过检查自己的Log和发送给Followers的AppendEntries的结果来对比出来commit,所以不持久化,也能比出来

7.5 日志快照(Log Snapshot)

Log压缩和快照解决的问题是:在系统长时间运行,日志追加,日志会变得和庞大,消耗磁盘的大量空间 如果还是用日志重放的方式去恢复数据,时间也会很漫长

对于大多数的应用程序来说,应用程序的状态远小于Log的大小,因为一条数据总是有多个版本,多条LOG

7.6 线性一致(Linearizability)

通常来说,线性一致等价于强一致。一个服务是线性一致的,那么它表现的就像只有一个服务器,并且服务器没有故障,这个服务器每次执行一个客户端请求,并且没什么奇怪的是事情发生。

线性一致对于这个顺序,有两个限制条件:

  • 如果一个操作在另一个操作开始前就结束了,那么这个操作必须在执行历史中出现在另一个操作前面。
  • 执行历史中,读操作,必须在相应的key的写操作之后。

8.1 线性一致(Linearizability)

线性一致性的定义就是,所有客户端的 读写请求历史记录 能构成线性,没有环

线性一致性大致等于强一致性, 但生产系统很少有线性一致性的,

  • 快照一致性不是线性一致性,因为快照 它不会包括该快照之后的写入
  • zk也不是线性的,他的副本读,可能读到旧数据

8.3 线性一致(Linearizability)(3)

对于读请求不允许返回旧的数据,只能返回最新的数据。或者说,对于读请求,线性一致系统只能返回最近一次完成的写请求写入的值

在一个实际的系统实现中,可能有任何原因导致这个结果,例如:

  • Leader在某个时间故障了
  • 这个客户端发送了一个读请求,但是这个请求丢包了因此Leader没有收到这个请求
  • Leader收到了这个读请求并且执行了它,但是回复的报文被网络丢包了
  • Leader收到了请求并开始执行,在完成执行之前故障了
  • Leader执行了这个请求,但是在返回响应的时候故障了

一般来说,执行失败,客户端可能有重试机制, 所以,服务端一定要有幂等,根据请求的唯一号或者其他的客户端信息来保存一个表

8.4 Zookeeper

Zookeeper,作为一个多副本系统,Zookeeper是一个容错的,通用的协调服务,它与其他系统一样,通过多副本来完成容错

但是他3台,5台,7台 ,并不能直接提升性能,因为zk的 leader会成为性能瓶颈 ,而且会降低,他需要将更多的操作日志发出去(只考虑写请求)

为此zk 放弃了线性一致,他是可以提供旧数据的,读请求就不用必须走leader了,提升了性能 (zk 自己管这种一致性叫 顺序一致性)

8.5 一致保证(Consistency Guarantees)

Zookeeper 会保证写请求的一致性, 表现的以某种顺序 ,一次执行一次写请求

另一个保证是,任何一个客户端的请求,都会按照客户端指定的顺序来执行,论文里称之为FIFO(First In First Out)客户端序列

对于读写混合的模式, 因为客户端会给每个请求打上顺序, 读之前有写, 读从副本节点读, 副本会判断他前面顺序的写请求有没有执行,

在执行之前,副本是不能返回结果给客户端的(可能是阻塞,可能是返回失败)

8.6 同步操作(sync)

Zookeeper有一个操作类型是sync,它本质上就是一个写请求

我想读出Zookeeper中最新的数据。这个时候,我可以发送一个sync请求,它的效果相当于一个写请求,发送一个sync请求,之后再发送读请求。

这个读请求可以保证看到sync对应的状态,所以可以合理的认为是最新的 但这是一个有代价的操作

8.7 就绪文件(Ready file/znode)

所有的client 向zk发送操作的顺序,和这些操作被执行的顺序,是一致的

保证 每个client的“写”的顺序在zk执行的是一致的 操作的原子性用“ready file”来实现

写的时候:

  • 大致的思想是,要操作到某个数据,先检查对于的标记“ready file”是否存在,存在才能操作
  • 在修改对应的数据的时候,会先删除这个“ready file”标记,修改完再create 这个"ready file"( 在)

读的时候:

判断"ready file"是否存在(在写读混合模式下)

  • 存在,说明之前的写操作已经执行,直接读
  • 不存在,这个客户端会在特定(zxid)上面建立watch事件监听,后续有操作,他再尝试读取

9.1 Zookeeper API

Zookeeper的特点:

  • Zookeeper基于(类似于)Raft框架,所以我们可以认为它是,当然它的确是容错的,它在发生网络分区的时候,也能有正确的行为。
  • 当我们在分析各种Zookeeper的应用时,我们也需要记住Zookeeper有一些性能增强,使得读请求可以在任何副本被处理,因此,可能会返回旧数据。
  • 另一方面,Zookeeper可以确保一次只处理一个写请求,并且所有的副本都能看到一致的写请求顺序。这样,所有副本的状态才能保证是一致的(写请求会改变状态,一致的写请求顺序可以保证状态一致)。
  • 由一个客户端发出的所有读写请求会按照客户端发出的顺序执行。
  • 一个特定客户端的连续请求,后来的请求总是能看到相比较于前一个请求相同或者更晚的状态(详见8.5 FIFO客户端序列)

Zookeeper的目标是解决什么问题,或者期望用来解决什么问题?

  • 对于我来说,使用Zookeeper的一个主要原因是,它可以是一个VMware FT所需要的Test-and-Set服务(详见4.7)的实现。Test-and-Set服务在发生主备切换时是必须存在的,但是在VMware FT论文中对它的描述却又像个谜一样,论文里没有介绍:这个服务究竟是什么,它是容错的吗,它能容忍网络分区吗?Zookeeper实际的为我们提供工具来写一个容错的,完全满足VMware FT要求的Test-and-Set服务,并且可以在网络分区时,仍然有正确的行为。这是Zookeeper的核心功能之一。
  • 使用Zookeeper还可以做很多其他有用的事情。其中一件是,人们可以用它来发布其他服务器使用的配置信息。例如,向某些Worker节点发布当前Master的IP地址。
  • 另一个Zookeeper的经典应用是选举Master。当一个旧的Master节点故障时,哪怕说出现了网络分区,我们需要让所有的节点都认可同一个新的Master节点。
  • 如果新选举的Master需要将其状态保持到最新,比如说GFS的Master需要存储对于一个特定的Chunk的Primary节点在哪,现在GFS的Master节点可以将其存储在Zookeeper中,并且知道Zookeeper不会丢失这个信息。当旧的Master崩溃了,一个新的Master被选出来替代旧的Master,这个新的Master可以直接从Zookeeper中读出旧Master的状态。
  • 其他还有,对于一个类似于MapReduce的系统,Worker节点可以通过在Zookeeper中创建小文件来注册自己。
  • 同样还是类似于MapReduce这样的系统,你可以设想Master节点通过向Zookeeper写入具体的工作,之后Worker节点从Zookeeper中一个一个的取出工作,执行,完成之后再删除工作。

Zookeeper以RPC的方式暴露以下API。

  • CREATE(PATH,DATA,FLAG)。入参分别是文件的全路径名PATH,数据DATA,和表明znode类型的FLAG。这里有意思的是,CREATE的语义是排他的。也就是说,如果我向Zookeeper请求创建一个文件,如果我得到了yes的返回,那么说明这个文件之前不存在,我是第一个创建这个文件的客户端;如果我得到了no或者一个错误的返回,那么说明这个文件之前已经存在了。所以,客户端知道文件的创建是排他的。在后面有关锁的例子中,我们会看到,如果有多个客户端同时创建同一个文件,实际成功创建文件(获得了锁)的那个客户端是可以通过CREATE的返回知道的。
  • DELETE(PATH,VERSION)。入参分别是文件的全路径名PATH,和版本号VERSION。有一件事情我之前没有提到,每一个znode都有一个表示当前版本号的version,当znode有更新时,version也会随之增加。对于delete和一些其他的update操作,你可以增加一个version参数,表明当且仅当znode的当前版本号与传入的version相同,才执行操作。当存在多个客户端同时要做相同的操作时,这里的参数version会非常有帮助(并发操作不会被覆盖)。所以,对于delete,你可以传入一个version表明,只有当znode版本匹配时才删除。
  • EXIST(PATH,WATCH)。入参分别是文件的全路径名PATH,和一个有趣的额外参数WATCH。通过指定watch,你可以监听对应文件的变化。不论文件是否存在,你都可以设置watch为true,这样Zookeeper可以确保如果文件有任何变更,例如创建,删除,修改,都会通知到客户端。此外,判断文件是否存在和watch文件的变化,在Zookeeper内是原子操作。所以,当调用exist并传入watch为true时,不可能在Zookeeper实际判断文件是否存在,和建立watch通道之间,插入任何的创建文件的操作,这对于正确性来说非常重要。
  • GETDATA(PATH,WATCH)。入参分别是文件的全路径名PATH,和WATCH标志位。这里的watch监听的是文件的内容的变化。
  • SETDATA(PATH,DATA,VERSION)。入参分别是文件的全路径名PATH,数据DATA,和版本号VERSION。如果你传入了version,那么Zookeeper当且仅当文件的版本号与传入的version一致时,才会更新文件。
  • LIST(PATH)。入参是目录的路径名,返回的是路径下的所有文件。

9.2 使用Zookeeper实现计数器

zk 的多客户端,get->put 操作不是原子的

WHILE TRUE:
    X, V = GETDATA("F")
    IF SETDATA("f", X + 1, V):
        BREAK

这是通常写法, 但这种写法值在低负载的场景使用, 因为他重试的次数和客户端多少挂钩,复杂度是 O(n^2)

9.3 使用Zookeeper实现非扩展锁

WHILE TRUE:
    IF CREATE("f", data, ephemeral=TRUE): RETURN
    IF EXIST("f", watch=TRUE):
        WAIT

一般来说会尝试去创建:

  • 成功,加锁成功
  • 失败,会在节点上加watch 监听 ,直到之前成功的del

但监听会有和上面,累加的场景一样的问题 ,在del的时候 会有羊群效应,一般解决方案是,watch 序号节点,给他排队(见9.4)

9.4 使用Zookeeper实现可扩展锁

CREATE("f", data, sequential=TRUE, ephemeral=TRUE)
WHILE TRUE:
    LIST("f*")
    IF NO LOWER #FILE: RETURN
    IF EXIST(NEXT LOWER #FILE, watch=TRUE):
        WAIT

这有问题,就是 中间序号的客户端节点 如果挂了, 或者持有锁挂了, 这种可以依赖,zk的临时znode自动del的机制做

9.5 链复制(Chain Replication)(CRAQ)

  • 第一个是它通过复制实现了容错;
  • 第二是它通过以链复制API请求这种有趣的方式,提供了与Raft相比不一样的属性。

CRAQ是对于一个叫链式复制(Chain Replication)的旧方案的改进,有许多系统使用了他

,Zookeeper为了能够从任意副本执行读请求,不得不牺牲数据的实时性,因此也就不是线性一致的。CRAQ却可以从任意副本执行读请求,同时也保留线性一致性

这里只是Chain Replication,并不是CRAQ。Chain Replication本身是线性一致的,在没有故障,他是一致的

从全局看,只有一个请求,tail 尾节点处理完了,才算commit,读请求才能读到,意味着链条上所有节点都成功

9.6 链复制的故障恢复(Fail Recover)

在这个模式下 只有两种可能

  • 一种没有故障,tail 处理完成,commit
  • 一种中间链表节点有一个出现故障, 链表后面的都没有
  • 如果HEAD出现故障,作为最接近的服务器,下一个节点可以接手成为新的HEAD,并不需要做任何其他的操作。对于还在处理中的请求,可以分为两种情况:

    • 对于任何已经发送到了第二个节点的写请求,不会因为HEAD故障而停止转发,它会持续转发直到commit。
    • 如果写请求发送到HEAD,在HEAD转发这个写请求之前HEAD就故障了,那么这个写请求必然没有commit,也必然没有人知道这个写请求,我们也必然没有向发送这个写请求的客户端确认这个请求,因为写请求必然没能送到TAIL。所以,对于只送到了HEAD,并且在HEAD将其转发前HEAD就故障了的写请求,我们不必做任何事情。或许客户端会重发这个写请求,但是这并不是我们需要担心的问题。
  • 如果TAIL出现故障,处理流程也非常相似,TAIL的前一个节点可以接手成为新的TAIL。所有TAIL知道的信息,TAIL的前一个节点必然都知道,因为TAIL的所有信息都是其前一个节点告知的

  • 中间节点出现问题就去除故障节点

Chain Replication与Raft进行对比,有以下差别:

  • 从性能上看,raft的leader 需要处理所有副本,Chain Replication只需要处理后继节点 ,所有性能瓶颈来的更晚
  • raft 的读写都会从leader, CRAQ,写请求走head ,读请求是tail节点发的,所以压力分摊了
  • 故障恢复,Chain Replication也比Raft更加简单,这是主要动力

9.7 链复制的配置管理器(Configuration Manager)

CRAQ 并不能处理脑裂和网络分区 ,这意味它不能单独使用。

总是会有一个外部的权威(External Authority)来决定谁是活的,谁挂了,并确保所有参与者都认可由哪些节点组成一条链,这样在链的组成上就不会有分歧。这个外部的权威通常称为Configuration Manager。

有一个基于Raft或者Paxos的Configuration Manager,它是容错的,也不会受脑裂的影响。

之后,通过一系列的配置更新通知,Configuration Manager将数据中心内的服务器分成多个链。

比如说,Configuration Manager决定链A由服务器S1,S2,S3组成,链B由服务器S4,S5,S6组成。

10.1 Aurora 背景历史

10.2 故障可恢复事务(Crash Recoverable Transaction)

Aurora 实际上主要关注的是,如何实现一个故障可恢复事务(Crash Recoverable Transaction)。所以这一部分我们主要看的是事务(Transaction)和故障可恢复(Crash Recovery)

事务是指将多个操作打包成原子操作,并确保多个操作顺序执行 ,并在执行的时候 ,其他事务是不可以看到中间状态的 ,并在故障的时候保持原子

在单机数据库系统中, 用B-tree 存储在硬盘上, 用预写日志(WAL) 来保证系统容错

在数据库操作的时候 ,都会对最近从磁盘读取的page 有缓存

在写数据的时候,并不会直接写磁盘 还在本地缓存(change buffer) , 在事务提交前,都会写WAL (mysql redo log),日志落盘,顺序写入一般

ps:   mysql 实现,非唯一索引,在操作的的时候回存到 change buffer( change buffer会落盘), 之后merge 进磁盘, 维护了二级索引

redo日志属于物理日志, 只是记录一下事务对数据库做了哪些修改 
undo log 是逻辑日志,他实际是跟在 mvcc的多版本控制链表上的


物理日志VS逻辑日志
物理日志: 记录的是每一个page页中具体存储的值是多少,在这个数据页上做了什么修改.  比如: 某个事物将系统表空间中的第100个页面中偏移量为1000处的那个字节的值1改为2.
逻辑日志: 记录的是每一个page页面中具体数据是怎么变动的,它会记录一个变动的过程或SQL语句的逻辑, 比如: 把一个page页中的一个数据从1改为2,再从2改为3,逻辑日志就会记录1->2,2->3这个数据变化的过程

~~### 10.3 关系型数据库(Amazon RDS)

Amazon RDS用EC2实例作为数据库,它的data page和WAL Log存储在EBS,而不是对应服务器的本地硬盘。(我猜polarDB 在存储上的思路也差不多,存算分离 )

但这种方式,日志会很大,副本备份 网络传输很吃心性能

10.4 Aurora 初探

Aurora 有多个副本 在不同数据中心(AZ), 在同步的时候只同步日志, 每条日志 ,只记新值,旧值 实际上比数据大小更小

ps: 因为之前笔记还没补,所以这儿ps下, 复制的两种方式: 状态转移和 复制状态机(一般实现 就是快照复制, 日志回放,优缺点可以类比redis两种备份)

只存日志 ,会导致,系统不再通用,只用于数据库日志

他写是采用 Quorum 方式,全部响应 (分布式事务: 两段式原子提交 和 共识算法,之后有时间谢谢ddia的笔记)

10.5 Aurora存储服务器的容错目标(Fault-Tolerant Goals)

Aurora的容错目标是什么?

  • 一个AZ挂了之后 ,写不受影响
  • 对于读操作,当一个AZ和一个其他AZ的服务器挂了之后,读操作不受影响,对于Aurora 读,他们可以容忍 额外的一个AZ挂掉
  • Aurora期望能够容忍暂时的慢副本
  • 如果一个服务器看起来永久故障了,我们期望能够尽可能快的根据剩下的副本,生成一个新的副本。(因为一个出来故障,那意味着很多也会出现故障)

这里只针对他的存储服务器和 容错

10.6 Quorum 复制机制(Quorum Replication)

假设有N个副本。为了能够执行写请求,必须要确保写操作被W个副本确认,W小于N。所以你需要将写请求发送到这W个副本。
如果要执行读请求,那么至少需要从R个副本得到所读取的信息。这里的W对应的数字称为Write Quorum,R对应的数字称为Read Quorum。
这是一个典型的Quorum配置

要点:R加上W必须大于N( 至少满足R + W = N + 1 ),这样任意W个服务器至少与任意R个服务器有一个重合。

这说明你读都是能读到正确值的(但不一定一定对)

这时候的读 就可能读到错的,读取你投票也是没用的 ,所以一般方案是 写的时候加版本,读选一个版本号最高的 返回

你如果不能从 quorum数量的节点,那你结果是不能保证的 ,会重试或者返回错误

和CRAQ 相比,有点就是剔除了耗时久的节点 ,不用全部执行一遍 ,能动态临时修改数量 提升性能

10.7 Aurora读写存储服务器

Aurora的写,并不会更新数据 ,而是增加版本 追加日志(这点类似es所用的lsm日志) ,写的时候 Quorum 共识方案,

而且先只写日志 惰性合并,后面读取最新数据才会合并

在普通操作中,是可以避免 Read Quorum的, 每一个读序号,你只需要读 副本存在最新的就行(类似kafka的 ISR机制), 这样就不用去读Quorum数量 ,提升课性能

但是,数据库服务器有时也会使用Quorum Read。 硬件故障,挂掉了一台,基础设施 会开一个新的节点, 新的数据库 需要同步之前的数据, 他是需要Quorum的

我们可能会有这样一种场景,第一个副本有第101个Log条目,第二个副本有第102个Log条目,第三个副本有第104个Log条目,但是没有一个副本持有第103个Log条目。

10.8 数据分片(Protection Group)

为了能支持超过10TB数据的大型数据库. 数据库的数据,分割存储到多组存储服务器上,每组6个副本

但分片之后,log 的存储就不那么直观了,Aurora 的做法是,data page刚在哪,log放在哪 ,实际上常见的 分库分表的 根据具体属性计算出来 , hdfs 的nameNode 的存到内存管理都行

他的副本机制 意味着 ,如果一个服务器挂了,它可以并行的,快速的在数百台服务器上恢复, 挂的服务器太多可能有问题

10.9 只读数据库(Read-only Database)

Aurora不仅有主数据库实例,同时多个数据库的副本

对于写请求,可以只发送给一个数据库,因为无主分布式事务是有点难搞的 (facebook的 Cassandra 是无主的,有兴趣可以看看)_

对于读请求, 不用只给主了 ,只读数据库需要弄清楚它需要哪些data page来处理这个读请求,之后直接从存储服务器读取这些data page,

然后缓存起来 , 但有修改,主节点会吧log 日志给只读数据库,但这意味着 主从是有延时的(polarDB 有这个问题,具体是不是如果有大佬知道 请告知)

其实还有一些问题

  • 我们不想只读数据库看到未commit的数据。在主数据库发给只读数据库的Log流中,主数据库需要指出,哪些事务commit了,
    而只读数据库需要小心的不要应用未commit的事务到自己的缓存中,它们需要等到事务commit了再应用对应的Log
  • 数据库的b-tree 非常复杂, 是需要经常rebalance,来调整树上的节点 ,是原子的, 但他是有中间状态的 ,只读数据库 直接从存储服务器上读数据,
    他是能看到中间状态数据的

论文中讨论了微事务(Mini-Transaction)和VDL/VCL。这部分实际讨论的就是,数据库服务器可以通知存储服务器说,这部分复杂的Log序列只能以原子性向只读数据库展示,也就是要么全展示,要么不展示。这就是微事务(Mini-Transaction)和VDL。所以当一个只读数据库需要向存储服务器查看一个data page时,存储服务器会小心的,要么展示微事务之前的状态,要么展示微事务之后的状态,但是绝不会展示中间状态。

总结:

  • 事务型数据库是如何工作的,并且知道事务型数据库与后端存储之间交互带来的影响。这里涉及了性能,故障修复,以及运行一个数据库的复杂度
  • Quorum思想。通过读写Quorum的重合,可以确保总是能看见最新的数据,但是又具备容错性。这种思想在Raft中也有体现,Raft可以认为是一种强Quorum的实现(读写操作都要过半服务器认可)
  • 数据库和存储系统基本是一起开发出来的,数据库和存储系统以一种有趣的方式集成在了一起。通常我们设计系统时,需要有好的隔离解耦来区分上层服务和底层的基础架构。所以通常来说,存储系统是非常通用的,并不会为某个特定的应用程序定制。因为一个通用的设计可以被大量服务使用。但是在Aurora面临的问题中,性能问题是非常严重的,它不得不通过模糊服务和底层基础架构的边界来获得35倍的性能提升,这是个巨大的成功。
  • 最后一件有意思的事情是,论文中的一些有关云基础架构中什么更重要的隐含信息。例如:
    • 需要担心整个AZ会出现故障;
    • 需要担心短暂的慢副本,这是经常会出现的问题;
    • 网络是主要的瓶颈,毕竟Aurora通过网络发送的是极短的数据,但是相应的,存储服务器需要做更多的工作(应用Log),
      因为有6个副本,所以有6个CPU在复制执行这些redo Log条目,明显,从Amazon看来,网络容量相比CPU要重要的多。
      ps: 时代发展,现在架构中几乎不把网络当成主要瓶颈了, 分布式数据库 都是这个概念

11.1 Frangipani 初探

缓存一致性是指,如果我缓存了一些数据,之后你修改了实际数据但是并没有考虑我缓存中的数据,必须有一些额外的工作的存在,这样我的缓存才能与实际数据保持一致

Frangipani通过RPC 将硬盘做成共享硬盘 ,在性能上 ,大多时候人们只在工作站操作自己的数据 ,而不会去读写远程的数据, 一般来说,在读取远程数据,
都会本地缓存一份数据 ,这样就可以微秒级别读出来 ,而不是毫秒级别读出来

除了最基本的缓存之外,Frangipani还支持Write-Back缓存,就是我修改了一个数据,其他机器不需要看到我的修改 ,我就值修改缓存数据,这样就大大提升数据,
如果你修改本地数据,其他机器没读,你就只需要改本机的数据

Frangipani的设计中,Petal作为共享存储系统存在,它不知道文件系统,文件,目录,它只是一个很直观简单的存储系统,所有的复杂的逻辑都在工作站中的Frangipani模块中

11.2 Frangipani的挑战(Challenges)

两方面,

  • 一个是缓存,
  • 另一个是这种去中心化的架构带来的大量的逻辑存在于客户端之中进而引起的问题。

假如本机创建一个文件, 虽然在缓存, 但其他机器 是需要同步看到的 强一致性或者线性一致

另一个问题是,因为所有的文件和目录都是共享的,非常容易会有两个工作站在同一个时间修改同一个目录

最后一个问题是,工作站修改了大量的内容,由于Write-Back缓存,可能会在本地的缓存中堆积了大量的修改,机器崩溃了数据丢失的问题

11.3 Frangipani的锁服务(Lock Server)

缓存一致性。在这里我们想要的是线性一致性和缓存带来的好处。对于线性一致性来说,当我查看文件系统中任何内容时,我总是能看到最新的数据。对于缓存来说,我们想要缓存带来的性能提升。某种程度上,我们想要同时拥有这两种特性的优点。

人们通常使用缓存一致性协议(Cache Coherence Protocol)来实现缓存一致性。

除了Frangipani服务器(也就是工作站),Petal存储服务器,在Frangipani系统中还有第三类服务器,锁服务器

实际上Frangipani中的锁更加复杂可以支持两种模式:要么允许一个写入者持有锁,要么允许多个读取者持有锁。

Frangipani有很多的规则,这些规则使得Frangipani以一种提供缓存一致性的方式来使用锁,并确保没有工作站会使用缓存中的旧数据。这些规则、锁、缓存数据需要配合使用。
这里的规则包括了:

  • 工作站不允许持有缓存的数据,除非同时也持有了与数据相关的锁
  • 如果你在释放锁之前,修改了锁保护的数据,那你必须将修改了的数据写回到Petal,只有在Petal确认收到了数据,你才可以释放锁
  • 最后再从工作站的lock表单中删除关文件的锁的记录和缓存的数据

11.4 缓存一致性(Cache Coherence)

  • 首先是Request消息,从工作站发给锁服务器。Request消息会说:hey锁服务器,我想获取这个锁
  • 锁服务器的lock表单中已经有人持有这个锁, 那锁服务器不能立即交出锁,一旦锁被释放了,锁服务器会回复一个Grant消息给工作站。这里的Request和Grant是异步的
    • 如果一个工作站在使用锁,并在执行读写操作,那么它会将锁标记为Busy,处理完了 锁状态变为 Idle
    • 如果锁服务器收到了一个加锁的请求,它查看自己的lock表单可以发现,这个锁现在正被工作站WS1所持有,锁服务器会发送一个Revoke消息给当前持有锁的工作站WS1。并说,现在别人要使用这个文件,请释放锁吧。
    • 一个工作站收到了一个Revoke请求,如果锁时在Idle状态,并且缓存的数据脏了,工作站会首先将修改过的缓存写回到Petal存储服务器中,因为前面的规则要求在释放锁之前,要先将数据写入Petal,工作站会向锁服务器发送一条Release消息。释放

这就是Frangipani使用的一致性协议的一个简单版本的描述

这里面没有考虑一个事实,那就是锁可以是为写入提供的排他锁(Exclusive Lock),也可以是为只读提供的共享锁(Shared Lock)。

在这个缓存一致性的协议中,有许多可以优化的地方:

  • 每个工作站用完了锁之后,不是立即向锁服务器释放锁,而是将锁的状态标记为Idle就是一种优化。
  • 另一个主要的优化是,Frangipani有共享的读锁(Shared Read Lock)和排他的写锁(Exclusive Write Lock)。如果有大量的工作站需要读取文件,但是没有人会修改这个文件,它们都可以同时持有对这个文件的读锁。

ps: 关于缓存中的数据,他会仿照Unix系统的做法,定时刷盘

11.5 原子性(Atomicity)

原子性。当我做了一个复杂的操作,其他工作站要么发现文件不存在,要么文件完全存在,但是我们绝不希望它看到中间状态。所以我们希望多个步骤的操作具备原子性。

Frangipani在内部实现了一个数据库风格的事务系统,并且是以锁为核心。并且,这是一个分布式事务系统,在分布式系统中是一种非常常见的需求。

比如我将文件从一个目录移到另一个目录,这涉及到修改两个目录的内容,我不想让人看到两个目录都没有文件的状态。为了实现这样的结果,Frangipani首先会获取执行操作所需要的所有数据的锁,

这里涉及多个锁, 类比数据库的谓词锁

11.6 Frangipani Log

故障恢复

比如他获取了大量的锁,一部分写到Petal了 ,一部分没有 ,这时服务器崩溃了

一些直观处理方法,但是都不太好:

  • 崩溃了 ,直接释放所有的锁, 他吧Petal,文件写到磁盘,但索引还没有(inode) 还没写入,这时候是不能释放的,因为Petal上有错误数据,其他服务器会读到
  • 不释放崩溃了的工作站所持有的锁。但这回导致,有部分锁永远释放不了

Frangipani与其他的系统一样,需要通过预写式日志(Write-Ahead Log,WAL,见10.2)

在工作站向Petal写入任何数据之前,先记录log ,然后在写数据到Petal, 所以只要写入数据,必定有数据

这是一种非常标准的行为,它就是WAL的行为。但是Frangipani在实现WAL时,有一些不同的地方:

  • 第一,在大部分的事务系统中,只有一个Log,系统中的所有事务都存在于这个Log中,但是Frangipani不是这么保存Log的,它对于每个工作站都保存了一份独立的Log。
  • 另一个有关Frangipani的Log系统有意思的事情是,工作站的Log存储在Petal,而不是本地磁盘中

每个工作站的Log存在于Petal已知的块中,并且,每个工作站以一种环形的方式使用它在Petal上的Log空间。(Redo Log)

每个Log条目都包含了Log序列号,这个序列号是个自增的数字,每个工作站按照12345为自己的Log编号

这里的完整的过程是。当工作站从锁服务器收到了一个Revoke消息,要自己释放某个锁,它需要执行好几个步骤。

  • 首先,工作站需要将内存中还没有写入到Petal的Log条目写入到Petal中。
  • 之后,再将被Revoke的Lock所保护的数据写入到Petal。
  • 最后,向锁服务器发送Release消息。

11.7 故障恢复(Crash Recovery)

故障了会发生什么 有这几种场景:

  • 要么工作站正在向Petal写入Log,所以这个时候工作站必然还没有向Petal写入任何文件或者目录。
  • 要么工作站正在向Petal写入修改的文件,所以这个时候工作站必然已经写入了完整的Log。~~

当持有锁的工作站崩溃, 当有一个其他工作站申请锁,如果没有其他工作站申请这些锁,可能永远不会发现崩溃了,所以锁使用了租约 ,当租约到期了,锁服务器会认定工作站已经崩溃了

之后他会初始化拎一个工作站,叫他读取哪些log,完成操作,这就是Log 为什么要存到Petal

发生故障的场景究竟有哪些呢

  • 第一种场景是,工作站WS1在向Petal写入任何信息之前就故障了,意味着Log 也没数据,什么也恢复不了
  • 第二种场景是,工作站WS1向Petal写了部分Log条目。 但这里的日志是不完整的,所以不会去恢复
  • 另一个有趣的可能是,工作站WS1在写入Log之后,并且在写入块数据的过程中崩溃了。他会从相同的位置重写数据,等于恢复了正确的操作

更复杂的场景:

  • 如果一个工作站,完成了上面流程的步骤1,2,在释放锁的过程中崩溃了,进而导致崩溃的工作站不是最后修改特定数据的工作站
  • ws1 删除了一个文件名为a的文件 del(a)
  • 另一个工作站WS2,在删除文件之后,以相同的名字创建了文件,当然这是一个不同的文件。所以之后,工作站WS2创建了同名的文件 create(a)
  • 在创建完成之后,工作站WS1崩溃了,
  • 这时候WS3 通过ws1 的日志执行del 操作,他会吧WS2 创建的的文件del ,结果完全不对

Frangipani是这样解决这个问题的,通过对每一份存储在Petal文件系统数据增加一个版本号,同时将版本号与Log中描述的更新关联起来

WS3会选择性的根据版本号执行Log,只有Log中的版本号高于Petal中存储的数据的版本时,Log才会被执行。

这里有个比较烦人的问题就是,WS3在执行恢复,但是其他的工作站还在频繁的读取文件系统,持有了一些锁并且在向Petal写数据

一种不可行的方法是,让执行恢复的WS3先获取所有关联数据的锁,再重新执行Log, 因为可能大范围电力故障 ,很多持有锁信息都丢了

但是幸运的是,执行恢复的工作站可以直接从Petal读取数据而不用关心锁,原因:

  • 执行恢复的工作站想要重新执行Log条目,并且有可能修改与目录d关联的数据,它就是需要读取Petal中目前存放的目录数据。
    +两种可能,要么故障了的工作站WS1释放了锁,要么没有,
  • 没有的话,那么没有其他人不可以拥有目录的锁,执行恢复的工作站可以放心的读取目录数据
  • 释放了锁,那么在它释放锁之前,它必然将有关目录的数据写回到了Petal。这意味着,Petal中存储的版本号,至少会和故障工作站的Log条目中的版本号一样大,事实上他就不会执行

11.8 Frangipani总结

  • Petal是什么
  • 缓存一致性
  • 分布式事务
  • 分布式故障恢复

Frangipani的目标 是小数据而且需要事物的情况,

还有另一种情况 大数据运算存储大的文件,例如MapReduce。实际上GFS某种程度上看起来就像是一个文件系统,但是实际上是为了MapReduce设计的存储系统

Frangipani 更多用缓存提升性能,但这GFS也好,还是大数据运算,反而不是很有用

Lecture 12 - Distributed Transaction(分布式事务)

12.1 分布式事务初探(Distributed Transaction)

分布式事务主要有两部分组成。第一个是并发控制(Concurrency Control)第二个是原子提交(Atomic Commit)。

现在对很多系统来说,数据都是分片存储的,比如银行,你需要转账, 一个账户在一个分片,一个账户在另一个分片, 所以需要分布式事务

在过去的几十年间,这都是设计数据库需要考虑的问题,所以很多现在的材料的介绍都是基于数据库。但现在很多分布式系统也会分片存储

数据库通常对于正确性有一个概念称为ACID。分别代表:

  • Atomic,原子性。它意味着,事务可能有多个步骤,比如说写多个数据记录,尽管可能存在故障,但是要么所有的写数据都完成了,要么没有写数据能完成,没有中间状态
  • Consistent,一致性。(这个可以看下上面的 一致性或者 ddia的 事务一致性)
  • Isolated,隔离性。这一点还比较重要。 就是多个事物同时进行,彼此不会看到他们之间的操作
  • Durable,持久化的。这意味着,在事务提交之后,在客户端或者程序提交事务之后,拿到ack 之后,数据呗错误干掉(这不是绝对的)

12.2 并发控制(Concurrency Control)

悲观并发控制(Pessimistic Concurrency Control)

实际上就是锁, 为了正确性牺牲了性能, 2pl/S2PL等等

乐观并发控制(Optimistic Concurrency Control)

一般操作是,执行时并不关心是否有其他事物才操作相同数据,写到临时区域,提交事物时,检查事物,没有,你就执行完成了

如果有其他事物修改了,那么你必须要Abort当前事务,并重试

锁机制

这里的锁是两阶段锁(Two-Phase Locking),这是一种最常见的锁。

  • 第一个规则是在使用任何数据之前,在执行任何数据的读写之前,先获取锁。
  • 第二个对于事务的规则是,事务必须持有任何已经获得的锁,直到事务提交或者Abort,你不允许在事务的中间过程释放锁。

他基本是为了迫使事务串行执行

为什么需要在事务结束前一直持有锁?

因为并发高,可能会有多个锁,这样就不能满足事物的一致性,会数据非法

但这个会导致死锁,如我们有两个事务,T1读取记录X,之后再读取记录Y,T2读取记录Y,之后再读取记录X (mysql 两阶段锁也会有这个问题)

12.3 两阶段提交(Two-Phase Commit)

在一个分布式环境中,数据被分割在多台机器上,如何构建数据库或存储系统以支持事务。所以这个话题是,如何构建分布式事务(Distributed Transaction)

分布式事务之外,我们也要确保出现错误时,数据库仍然具有可序列化和某种程度的All-or-Nothing原子性。

原子性,事务的每一个部分都执行,或者任何一个部分都不执行

如何应对各种各样的故障,机器故障,消息缺失。同时,还要考虑性能。原子提交协议,其中一种是两阶段提交

我们有一个计算机作为事务协调者(TC)

事务协调者会向服务器S1发消息说,请对X加1

每个持有数据的服务器会维护一个锁的表单,用来记录锁被哪个事务所持有。所以对于事务,需要有事务ID(Transaction ID),简称为TID。

我们有事务协调者,我们还有其他的服务器执行部分的事务,这些服务器被称为参与者(Participants)。

当事务协调者到达了事务的结束并想要提交事务,这样才能:

  • 释放所有的锁,
  • 并使得事务的结果对于外部是可见的,
  • 再向客户端回复。

执行顺序:

  • 开始执行事务时,TC需要确保,所有的事务参与者能够完成它们在事务中的那部分工作 ,TC为了确保这一点,会向所有的参与者发送Prepare消息。
  • 收到了Prepare消息,它们就知道事务要执行但是还没执行的内容,它们会查看自身的状态并决定它们实际上能不能完成事务 ,我有能力或者我没能力完成这个事务,它们会向TC回复Yes或者No。
  • 事务协调者会等待来自于每一个参与者的这些Yes/No投票。如果所有的参与者都回复Yes,那么事务可以提交,不会发生错误。之后事务协调者会发出一个Commit消息,给每一个事务的参与者
  • 事务参与者通常会回复ACK说,我们知道了要commit。
  • 当有一次参与者不能commit ,协调者会叫所有的参与者 回滚/丢弃(Abort)

在事务Commit之后,会发生两件事情

  • 事务协调者会向客户端发送代表了事务输出的内容,表明事务结束了,事务没有被Abort并且被持久化保存起来了
  • 为了遵守前面的锁规则(两阶段锁),事务参与者会释放锁(这里不论Commit还是Abort都会释放锁)。

为了遵循两阶段锁规则,每个事务参与者在参与事务时,会对任何涉及到的数据加锁。所以我们可以想象,在每个参与者中都会有个表单,表单会记录数据当前是为哪个事务加的锁。当收到Commit或者Abort消息时,事务参与者会对数据解锁,之后其他的事务才可以使用相应的数据

两个参与者只会一起Commit,如果其中一个需要Abort,那么它们两个都会Abort。这样就有了All-or-Noting的原子特性。

12.4 故障恢复(Crash Recovery)

2PC 对错误处理的能力,比如机器断电

  • 第一个场景是,参与者B可能在回复事务协调者的Prepare消息之前的崩溃了,

协调者(TC)也就不能Commit,因为它需要等待所有的参与者回复Yes。

如果B发现自己不可能发送Yes,比如发送YES前断电了,那么他知道TC 不能commit, 他本地也可以Abort

加入B 重启了,内存丢失,故障之后B不知道任何有关事务的信息,也不知道给谁回复过Yes。之后,如果事务协调者发送了一个Prepare消息过来,因为B不知道事务,B会回复No,并要求Abort事务。

  • B也可能在回复了Yes给事务协调者的Prepare消息之后崩溃的。B可能开心的回复给事务协调者说好的,我将会commit。但是在B收到来自事务协调者的commit消息之前崩溃了。

这种情况,B就必须记住中间状态,在Prepare之前 (seata 的undo_log 就是这个作用)

如果B在发送完Yes之后崩溃了,当它重启恢复时,通过查看自己的Log,它可以发现自己正在一个事务的中间,并且对一个事务的Prepare消息回复了Yes。Log里有Commit需要做的所有的修改,和事务持有的所有的锁。之后,当B最终收到了Commit而不是Abort,通过读取Log,B就知道如何完成它在事务中的那部分工作~~

  • 可能崩溃的地方是,B可能在收到Commit之后崩溃了。

b重启完, TC还是没收到ACK, 事务协调者会再次发送Commit消息。当B重启之后,收到了Commit消息时,它可能已经将Log中的修改写入到自己的持久化存储中、释放了锁、并删除了有关事务的Log

b 如果收到两次commit 信息。对于一个它不知道事务的Commit消息(指第二次),B会简单的ACK这条消息。

对于协调者,也可能出问题

如果事务的任何一个参与者可能已经提交了,或者事务协调者可能已经回复给客户端了,那么我们不能忽略事务。

比如TC 已经叫A 提交, 还没叫B commit的时候崩溃了, 所以协调者的崩溃时间点 一定要清晰

  • 事务协调者在发送Commit消息之前就崩溃了,那就无所谓了,因为没有一个参与者会Commit事务。

如果事务协调者在崩溃前没有发送Commit消息,它可以直接Abort事务。 事务的参与者会向事务协调者查询事务,事务协调者会发现自己不认识这个事务,它必然是之前崩溃的时候Abort的事务

  • 如果事务协调者在发送完一个或者多个Commit消息之后崩溃,

那么就不允许它忘记相关的事务。这意味着,在崩溃的时间点,也就是事务协调者决定要Commit而不是Abort事务,并且在发送任何Commit消息之前,它必须先将事务的信息写入到自己的Log,并存放在例如磁盘的持久化存储中,这样计算故障重启了,信息还会存在。

事务协调者在收到所有对于Prepare消息的Yes/No投票后,会将结果和事务ID写入存在磁盘中的Log,之后才会开始发送Commit消息。之后,可能在发送完第一个Commit消息就崩溃了,也可能发送了所有的Commit消息才崩溃,不管在哪,当事务协调者故障重启时,恢复软件查看Log可以发现哪些事务执行了一半,哪些事务已经Commit了,哪些事务已经Abort了

  • 这主要的服务器崩溃场景。我们还需要担心如果消息在网络传输的时候丢失了怎么办?或许你发送了一个消息,但是消息永远也没有送达

事务协调者发送了Prepare消息,但是并没有收到所有的Yes/No消息,事务协调者这时该怎么做呢?

其中一个选择是,事务协调者重新发送一轮Prepare消息,表明自己没有收到全部的Yes/No回复

但这样,一台机器可能停电很久, 事务会一直卡在这儿 ,可能永远等待

在事务协调者没有收到Yes/No回复一段时间之后,它可以单方面的Abort事务。

如果一个崩溃了的参与者重启了,来问TC ,TC 这是也不知道,他就会让参与者也Abort

如果参与者等待Prepare消息超时了,那意味着它必然还没有回复Yes消息,进而意味着事务协调者必然还没有发送Commit消息。所以如果一个参与者在这个位置因为等待Prepare消息而超时,

那么它也可以决定Abort事务

如果网络某个地方出现了问题,或者事务协调器挂了一会,事务参与者仍然在等待Prepare消息,总是可以允许事务参与者Abort事务,并释放锁,这样其他事务才可以继续

  • 假设B收到了Prepare消息,并回复了Yes

这个时候参与者没有收到Commit消息,它接下来怎么也等不到Commit消息。 这时候 ,他是不能自主Abort 的

它必须无限的等待Commit消息,这里通常称为Block。

这里的原因是,因为B对Prepare消息回复了Yes,这意味着事务协调者可能收到了来自于所有参与者的Yes,并且可能已经向部分参与者发送Commit消息。这意味着A可能已经看到了Commit消息,Commit事务,持久化存储事务的结果并释放锁。

Block行为是两阶段提交里非常重要的一个特性,并且它不是一个好的属性。因为它意味着 你会长时间等待

我们知道事务协调者必然在它的Log中记住了事务的信息,那么它在什么时候可以删除Log中有关事务的信息?

这里的答案是,如果事务协调者成功的得到了所有参与者的ACK,

当然事务协调者或许不能收到ACK,这时它会假设丢包了并重发Commit消息。这时,如果一个参与者收到了一个Commit消息,但是它并不知道对应的事务,因为它在之前回复ACK之后就忘记了这个事务,那么参与者会再次回复一个ACK。因为如果参与者收到了一个自己不知道的事务的Commit消息,那么必然是因为它之前已经完成对这个事务的Commit或者Abort,然后选择忘记这个事务了。

12.5 总结

两阶段提交,它实现了原子提交。

两阶段提交有着极差的名声。其中一个原因是,因为有多轮消息的存在,它非常的慢。

另一个原因是,这里有大量的写磁盘操作,记录各种日志

两阶段提交的架构中,本质上是有一个Leader(事务协调者),将消息发送给Follower(事务参与者),Leader只能在收到了足够多Follower的回复之后才能继续执行。这与Raft非常像,但是,这里协议的属性与Raft又非常的不一样。这两个协议解决的是完全不同的问题。

Raft的意义在于,即使部分参与的服务器故障了或者不可达,系统仍然能工作。Raft能做到这一点是因为所有的服务器都在做相同的事情,所以我们不需要所有的服务器都参与,我们只需要过半服务器参与

而两阶段提交,参与者完全没有在做相同的事情,每个参与者都在做事务中的不同部分,比如A可能在对X加1,B可能在对Y减1。所以在两阶段提交中,所有的参与者都在做不同的事情。所有的参与者都必须完成自己那部分工作,这样事务才能结束,所以这里需要等待所有的参与者。

Raft通过复制可以不用每一个参与者都在线,而两阶段提交每个参与者都做了不同的工作,并且每个参与者的工作都必须完成,所以两阶段提交对于可用性没有任何帮助

Raft完全就是可用性,而两阶段提交完全不是高可用的,系统中的任何一个部分出错了,系统都有可能等待直到这个部分修复。

是有可能结合这两种协议的。两阶段提交对于故障来说是非常脆弱的,在故障时它可以有正确的结果,但是不具备可用性。所以,这里的问题是,是否可以构建一个合并的系统,同时具备Raft的高可用性,但同时又有两阶段提交的能力将事务分包给不同的参与者。这里的结构实际上是,通过Raft或者Paxos或者其他协议,来复制两阶段提交协议里的每一个组成部分。

这里很复杂,但是它展示了你可以结合两种思想来同时获得高可用和原子提交。

实际上就是个分片的数据库,每个分片以这种形式进行复制,同时还有一个配置管理器,来允许将分片的数据从一个Raft集群移到另一个Raft集群。

Google使用的一种数据库,Spanner也使用了这里的结构来实现事务写。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值