format: ch5 格式修正

This commit is contained in:
Vonng 2018-03-07 21:28:34 +08:00
parent 8811de70b2
commit 0932aa6a31

View File

@ -36,7 +36,7 @@
2. 其他副本被称为**追随者followers**,亦称为**只读副本read replicas****从slaves****次要( sencondaries****热备hot-standby**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志replication log**记录或**变更流change stream**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。
[^i]: 不同的人对**热hot****温warn****冷cold** 备份服务器有不同的定义。 例如在PostgreSQL中**热备hot standby**指的是能接受客户端读请求的副本。而**温备warm standby**只是追随领导者,但不处理客户端的任何查询。 就本书而言差异并不重要。
[^i]: 不同的人对**热hot****温warn****冷cold** 备份服务器有不同的定义。 例如在PostgreSQL中**热备hot standby**指的是能接受客户端读请求的副本。而**温备warm standby**只是追随领导者,但不处理客户端的任何查询。 就本书而言差异并不重要。
![](img/fig5-1.png)
**图5-1 基于领导者(主-从)的复制**
@ -70,7 +70,7 @@
> ### 关于复制的研究
>
> 如果主库故障,那么异步复制系统会丢失数据可能是一个严重的问题,因此研究人员继续研究不丢失数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**[8,9]是同步复制的一种变体已经在一些系统如Microsoft Azure存储[10,11])中成功实现。
> 如果主库故障,那么异步复制系统会丢失数据可能是一个严重的问题,因此研究人员继续研究不丢失数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**[8,9]是同步复制的一种变体已经在一些系统如Microsoft Azure存储【10,11】)中成功实现。
>
> 复制的一致性与**共识consensus**使几个节点就某个值达成一致之间有着密切的联系第9章将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。
>
@ -83,7 +83,7 @@
可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违反高可用的目标。幸运的是,拉起一个从库通常不需要停机。从概念上讲,过程如下所示:
1. 在某个时间点对领导者的数据库取一个一致的快照如果可能的话而不必锁定整个数据库。大多数数据库都具有这个功能因为它是备份必需的。在某些情况下需要第三方工具如MySQL的*innobackupex* [12]
1. 在某个时间点对领导者的数据库取一个一致的快照如果可能的话而不必锁定整个数据库。大多数数据库都具有这个功能因为它是备份必需的。在某些情况下需要第三方工具如MySQL的*innobackupex* 【12】
2. 将快照复制到新的跟随者节点。
3. 追随者连接到领导者并请求快照后发生的所有数据更改。这要求快照与领导者的复制日志中的精确位置关联。该位置有不同的名称例如PostgreSQL将其称为**日志序列号lsn, log sequence number**MySQL将其称为**二进制日志坐标binlog coordinates**。
4. 当追随者处理完快照之后积压的数据变化,我们说它**赶上caught up**了主库。现在它可以继续处理领导者产生的数据变化了。
@ -107,16 +107,16 @@
故障转移可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的领导者)或自动进行。自动故障转移过程通常由以下步骤组成:
1. 确认领导者失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是使用**超时Timeout**节点相互之间频繁地来回传递消息并且如果一个节点在一段时间内例如30秒没有响应就认为它挂了计划内故意关闭领导者不算
2. 选择一个新的领导者。这可以通过选举过程(领导者由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点controller node**来指定新的领导者。领导者的最佳人选通常是拥有旧领导者最新数据副本的追随者(最小化数据损失)。让所有的节点同意一个新的领导是一个**共识**问题在第9章详细讨论。
2. 选择一个新的领导者。这可以通过选举过程(领导者由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点controller node**来指定新的领导者。领导者的最佳人选通常是拥有旧领导者最新数据副本的追随者(最小化数据损失)。让所有的节点同意一个新的领导是一个**共识**问题,在[第9章](ch9.md)详细讨论。
3. 重新配置系统以使用新的领导者。客户端现在需要将他们的写请求发送给新领导者(将在“请求路由”中讨论这个问题)。如果老领导回来,可能仍然认为自己是领导者,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个追随者。
故障转移会出现很多大麻烦:
* 如果使用异步复制,则新主库可能没有收到老主库宕机前最后的写入操作。在选出新主库后,如果老主库重新加入集群,新主库在此期间可能会收到冲突的写入,那这些写入该如何处理?最常见的解决方案是简单丢弃老主库未复制的写入,这很可能打破客户对于数据持久性的期望。
* 如果数据库需要和其他外部存储相协调那么丢弃写入内容是极其危险的操作。例如在GitHub [13]的一场事故中一个过时的MySQL从库被提升为主库。数据库使用自增ID作为主键因为新主库的计数器落后于老主库的计数器所以新主库重新分配了一些已经被老主库分配掉的ID作为主键。这些主键也在Redis中使用主键重用使得MySQL和Redis中数据产生不一致最后导致一些私有数据泄漏到错误的用户手中。
* 如果数据库需要和其他外部存储相协调那么丢弃写入内容是极其危险的操作。例如在GitHub 【13】的一场事故中一个过时的MySQL从库被提升为主库。数据库使用自增ID作为主键因为新主库的计数器落后于老主库的计数器所以新主库重新分配了一些已经被老主库分配掉的ID作为主键。这些主键也在Redis中使用主键重用使得MySQL和Redis中数据产生不一致最后导致一些私有数据泄漏到错误的用户手中。
* 发生某些故障时见第8章可能会出现两个节点都以为自己是领导者的情况。这种情况称为**脑裂(split brain)**非常危险如果两个领导者都可以接受写操作却没有冲突解决机制参见第168页的“多主复制”那么数据就可能丢失或损坏。一些系统采取了安全防范措施当检测到两个领导节点同时存在时会关闭其中一个节点[^ii],但设计粗糙的机制可能最后会导致两个节点都被关闭[14]
* 发生某些故障时(见[第8章](ch8.md))可能会出现两个节点都以为自己是领导者的情况。这种情况称为**脑裂(split brain)**非常危险如果两个领导者都可以接受写操作却没有冲突解决机制参见第168页的“多主复制”那么数据就可能丢失或损坏。一些系统采取了安全防范措施当检测到两个领导节点同时存在时会关闭其中一个节点[^ii],但设计粗糙的机制可能最后会导致两个节点都被关闭【14】
[^ii]: 这种机制称为**击剑fencing**,充满感情的术语是:***爆彼之头STONITHShoot The Other Node In The Head***。
@ -124,7 +124,7 @@
这些问题没有简单的解决方案。因此即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障转移。
节点故障;不可靠的网络;对副本一致性,持久性,可用性和延迟的权衡 这些问题实际上是分布式系统中的基本问题。第8章和第9章将更深入地讨论它们。
节点故障;不可靠的网络;对副本一致性,持久性,可用性和延迟的权衡 ,这些问题实际上是分布式系统中的基本问题。[第8章](ch8.md)[第9章](ch9.md)将更深入地讨论它们。
### 复制日志的实现
@ -146,16 +146,16 @@
#### 传输预写式日志WAL
在第3章中我们讨论了存储引擎如何在磁盘上表示数据并且我们发现通常每个写操作都附加到日志中
[第3章](ch3.md)中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现通常每个写操作都附加到日志中:
* 对于日志结构存储引擎(请参阅“SSTables和LSM-tree”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
* 对于日志结构存储引擎(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
* 对于覆写单个磁盘块的B树请参阅第79页的“B树”每次修改都会先写入**预写式日志WAL, Write Ahead Log**,以便崩溃后索引可以恢复到一个一致的状态。
在任何一种情况下,日志都是包含所有数据库写入的只追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,领导者还可以通过网络将其发送给其追随者。
当追随者应用这个日志时,它会建立和领导者一模一样数据结构的副本。
PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录的数据非常底层WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本通常不可能在领导者和追随者上运行不同版本的数据库软件。
PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志记录的数据非常底层WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本通常不可能在领导者和追随者上运行不同版本的数据库软件。
看上去这可能只是一个微小的实现细节但却可能对运维产生巨大的影响。如果复制协议允许跟随者使用比领导者更新的软件版本则可以先执行升级跟随者然后执行故障转移使升级后的节点之一成为新的领导者从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配传输WAL经常出现这种情况则此类升级需要停机。
@ -169,19 +169,19 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
* 对于删除的行,日志包含足够的信息来唯一标识已删除的行。通常是主键,但是如果表上没有主键,则需要记录所有列的旧值。
* 对于更新的行,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少所有已更改的列的新值)。
修改多行的事务会生成多个这样的日志记录,后面跟着一条记录,指出事务已经提交。 MySQL的二进制日志当配置为使用基于行的复制时使用这种方法[17]
修改多行的事务会生成多个这样的日志记录,后面跟着一条记录,指出事务已经提交。 MySQL的二进制日志当配置为使用基于行的复制时使用这种方法【17】
由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。
对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统(如数据),这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存[18]。 这种技术被称为**捕获数据变更change data capture**第11章将重新讲到它。
对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统(如数据),这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存【18】。 这种技术被称为**捕获数据变更change data capture**第11章将重新讲到它。
#### 基于触发器的复制
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果您只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果您需要冲突解决逻辑(参阅第171页的“处理写入冲突”),则可能需要将复制移动到应用程序层。
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果您只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果您需要冲突解决逻辑(参阅“[处理写入冲突](#处理写入冲突)”),则可能需要将复制移动到应用程序层。
一些工具如Oracle GoldenGate [19],可以通过读取数据库日志,使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能:触发器和存储过程。
一些工具如Oracle GoldenGate 【19】,可以通过读取数据库日志,使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能:触发器和存储过程。
触发器允许您注册在数据库系统中发生数据更改写入事务时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中使用外部程序读取这个表再加上任何业务逻辑处理会后将数据变更复制到另一个系统去。例如Databus for Oracle [20]和Bucardo for Postgres [21]就是这样工作的。
触发器允许您注册在数据库系统中发生数据更改写入事务时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中使用外部程序读取这个表再加上任何业务逻辑处理会后将数据变更复制到另一个系统去。例如Databus for Oracle 【20】和Bucardo for Postgres 【21】就是这样工作的。
基于触发器的复制通常比其他复制方法具有更高的开销,并且比数据库的内置复制更容易出错,也有很多限制。然而由于其灵活性,仍然是很有用的。
@ -195,9 +195,9 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
在这种扩展体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制 - 如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置将是非常不可靠的。
不幸的是,当应用程序从异步追随者读取时,如果追随者落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对领导者和跟随者执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在跟随者中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,追随者最终会赶上并与领导者保持一致。出于这个原因,这种效应被称为**最终一致性eventually consistency**[^iii][22,23]
不幸的是,当应用程序从异步追随者读取时,如果追随者落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对领导者和跟随者执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在跟随者中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,追随者最终会赶上并与领导者保持一致。出于这个原因,这种效应被称为**最终一致性eventually consistency**[^iii]【22,23】
[^iii]: 道格拉斯·特里Douglas Terry等人创造了术语最终一致性。 [24] 并经由Werner Vogels [22]推广成为许多NoSQL项目的战吼。 然而不只有NoSQL数据库是最终一致的关系型数据库中的异步复制追随者也有相同的特性。
[^iii]: 道格拉斯·特里Douglas Terry等人创造了术语最终一致性。 【24】 并经由Werner Vogels 【22】推广成为许多NoSQL项目的战吼。 然而不只有NoSQL数据库是最终一致的关系型数据库中的异步复制追随者也有相同的特性。
“最终”一词故意含糊不清:总的来说,副本落后的程度是没有限制的。在正常的操作中,**复制延迟replication lag**,即写入主库到反映至追随者之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。
@ -223,7 +223,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
* 客户端可以记住最近一次写入的时间戳,系统需要确保从库为该用户提供任何查询时,该时间戳前的变更都已经传播到了本从库中。如果当前从库不够新,则可以从另一个从库读,或者等待从库追赶上来。
时间戳可以是逻辑时间戳(指示写入顺序的东西,例如日志序列号)或实际系统时钟(在这种情况下,时钟同步变得至关重要;参阅“不可靠的时钟”)。
时间戳可以是逻辑时间戳(指示写入顺序的东西,例如日志序列号)或实际系统时钟(在这种情况下,时钟同步变得至关重要;参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”)。
* 如果您的副本分布在多个数据中心(出于可用性目的与用户尽量在地理上接近),则会增加复杂性。任何需要由领导者提供服务的请求都必须路由到包含主库的数据中心。
@ -246,7 +246,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
**图5-4 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。**
**单调读Monotonic reads**[23]是这种异常不会发生的保证。这是一个比**强一致性strong consistency**更弱,但比**最终一致性eventually consistency**更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
**单调读Monotonic reads**【23】是这种异常不会发生的保证。这是一个比**强一致性strong consistency**更弱,但比**最终一致性eventually consistency**更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取不同的用户可以从不同的副本读取。例如可以基于用户ID的散列来选择副本而不是随机选择副本。但是如果该副本失败用户的查询将需要重新路由到另一个副本。
@ -264,7 +264,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
这两句话之间有因果关系Cake夫人听到了Poons先生的问题并回答了这个问题。
现在,想象第三个人正在通过从库来听这个对话。 Cake夫人说的内容是从一个延迟很低的从库读取的但Poons先生所说的内容从库的延迟要大的多见图5-5。 于是,这个观察者会听到以下内容:
现在,想象第三个人正在通过从库来听这个对话。 Cake夫人说的内容是从一个延迟很低的从库读取的但Poons先生所说的内容从库的延迟要大的多[图5-5](img/fig5-5.png))。 于是,这个观察者会听到以下内容:
> *Mrs. Cake*
> 通常约十秒钟Mr. Poons.
@ -273,13 +273,13 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
> Mrs. Cake你能看到多远的未来
对于观察者来说看起来好像Cake夫人在Poons先生发问前就回答了这个问题。
这种超能力让人印象深刻,但也会把人搞糊涂。[25]
这种超能力让人印象深刻,但也会把人搞糊涂。【25】
![](img/fig5-5.png)
**图5-5 如果某些分区的复制速度慢于其他分区,那么观察者在看到问题之前可能会看到答案。**
防止这种异常,需要另一种类型的保证:**一致前缀读consistent prefix reads**[23]。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
防止这种异常,需要另一种类型的保证:**一致前缀读consistent prefix reads**【23】。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
这是**分区partitioned****分片sharded**数据库中的一个特殊问题将在第6章中讨论。如果数据库总是以相同的顺序应用写入则读取总是会看到一致的前缀所以这种异常不会发生。但是在许多分布式数据库中不同的分区独立运行因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
@ -305,7 +305,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
[^iv]: 如果数据库被分区见第6章每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。
基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为多领导者配置(也称多主、多活复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者。
基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为**多领导者配置**(也称多主、多活复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者。
### 多主复制的应用场景
@ -335,11 +335,11 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
数据中心之间的通信通常穿过公共互联网,这可能不如数据中心内的本地网络可靠。单主配置对这数据中心间的连接问题非常敏感,因为通过这个连接进行的写操作是同步的。采用异步复制功能的多活配置通常能更好地承受网络问题:临时的网络中断并不会妨碍正在处理的写入。
有些数据库默认情况下支持多主配置但使用外部工具实现也很常见例如用于MySQL的Tungsten Replicator [26]用于PostgreSQL的BDR [27]以及用于Oracle的GoldenGate [19]
有些数据库默认情况下支持多主配置但使用外部工具实现也很常见例如用于MySQL的Tungsten Replicator 【26】用于PostgreSQL的BDR【27】以及用于Oracle的GoldenGate 【19】
尽管多主复制有这些优势但也有一个很大的缺点两个不同的数据中心可能会同时修改相同的数据写冲突是必须解决的如图5-6中“冲突解决”。本书将在“处理写入冲突”中详细讨论这个问题。
尽管多主复制有这些优势,但也有一个很大的缺点:两个不同的数据中心可能会同时修改相同的数据,写冲突是必须解决的(如[图5-6](img/fig5-6.png)中“[冲突解决](#冲突解决)”)。本书将在“[处理写入冲突](#处理写入冲突)”中详细讨论这个问题。
由于多主复制在许多数据库中都属于改装的功能,所以常常存在微妙的配置缺陷,且经常与其他数据库功能之间出现意外的反应。例如自增主键、触发器、完整性约束等,都可能会有麻烦。因此,多主复制往往被认为是危险的领域,应尽可能避免[28]
由于多主复制在许多数据库中都属于改装的功能,所以常常存在微妙的配置缺陷,且经常与其他数据库功能之间出现意外的反应。例如自增主键、触发器、完整性约束等,都可能会有麻烦。因此,多主复制往往被认为是危险的领域,应尽可能避免【28】
#### 需要离线操作的客户端
@ -351,21 +351,21 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
从架构的角度来看,这种设置实际上与数据中心之间的多领导者复制类似,每个设备都是一个“数据中心”,而它们之间的网络连接是极度不可靠的。从历史上各类日历同步功能的破烂实现可以看出,想把多活配好是多么困难的一件事。
有一些工具旨在使这种多领导者配置更容易。例如CouchDB就是为这种操作模式而设计的[29]
有一些工具旨在使这种多领导者配置更容易。例如CouchDB就是为这种操作模式而设计的【29】
#### 协同编辑
实时协作编辑应用程序允许多个人同时编辑文档。例如Etherpad [30]和Google Docs [31]允许多人同时编辑文本文档或电子表格(该算法在第174页的“自动冲突解决”中简要讨论。我们通常不会将协作式编辑视为数据库复制问题但与前面提到的离线编辑用例有许多相似之处。当一个用户编辑文档时所做的更改将立即应用到其本地副本Web浏览器或客户端应用程序中的文档状态并异步复制到服务器和编辑同一文档的任何其他用户。
实时协作编辑应用程序允许多个人同时编辑文档。例如Etherpad 【30】和Google Docs 【31】允许多人同时编辑文本文档或电子表格(该算法在“[自动冲突解决](#自动冲突解决)”中简要讨论。我们通常不会将协作式编辑视为数据库复制问题但与前面提到的离线编辑用例有许多相似之处。当一个用户编辑文档时所做的更改将立即应用到其本地副本Web浏览器或客户端应用程序中的文档状态并异步复制到服务器和编辑同一文档的任何其他用户。
如果要保证不会发生编辑冲突,则应用程序必须先取得文档的锁定,然后用户才能对其进行编辑。如果另一个用户想要编辑同一个文档,他们首先必须等到第一个用户提交修改并释放锁定。这种协作模式相当于在领导者上进行交易的单领导者复制。
但是,为了加速协作,您可能希望将更改的单位设置得非常小(例如,一个按键),并避免锁定。这种方法允许多个用户同时进行编辑,但同时也带来了多领导者复制的所有挑战,包括需要解决冲突[32]
但是,为了加速协作,您可能希望将更改的单位设置得非常小(例如,一个按键),并避免锁定。这种方法允许多个用户同时进行编辑,但同时也带来了多领导者复制的所有挑战,包括需要解决冲突【32】
### 处理写入冲突
多领导者复制的最大问题是可能发生写冲突,这意味着需要解决冲突。
例如,考虑一个由两个用户同时编辑的维基页面,如[图5-7](img/fig5-7.png)所示。用户1将页面的标题从A更改为B并且用户2同时将标题从A更改为C。每个用户的更改已成功应用到其本地主库。但当异步复制时会发现冲突[33]。单主数据库中不会出现此问题。
例如,考虑一个由两个用户同时编辑的维基页面,如[图5-7](img/fig5-7.png)所示。用户1将页面的标题从A更改为B并且用户2同时将标题从A更改为C。每个用户的更改已成功应用到其本地主库。但当异步复制时会发现冲突【33】。单主数据库中不会出现此问题。
![](img/fig5-7.png)
@ -379,7 +379,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
#### 避免冲突
处理冲突的最简单的策略就是避免它们:如果应用程序可以确保特定记录的所有写入都通过同一个领导者,那么冲突就不会发生。由于多领导者复制处理的许多实现冲突相当不好,避免冲突是一个经常推荐的方法[34]
处理冲突的最简单的策略就是避免它们:如果应用程序可以确保特定记录的所有写入都通过同一个领导者,那么冲突就不会发生。由于多领导者复制处理的许多实现冲突相当不好,避免冲突是一个经常推荐的方法【34】
例如,在用户可以编辑自己的数据的应用程序中,可以确保来自特定用户的请求始终路由到同一数据中心,并使用该数据中心的领导者进行读写。不同的用户可能有不同的“家庭”数据中心(可能根据用户的地理位置选择),但从任何用户的角度来看,配置基本上都是单一的领导者。
@ -395,9 +395,9 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
实现冲突合并解决有多种途径:
* 给每个写入一个唯一的ID例如一个时间戳一个长的随机数一个UUID或者一个键和值的哈希挑选最高ID的写入作为胜利者并丢弃其他写入。如果使用时间戳这种技术被称为**最后写入胜利LWW, last write wins**。虽然这种方法很流行,但是很容易造成数据丢失[35]。我们将在本章末尾更详细地讨论LWW第184页的“检测并发写入”
* 给每个写入一个唯一的ID例如一个时间戳一个长的随机数一个UUID或者一个键和值的哈希挑选最高ID的写入作为胜利者并丢弃其他写入。如果使用时间戳这种技术被称为**最后写入胜利LWW, last write wins**。虽然这种方法很流行,但是很容易造成数据丢失【35】。我们将在[本章末尾](#检测并发写入)更详细地讨论LWW
* 为每个副本分配一个唯一的IDID编号更高的写入具有更高的优先级。这种方法也意味着数据丢失。
* 以某种方式将这些值合并在一起 - 例如按字母顺序排序然后连接它们在图5-7中合并的标题可能类似于“B / C”
* 以某种方式将这些值合并在一起 - 例如,按字母顺序排序,然后连接它们(在[图5-7](img/fig5-7.png)合并的标题可能类似于“B/C”
* 在保留所有信息的显式数据结构中记录冲突,并编写解决冲突的应用程序代码(也许通过提示用户的方式)。
@ -414,19 +414,19 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
当检测到冲突时所有冲突写入被存储。下一次读取数据时会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突并将结果写回数据库。例如CouchDB以这种方式工作。
请注意,冲突解决通常适用于单个行或文档层面,而不是整个事务[36]。因此如果您有一个事务会原子性地进行几次不同的写入请参阅第7章则对于冲突解决而言每个写入仍需分开单独考虑。
请注意,冲突解决通常适用于单个行或文档层面,而不是整个事务【36】。因此如果您有一个事务会原子性地进行几次不同的写入请参阅第7章则对于冲突解决而言每个写入仍需分开单独考虑。
> #### 题外话:自动冲突解决
>
> 冲突解决规则可能很快变得复杂,并且自定义代码可能容易出错。亚马逊是一个经常被引用的例子,由于冲突解决处理程序令人惊讶的效果:一段时间以来,购物车上的冲突解决逻辑将保留添加到购物车的物品,但不包括从购物车中移除的物品。因此,顾客有时会看到物品重新出现在他们的购物车中,即使他们之前已经被移走[37]
> 冲突解决规则可能很快变得复杂,并且自定义代码可能容易出错。亚马逊是一个经常被引用的例子,由于冲突解决处理程序令人惊讶的效果:一段时间以来,购物车上的冲突解决逻辑将保留添加到购物车的物品,但不包括从购物车中移除的物品。因此,顾客有时会看到物品重新出现在他们的购物车中,即使他们之前已经被移走【37】
>
> 已经有一些有趣的研究来自动解决由于数据修改引起的冲突。有几行研究值得一提:
>
> * **无冲突复制数据类型Conflict-free replicated datatypes**CRDT[32,38]是可以由多个用户同时编辑的集合映射有序列表计数器等的一系列数据结构它们以合理的方式自动解决冲突。一些CRDT已经在Riak 2.0中实现[39,40]
> * **可合并的持久数据结构Mergeable persistent data structures**[41]显式跟踪历史记录类似于Git版本控制系统并使用三向合并功能而CRDT使用双向合并
> * **可执行的转换operational transformation**[42]是Etherpad [30]和Google Docs [31]等合作编辑应用背后的冲突解决算法。它是专为同时编辑项目的有序列表而设计的,例如构成文本文档的字符列表。
> * **无冲突复制数据类型Conflict-free replicated datatypes**CRDT【32,38】是可以由多个用户同时编辑的集合映射有序列表计数器等的一系列数据结构它们以合理的方式自动解决冲突。一些CRDT已经在Riak 2.0中实现【39,40】
> * **可合并的持久数据结构Mergeable persistent data structures**【41】显式跟踪历史记录类似于Git版本控制系统并使用三向合并功能而CRDT使用双向合并
> * **可执行的转换operational transformation**[42]是Etherpad 【30】和Google Docs 【31】等合作编辑应用背后的冲突解决算法。它是专为同时编辑项目的有序列表而设计的,例如构成文本文档的字符列表。
>
> 这些算法在数据库中的实现还很年轻,但很可能将来它们将被集成到更多的复制数据系统中。自动冲突解决方案可以使应用程序处理多领导者数据同步更为简单。
>
@ -439,7 +439,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
其他类型的冲突可能更为微妙,难以发现。例如,考虑一个会议室预订系统:它记录谁腚了哪个时间段的哪个房间。应用需要确保每个房间只有一组人同时预定(即不得有相同房间的重叠预订)。在这种情况下,如果同时为同一个房间创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查可用性,如果两次预订是由两个不同的领导者进行的,则可能会有冲突。
现在还没有一个现成的答案但在接下来的章节中我们将追溯到对这个问题有很好的理解。我们将在第7章中看到更多的冲突示例在第12章中我们将讨论用于检测和解决复制系统中冲突的可扩展方法。
现在还没有一个现成的答案但在接下来的章节中我们将追溯到对这个问题有很好的理解。我们将在第7章中看到更多的冲突示例[第12章](ch12.md)中我们将讨论用于检测和解决复制系统中冲突的可扩展方法。
@ -451,25 +451,25 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
**图5-8 三个可以设置多领导者复制的示例拓扑。**
最普遍的拓扑是全部到全部图5-8 [c]其中每个领导者将其写入每个其他领导。但是也会使用更多受限制的拓扑例如默认情况下MySQL仅支持**环形拓扑circular topology**[34],其中每个节点接收来自一个节点的写入,并将这些写入(加上自己的任何写入)转发给另一个节点。另一种流行的拓扑结构具有星形的形状[^v]。个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。
最普遍的拓扑是全部到全部([图5-8 [c]]()其中每个领导者将其写入每个其他领导。但是也会使用更多受限制的拓扑例如默认情况下MySQL仅支持**环形拓扑circular topology**【34】,其中每个节点接收来自一个节点的写入,并将这些写入(加上自己的任何写入)转发给另一个节点。另一种流行的拓扑结构具有星形的形状[^v]。个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。
[^v]: 不要与星型模式混淆(请参阅第93页的“星星和雪花分析模式”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
[^v]: 不要与星型模式混淆(请参阅“[分析模式:星型还是雪花](ch2.md#分析模式:星型还是雪花)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
在圆形和星形拓扑中,写入可能需要在到达所有副本之前通过多个节点。因此,节点需要转发从其他节点收到的数据更改。为了防止无限复制循环,每个节点被赋予一个唯一的标识符,并且在复制日志中,每个写入都被标记了所有已经通过的节点的标识符[43]。当一个节点收到用自己的标识符标记的数据更改时,该数据更改将被忽略,因为节点知道它已经被处理。
在圆形和星形拓扑中,写入可能需要在到达所有副本之前通过多个节点。因此,节点需要转发从其他节点收到的数据更改。为了防止无限复制循环,每个节点被赋予一个唯一的标识符,并且在复制日志中,每个写入都被标记了所有已经通过的节点的标识符【43】。当一个节点收到用自己的标识符标记的数据更改时,该数据更改将被忽略,因为节点知道它已经被处理。
循环和星型拓扑的问题是,如果只有一个节点发生故障,则可能会中断其他节点之间的复制消息流,导致它们无法通信,直到节点修复。拓扑结构可以重新配置为在发生故障的节点上工作,但在大多数部署中,这种重新配置必须手动完成。更密集连接的拓扑结构(例如全部到全部)的容错性更好,因为它允许消息沿着不同的路径传播,避免单点故障。
另一方面全能拓扑也可能有问题。特别是一些网络链接可能比其他网络链接更快例如由于网络拥塞结果是一些复制消息可能“超过”其他复制消息如图5-9所示。
另一方面,全能拓扑也可能有问题。特别是,一些网络链接可能比其他网络链接更快(例如,由于网络拥塞),结果是一些复制消息可能“超过”其他复制消息,如[图5-9](img/fig5-9.png)所示。
![](img/fig5-9.png)
**图5-9 使用多主程序复制时,可能会在某些副本中写入错误的顺序。**
在图5-9中客户端A向*林登万Leader One*的表中插入一行客户端B在主库3上更新该行。然而主库2可以以不同的顺序接收写入它可以首先接收更新其中从它的角度来看是对数据库中不存在的行的更新并且仅在稍后接收到相应的插入其应该在更新之前
在图5-9中客户端A向**林登万Leader One**的表中插入一行客户端B在主库3上更新该行。然而主库2可以以不同的顺序接收写入它可以首先接收更新其中从它的角度来看是对数据库中不存在的行的更新并且仅在稍后接收到相应的插入其应该在更新之前
这是一个因果关系的问题,类似于我们在第165页上的“一致前缀读取”中看到的更新取决于先前的插入所以我们需要确保所有节点先处理插入然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的因为时钟不可能被充分地同步以便在主库2处正确地排序这些事件见第8章
这是一个因果关系的问题,类似于我们在“[一致前缀读](ch8.md#一致前缀读)”中看到的更新取决于先前的插入所以我们需要确保所有节点先处理插入然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的因为时钟不可能被充分地同步以便在主库2处正确地排序这些事件[第8章](ch8.md))。
要正确排序这些事件,可以使用一种称为**版本向量version vectors**的技术,本章稍后将讨论这种技术(参阅“[检测并发写入](#检测并发写入)”。然而冲突检测技术在许多多领导者复制系统中执行得不好。例如在撰写本文时PostgreSQL BDR不提供写入的因果排序[27]而Tungsten Replicator for MySQL甚至不尝试检测冲突[34]
要正确排序这些事件,可以使用一种称为**版本向量version vectors**的技术,本章稍后将讨论这种技术(参阅“[检测并发写入](#检测并发写入)”。然而冲突检测技术在许多多领导者复制系统中执行得不好。例如在撰写本文时PostgreSQL BDR不提供写入的因果排序【27】而Tungsten Replicator for MySQL甚至不尝试检测冲突【34】
如果您正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试您的数据库,以确保它确实提供了您认为具有的保证。
@ -479,7 +479,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
我们在本章到目前为止所讨论的复制方法 ——单主复制、多主复制——都是这样的想法:客户端向一个主库发送写请求,而数据库系统负责将写入复制到其他副本。主库决定写入的顺序,而从库按相同顺序应用主库的写入。
一些数据存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。最早的一些的复制数据系统是**无领导的leaderless**[1,44]但是在关系数据库主导的时代这个想法几乎已被忘却。在亚马逊将其用于其内部的Dynamo系统[^vi]之后,它再一次成为数据库的一种时尚架构[37] .Dynamo不适用于Amazon以外的用户。 令人困惑的是AWS提供了一个名为DynamoDB的托管数据库产品它使用了完全不同的体系结构它基于单主程序复制。 RiakCassandra和Voldemort是由Dynamo启发的无领导复制模型的开源数据存储所以这类数据库也被称为*Dynamo风格*。
一些数据存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。最早的一些的复制数据系统是**无领导的leaderless**【1,44】但是在关系数据库主导的时代这个想法几乎已被忘却。在亚马逊将其用于其内部的Dynamo系统[^vi]之后,它再一次成为数据库的一种时尚架构【37】。Dynamo不适用于Amazon以外的用户。 令人困惑的是AWS提供了一个名为DynamoDB的托管数据库产品它使用了完全不同的体系结构它基于单主程序复制。 RiakCassandra和Voldemort是由Dynamo启发的无领导复制模型的开源数据存储所以这类数据库也被称为*Dynamo风格*。
[^vi]: Dynamo不适用于Amazon以外的用户。 令人困惑的是AWS提供了一个名为DynamoDB的托管数据库产品它使用了完全不同的体系结构它基于单引导程序复制。
@ -487,9 +487,9 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
### 当节点故障时写入数据库
假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于主机的配置中,如果要继续处理写入,则可能需要执行故障切换(请参阅第133页的「处理节点中断」)。
假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于主机的配置中,如果要继续处理写入,则可能需要执行故障切换(参阅「[处理节点宕机](#处理节点宕机)」)。
另一方面在无领导配置中故障切换不存在。图5-10显示了发生了什么事情客户端用户1234并行发送写入到所有三个副本并且两个可用副本接受写入但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的在用户1234已经收到两个确定的响应之后我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
另一方面,在无领导配置中,故障切换不存在。[图5-10](img/fig5-10.png)显示了发生了什么事情客户端用户1234并行发送写入到所有三个副本并且两个可用副本接受写入但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的在用户1234已经收到两个确定的响应之后我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
![](img/fig5-10.png)
@ -497,7 +497,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
现在想象一下,不可用的节点重新联机,客户端开始读取它。节点关闭时发生的任何写入都从该节点丢失。因此,如果您从该节点读取数据,则可能会将陈旧(过时)值视为响应。
为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新(参阅第174页的“检测并发写入”)。
为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新(参阅“[检测并发写入](#检测并发写入)”)。
#### 读修复和反熵
@ -507,7 +507,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
* ***读修复Read repair***
当客户端并行读取多个节点时它可以检测到任何陈旧的响应。例如在图5-10中用户2345获得了来自Replica 3的版本6值和来自副本1和2的版本7值。客户端发现副本3具有陈旧值并将新值写回复制品。这种方法适用于频繁阅读的值。
当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在[图5-10](img/fig5-10.png)用户2345获得了来自Replica 3的版本6值和来自副本1和2的版本7值。客户端发现副本3具有陈旧值并将新值写回复制品。这种方法适用于频繁阅读的值。
***反熵过程Anti-entropy process***
@ -517,11 +517,11 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
#### 读写的法定人数
在图5-10的示例中我们认为即使仅在三个副本中的两个上进行处理写入仍然是成功的。如果三个副本中只有一个接受了写入会怎样我们能推多远呢
[图5-10](img/fig5-10.png)的示例中,我们认为即使仅在三个副本中的两个上进行处理,写入仍然是成功的。如果三个副本中只有一个接受了写入,会怎样?我们能推多远呢?
如果我们知道,每个成功的写操作意味着在三个副本中至少有两个出现,这意味着至多有一个副本可能是陈旧的。因此,如果我们从至少两个副本读取,我们可以确定至少有一个是最新的。如果第三个副本停机或响应速度缓慢,则读取仍可以继续返回最新值。
更一般地说如果有n个副本每个写入必须由w节点确认才能被认为是成功的并且我们必须至少为每个读取查询r个节点。 (在我们的例子中,$n = 3w = 2r = 2$)。只要$w + r> n$我们期望在读取时获得最新的值因为r个读取中至少有一个节点是最新的。遵循这些r值w值的读写称为**法定人数quorum**[^vii]的读和写。[44] 你可以认为r和w是有效读写所需的最低票数。
更一般地说如果有n个副本每个写入必须由w节点确认才能被认为是成功的并且我们必须至少为每个读取查询r个节点。 (在我们的例子中,$n = 3w = 2r = 2$)。只要$w + r> n$我们期望在读取时获得最新的值因为r个读取中至少有一个节点是最新的。遵循这些r值w值的读写称为**法定人数quorum**[^vii]的读和写。【44】 你可以认为r和w是有效读写所需的最低票数。
[^vii]: 有时候这种法定人数被称为严格的法定人数,相对“松散的法定人数”而言(见“[松散法定人数与带提示的接力](#松散法定人数与带提示的接力)”)
@ -535,7 +535,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
* 如果$w <n$如果节点不可用我们仍然可以处理写入
* 如果$r <n$如果节点不可用我们仍然可以处理读取
* 对于$n = 3w = 2r = 2$,我们可以容忍一个不可用的节点。
* 对于$n = 5w = 3r = 3$,我们可以容忍两个不可用的节点。 这个案例如图5-11所示。
* 对于$n = 5w = 3r = 3$,我们可以容忍两个不可用的节点。 这个案例如[图5-11](img/fig5-11.png)所示。
* 通常读取和写入操作始终并行发送到所有n个副本。 参数w和r决定我们等待多少个节点即在我们认为读或写成功之前有多少个节点需要报告成功。
![](img/fig5-11.png)
@ -548,9 +548,9 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
### 仲裁一致性的局限性
如果你有n个副本并且你选择w和r使得$w + r> n$你通常可以期望每个读取返回为一个键写的最近的值。情况就是这样因为你写的节点集合和你读过的节点集合必须重叠。也就是说您读取的节点中必须至少有一个具有最新值的节点如图5-11所示
如果你有n个副本并且你选择w和r使得$w + r> n$,你通常可以期望每个读取返回为一个键写的最近的值。情况就是这样,因为你写的节点集合和你读过的节点集合必须重叠。也就是说,您读取的节点中必须至少有一个具有最新值的节点(如[图5-11](img/fig5-11.png)所示)。
通常r和w被选为多数超过 n/2 )节点,因为这确保了$w + r> n$,同时仍然容忍多达$n/2$个节点故障。但是,法定人数不一定必须是大多数,只是读写使用的节点交集至少需要包括一个节点。其他法定人数的配置是可能的,这使得分布式算法的设计有一定的灵活性[45]
通常r和w被选为多数超过 $n/2$ )节点,因为这确保了$w + r> n$,同时仍然容忍多达$n/2$个节点故障。但是,法定人数不一定必须是大多数,只是读写使用的节点交集至少需要包括一个节点。其他法定人数的配置是可能的,这使得分布式算法的设计有一定的灵活性【45】
您也可以将w和r设置为较小的数字以使$w + r≤n$即法定条件不满足。在这种情况下读取和写入操作仍将被发送到n个节点但操作成功只需要少量的成功响应。
@ -558,16 +558,16 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
但是,即使在$w + r> n$的情况下,也可能存在返回陈旧值的边缘情况。这取决于实现,但可能的情况包括:
* 如果使用松散的法定人数(见“[松散法定人数与带提示的接力](#松散法定人数与带提示的接力)”w个写入和r个读取落在完全不同的节点上因此r节点和w之间不再保证有重叠节点[46]
* 如果两个写入同时发生不清楚哪一个先发生。在这种情况下唯一安全的解决方案是合并并发写入请参阅第171页的“处理写入冲突”。如果根据时间戳最后写入成功挑选出胜者则由于时钟偏差[35],写入可能会丢失。我们将返回第184页上的“检测并发写入”中的此主题。
* 如果使用松散的法定人数(见“[松散法定人数与带提示的接力](#松散法定人数与带提示的接力)”w个写入和r个读取落在完全不同的节点上因此r节点和w之间不再保证有重叠节点【46】
* 如果两个写入同时发生不清楚哪一个先发生。在这种情况下唯一安全的解决方案是合并并发写入请参阅第171页的“处理写入冲突”。如果根据时间戳最后写入成功挑选出胜者则由于时钟偏差[35],写入可能会丢失。我们将返回“[检测并发写入](#检测并发写入)”中的此主题。
* 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
* 如果写操作在某些副本上成功而在其他节点上失败例如因为某些节点上的磁盘已满在小于w个副本上写入成功。所以整体判定写入失败但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败后续的读取仍然可能会读取这次失败写入的值[47]
* 如果写操作在某些副本上成功而在其他节点上失败例如因为某些节点上的磁盘已满在小于w个副本上写入成功。所以整体判定写入失败但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败后续的读取仍然可能会读取这次失败写入的值【47】
* 如果携带新值的节点失败需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复则存储新值的副本数可能会低于w从而打破法定人数条件。
* 即使一切工作正常,有时也会不幸地出现关于**时序timing**的边缘情况在这种情况下您可能会感到不安因为我们将在第334页上的“[线性化和法定人数]()”中看到。
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数w和r来调整读取陈旧值的概率但把它们当成绝对的保证是不明智的。
尤其是,通常没有得到第161页上的“与延迟有关的问题”(读取您的写入,单调读取或一致的前缀读取)中讨论的保证,因此前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要**事务**或**共识**。我们将在[第七章](ch7.md)和[第九章](ch9.md)回到这些话题。
尤其是,通常没有得到“[与延迟有关的问题](#)”(读取您的写入,单调读取或一致的前缀读取)中讨论的保证,因此前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要**事务**或**共识**。我们将在[第七章](ch7.md)和[第九章](ch9.md)回到这些话题。
#### 监控陈旧度
@ -577,7 +577,7 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读取修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究并根据参数nw和r来预测陈旧读取的预期百分比[48]。不幸的是,这还不是很常见的做法,但是将过时测量值包含在数据库的标准度量标准中是一件好事。最终的一致性是故意模糊的保证,但是对于可操作性来说,能够量化“最终”是很重要的。
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究并根据参数nw和r来预测陈旧读取的预期百分比【48】。不幸的是,这还不是很常见的做法,但是将过时测量值包含在数据库的标准度量标准中是一件好事。最终的一致性是故意模糊的保证,但是对于可操作性来说,能够量化“最终”是很重要的。
### 松散法定人数与带提示的接力
@ -590,23 +590,23 @@ PostgreSQL和Oracle等使用这种复制方法[16]。主要缺点是日志记录
* 将错误返回给我们无法达到w或r节点的法定数量的所有请求是否更好
* 或者我们是否应该接受写入然后将它们写入一些可达的节点但不在n值通常存在的n个节点之间
后者被认为是一个**松散的法定人数sloppy quorum**[37]写和读仍然需要w和r成功的响应但是那些可能包括不在指定的n个“主”节点中的值。比方说如果你把自己锁在房子外面你可能会敲开邻居的门问你是否可以暂时停留在沙发上。
后者被认为是一个**松散的法定人数sloppy quorum**【37】写和读仍然需要w和r成功的响应但是那些可能包括不在指定的n个“主”节点中的值。比方说如果你把自己锁在房子外面你可能会敲开邻居的门问你是否可以暂时停留在沙发上。
一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的“本地”节点。这就是所谓的**带提示的接力hinted handoff**。 (一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家。)
松散法定人数提高写入可用性特别有用只要有任何w节点可用数据库就可以接受写入。然而这意味着即使当$w + r> n$时也不能确定读取某个键的最新值因为最新的值可能已经临时写入了n之外的某些节点[47]
松散法定人数提高写入可用性特别有用只要有任何w节点可用数据库就可以接受写入。然而这意味着即使当$w + r> n$时也不能确定读取某个键的最新值因为最新的值可能已经临时写入了n之外的某些节点【47】
因此在传统意义上一个松散的法定人数实际上不是一个法定人数。这只是一个保证即数据存储在w节点的地方。不能保证r节点的读取直到提示已经完成。
在所有常见的Dynamo实现中松散法定人数是可选的。在Riak中它们默认是启用的而在Cassandra和Voldemort中它们默认是禁用的[46,49,50]
在所有常见的Dynamo实现中松散法定人数是可选的。在Riak中它们默认是启用的而在Cassandra和Voldemort中它们默认是禁用的【46,49,50】
#### 多数据中心操作
我们先前讨论了跨数据中心复制作为多主复制的用例请参阅第162页的“多重复制复制”)。无主复制还适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
我们先前讨论了跨数据中心复制作为多主复制的用例请参阅第162页的“[多重复制]()”)。无主复制还适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持副本的数量n包括所有数据中心的节点在配置中您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何每个来自客户端的写入都会发送到所有副本但客户端通常只等待来自其本地数据中心内的法定节点的确认从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生尽管配置有一定的灵活性[50,51]
Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持副本的数量n包括所有数据中心的节点在配置中您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何每个来自客户端的写入都会发送到所有副本但客户端通常只等待来自其本地数据中心内的法定节点的确认从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生尽管配置有一定的灵活性【50,51】
Riak将客户端和数据库节点之间的所有通信保持在一个数据中心本地因此n描述了一个数据中心内的副本数量。数据库集群之间的跨数据中心复制在后台异步发生其风格类似于多领导者复制[52]
Riak将客户端和数据库节点之间的所有通信保持在一个数据中心本地因此n描述了一个数据中心内的副本数量。数据库集群之间的跨数据中心复制在后台异步发生其风格类似于多领导者复制【52】
### 检测并发写入
@ -622,7 +622,7 @@ Dynamo风格的数据库允许多个客户端同时写入相同的Key这意
**图5-12 并发写入Dynamo风格的数据存储没有明确定义的顺序。**
如果每个节点只要接收到来自客户端的写入请求就简单地覆盖了某个键的值那么节点就会永久地不一致如图5-12中的最终获取请求所示节点2认为X的最终值是B而其他节点认为值是A.
如果每个节点只要接收到来自客户端的写入请求就简单地覆盖了某个键的值,那么节点就会永久地不一致,如[图5-12](img/fig5-12.png)中的最终获取请求所示节点2认为X的最终值是B而其他节点认为值是A.
为了最终达成一致,副本应该趋于相同的值。如何做到这一点?有人可能希望复制的数据库能够自动处理,但不幸的是,大多数的实现都很糟糕:如果你想避免丢失数据,你(应用程序开发人员)需要知道很多有关数据库冲突处理的内部信息。
@ -634,13 +634,13 @@ Dynamo风格的数据库允许多个客户端同时写入相同的Key这意
正如**“最近”**的引号所表明的这个想法其实颇具误导性。在图5-12的例子中当客户端向数据库节点发送写入请求时客户端都不知道另一个客户端因此不清楚哪一个先发生了。事实上说“发生”是没有意义的我们说写入是**并发concurrent**的,所以它们的顺序是不确定的。
即使写入没有自然的排序,我们也可以强制任意排序。例如,可以为每个写入附加一个时间戳,挑选最**“最近”**的最大时间戳,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为**最后写入为准LWW, last write wins**是Cassandra [53]唯一支持的冲突解决方法也是Riak [35]中的一个可选特征。
即使写入没有自然的排序,我们也可以强制任意排序。例如,可以为每个写入附加一个时间戳,挑选最**“最近”**的最大时间戳,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为**最后写入为准LWW, last write wins**是Cassandra 【53】唯一支持的冲突解决方法也是Riak 【35】中的一个可选特征。
LWW实现了最终收敛的目标但以**持久性**为代价如果同一个Key有多个并发写入即使它们都被报告为客户端成功因为它们被写入w个副本其中一个写道会生存下来其他的将被无声丢弃。此外LWW甚至可能会删除不是并发的写入我们将在第291页的“[有序事件的时间戳]()”中讨论。
LWW实现了最终收敛的目标但以**持久性**为代价如果同一个Key有多个并发写入即使它们都被报告为客户端成功因为它们被写入w个副本其中一个写道会生存下来其他的将被无声丢弃。此外LWW甚至可能会删除不是并发的写入我们将在的“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中讨论。
有一些情况如缓存其中丢失的写入可能是可以接受的。如果丢失数据不可接受LWW是解决冲突的一个很烂的选择。
与LWW一起使用数据库的唯一安全方法是确保一个Key只写入一次然后视为不可变从而避免对同一个密钥进行并发更新。例如推荐使用Cassandra的方法是使用UUID作为键从而为每个写操作提供一个唯一的键[53]
与LWW一起使用数据库的唯一安全方法是确保一个Key只写入一次然后视为不可变从而避免对同一个密钥进行并发更新。例如推荐使用Cassandra的方法是使用UUID作为键从而为每个写操作提供一个唯一的键【53】
#### “此前发生”的关系和并发
@ -649,7 +649,7 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
* 在[图5-9](fig5-9.png)中两个写入不是并发的A的插入发生在B的增量之前因为B递增的值是A插入的值。换句话说B的操作建立在A的操作上所以B的操作必须有后来发生。我们也可以说B是**因果依赖causally dependent**于A
* 另一方面,[图5-12](fig5-12.png)中的两个写入是并发的当每个客户端启动操作时它不知道另一个客户端也正在执行操作同样的Key。因此操作之间不存在因果关系。
如果操作B了解操作A或者依赖于A或者以某种方式构建于操作A之上则操作A在另一个操作B之前发生。在另一个操作之前是否发生一个操作是定义什么并发的关键。事实上我们可以简单地说如果两个操作都不在另一个之前发生那么两个操作是并发的两个操作都不知道另一个[54]
如果操作B了解操作A或者依赖于A或者以某种方式构建于操作A之上则操作A在另一个操作B之前发生。在另一个操作之前是否发生一个操作是定义什么并发的关键。事实上我们可以简单地说如果两个操作都不在另一个之前发生那么两个操作是并发的两个操作都不知道另一个【54】
因此只要有两个操作A和B就有三种可能性A在B之前发生或者B在A之前发生或者A和B并发。我们需要的是一个算法来告诉我们两个操作是否是并发的。如果一个操作发生在另一个操作之前则后面的操作应该覆盖较早的操作但是如果这些操作是并发的则存在需要解决的冲突。
@ -659,7 +659,7 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
>
> 如果两个操作**“同时”**发生,似乎应该称为并发——但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否**同时**发生的,这个问题我们将在[第8章](ch8.md)中详细讨论。
>
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来[54],它引入了信息不能比光速更快的思想。因此,如果事件之间的时间短于光通过它们之间的距离,那么发生一定距离的两个事件不可能相互影响。
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】,它引入了信息不能比光速更快的思想。因此,如果事件之间的时间短于光通过它们之间的距离,那么发生一定距离的两个事件不可能相互影响。
>
> 在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作也可能是**并行的**。例如,如果网络缓慢或中断,两个操作间可能会出现一段时间间隔,且仍然是并发的,因为网络问题阻止一个操作意识到另一个操作的存在。
@ -674,14 +674,14 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
1. 客户端1将牛奶加入购物车。这是该键的第一次写入服务器成功存储了它并为其分配版本号1最后将值与版本号一起回送给客户端。
2. 客户端2将鸡蛋加入购物车不知道客户端1同时添加了牛奶客户端2认为它的鸡蛋是购物车中的唯一物品。服务器为此写入分配版本号2并将鸡蛋和牛奶存储为两个单独的值。然后它将这两个值**都**反回给客户端2并附上版本号2。
3. 客户端1不知道客户端2的写入想要将面粉加入购物车因此认为当前的购物车内容应该是 [牛奶,面粉]。它将此值与服务器先前向客户端1提供的版本号1一起发送到服务器。服务器可以从版本号中知道[牛奶,面粉]的写入取代了[牛奶]的先前值,但与[鸡蛋]的值是**并发**的。因此服务器将版本3分配给[牛奶,面粉]覆盖版本1值[牛奶]但保留版本2的值[蛋]并将所有的值返回给客户端1。
4. 同时客户端2想要加入火腿不知道客端户1刚刚加了面粉。客户端2在最后一个响应中从服务器收到了两个值[牛奶]和[蛋]所以客户端2现在合并这些值并添加火腿形成一个新的值[鸡蛋,牛奶,火腿]。它将这个值发送到服务器带着之前的版本号2。服务器检测到新值会覆盖版本2 [eggs]但新值也会与v3 [milkflour]**并发**所以剩下的两个值是v3 [milkflour]和v4[鸡蛋,牛奶,火腿]。
4. 同时客户端2想要加入火腿不知道客端户1刚刚加了面粉。客户端2在最后一个响应中从服务器收到了两个值[牛奶]和[蛋]所以客户端2现在合并这些值并添加火腿形成一个新的值[鸡蛋,牛奶,火腿]。它将这个值发送到服务器带着之前的版本号2。服务器检测到新值会覆盖版本2 [eggs]但新值也会与v3 [牛奶,面粉]**并发**所以剩下的两个值是v3 [milkflour]和v4[鸡蛋,牛奶,火腿]。
5. 最后客户端1想要加培根。它以前在v3中从服务器接收[牛奶,面粉]和[鸡蛋],所以它合并这些,添加培根,并将最终值[牛奶,面粉,鸡蛋,培根]连同版本号v3发往服务器。这会覆盖v3[牛奶,面粉](请注意[鸡蛋]已经在最后一步被覆盖但与v4[鸡蛋,牛奶,火腿]并发,所以服务器保留这两个并发值。
![](img/fig5-13.png)
**图5-13 捕获两个客户端之间的因果关系,同时编辑购物车。**
图5-13中的操作之间的数据流如图5-14所示。 箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。 在这个例子中,客户端永远不会完全掌握服务器上的数据,因为总是有另一个操作同时进行。 但是,旧版本的值最终会被覆盖,并且不会丢失任何写入。
[图5-13](img/fig5-13.png)中的操作之间的数据流如[图5-14](img/fig5-14.png)所示。 箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。 在这个例子中,客户端永远不会完全掌握服务器上的数据,因为总是有另一个操作同时进行。 但是,旧版本的值最终会被覆盖,并且不会丢失任何写入。
![](img/fig5-14.png)
@ -700,29 +700,29 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:如果多个操作并发发生,则客户端必须通过合并并发写入的值来擦屁股。 Riak称这些并发值**兄弟siblings**。
合并兄弟值本质上是与多领导者复制中的冲突解决相同的问题我们先前讨论过请参阅第171页的“处理写冲突”。一个简单的方法是根据版本号或时间戳最后写入胜利选择一个值但这意味着丢失数据。所以你可能需要在应用程序代码中做更聪明的事情。
合并兄弟值本质上是与多领导者复制中的冲突解决相同的问题我们先前讨论过请参阅第171页的“[处理写冲突](#处理写冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中做更聪明的事情。
以购物车为例一种合理的合并兄弟方法就是集合求并。在图5-14中最后的两个兄弟是[牛奶,面粉,鸡蛋,熏肉]和[鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋出现在两个,即使他们每个只写一次。合并的价值可能是像[牛奶,面粉,鸡蛋,培根,火腿],没有重复。
以购物车为例,一种合理的合并兄弟方法就是集合求并。在[图5-14](img/fig5-14.png)中,最后的两个兄弟是[牛奶,面粉,鸡蛋,熏肉]和[鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋出现在两个,即使他们每个只写一次。合并的价值可能是像[牛奶,面粉,鸡蛋,培根,火腿],没有重复。
然而,如果你想让人们也可以从他们的手推车中**删除**东西,而不是仅仅添加东西,那么把兄弟求并可能不会产生正确的结果:如果你合并了两个兄弟手推车,并且只在其中一个兄弟值里删掉了它,那么被删除的项目会重新出现在兄弟的并集中[37]。为了防止这个问题,一个项目在删除时不能简单地从数据库中删除;相反,系统必须留下一个具有合适版本号的标记,以指示合并兄弟时该项目已被删除。这种删除标记被称为**墓碑tombstone**。 (我们之前在第72页的“哈希索引”中的日志压缩的上下文中看到了墓碑。)
然而,如果你想让人们也可以从他们的手推车中**删除**东西,而不是仅仅添加东西,那么把兄弟求并可能不会产生正确的结果:如果你合并了两个兄弟手推车,并且只在其中一个兄弟值里删掉了它,那么被删除的项目会重新出现在兄弟的并集中【37】。为了防止这个问题,一个项目在删除时不能简单地从数据库中删除;相反,系统必须留下一个具有合适版本号的标记,以指示合并兄弟时该项目已被删除。这种删除标记被称为**墓碑tombstone**。 (我们之前在“[哈希索引”](ch3.md#哈希索引)中的日志压缩的上下文中看到了墓碑。)
因为在应用程序代码中合并兄弟是复杂且容易出错的,所以有一些数据结构被设计出来用于自动执行这种合并,如“[自动冲突解决]()”中讨论的。例如Riak的数据类型支持使用称为CRDT的数据结构家族[38,39,55]可以以合理的方式自动合并兄弟,包括保留删除。
因为在应用程序代码中合并兄弟是复杂且容易出错的,所以有一些数据结构被设计出来用于自动执行这种合并,如“[自动冲突解决]()”中讨论的。例如Riak的数据类型支持使用称为CRDT的数据结构家族【38,39,55】可以以合理的方式自动合并兄弟,包括保留删除。
#### 版本向量
图5-13中的示例只使用一个副本。如果有没有主库有多个副本算法如何改变
[图5-13](img/fig5-13.png)中的示例只使用一个副本。如果有没有主库,有多个副本,算法如何改变?
图5-13使用单个版本号来捕获操作之间的依赖关系但是当多个副本并发接受写入时这是不够的。相反除了对每个键使用版本号之外还需要在**每个副本**中版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些值,以及保留哪些值作为兄弟。
[图5-13](img/fig5-13.png)使用单个版本号来捕获操作之间的依赖关系,但是当多个副本并发接受写入时,这是不够的。相反,除了对每个键使用版本号之外,还需要在**每个副本**中版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些值,以及保留哪些值作为兄弟。
所有副本的版本号集合称为**版本向量version vector**[56]。这个想法的一些变体正在使用但最有趣的可能是在Riak 2.0 [58,59]中使用的**分散版本矢量dotted version vector**[57]。我们不会深入细节,但是它的工作方式与我们在购物车示例中看到的非常相似。
所有副本的版本号集合称为**版本向量version vector**【56】。这个想法的一些变体正在使用但最有趣的可能是在Riak 2.0 【58,59】中使用的**分散版本矢量dotted version vector**【57】。我们不会深入细节,但是它的工作方式与我们在购物车示例中看到的非常相似。
与图5-13中的版本号一样当读取值时版本向量会从数据库副本发送到客户端并且随后写入值时需要将其发送回数据库。 Riak将版本向量编码为一个字符串它称为**因果上下文causal context**)。版本向量允许数据库区分覆盖写入和并发写入。
[图5-13](img/fig5-13.png)中的版本号一样,当读取值时,版本向量会从数据库副本发送到客户端,并且随后写入值时需要将其发送回数据库。 Riak将版本向量编码为一个字符串它称为**因果上下文causal context**)。版本向量允许数据库区分覆盖写入和并发写入。
另外,就像在单个副本的例子中,应用程序可能需要合并兄弟。版本向量结构确保从一个副本读取并随后写回到另一个副本是安全的。这样做可能会创建兄弟,但只要兄弟姐妹合并正确,就不会丢失数据。
> #### 版本向量和向量时钟
>
> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——请参阅参考资料的细节[576061]。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——请参阅参考资料的细节【576061】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
>
## 本章小结
@ -745,8 +745,6 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
能够处理比单个机器更高的读取量可以通过对副本进行读取来处理
尽管是一个简单的目标 - 在几台机器上保留相同数据的副本,但复制却是一个非常棘手的问题。它需要仔细考虑并发和所有可能出错的事情,并处理这些故障的后果。至少,我们需要处理不可用的节点和网络中断(甚至不考虑更隐蔽的故障,例如由于软件错误导致的无提示数据损坏)。
我们讨论了复制的三种主要方法:
@ -764,8 +762,6 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
客户端发送每个写入到几个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。
每种方法都有优点和缺点。单主复制是非常流行的,因为它很容易理解,不需要担心冲突解决。在出现故障节点,网络中断和延迟峰值的情况下,多领导者和无领导者复制可以更加稳健,但代价很难推理,只能提供非常弱的一致性保证。
复制可以是同步的,也可以是异步的,在发生故障时对系统行为有深远的影响。尽管在系统运行平稳时异步复制速度很快,但是在复制滞后增加和服务器故障时要弄清楚会发生什么,这一点很重要。如果一个领导者失败了,并且你推动一个异步更新的追随者成为新的领导者,那么最近承诺的数据可能会丢失。
我们研究了一些可能由复制滞后引起的奇怪效应,我们讨论了一些有助于决定应用程序在复制滞后时的行为的一致性模型: