ddia/ddia/ch5.md
2018-02-08 18:29:00 +08:00

95 KiB
Raw Blame History

5. 复制

与可能出错的东西比,'不可能'出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了。

——道格拉斯·亚当斯1992


[TOC]

复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在第二部分的介绍中所讨论的那样,有几种想要复制数据的原因:

  • 保持数据在地理位置上接近用户(从而减少延迟)
  • 即使部分零件出现故障,系统也能继续工作(从而提高可用性)
  • 扩充可以接受读请求的机器数量(从而提高读取吞吐量)

本章将假设您的数据集非常小,每台机器都可以保存整个数据集的副本。在第6章中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。

如果正在复制的数据不会随时间而改变,那么复制太简单了:将数据复制到每个节点一次就完事了。复制的困难之处在于应对复制数据的变化,这就是本章的内容。我们将讨论三种流行的变化复制算法:单主single leader多主multi leader无主leaderless。几乎所有分布式数据库都使用这三种方法之一。

在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?。这些通常是数据库中的配置项,细节因数据库而异,但原理在许多不同的实现中都是相似的。本章将讨论这些决策的后果。

数据库的复制是一个老话题 ——自1970年代研究以来这些原则并没有太大的改变[1],因为网络的基本约束保持不变。然而在研究之外,许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近的事。由于许多程序员都是这方面的新手,对**最终一致性eventual consistency等问题存在诸多误解。在“复制滞后问题”一节,我们将更加精确地了解最终的一致性,并讨论诸如读己之写read-your-writes单调读monotonic read**保证等内容。

领导者与追随者

存储数据库副本的每个节点称为副本replica。当有多个副本时就会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?

每一次向数据库的写入都需要传播到所有副本上,否则副本包含的数据就会不一样。最常见的解决方案称为:基于领导者的复制leader-based replication(也称主动/被动 active/passive主/从 master/slave复制),如图5-1所示。它的工作原理如下:

  1. 副本之一被指定为领导者leader,也称为 master 首要primary。当客户端要写入数据库时,它们必须将请求发送给领导者,首先将新数据写入领导者的本地存储。
  2. 其他副本被称为追随者followers,亦称为只读副本read replicasslaves次要( sencondaries热备hot-standby1。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为复制日志replication log记录或变更流change stream。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
  3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。

图5-1 基于领导者(主-从)的复制

这种复制模式是许多关系数据库的内置功能如PostgreSQL从9.0版本开始MySQLOracle Data Guard [2]和SQL Server的AlwaysOn可用性组[3]。 它也用于一些非关系数据库包括MongoDBRethinkDB和Espresso [4]。 最后基于领导者的复制并不仅限于数据库像Kafka [5]和RabbitMQ高可用队列[6]这样的分布式消息代理也使用它。 某些网络文件系统例如DRBD这样的块复制设备也与之类似。

同步复制与异步复制

复制系统的一个重要细节是复制是**同步synchronously发生还是异步asynchronously**发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)。

想想图5-1中发生的情况,网站的用户更新他们的个人资料图像。在某个时间点,客户发送更新请求给领导者;不久之后领导者就收到了。在某个时候,领导者将数据转换转发给追随者。最后,领导通知客户更新成功。

图5-2显示了系统各个组件之间的通信:用户的客户端,领导者和两个关注者。时间从左到右流动。请求或响应消息显示为粗箭头。

图5-2 基于领导者的复制:一个同步从库和一个异步从库

图5-2的示例中跟随者1的复制是同步的领导者等待直到跟随者1确认在向用户报告成功之前以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的领导者发送消息但不等待跟随者的响应。

该图表明在跟随者2处理消息之前存在显着的延迟。通常情况下复制速度非常快大多数数据库系统在不到一秒的时间内对关注者进行更改。但是不能保证需要多长时间。有些情况下追随者可能落后于领导者几分钟或更长时间例如如果跟随者正在从故障中恢复如果系统正在接近最大容量或者如果节点之间存在网络问题。

同步复制的优点是,跟随者保证有与领导者一致的最新数据副本。如果领导者突然失败,我们可以肯定的是,这些数据仍然可以在追随者身上找到。缺点是如果同步跟随器没有响应(因为它已经崩溃,或者出现网络故障,或者出于任何其他原因),写入不能被处理。领导者必须阻止所有写入,并等待同步副本再次可用。

因此,所有追随者都是同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中一个跟随者是同步的,而其他的则是异步的。如果同步跟随器变得不可用或缓慢,则使一个异步跟随器同步。这保证您至少在两个节点上拥有最新的数据副本:领导者和同步追随者。 这种配置有时也被称为半同步semi-synchronous[7]。

通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果领导失败并且不可恢复,则任何尚未复制给追随者的写入都将丢失。 这意味着即使已经向客户端确认成功,写入也不能保证持久Durable。 然而,一个完全异步的配置也有优点:即使所有的追随者都落后了,领导者也可以继续处理写入。

弱化的持久性可能听起来像是一个坏的折衷,无论如何,异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在“复制延迟”中回到这个问题。

关于复制的研究

如果主库故障,那么异步复制系统会丢失数据可能是一个严重的问题,因此研究人员继续研究不丢失数据但仍能提供良好性能和可用性的复制方法。 例如,链式复制[8,9]是同步复制的一种变体已经在一些系统如Microsoft Azure存储[10,11])中成功实现。

复制的一致性与共识consensus使几个节点就某个值达成一致之间有着密切的联系第9章将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。

设置新追随者

有时候需要设置一个新的追随者:也许是为了增加副本的数量,或替换失败的节点。如何确保新的追随者有领导者数据的准确副本?

简单地将数据文件从一个节点复制到另一个节点通常是不够的:客户端不断向数据库写入数据,数据总是在不断变化,标准的数据副本会在不同的时间点总是不一样。结果可能没有任何意义。

可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违反高可用的目标。幸运的是,拉起一个从库通常不需要停机。从概念上讲,过程如下所示:

  1. 在某个时间点对领导者的数据库取一个一致的快照如果可能的话而不必锁定整个数据库。大多数数据库都具有这个功能因为它是备份必需的。在某些情况下需要第三方工具如MySQL的innobackupex [12]。
  2. 将快照复制到新的跟随者节点。
  3. 追随者连接到领导者并请求快照后发生的所有数据更改。这要求快照与领导者的复制日志中的精确位置关联。该位置有不同的名称例如PostgreSQL将其称为日志序列号lsn, log sequence numberMySQL将其称为二进制日志坐标binlog coordinates
  4. 当追随者处理完快照之后积压的数据变化,我们说它**赶上caught up**了主库。现在它可以继续处理领导者产生的数据变化了。

建立跟随者的实际步骤因数据库而异。在某些系统中,这个过程是完全自动化的,而在另外一些系统中,它可能是一个有点神秘的多步骤工作流程,需要由管理员手动执行。

处理节点停机

系统中的任何节点都可能停机,也许因为意外的故障,也可能是计划内的维护(例如,重启机器以安装内核安全补丁)。对运维而言,能在系统不中断服务的情况下重启单个节点好处多多。我们的目标是,即使个别节点失效,也能保持整个系统运行,并尽可能控制节点停机带来的影响。

如何通过基于领导者的复制实现高可用?

从库失效:追赶恢复

在其本地磁盘上,每个追随者记录从领导者收到的数据变更。如果跟随者崩溃并重新启动,或者如果领导者和追随者之间的网络暂时中断,则追随者可以很容易地恢复:从日志中知道在发生故障之前处理的最后一个事务。因此,追随者可以连接到领导者并请求在追随者断开连接时发生的所有数据改变。当它应用了这些变化后,它已经赶上了领导者,并可以像以前一样继续接收数据流的变化。

主库失效:故障转移

处理领导者的失败相当棘手:其中一个追随者需要被提升为新的领导者,需要重新配置客户端,以将它们的写操作发送给新的领导者,其他追随者需要开始拉取来自新领导者的数据变更。这个过程被称为故障转移failover

故障转移可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的领导者)或自动进行。自动故障转移过程通常由以下步骤组成:

  1. 确认领导者失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是使用超时Timeout节点相互之间频繁地来回传递消息并且如果一个节点在一段时间内例如30秒没有响应就认为它挂了计划内故意关闭领导者不算
  2. 选择一个新的领导者。这可以通过选举过程(领导者由剩余副本以多数选举产生)来完成,或者可以由之前选定的控制器节点controller node来指定新的领导者。领导者的最佳人选通常是拥有旧领导者最新数据副本的追随者(最小化数据损失)。让所有的节点同意一个新的领导是一个共识问题在第9章详细讨论。
  3. 重新配置系统以使用新的领导者。客户端现在需要将他们的写请求发送给新领导者(将在“请求路由”中讨论这个问题)。如果老领导回来,可能仍然认为自己是领导者,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个追随者。

故障转移会出现很多大麻烦:

  • 如果使用异步复制,则新主库可能没有收到老主库宕机前最后的写入操作。在选出新主库后,如果老主库重新加入集群,新主库在此期间可能会收到冲突的写入,那这些写入该如何处理?最常见的解决方案是简单丢弃老主库未复制的写入,这很可能打破客户对于数据持久性的期望。

  • 如果数据库需要和其他外部存储相协调那么丢弃写入内容是极其危险的操作。例如在GitHub [13]的一场事故中一个过时的MySQL从库被提升为主库。数据库使用自增ID作为主键因为新主库的计数器落后于老主库的计数器所以新主库重新分配了一些已经被老主库分配掉的ID作为主键。这些主键也在Redis中使用主键重用使得MySQL和Redis中数据产生不一致最后导致一些私有数据泄漏到错误的用户手中。

  • 发生某些故障时见第8章可能会出现两个节点都以为自己是领导者的情况。这种情况称为脑裂(split brain)非常危险如果两个领导者都可以接受写操作却没有冲突解决机制参见第168页的“多主复制”那么数据就可能丢失或损坏。一些系统采取了安全防范措施当检测到两个领导节点同时存在时会关闭其中一个节点2,但设计粗糙的机制可能最后会导致两个节点都被关闭[14]。

  • 领导者被宣布死亡之前的正确超时应该怎么设?在领导者失效的情况下,超时时间越长,意味着恢复时间也越长。但是如果超时太短,又可能会出现不必要的故障转移。例如,临时负载峰值可能导致节点的响应时间超时,或网络故障可能导致数据包延迟。如果系统已经处于高负载或网络问题的困扰之中,那么不必要的故障切换可能会让情况变得更糟糕。

这些问题没有简单的解决方案。因此即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障转移。

节点故障;不可靠的网络;对副本一致性,持久性,可用性和延迟的权衡 这些问题实际上是分布式系统中的基本问题。第8章和第9章将更深入地讨论它们。

复制日志的实现

基于领导者的复制底层是如何工作的?实践中有好几种不同的复制方式,所以先简要地看一下。

基于语句的复制

在最简单的情况下,领导者记录它执行的每个写入请求(语句statement)并将该语句日志发送给其追随者。对于关系数据库来说,这意味着每个INSERTUPDATEDELETE语句都被转发给关注者和每个关注者。跟随者解析并执行该SQL语句就像从客户端收到一样。

虽然听上去很合理,但有很多问题会打破这种复制方式:

  • 任何调用**非确定性函数nondeterministic**的语句(如NOW()获取当前日期和时间或RAND()获取一个随机数)可能会在每个副本上生成不同的值。
  • 如果语句使用自增列auto increment,或者它们依赖于数据库中的现有数据(例如,UPDATE ... WHERE <某些条件>),则必须在每个副本上按照完全相同的顺序执行它们,否则可能会有不同的效果。当有多个同时执行的事务时,这可能产生很大的限制。
  • 有副作用的语句(例如,触发器,存储过程,用户定义的函数)可能会在每个副本上产生不同的副作用,除非副作用是绝对确定的。

有办法绕开问题 ——例如,当语句被记录时,领导者可以用固定的返回值替换任何不确定的函数调用,以便追随者获得相同的值。但是由于边缘情况如此之多,所以现在通常选择其他的复制方法。

基于语句的复制在5.1版本前的MySQL中使用。现在也还在一些地方用因为它非常紧凑。但默认情况下如果语句中有任何不确定性MySQL现在会切换到基于行的复制稍后讨论。 VoltDB使用基于语句的复制通过要求事务是确定性来保证安全[15]。

传输预写式日志WAL

在第3章中我们讨论了存储引擎如何在磁盘上表示数据并且我们发现通常每个写操作都附加到日志中

  • 对于日志结构存储引擎请参阅“SSTables和LSM-tree”日志是主要的存储位置。日志段在后台压缩并进行垃圾回收。
  • 对于覆写单个磁盘块的B树请参阅第79页的“B树”每次修改都会先写入预写式日志WAL, Write Ahead Log,以便崩溃后索引可以恢复到一个一致的状态。

在任何一种情况下,日志都是包含所有数据库写入的只追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,领导者还可以通过网络将其发送给其追随者。

当追随者应用这个日志时,它会建立和领导者一模一样数据结构的副本。

PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录的数据非常底层WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本通常不可能在领导者和追随者上运行不同版本的数据库软件。

看上去这可能只是一个微小的实现细节但却可能对运维产生巨大的影响。如果复制协议允许跟随者使用比领导者更新的软件版本则可以先执行升级跟随者然后执行故障转移使升级后的节点之一成为新的领导者从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配传输WAL经常出现这种情况则此类升级需要停机。

逻辑日志复制(基于行)

另一种方法是,复制和存储引擎使用不同的日志格式,这样可以使复制日志从存储引擎内部分离出来。这种复制日志被称为逻辑日志,以将其与存储引擎的(物理)数据表示区分开来。

关系数据库的逻辑日志通常是以行的粒度描述对数据库表的写入的记录序列:

  • 对于插入的行,日志包含所有列的新值。
  • 对于删除的行,日志包含足够的信息来唯一标识已删除的行。通常是主键,但是如果表上没有主键,则需要记录所有列的旧值。
  • 对于更新的行,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少所有已更改的列的新值)。

修改多行的事务会生成多个这样的日志记录,后面跟着一条记录,指出事务已经提交。 MySQL的二进制日志当配置为使用基于行的复制时使用这种方法[17]。

由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。

对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统(如数据),这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存[18]。 这种技术被称为捕获数据变更change data capture第11章将重新讲到它。

基于触发器的复制

到目前为止描述的复制方法是由数据库系统实现的不涉及任何应用程序代码。在很多情况下这就是你想要的。但在某些情况下需要更多的灵活性。例如如果您只想复制数据的一个子集或者想从一种数据库复制到另一种数据库或者如果您需要冲突解决逻辑请参阅第171页的“处理写入冲突”则可能需要将复制移动到应用程序层。

一些工具如Oracle GoldenGate [19],可以通过读取数据库日志,使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能:触发器和存储过程。

触发器允许您注册在数据库系统中发生数据更改写入事务时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中使用外部程序读取这个表再加上任何业务逻辑处理会后将数据变更复制到另一个系统去。例如Databus for Oracle [20]和Bucardo for Postgres [21]就是这样工作的。

基于触发器的复制通常比其他复制方法具有更高的开销,并且比数据库的内置复制更容易出错,也有很多限制。然而由于其灵活性,仍然是很有用的。

复制延迟问题

容忍节点故障只是需要复制的一个原因。正如在第二部分的介绍中提到的,另一个原因是可扩展性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更靠近用户)。

基于领导者的复制要求所有写入都由一个节点处理但只读查询可以由任何副本处理。所以对于读多写少Web上的常见模式的场景一个有吸引力的选择是创建很多追随者并将读取请求分散到所有的从库上去。这样能减小主库的负载并允许向最近的副本发送读请求。

在这种扩展体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制 - 如果您尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置将是非常不可靠的。

不幸的是,当应用程序从异步追随者读取时,如果追随者落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对领导者和跟随者执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在跟随者中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,追随者最终会赶上并与领导者保持一致。出于这个原因,这种效应被称为最终一致性eventually consistency3[22,23]

“最终”一词故意含糊不清:总的来说,副本落后的程度是没有限制的。在正常的操作中,复制延迟replication lag,即写入主库到反映至追随者之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。

因为滞后时间太长引入的不一致性,可不仅是一个理论问题,更是应用设计中会遇到的真实问题。本节将重点介绍三个由复制延迟问题的例子,并简述解决这些问题的一些方法。

读已之写

许多应用程序让用户提交一些数据,然后查看他们提交的内容。可能是用户数据库中的记录,也可能是对讨论主题的评论,或其他类似的内容。提交新数据时,必须将其发送给领导者,但是当用户查看数据时,可以从追随者读取。如果数据经常被查看,但只是偶尔写入,这是非常合适的。

但对于异步复制,问题就来了。如图5-3所示:如果用户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户而言,看起来好像是刚提交的数据丢失了,用户会不高兴,可以理解。

图5-3 用户写入后从旧副本中读取数据。需要写后读(read-after-write)的一致性来防止这种异常

在这种情况下,我们需要写后读一致性read-after-write consistency,也称为读己之写一致性read-your-writes consistency[24]。这是一个保证,如果用户重新加载页面,他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺:其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。

如何在基于领导者的复制系统中实现读后一致性?有各种可能的技术,这里说一些:

  • 读用户可能已经修改过的内容时,都从主库读;这就要求有一些方法,不用实际查询就可以知道用户是否修改了某些东西。举个例子,社交网络上的用户个人资料信息通常只能由用户本人编辑,而不能由其他人编辑。因此一个简单的规则是:从主库读取用户自己的档案,在从库读取其他用户的档案。

  • 如果应用中的大部分内容都可能被用户编辑,那这种方法就没用了,因为大部分内容都必须从主库读取(扩容读就没效果了)。在这种情况下可以使用其他标准来决定是否从主库读取。例如可以跟踪上次更新的时间,在上次更新后的一分钟内,从主库读。还可以监控从库的复制延迟,防止任向任何滞后超过一分钟到底从库发出查询。

  • 客户端可以记住最近一次写入的时间戳,系统需要确保从库为该用户提供任何查询时,该时间戳前的变更都已经传播到了本从库中。如果当前从库不够新,则可以从另一个从库读,或者等待从库追赶上来。

    时间戳可以是逻辑时间戳(指示写入顺序的东西,例如日志序列号)或实际系统时钟(在这种情况下,时钟同步变得至关重要;参阅“不可靠的时钟”)。

  • 如果您的副本分布在多个数据中心(出于可用性目的与用户尽量在地理上接近),则会增加复杂性。任何需要由领导者提供服务的请求都必须路由到包含主库的数据中心。

另一种复杂的情况是如果同一个用户从多个设备请求服务例如桌面浏览器和移动APP。这种情况下可能就需要提供跨设备的写后读一致性如果用户在某个设备上输入了一些信息然后在另一个设备上查看则应该看到他们刚输入的信息。

在这种情况下,还有一些需要考虑的问题:

  • 记住用户上次更新时间戳的方法变得更加困难,因为一台设备上运行的程序不知道另一台设备上发生了什么。元数据需要一个中心存储。
  • 如果副本分布在不同的数据中心,很难保证来自不同设备的连接会路由到同一数据中心。 (例如,用户的台式计算机使用家庭宽带连接,而移动设备使用蜂窝数据网络,则设备的网络路线可能完全不同)。如果你的方法需要读主库,可能首先需要把来自同一用户的请求路由到同一个数据中心。

单调读

从异步从库读取第二个异常例子是,用户可能会遇到时光倒流moving backward in time

如果用户从不同从库进行多次读取,就可能发生这种情况。例如,图5-4显示了用户2345两次进行相同的查询首先查询了一个延迟很小的从库然后是一个延迟较大的从库。 如果用户刷新网页而每个请求被路由到一个随机的服务器这种情况是很有可能的。第一个查询返回最近由用户1234添加的评论但是第二个查询不返回任何东西因为滞后的从库还没有拉取写入内容。在效果上相比第一个查询第二个查询是在更早的时间点来观察系统。如果第一个查询没有返回任何内容那问题并不大因为用户2345可能不知道用户1234最近添加了评论。但如果用户2345先看见用户1234的评论然后又看到它消失那么对于用户2345就很让人头大了。

图5-4 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。

单调读Monotonic reads[23]是这种异常不会发生的保证。这是一个比**强一致性strong consistency更弱,但比最终一致性eventually consistency**更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。

实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取不同的用户可以从不同的副本读取。例如可以基于用户ID的散列来选择副本而不是随机选择副本。但是如果该副本失败用户的查询将需要重新路由到另一个副本。

一致前缀读

第三个复制延迟例子违反了因果律。 想象一下Poons先生和Cake夫人之间的以下简短对话

Mr. Poons Mrs. Cake你能看到未来有多远 Mrs. Cake 通常约十秒钟Mr. Poons.

这两句话之间有因果关系Cake夫人听到了Poons先生的问题并回答了这个问题。

现在,想象第三个人正在通过从库来听这个对话。 Cake夫人说的内容是从一个延迟很低的从库读取的但Poons先生所说的内容从库的延迟要大的多见图5-5。 于是,这个观察者会听到以下内容:

Mrs. Cake 通常约十秒钟Mr. Poons.

Mr. Poons Mrs. Cake你能看到未来有多远

对于观察者来说看起来好像Cake夫人在Poons先生发问前就回答了这个问题。 这种超能力让人印象深刻,但也会把人搞糊涂。[25]。

图5-5 如果某些分区的复制速度慢于其他分区,那么观察者在看到问题之前可能会看到答案。

防止这种异常,需要另一种类型的保证:一致前缀读consistent prefix reads[23]。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。

这是分区partitioned分片sharded数据库中的一个特殊问题将在第6章中讨论。如果数据库总是以相同的顺序应用写入则读取总是会看到一致的前缀所以这种异常不会发生。但是在许多分布式数据库中不同的分区独立运行因此不存在全局写入顺序:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。

一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“关系与并发”一节中返回这个主题。

复制延迟的解决方案

在使用最终一致的系统时,如果复制延迟增加到几分钟甚至几小时,则应该考虑应用程序的行为。如果答案是“没问题”,那很好。但如果结果对于用户来说是不好体验,那么设计系统来提供更强的保证是很重要的,例如写后读。明明是异步复制却假设复制是同步的,这是很多麻烦的根源。

如前所述,应用程序可以提供比底层数据库更强有力的保证,例如通过主库进行某种读取。但在应用程序代码中处理这些问题是复杂的,容易出错。

如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库“做了正确的事情”,那该多好呀。这就是**事务transaction**存在的原因:数据库通过事务提供强大的保证,所以应用程序可以更假简单。

单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务。声称事务在性能和可用性上的代价太高,并断言在可扩展系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。第七章和第九章将回到事务的话题,并讨论一些替代机制。

多主复制

本章到目前为止,我们只考虑使用单个领导的复制架构。 虽然这是一种常见的方法,但也有一些有趣的选择。

基于领导者的复制有一个主要的缺点:只有一个领导者,所有的写入都必须通过它.如果数据库被分区见第6章每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。)如果由于任何原因(例如由于你和领导之间的网络中断)而无法连接到领导者, 你不能写入数据库。

基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 我们称之为多领导配置(也称为主 - 主或主动/主动复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者。

多主复制的应用场景

在单个数据中心内部使用多领导者设置很少有意义,因为这些好处很少超过复杂性。 但是,在某些情况下,这种配置是合理的。

运维多数据中心

想象一下,你有一个数据库在几个不同的数据中心(也许这样你可以容忍整个数据中心的故障,或者为了更接近你的用户)有副本。 使用常规的基于领导者的复制设置,领导者必须位于其中一个数据中心,并且所有写入都必须经过该数据中心。

在多领导配置中,您可以在每个数据中心都有领导。 图5-6显示了这个架构的外观。 在每个数据中心内,使用常规的领导者跟随者复制; 在数据中心之间,每个数据中心的负责人都会将其更改复制到其他数据中心的领导。

图5-6 跨多个数据中心的多领导复制

我们来比较一下多数据中心部署中的单引擎和多引导器配置:

性能

在单领导配置中,每个写作都必须通过互联网与领导者一起进入数据中心。这可能会增加写入时间,并可能违反了首先有多个数据中心的目的。在多领导配置中,每个写操作都可以在本地数据中心进行处理,并与其他数据中心异步复制。因此,数据中心之间的网络延迟对用户来说是隐藏的,这意味着感知的性能可能会更好。

数据中心的中断容忍

在单引导者配置中,如果引导者的数据中心发生故障,故障转移可以促使另一个数据中心的追随者成为领导者。在多领导者配置中,每个数据中心可以独立于其他数据中心继续运行,并且当发生故障的数据中心恢复联机时,复制将迎头赶上。

容忍网络问题

数据中心之间的通信通常通过公共互联网,这可能不如数据中心内的本地网络可靠。单引号配置对这个数据中心链接中的问题非常敏感,因为通过这个链接进行写操作是同步的。具有异步复制功能的多领导者配置通常可以更好地承受网络问题:暂时的网络中断不会妨碍正在处理的写入。

有些数据库默认情况下支持多领导配置但通常也使用外部工具实现例如用于MySQL的Tungsten Replicator [26]用于PostgreSQL的BDR [27]以及用于Oracle的GoldenGate [19]。

尽管多领导者复制具有优势但也有一个很大的缺点在两个不同的数据中心可能会同时修改相同的数据并且必须解决这些写冲突如图5-6中的“冲突解决”所示。我们将在第171页的“处理写入冲突”中讨论这个问题。 由于多引导程序复制在许多数据库中都有所改进,所以常常存在微妙的配置缺陷,并且与其他数据库功能之间出现意外的交互。例如,自动增量键,触发器和完整性约束可能是有问题的。出于这个原因,多领导者复制往往被认为是危险的领域,应尽可能避免[28]。

需要离线操作的客户端

多领导者复制的另一种情况是如果您的应用程序在与Internet断开连接时需要继续工作

例如,考虑手机,笔记本电脑和其他设备上的日历应用程序。无论您的设备目前是否具有互联网连接,您都需要能够随时查看您的会议(发出读取请求)并输入新的会议(发出写入请求)。如果您在离线状态下进行任何更改,则当设备下次上线时,需要与服务器和其他设备同步。

在这种情况下,每个设备都有一个充当领导者的本地数据库(它接受写请求),并且在您所有设备上的日历副本之间存在异步多领导者复制过程(同步)。复制延迟可能是几小时甚至几天,具体取决于您何时可以访问互联网。

从架构的角度来看,这种设置基本上与数据中心之间的多领导者复制相同,极端:每个设备都是一个“数据中心”,它们之间的网络连接是非常不可靠的。正如破碎的日历同步实现的丰富历史所表明的,多领导者复制是一件棘手的事情。

有一些工具旨在使这种多领导者配置更容易。例如CouchDB就是为这种操作模式而设计的[29]。

协同编辑

实时协作编辑应用程序允许多个人同时编辑文档。例如Etherpad [30]和Google Docs [31]允许多人同时编辑文本文档或电子表格该算法在第174页的“自动冲突解决”中简要讨论。我们通常不会将协作式编辑视为数据库复制问题但与前面提到的离线编辑用例有许多相似之处。当一个用户编辑文档时所做的更改将立即应用到其本地副本Web浏览器或客户端应用程序中的文档状态并异步复制到服务器和编辑同一文档的任何其他用户。

如果要保证不会发生编辑冲突,则应用程序必须先取得文档的锁定,然后用户才能对其进行编辑。如果另一个用户想要编辑同一个文档,他们首先必须等到第一个用户提交修改并释放锁定。这种协作模式相当于在领导者上进行交易的单领导者复制。

但是,为了加速协作,您可能希望将更改的单位设置得非常小(例如,一个按键),并避免锁定。这种方法允许多个用户同时进行编辑,但同时也带来了多领导者复制的所有挑战,包括需要解决冲突[32]。

处理写冲突

多领导者复制的最大问题是可能发生写冲突,这意味着需要解决冲突。

例如考虑一个由两个用户同时编辑的维基页面如图5-7所示。用户1将页面的标题从A更改为B并且用户2同时将标题从A更改为C.每个用户的更改已成功应用到其本地领导。但是,当异步复制时,会发现冲突[33]。单引导数据库中不会出现此问题。

图5-7 由两位领导同时更新同一记录引起的写入冲突

同步与异步冲突检测

在单主数据库中,第二个写入器将阻塞并等待第一个写入完成,或中止第二个写入事务,强制用户重试写入。另一方面,在多领导者设置中,两个写入都是成功的,并且在稍后的时间点仅仅异步地检测到冲突。那时要求用户解决冲突可能为时已晚。

原则上,可以使冲突检测同步 - 即等待写入被复制到所有副本,然后再告诉用户写入成功。但是,通过这样做,您将失去多领导者复制的主要优点:允许每个副本独立接受写入。如果您想要同步冲突检测,那么您可以使用单引导程序复制。

避免冲突

处理冲突的最简单的策略就是避免它们:如果应用程序可以确保特定记录的所有写入都通过同一个领导者,那么冲突就不会发生。由于多领导者复制处理的许多实现冲突相当不好,避免冲突是一个经常推荐的方法[34]。

例如,在用户可以编辑自己的数据的应用程序中,可以确保来自特定用户的请求始终路由到同一数据中心,并使用该数据中心的领导者进行读写。不同的用户可能有不同的“家庭”数据中心(可能根据用户的地理位置选择),但从任何用户的角度来看,配置基本上都是单一的领导者。

但是,有时您可能需要更改指定的记录的领导 - 可能是因为一个数据中心出现故障,您需要将流量重新路由到另一个数据中心,或者可能是因为用户已经迁移到另一个位置,现在更接近不同的数据中心。在这种情况下,冲突避免会中断,你必须处理不同领导人同时写入的可能性。

趋于一致的状态

单主程序数据库按顺序应用写操作:如果同一个字段有多个更新,则最后一个写操作将确定该字段的最终值。

在多领导者配置中没有定义的写入顺序所以不清楚最终值应该是什么。在图5-7中标题1首先更新为B然后更新为C;在领导者2中首先更新为C然后更新为B.两个订单都不是“更正确”的。

如果每个副本只是按照它看到写入的顺序写入那么数据库最终将处于不一致的状态最终值将是在领导者1处的C和在领导者2处的B.这是不可接受的 - 每个复制方案都必须确保数据在所有副本中最终都是相同的。因此,数据库必须以一种趋同的方式解决冲突,这意味着所有副本必须在所有更改都被复制时达到相同的最终值。

实现融合冲突解决有多种途径:

  • 给每个写入一个唯一的ID例如一个时间戳一个长的随机数一个UUID或者一个键和值的哈希挑选最高ID的写入作为胜利者并丢弃其他写入。如果使用时间戳这种技术被称为最后一次写入胜利LWW。虽然这种方法很流行但是很容易造成数据丢失[35]。我们将在本章末尾更详细地讨论LWW第184页的“检测并发写入”
  • 为每个副本分配一个唯一的ID并让始发于较高编号副本的写入始终优先于源自较低编号副本的写入。这种方法也意味着数据丢失。
  • 以某种方式将这些值合并在一起 - 例如按字母顺序排序然后连接它们在图5-7中合并的标题可能类似于“B / C”
  • 在保留所有信息的显式数据结构中记录冲突,并编写解决冲突的应用程序代码(可能通过提示用户)。

自定义冲突解决逻辑

作为解决冲突最合适的方法可能取决于应用程序,大多数领导者复制工具允许您使用应用程序代码编写冲突解决逻辑。该代码可以在写入或读取时执行:

  • 写时执行

只要数据库系统检测到复制更改日志中存在冲突就会调用冲突处理程序。例如Bucardo允许您为此编写一段Perl代码。这个处理程序通常不能提示用户 - 它在后台进程中运行,并且必须快速执行。

  • 读时执行

当检测到冲突时所有冲突写入被存储。下一次读取数据时会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突并将结果写回数据库。例如CouchDB以这种方式工作。

请注意,冲突解决通常适用于单个行或文档层面,而不是整个事务[36]。因此如果您有一笔交易原本会进行几次不同的写入请参阅第7章则为了冲突解决的目的每个写入仍被分开考虑。

题外话:自动冲突解决

冲突解决规则可能很快变得复杂,并且自定义代码可能容易出错。亚马逊是一个经常被引用的例子,由于冲突解决处理程序令人惊讶的效果:一段时间以来,购物车上的冲突解决逻辑将保留添加到购物车的物品,但不包括从购物车中移除的物品。因此,顾客有时会看到物品重新出现在他们的购物车中,即使他们之前已经被移走[37]。

已经有一些有趣的研究来自动解决由于数据修改引起的冲突。有几行研究值得一提:

  • 无冲突的复制数据类型CRDT[32,38]是可以由多个用户同时编辑的集合映射有序列表计数器等的一系列数据结构它们以合理的方式自动解决冲突。一些CRDT已经在Riak 2.0中实现[39,40]。
  • 可合并的持久数据结构[41]显式跟踪历史记录类似于Git版本控制系统并使用三向合并功能而CRDT使用双向合并
  • 可进行的转换[42]是Etherpad [30]和Google Docs [31]等合作编辑应用背后的冲突解决算法。它是专为同时编辑项目的有序列表而设计的,例如构成文本文档的字符列表。

这些算法在数据库中的实现还很年轻,但很可能将来它们将被集成到更多的复制数据系统中。自动冲突解决方案可以使应用程序处理多领导者数据同步更为简单。

什么是冲突?

有些冲突是显而易见的。在图5-7的例子中两个写操作并发地修改了同一个记录中的同一个字段并将其设置为两个不同的值。毫无疑问这是一个冲突。

其他类型的冲突可能更加微妙地被发现。例如,考虑一个会议室预订系统:它跟踪哪个房间是哪个人在哪个时间预订的。这个应用程序需要确保每个房间只有一组人同时预定(即不得有相同房间的重叠预订)。在这种情况下,如果同时为同一个房间创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查可用性,如果两次预订是由两个不同的领导者进行的,则可能会有冲突。

现在还没有一个现成的答案但在接下来的章节中我们将追溯到对这个问题有很好的理解。我们将在第7章中看到更多的冲突示例在第12章中我们将讨论用于检测和解决复制系统中冲突的可扩展方法。

多主复制拓扑

复制拓扑描述写入从一个节点传播到另一个节点的通信路径。如果你有两个领导者如图5-7所示只有一个合理的拓扑结构领导者1必须把他所有的写到领导者2反之亦然。有两个以上的领导各种不同的拓扑是可能的。图5-8举例说明了一些例子。

图5-8 三个可以设置多领导者复制的示例拓扑。

最普遍的拓扑是全部到全部图5-8 [c]其中每个领导者将其写入每个其他领导。但是也会使用更多受限制的拓扑例如默认情况下MySQL仅支持圆形拓扑[34]其中每个节点接收来自一个节点的写入并将这些写入加上自己的任何写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状v一个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。

在圆形和星形拓扑中,写入可能需要在到达所有副本之前通过多个节点。因此,节点需要转发从其他节点收到的数据更改。为了防止无限复制循环,每个节点被赋予一个唯一的标识符,并且在复制日志中,每个写入都被标记了所有已经通过的节点的标识符[43]。当一个节点收到用自己的标识符标记的数据更改时,该数据更改将被忽略,因为节点知道它已经被处理。

循环和星型拓扑的问题是,如果只有一个节点发生故障,则可能会中断其他节点之间的复制消息流,导致它们无法通信,直到节点修复。拓扑结构可以重新配置为在发生故障的节点上工作,但在大多数部署中,这种重新配置必须手动完成。更密集连接的拓扑结构(例如全部到全部)的容错性更好,因为它允许消息沿着不同的路径传播,避免单点故障。

另一方面全能拓扑也可能有问题。特别是一些网络链接可能比其他网络链接更快例如由于网络拥塞结果是一些复制消息可能“超过”其他复制消息如图5-9所示。

图5-9 使用多主程序复制时,可能会在某些副本中写入错误的顺序。

在图5-9中客户端A向领导者1的表中插入一行客户端B在领导者3上更新该行。然而领导者2可以以不同的顺序接收写入它可以首先接收更新其中从它的角度来看是对数据库中不存在的行的更新并且仅在稍后接收到相应的插入其应该在更新之前

这是一个因果关系的问题类似于我们在第165页上的“一致前缀读取”中看到的更新取决于先前的插入所以我们需要确保所有节点先处理插入然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的因为时钟不可能被充分地同步以便在引导2处正确地排序这些事件见第8章

要正确命令这些事件,可以使用一种称为**版本向量version vectors**的技术本章稍后将讨论这种技术请参阅第174页的“检测并发写入”。然而冲突检测技术在许多多领导者复制系统中执行得不好。例如在撰写本文时PostgreSQL BDR不提供写操作的因果排序[27]而Tungsten Replicator for MySQL甚至不尝试检测冲突[34]。

如果您正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试您的数据库,以确保它确实提供了您认为具有的保证。

无主复制

我们在本章到目前为止所讨论的复制方法 - 单引导者和多引导者复制 - 是基于客户端向一个节点(领导者)发送写请求的想法,数据库系统负责复制写入其他副本。领导决定了写入的顺序,而跟随者按相同的顺序应用领导的写入。

一些数据存储系统采用不同的方法,放弃领导者的概念,并允许任何副本直接接受来自客户端的写入。一些最早的复制数据系统是无领导的[1,44]但是在关系数据库主导时代这个想法大多被遗忘。在亚马逊将其用于其内部的Dynamo系统之后它再一次成为数据库的一种时尚架构[37] .Dynamo不适用于Amazon以外的用户。 令人困惑的是AWS提供了一个名为DynamoDB的托管数据库产品它使用了完全不同的体系结构它基于单引导程序复制。 RiakCassandra和Voldemort是由Dynamo启发的无领导复制模型的开源数据存储所以这类数据库也被称为Dynamo风格

在一些无领导者的实现中,客户端直接将其写入到几个副本中,而在另一些情况下,协调器节点代表客户端进行写入。但是,与领导者数据库不同,协调员不执行特定的写入顺序。我们将会看到,这种设计上的差异对数据库的使用方式有着深远的影响。

当节点故障时写入数据库

假设你有一个带有三个副本的数据库而其中一个副本目前不可用或许正在重新启动以安装系统更新。在基于主机的配置中如果要继续处理写入则可能需要执行故障切换请参阅第133页的「处理节点中断」

另一方面在无领导配置中故障切换不存在。图5-10显示了发生了什么事情客户端用户1234并行发送写入到所有三个副本并且两个可用副本接受写入但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的在用户1234已经收到两个确定的响应之后我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。

图5-10 仲裁写入,法定读取,并在节点中断后读取修复。

现在想象一下,不可用的节点重新联机,客户端开始读取它。节点关闭时发生的任何写入都从该节点丢失。因此,如果您从该节点读取数据,则可能会将陈旧(过时)值视为响应。

为了解决这个问题当一个客户端从数据库中读取数据时它不仅仅发送它的请求到一个副本读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新请参阅第174页的“检测并发写入”

阅读修复和反熵

复制方案应确保最终将所有数据复制到每个副本。在一个不可用的节点重新联机之后,它如何赶上它错过的写入?

在Dynamo风格的数据存储中经常使用两种机制

  • 修复读

当客户端并行读取多个节点时它可以检测到任何陈旧的响应。例如在图5-10中用户2345获得了来自Replica 3的版本6值和来自副本1和2的版本7值。客户端发现副本3具有陈旧值并将新值写回复制品。这种方法适用于频繁阅读的值。

  • 反熵过程

此外,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。与基于领导者的复制中的复制日志不同,此反熵过程不会以任何特定的顺序复制写入,并且在复制数据之前可能会有显着的延迟。

并不是所有的系统都实现了这两个;例如Voldemort目前没有反熵过程。请注意如果没有反熵过程某些副本中很少读取的值可能会丢失从而降低了持久性因为只有在应用程序读取值时才执行读取修复。

法定人数的读写

在图5-10的示例中我们认为即使仅在三个副本中的两个上进行处理写入仍然是成功的。如果三个副本中只有一个接受了写入会怎样我们能推多远呢

如果我们知道,每个成功的写操作意味着在三个副本中至少有两个出现,这意味着至多有一个副本可能是陈旧的。因此,如果我们从至少两个副本读取,我们可以确定至少有一个是最新的。如果第三个副本停机或响应速度缓慢,则读取仍可以继续返回最新值。

更一般地说如果有n个副本每个写入必须由w节点确认才能被认为是成功的并且我们必须至少为每个读取查询r个节点。 (在我们的例子中,$n = 3w = 2r = 2$)。只要$w + r> n$我们期望在读取时获得最新的值因为至少有一个r节点从阅读必须是最新的。读取和写入服从这些r和w值被称为法定读取和写入。[44] 有时候这种法定人数被称为严格的法定人数与马虎法定人数形成对比见第183页“马虎法定人数和暗示交接法”你可以认为r和w作为读或写所需的最低票数是有效的。

在Dynamo风格的数据库中参数nw和r通常是可配置的。一个常见的选择是使n为奇数通常为3或5并设置 $w = r =n + 1/ 2$(向上舍入)。但是,您可以根据需要更改数字。例如,设置$w = n$和$r = 1$的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。

可能有多于n个节点的集群但是任何给定的值只能存储在n个节点上。 这允许对数据集进行分区,从而支持比您可以放在一个节点上的数据集更大的数据集。 我们将在第6章回到分区。

仲裁条件$w + r> n$允许系统容忍不可用的节点,如下所示:

  • 如果$w <n$,如果节点不可用,我们仍然可以处理写入。
  • 如果$r <n$,如果节点不可用,我们仍然可以处理读取。
  • 对于$n = 3w = 2r = 2$,我们可以容忍一个不可用的节点。
  • 对于$n = 5w = 3r = 3$,我们可以容忍两个不可用的节点。 这个案例如图5-11所示。
  • 通常读取和写入操作始终并行发送到所有n个副本。 参数w和r决定我们等待多少个节点即在我们认为读或写成功之前有多少个节点需要报告成功。

图5-11 如果w + r > n读取r个副本至少有一个r副本必然包含了最近的成功写入

如果少于所需的w或r节点可用则写入或读取将返回错误。 由于许多原因,节点可能不可用:因为由于执行操作的错误(由于磁盘已满而无法写入)导致节点关闭(崩溃,关闭电源),由于客户端和服务器之间的网络中断 节点,或任何其他原因。 我们只关心节点是否返回了成功的响应,而不需要区分不同类型的错误。

仲裁一致性的局限性

如果你有n个副本并且你选择w和r使得$w + r> n$你通常可以期望每个读取返回为一个键写的最近的值。情况就是这样因为你写的节点集合和你读过的节点集合必须重叠。也就是说您读取的节点中必须至少有一个具有最新值的节点如图5-11所示

通常r和w被选为多数超过 n/2 )节点,因为这确保了$w + r> n$同时仍然容忍多达n / 2个节点故障。但是法定人数不一定必须是大多数只是读和写操作使用的节点集合至少需要在一个节点上重叠。其他法定人数的分配是可能的这在分布式算法的设计中有一定的灵活性[45]。

您也可以将w和r设置为较小的数字以使$w + r≤n$即法定条件不满足。在这种情况下读取和写入操作仍将被发送到n个节点但操作成功需要少量的成功响应。

对于较小的w和r您更可能读取陈旧的值因为您的读取更有可能不包含具有最新值的节点。另一方面这种配置允许更低的延迟和更高的可用性如果存在网络中断并且许多副本变得无法访问则可以继续处理读取和写入的机会更大。只有当可达副本的数量低于w或r时数据库才分别变得不可用于写入或读取。

但是,即使在$w + r> n$的情况下,也可能存在返回陈旧值的边缘情况。这取决于实施,但可能的情况包括:

  • 如果使用松散的法定人数请参阅第181页上的“马虎的仲裁与暗示交接”写入可能会以不同于r读取的节点结束因此r节点和w之间不再有保证重叠节点[46]。
  • 如果两个写入同时发生不清楚哪一个先发生。在这种情况下唯一安全的解决方案是合并并发写入请参阅第171页的“处理写入冲突”。如果根据时间戳最后写入成功挑选出胜者则由于时钟偏差[35]写入可能会丢失。我们将返回第184页上的“检测并发写入”中的此主题。
  • 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
  • 如果在某些副本上写入成功而在其他节点上写入失败例如因为某些节点上的磁盘已满并且总体z上成功的次数少于w个副本不会在成功的副本上回滚。这意味着如果一个写入报告失败了后续的读取可能会或可能不会返回写入的值[47]。
  • 如果携带新值的节点失败并且其数据从带有旧值的副本中恢复则存储新值的副本数可能会低于w从而打破法定条件。
  • 即使一切工作正常也会出现边缘情况在这种情况下您可能会感到不安因为我们将在第334页上的“线性化和法定人数”中看到。

因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo风格的数据库通常针对可以容忍最终一致性的用例进行优化。参数w和r允许您调整陈旧值读取的概率但不要把它们作为绝对保证。

尤其是您通常没有得到第161页上的“与延迟有关的问题”读取您的写入单调读取或一致的前缀读取中讨论的保证因此前面提到的异常可能会发生在应用程序中。更强有力的担保通常需要交易或共识。我们将在第七章和第九章回到这些话题。

监控陈旧度

从操作的角度来看,监视你的数据库是否返回最新的结果是很重要的。即使您的应用程序可以容忍陈旧的读取,您也需要了解复制的健康状况。如果显着落后,应该提醒您,以便您可以调查原因(例如,网络中的问题或超载节点)。

对于基于领导者的复制,数据库通常会公开复制滞后的度量标准,您可以将其提供给监视系统。这是可能的,因为写入按照相同的顺序应用于领导者和追随者,并且每个节点在复制日志中具有一个位置(在本地应用的写入次数)。通过从领导者的当前位置中减去随从者的当前位置,您可以测量复制滞后量。

然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读取修复(没有反熵),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。

已经有一些关于衡量无主复制数据库中的复制陈旧度的研究并根据参数nw和r来预测陈旧读取的预期百分比[48]。不幸的是,这还不是很常见的做法,但是将过时测量值包含在数据库的标准度量标准中是一件好事。最终的一致性是故意模糊的保证,但是对于可操作性来说,能够量化“最终”是很重要的。

松散仲裁与意见交换

具有适当配置的仲裁的数据库可以容忍个别节点的故障而不需要故障切换。他们也可以容忍个别节点变慢因为请求不必等待所有n个节点响应 - 当w或r节点响应时它们可以返回。这些特性使得数据库具有无需复制的吸引力适用于需要高可用性和低延迟的用例并且可以容忍偶尔的陈旧读取。

然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能能够连接到它们,但是从数据库节点切断的客户端,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于可用节点,因此客户端可能无法达到法定人数。

在一个大型的群集中节点数量明显多于n个在网络中断期间客户端可能连接到某些数据库节点而不是为了为特定值组装法定数量的节点。在这种情况下数据库设计人员需要权衡一下

  • 将错误返回给我们无法达到w或r节点的法定数量的所有请求是否更好
  • 或者我们是否应该接受写入然后将它们写入一些可达的节点但不在n值通常存在的n个节点之间

后者被认为是一个马虎的法定人数[37]写和读仍然需要w和r成功的响应但是那些可能包括不在指定的n个“主”节点中的值。比方说如果你把自己锁在房子外面你可能会敲开邻居的门问你是否可以暂时停留在沙发上。

一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的“本地”节点。这就是所谓的提示。 (一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家。)

松散仲裁提高写入可用性特别有用只要有任何w节点可用数据库就可以接受写入。然而这意味着即使当$w + r> n$时也不能确定读取某个键的最新值因为最新的值可能已经临时写入了n之外的某些节点[47]。

因此在传统意义上一个马虎的法定人数实际上不是一个法定人数。这只是一个保证即数据存储在w节点的地方。不能保证r节点的读取直到提示已经完成。

在所有常见的Dynamo实现中松散的仲裁是可选的。在Riak中它们默认是启用的而在Cassandra和Voldemort中它们默认是禁用的[46,49,50]。

多数据中心操作

我们先前讨论了跨数据中心复制作为多主复制的用例请参阅第162页的“多重复制复制”。无主复制还适用于多数据中心操作因为它旨在容忍冲突的并发写入网络中断和延迟尖峰。

Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持副本的数量n包括所有数据中心的节点在配置中您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何每个来自客户端的写入都会发送到所有副本但客户端通常只等待来自其本地数据中心内的法定节点的确认从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生尽管配置有一定的灵活性[50,51]。 Riak将客户端和数据库节点之间的所有通信保持在一个数据中心本地因此n描述了一个数据中心内的副本数量。数据库集群之间的跨数据中心复制在后台异步发生其风格类似于多领导者复制[52]。

侦测并发写入

Dynamo风格的数据库允许多个客户端同时写入相同的Key这意味着即使使用严格的法定人数也会发生冲突。这种情况与多领导者复制相似请参阅第171页的“处理写冲突”但在Dynamo样式的数据库中在读取修复或提示性切换期间也可能会产生冲突。

问题在于由于可变的网络延迟和部分故障事件可能在不同的节点以不同的顺序到达。例如图5-12显示了两个客户机A和B同时写入三节点数据存储区中的键X

  • 节点1接收来自A的写入但由于暂时中断从不接收来自B的写入。
  • 节点2首先接收来自A的写入然后接收来自B的写入。
  • 节点3首先接收来自B的写入然后从A写入。

图5-12 并发写入Dynamo风格的数据存储没有明确定义的顺序。

如果每个节点只要接收到来自客户端的写入请求就简单地覆盖了某个键的值那么节点就会永久地不一致如图5-12中的最终获取请求所示节点2认为X的最终值是B而其他节点认为值是A.

为了最终达成一致,副本应该趋于相同的价值。他们如何做到这一点?有人可能希望复制的数据库能够自动处理,但不幸的是,大多数的实现都很糟糕:如果你想避免丢失数据,你(应用程序开发人员)需要知道很多有关数据库冲突处理的内部信息。

我们在第171页的“处理写冲突”中简要介绍了一些解决冲突的技术。在总结本章之前让我们来更详细地探讨这个问题。

最后写入胜(丢弃并发写入)

实现最终融合的一种方法是声明每个副本只需要存储最“最近”的值,并允许“更旧”的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是“最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。

正如“最近”的引用所表明的这个想法其实颇具误导性。在图5-12的例子中当客户端向数据库节点发送写入请求时客户端都不知道另一个客户端因此不清楚哪一个先发生了。事实上说“发生”是没有意义的我们说写入是并发的所以它们的顺序是不确定的。

即使写入没有自然排序我们也可以强制任意排序。例如我们可以为每个写入附加一个时间戳挑选最“最近”的最大时间戳并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为最后写入胜利LWW是Cassandra [53]唯一支持的冲突解决方法也是Riak [35]中的一个可选特征。

LWW实现了最终收敛的目标但是以持久性为代价如果同一个Key有多个并发写入即使它们都被报告为客户端成功因为它们被写入w个副本其中一个写道会生存下来其他的将被无声丢弃。此外LWW甚至可能会删除不是并发的写入我们将在第291页的“有序事件的时间戳”中讨论。

有一些情况如缓存其中丢失的写入可能是可以接受的。如果丢失数据是不可接受的LWW是解决冲突的一个很烂的选择。

与LWW一起使用数据库的唯一安全方法是确保一个Key只写入一次然后视为不可变从而避免对同一个密钥进行并发更新。例如推荐使用Cassandra的方法是使用UUID作为键从而为每个写操作提供一个唯一的键[53]。

“此前发生”的关系和并发

我们如何判断两个操作是否是并发的?发展一个直觉,让我们看看一些例子:

  • 在图5-9中两个写入不是并发的A的插入发生在B的增量之前因为B递增的值是A插入的值。换句话说B的操作建立在A的操作上所以B的操作必须有后来发生。我们也可以说B是因果依赖于A
  • 另一方面图5-12中的两个写入是并发的当每个客户端启动操作时它不知道另一个客户端也正在执行操作同样的Key。因此操作之间不存在因果关系。

如果操作B了解操作A或者依赖于A或者以某种方式构建于操作A之上则操作A在另一个操作B之前发生。在另一个操作之前是否发生一个操作是定义什么并发的关键。事实上我们可以简单地说如果两个操作都不在另一个之前发生那么两个操作是并发的两个操作都不知道另一个[54]。

因此只要有两个操作A和B就有三种可能性A在B之前发生或者B在A之前发生或者A和B并发。我们需要的是一个算法来告诉我们两个操作是否是并发的。如果一个操作发生在另一个操作之前则后面的操作应该覆盖较早的操作但是如果这些操作是并发的则存在需要解决的冲突。

并发性,时间和相对性

如果两个操作“同时”发生,似乎应该称为并发 - 但事实上它们是否在时间上重叠并不重要。由于分布式系统中的时钟问题实际上很难判断两个事件是否同时发生这个问题我们将在第8章中详细讨论。

为了定义并发性,确切的时间并不重要:如果两个操作都不知道对方,我们只需调用两个并发操作,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来[54],它引入了信息不能比光速更快的思想。因此,如果事件之间的时间短于光通过它们之间的距离,那么发生一定距离的两个事件不可能相互影响。

在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作可能是并行的。例如,如果网络缓慢或中断,两个操作可能会发生一段时间,并且仍然是并发的,因为网络问题阻止一个操作能够知道另一个操作。

捕捉此前发生关系

让我们来看一个算法,它确定两个操作是并发的,还是一个在另一个之前。为了简单起见,我们从一个只有一个副本的数据库开始。一旦我们已经制定了如何在单个副本上完成这项工作,我们可以将该方法概括为具有多个副本的无领导者数据库。

图5-13显示了两个客户端同时向同一购物车添加项目。 (如果这样的例子让你觉得太麻烦了,那么可以想象,两个空中交通管制员同时把飞机添加到他们正在跟踪的部门。)最初,购物车是空的。在它们之间,客户端向数据库发出五次写入:

  1. 客户1将牛奶加入购物车。这是第一次写入该Key所以服务器成功存储并为其分配版本1.服务器还将值与版本号一起回送给客户端。
  2. 客户2将鸡蛋加入购物车不知道客户1同时添加了牛奶客户2认为其蛋是购物车中的唯一物品。服务器为此写入分配版本2并将蛋和牛奶存储为两个单独的值。然后它将这两个值和版本号2一起返回给客户端。
  3. 客户1不知道客户2的写入想要将面粉加入购物车因此认为当前的购物车内容应该是[牛奶,面粉]。它将此值与服务器先前向客户端1提供的版本号1一起发送到服务器。服务器可以从版本号中知道[牛奶,面粉]的写入取代了[牛奶]的先前值,但与[鸡蛋]同时出现。因此服务器将版本3分配给[牛奶,面粉]覆盖版本1值[牛奶]但保留版本2值[蛋]并将剩余的值返回给客户端。
  4. 同时客户2想要加入火腿不知道客户1刚刚加了面粉。客户端2在最后一个响应中从服务器收到了两个值[牛奶]和[蛋],所以客户现在合并这些值,并添加火腿形成一个新的值,[鸡蛋,牛奶,火腿]。它将这个值发送到服务器以及以前的版本2.服务器检测到版本2会覆盖[eggs],但与[milkflour]同时发生,所以剩下的两个值是[milkflour]版本3和[鸡蛋,牛奶,火腿]与版本4。
  5. 最后客户1想要加培根。它以前在版本3中从服务器接收[牛奶,面粉]和[鸡蛋],所以它合并这些,添加培根,并将最终值[牛奶,面粉,鸡蛋,培根]连同版本号3.这会覆盖[牛奶,面粉](请注意[鸡蛋]已经在最后一步被覆盖),但与[鸡蛋,牛奶,火腿]并发,所以服务器保留这两个并发值。

图5-13 捕获两个客户端之间的因果关系,同时编辑购物车。

图5-13中的操作之间的数据流如图5-14所示。 箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。 在这个例子中,客户端永远不会完全掌握服务器上的数据,因为总是有另一个操作同时进行。 但是,旧版本的值最终会被覆盖,并且不会丢失任何写入。

图5-14 图5-13中的因果依赖关系图。

请注意,服务器可以通过查看版本号来确定两个操作是否是并发的 - 它不需要解释该值本身(因此该值可以是任何数据结构)。该算法的工作原理如下:

  • 服务器为每个密钥保留一个版本号,每次写入密钥时都增加版本号,并将新版本号与写入的值一起存储。
  • 当客户端读取密钥时,服务器将返回所有未覆盖的值以及最新的版本号。在写之前,客户必须先读钥匙。
  • 客户端写入密钥时,必须包含之前读取的版本号,并且必须将之前读取的所有值合并在一起。 (来自写入请求的响应可以像读取一样,返回所有当前值,这使得我们可以像购物车示例那样链接多个写入。)
  • 当服务器接收到具有特定版本号的写入时,它可以覆盖该版本号或更低版本的所有值(因为它知道它们已经被合并到新的值中),但是它必须保持所有值更高版本号(因为这些值与传入的写入同时发生)。

当一个写入包含前一次读取的版本号时,它会告诉我们写入的是哪一种状态。如果在不包含版本号的情况下进行写操作,则与所有其他写操作并发,因此它不会覆盖任何内容 - 只会在随后的读取中作为其中一个值返回。

合并同时写入的值

这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:如果多个操作并发发生,则客户端必须通过合并并行写入的值来进行清理。 Riak称这些并发值兄弟姐妹。

合并兄弟值本质上是与多领导者复制中的冲突解决相同的问题我们先前讨论过请参阅第171页的“处理写冲突”。一个简单的方法是根据版本号或时间戳最后写入胜利选择一个值但这意味着丢失数据。所以你可能需要在应用程序代码中做更聪明的事情。

以购物车为例合并兄弟姐妹的一种合理方法就是参加工会。在图5-14中最后的两个兄弟姐妹是[牛奶,面粉,鸡蛋,熏肉]和[鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋出现在两个,即使他们每个只写一次。合并的价值可能是像[牛奶,面粉,鸡蛋,培根,火腿],没有重复。

然而,如果你想让人们也可以从他们的手推车中删除东西,而不是仅仅添加东西,那么把兄弟姐妹联合起来可能不会产生正确的结果:如果你合并了两个兄弟手推车,并且只有一个那么被删除的项目会重新出现在兄弟姐妹的联合中[37]。为了防止这个问题,一个项目在删除时不能简单地从数据库中删除;相反,系统必须留下一个具有合适版本号的标记,以指示合并兄弟时该项目已被删除。这种删除标记被称为墓碑。 我们之前在第72页的“哈希索引”中的日志压缩环境中看到了墓碑。

因为在应用程序代码中合并兄弟是复杂且容易出错的所以设计数据结构可以自动执行这种合并如“自动冲突解决”第174页中讨论的。例如Riak的数据类型支持使用称为CRDT的数据结构家族[38,39,55]可以以合理的方式自动合并兄弟,包括保留删除。

版本向量

图5-13中的示例只使用一个副本。如果有多个副本但没有领导者算法如何改变

图5-13使用单个版本号来捕获操作之间的依赖关系但是当多个副本接受写入连接时这是不够的。相反我们需要使用每个副本的版本号以及每个密钥。每个副本在处理写入时增加自己的版本号并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些值以及保留哪些值作为兄弟。

所有副本的版本号集合称为版本向量[56]。这个想法的一些变体正在使用但最有趣的可能是在Riak 2.0 [58,59]中使用的虚线版本矢量[57]。我们不会深入细节,但是它的工作方式与我们在购物车示例中看到的非常相似。

与图5-13中的版本号一样当读取值时版本向量会从数据库副本发送到客户端并且随后写入值时需要将其发送回数据库。 Riak将版本向量编码为一个字符串它称为因果上下文。版本向量允许数据库区分重写和并发写入。

另外,就像在单个副本的例子中,应用程序可能需要合并兄弟。版本向量结构确保从一个副本读取并随后写回到另一个副本是安全的。这样做可能会导致兄弟姐妹被创建,但只要兄弟姐妹合并正确,就不会丢失数据。

版本矢量和矢量时钟

版本矢量有时也被称为矢量时钟,即使它们不完全相同。 细微差别 - 请参阅参考资料的细节[576061]。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。

本章小结

在本章中,我们考察了复制的问题。复制可以用于几个目的:

高可用性

即使在一台机器(或多台机器,或整个数据中心)停机的情况下也能保持系统正常运行

断开连接的操作

允许应用程序在网络中断时继续工作

延迟

将数据放置在距离用户较近的地方,以便用户能够更快地与其交互

可扩展性

能够处理比单个机器更高的读取量可以通过对副本进行读取来处理

尽管是一个简单的目标 - 在几台机器上保留相同数据的副本,但复制却是一个非常棘手的问题。它需要仔细考虑并发和所有可能出错的事情,并处理这些故障的后果。至少,我们需要处理不可用的节点和网络中断(甚至不考虑更隐蔽的故障,例如由于软件错误导致的无提示数据损坏)。

我们讨论了复制的三种主要方法:

单主复制

客户端将所有写入操作发送到单个节点(领导者),该节点将数据更改事件流发送到其他副本(追随者)。读取可以在任何副本上执行,但从追随者读取可能是陈旧的。

多主复制

客户端发送每个写入到几个领导节点之一,其中任何一个都可以接受写入。领导者将数据更改事件流发送给彼此以及任何跟随者节点。

无主复制

客户端发送每个写入到几个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。 每种方法都有优点和缺点。单引导复制是非常流行的,因为它很容易理解,不需要担心冲突解决。在出现故障节点,网络中断和延迟峰值的情况下,多领导者和无领导者复制可以更加稳健,但代价很难推理,只能提供非常弱的一致性保证。

复制可以是同步的,也可以是异步的,在发生故障时对系统行为有深远的影响。尽管在系统运行平稳时异步复制速度很快,但是在复制滞后增加和服务器故障时要弄清楚会发生什么,这一点很重要。如果一个领导者失败了,并且你推动一个异步更新的追随者成为新的领导者,那么最近承诺的数据可能会丢失。

我们研究了一些可能由复制滞后引起的奇怪效应,我们讨论了一些有助于决定应用程序在复制滞后时的行为的一致性模型:

写后读

用户应该总是看到自己提交的数据。

单调读

当用户在某个时间点看到数据后,他们不应该在较早的时间点看到数据。

一致前缀读

用户应该将数据视为具有因果意义的状态:例如,按照正确的顺序查看问题及其答复。

最后,我们讨论了多领导者和无领导者复制方法所固有的并发问题:因为他们允许多个写入并发发生冲突。我们研究了一个数据库可能使用的算法来确定一个操作是否发生在另一个操作之前,或者它们是否同时发生。我们还谈到了通过合并并发更新来解决冲突的方法。

在下一章中,我们将继续研究分布在多个机器上的数据,通过复制的对应方式:将大数据集分割成分区。

参考文献

  1. Bruce G. Lindsay, Patricia Griffiths Selinger, C. Galtieri, et al.: “Notes on Distributed Databases,” IBM Research, Research Report RJ2571(33471), July 1979.

  2. Oracle Active Data Guard Real-Time Data Protection and Availability,” Oracle White Paper, June 2013.

  3. AlwaysOn Availability Groups,” in SQL Server Books Online, Microsoft, 2012.

  4. Lin Qiao, Kapil Surlaker, Shirshanka Das, et al.: “On Brewing Fresh Espresso: LinkedIns Distributed Data Serving Platform,” at ACM International Conference on Management of Data (SIGMOD), June 2013.

  5. Jun Rao: “Intra-Cluster Replication for Apache Kafka,” at ApacheCon North America, February 2013.

  6. Highly Available Queues,” in RabbitMQ Server Documentation, Pivotal Software, Inc., 2014.

  7. Yoshinori Matsunobu: “Semi-Synchronous Replication at Facebook,” yoshinorimatsunobu.blogspot.co.uk, April 1, 2014.

  8. Robbert van Renesse and Fred B. Schneider: “Chain Replication for Supporting High Throughput and Availability,” at 6th USENIX Symposium on Operating System Design and Implementation (OSDI), December 2004.

  9. Jeff Terrace and Michael J. Freedman: “Object Storage on CRAQ: High-Throughput Chain Replication for Read-Mostly Workloads,” at USENIX Annual Technical Conference (ATC), June 2009.

  10. Brad Calder, Ju Wang, Aaron Ogus, et al.: “Windows Azure Storage: A Highly Available Cloud Storage Service with Strong Consistency,” at 23rd ACM Symposium on Operating Systems Principles (SOSP), October 2011.

  11. Andrew Wang: “Windows Azure Storage,” umbrant.com, February 4, 2016.

  12. Percona Xtrabackup - Documentation,” Percona LLC, 2014.

  13. Jesse Newland: “GitHub Availability This Week,” github.com, September 14, 2012.

  14. Mark Imbriaco: “Downtime Last Saturday,” github.com, December 26, 2012.

  15. John Hugg: “All in with Determinism for Performance and Testing in Distributed Systems,” at Strange Loop, September 2015.

  16. Amit Kapila: “WAL Internals of PostgreSQL,” at PostgreSQL Conference (PGCon), May 2012.

  17. MySQL Internals Manual. Oracle, 2014.

  18. Yogeshwer Sharma, Philippe Ajoux, Petchean Ang, et al.: “Wormhole: Reliable Pub-Sub to Support Geo-Replicated Internet Services,” at 12th USENIX Symposium on Networked Systems Design and Implementation (NSDI), May 2015.

  19. Oracle GoldenGate 12c: Real-Time Access to Real-Time Information,” Oracle White Paper, October 2013.

  20. Shirshanka Das, Chavdar Botev, Kapil Surlaker, et al.: “All Aboard the Databus!,” at ACM Symposium on Cloud Computing (SoCC), October 2012.

  21. Greg Sabino Mullane: “Version 5 of Bucardo Database Replication System,” blog.endpoint.com, June 23, 2014.

  22. Werner Vogels: “Eventually Consistent,” ACM Queue, volume 6, number 6, pages 1419, October 2008. doi:10.1145/1466443.1466448

  23. Douglas B. Terry: “Replicated Data Consistency Explained Through Baseball,” Microsoft Research, Technical Report MSR-TR-2011-137, October 2011.

  24. Douglas B. Terry, Alan J. Demers, Karin Petersen, et al.: “Session Guarantees for Weakly Consistent Replicated Data,” at 3rd International Conference on Parallel and Distributed Information Systems (PDIS), September 1994. doi:10.1109/PDIS.1994.331722

  25. Terry Pratchett: Reaper Man: A Discworld Novel. Victor Gollancz, 1991. ISBN: 978-0-575-04979-6

  26. Tungsten Replicator,” Continuent, Inc., 2014.

  27. BDR 0.10.0 Documentation,” The PostgreSQL Global Development Group, bdr-project.org, 2015.

  28. Robert Hodges: “If You Must Deploy Multi-Master Replication, Read This First,” scale-out-blog.blogspot.co.uk, March 30, 2012.

  29. J. Chris Anderson, Jan Lehnardt, and Noah Slater: CouchDB: The Definitive Guide. O'Reilly Media, 2010. ISBN: 978-0-596-15589-6

  30. AppJet, Inc.: “Etherpad and EasySync Technical Manual,” github.com, March 26, 2011.

  31. John Day-Richter: “Whats Different About the New Google Docs: Making Collaboration Fast,” googledrive.blogspot.com, 23 September 2010.

  32. Martin Kleppmann and Alastair R. Beresford: “A Conflict-Free Replicated JSON Datatype,” arXiv:1608.03960, August 13, 2016.

  33. Frazer Clement: “Eventual Consistency Detecting Conflicts,” messagepassing.blogspot.co.uk, October 20, 2011.

  34. Robert Hodges: “State of the Art for MySQL Multi-Master Replication,” at Percona Live: MySQL Conference & Expo, April 2013.

  35. John Daily: “Clocks Are Bad, or, Welcome to the Wonderful World of Distributed Systems,” basho.com, November 12, 2013.

  36. Riley Berton: “Is Bi-Directional Replication (BDR) in Postgres Transactional?,” sdf.org, January 4, 2016.

  37. Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, et al.: “Dynamo: Amazon's Highly Available Key-Value Store,” at 21st ACM Symposium on Operating Systems Principles (SOSP), October 2007.

  38. Marc Shapiro, Nuno Preguiça, Carlos Baquero, and Marek Zawirski: “A Comprehensive Study of Convergent and Commutative Replicated Data Types,” INRIA Research Report no. 7506, January 2011.

  39. Sam Elliott: “CRDTs: An UPDATE (or Maybe Just a PUT),” at RICON West, October 2013.

  40. Russell Brown: “A Bluffers Guide to CRDTs in Riak,” gist.github.com, October 28, 2013.

  41. Benjamin Farinier, Thomas Gazagnaire, and Anil Madhavapeddy: “Mergeable Persistent Data Structures,” at 26es Journées Francophones des Langages Applicatifs (JFLA), January 2015.

  42. Chengzheng Sun and Clarence Ellis: “Operational Transformation in Real-Time Group Editors: Issues, Algorithms, and Achievements,” at ACM Conference on Computer Supported Cooperative Work (CSCW), November 1998.

  43. Lars Hofhansl: “HBASE-7709: Infinite Loop Possible in Master/Master Replication,” issues.apache.org, January 29, 2013.

  44. David K. Gifford: “Weighted Voting for Replicated Data,” at 7th ACM Symposium on Operating Systems Principles (SOSP), December 1979. doi:10.1145/800215.806583

  45. Heidi Howard, Dahlia Malkhi, and Alexander Spiegelman: “Flexible Paxos: Quorum Intersection Revisited,” arXiv:1608.06696, August 24, 2016.

  46. Joseph Blomstedt: “Re: Absolute Consistency,” email to riak-users mailing list, lists.basho.com, January 11, 2012.

  47. Joseph Blomstedt: “Bringing Consistency to Riak,” at RICON West, October 2012.

  48. Peter Bailis, Shivaram Venkataraman, Michael J. Franklin, et al.: “Quantifying Eventual Consistency with PBS,” Communications of the ACM, volume 57, number 8, pages 93102, August 2014. doi:10.1145/2632792

  49. Jonathan Ellis: “Modern Hinted Handoff,” datastax.com, December 11, 2012.

  50. Project Voldemort Wiki,” github.com, 2013.

  51. Apache Cassandra 2.0 Documentation,” DataStax, Inc., 2014.

  52. Riak Enterprise: Multi-Datacenter Replication.” Technical whitepaper, Basho Technologies, Inc., September 2014.

  53. Jonathan Ellis: “Why Cassandra Doesn't Need Vector Clocks,” datastax.com, September 2, 2013.

  54. Leslie Lamport: “Time, Clocks, and the Ordering of Events in a Distributed System,” Communications of the ACM, volume 21, number 7, pages 558565, July 1978. doi:10.1145/359545.359563

  55. Joel Jacobson: “Riak 2.0: Data Types,” blog.joeljacobson.com, March 23, 2014.

  56. D. Stott Parker Jr., Gerald J. Popek, Gerard Rudisin, et al.: “Detection of Mutual Inconsistency in Distributed Systems,” IEEE Transactions on Software Engineering, volume 9, number 3, pages 240247, May 1983. doi:10.1109/TSE.1983.236733

  57. Nuno Preguiça, Carlos Baquero, Paulo Sérgio Almeida, et al.: “Dotted Version Vectors: Logical Clocks for Optimistic Replication,” arXiv:1011.5808, November 26, 2010.

  58. Sean Cribbs: “A Brief History of Time in Riak,” at RICON, October 2014.

  59. Russell Brown: “Vector Clocks Revisited Part 2: Dotted Version Vectors,” basho.com, November 10, 2015.

  60. Carlos Baquero: “Version Vectors Are Not Vector Clocks,” haslab.wordpress.com, July 8, 2011.

  61. Reinhard Schwarz and Friedemann Mattern: “Detecting Causal Relationships in Distributed Computations: In Search of the Holy Grail,” Distributed Computing, volume 7, number 3, pages 149174, March 1994. doi:10.1007/BF02277859


  1. 不同的人对热hot温warn冷cold 备份服务器有不同的定义。 例如在PostgreSQL中**热备hot standby指的是能接受客户端读请求的副本。而温备warm standby**只是追随领导者,但不处理客户端的任何查询。 就本书而言差异并不重要。 ↩︎

  2. 这种机制称为击剑fencing,充满感情的术语是:爆彼之头STONITHShoot The Other Node In The Head↩︎

  3. 道格拉斯·特里Douglas Terry等人创造了术语最终一致性。 [24] 并经由Werner Vogels [22]推广成为许多NoSQL项目的战吼。 然而不只有NoSQL数据库是最终一致的关系型数据库中的异步复制追随者也有相同的特性。 ↩︎