2
0
Fork 0
mirror of https://github.com/Vonng/ddia.git synced 2026-06-21 00:47:05 +08:00

format fix

This commit is contained in:
Vonng 2018-06-03 21:43:15 +08:00
parent a52f11f30d
commit 11087acbc8
5 changed files with 289 additions and 288 deletions

367
ch5.md
View file

@ -10,7 +10,7 @@
[TOC]
复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在[第二部分简介](part-ii.md)中所讨论的那样,我们希望能复制数据,可能出于各种各样的原因:
复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在[第二部分简介](part-ii.md)中所讨论的那样,我们希望能复制数据,可能出于各种各样的原因:
* 使得数据与用户在地理上接近(从而减少延迟)
* 即使系统的一部分出现故障,系统也能继续工作(从而提高可用性)
@ -18,17 +18,17 @@
本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在[第6章](ch6.md)中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的**变更change**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者single leader****多领导者multi leader**和**无领导者leaderless**。几乎所有分布式数据库都使用这三种方法之一。
如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的**变更change**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者single leader****多领导者multi leader**和**无领导者leaderless**。几乎所有分布式数据库都使用这三种方法之一。
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?这些通常是数据库中的配置选项,细节因数据库而异,但原理在许多不同的实现中都类似。本章会讨论这些决策的后果。
在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?这些通常是数据库中的配置选项,细节因数据库而异,但原理在许多不同的实现中都类似。本章会讨论这些决策的后果。
数据库的复制算得上是老生常谈了 ——70年代研究得出的基本原则至今没有太大变化【1】因为网络的基本约束仍保持不变。然而在研究之外许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近发生的事。许多程序员都是这一领域的新手因此对于诸如**最终一致性eventual consistency**等问题存在许多误解。在“[复制延迟问题](#复制延迟问题)”一节,我们将更加精确地了解最终的一致性,并讨论诸如**读己之写read-your-writes**和**单调读monotonic read**保证等内容。
数据库的复制算得上是老生常谈了 ——70年代研究得出的基本原则至今没有太大变化【1】因为网络的基本约束仍保持不变。然而在研究之外许多开发人员仍然假设一个数据库只有一个节点。分布式数据库变为主流只是最近发生的事。许多程序员都是这一领域的新手因此对于诸如**最终一致性eventual consistency**等问题存在许多误解。在“[复制延迟问题](#复制延迟问题)”一节,我们将更加精确地了解最终的一致性,并讨论诸如**读己之写read-your-writes**和**单调读monotonic read**保证等内容。
## 领导者与追随者
存储数据库副本的每个节点称为**副本replica**。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
存储数据库副本的每个节点称为**副本replica**。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?
每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案被称为**基于领导者的复制leader-based replication**(也称**主动/被动active/passive** 或 **主/从master/slave**复制),如[图5-1](#fig5-1.png)所示。它的工作原理如下:
每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案被称为**基于领导者的复制leader-based replication**(也称**主动/被动active/passive** 或 **主/从master/slave**复制),如[图5-1](#fig5-1.png)所示。它的工作原理如下:
1. 副本之一被指定为**领导者leader**,也称为 **主库master** **首要primary**。当客户端要向数据库写入时,它必须将请求发送给**领导者**,领导者会将新数据写入其本地存储。
2. 其他副本被称为**追随者followers**,亦称为**只读副本read replicas****从库slaves****次要( sencondaries****热备hot-standby**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志replication log**记录或**变更流change stream**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。
@ -39,30 +39,30 @@
![](img/fig5-1.png)
**图5-1 基于领导者(主-从)的复制**
这种复制模式是许多关系数据库的内置功能如PostgreSQL从9.0版本开始MySQLOracle Data Guard 【2】和SQL Server的AlwaysOn可用性组【3】。 它也被用于一些非关系数据库包括MongoDBRethinkDB和Espresso 【4】。 最后基于领导者的复制并不仅限于数据库像Kafka 【5】和RabbitMQ高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统例如DRBD这样的块复制设备也与之类似。
这种复制模式是许多关系数据库的内置功能如PostgreSQL从9.0版本开始MySQLOracle Data Guard 【2】和SQL Server的AlwaysOn可用性组【3】。 它也被用于一些非关系数据库包括MongoDBRethinkDB和Espresso 【4】。 最后基于领导者的复制并不仅限于数据库像Kafka 【5】和RabbitMQ高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统例如DRBD这样的块复制设备也与之类似。
### 同步复制与异步复制
复制系统的一个重要细节是:复制是**同步synchronously**发生还是**异步asynchronously**发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)。
复制系统的一个重要细节是:复制是**同步synchronously**发生还是**异步asynchronously**发生。 (在关系型数据库中这通常是一个配置项,其他系统通常硬编码为其中一个)。
想象[图5-1](fig5-1.png)中发生的情况,网站的用户更新他们的个人头像。在某个时间点,客户向主库发送更新请求;不久之后主库就收到了请求。在某个时刻,主库又会将数据变更转发给自己的从库。最后,主库通知客户更新成功。
想象[图5-1](fig5-1.png)中发生的情况,网站的用户更新他们的个人头像。在某个时间点,客户向主库发送更新请求;不久之后主库就收到了请求。在某个时刻,主库又会将数据变更转发给自己的从库。最后,主库通知客户更新成功。
[图5-2](img/fig5-2.png)显示了系统各个组件之间的通信:用户客户端,主库和两个从库。时间从左到右流动。请求或响应消息用粗箭头表示。
![](img/fig5-2.png)
**图5-2 基于领导者的复制:一个同步从库和一个异步从库**
在[图5-2]()的示例中从库1的复制是同步的在向用户报告写入成功并使结果对其他用户可见之前主库需要等待从库1的确认确保从库1已经收到写入操作。以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的主库发送消息但不等待从库的响应。
在[图5-2]()的示例中从库1的复制是同步的在向用户报告写入成功并使结果对其他用户可见之前主库需要等待从库1的确认确保从库1已经收到写入操作。以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的主库发送消息但不等待从库的响应。
在这幅图中从库2处理消息前存在一个显着的延迟。通常情况下复制的速度相当快大多数数据库系统能在一秒向从库应用变更但它们不能提供复制用时的保证。有些情况下从库可能落后主库几分钟或更久例如从库正在从故障中恢复系统在最大容量附近运行或者如果节点间存在网络问题。
在这幅图中从库2处理消息前存在一个显着的延迟。通常情况下复制的速度相当快大多数数据库系统能在一秒向从库应用变更但它们不能提供复制用时的保证。有些情况下从库可能落后主库几分钟或更久例如从库正在从故障中恢复系统在最大容量附近运行或者如果节点间存在网络问题。
同步复制的优点是,从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
同步复制的优点是,从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为**半同步semi-synchronous**【7】。
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中**一个**跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为**半同步semi-synchronous**【7】。
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证**持久Durable**。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证**持久Durable**。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
弱化的持久性可能听起来像是一个坏的折衷,无论如何,异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在“[复制延迟问题](#复制延迟问题)”中回到这个问题。
弱化的持久性可能听起来像是一个坏的折衷,无论如何,异步复制已经被广泛使用了,特别当有很多追随者,或追随者异地分布时。 稍后将在“[复制延迟问题](#复制延迟问题)”中回到这个问题。
> ### 关于复制的研究
>
@ -73,11 +73,11 @@
### 设置新从库
有时候需要设置一个新的从库:也许是为了增加副本的数量,或替换失败的节点。如何确保新的从库拥有主库数据的精确副本?
有时候需要设置一个新的从库:也许是为了增加副本的数量,或替换失败的节点。如何确保新的从库拥有主库数据的精确副本?
简单地将数据文件从一个节点复制到另一个节点通常是不够的:客户端不断向数据库写入数据,数据总是在不断变化,标准的数据副本会在不同的时间点总是不一样。复制的结果可能没有任何意义。
简单地将数据文件从一个节点复制到另一个节点通常是不够的:客户端不断向数据库写入数据,数据总是在不断变化,标准的数据副本会在不同的时间点总是不一样。复制的结果可能没有任何意义。
可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违背高可用的目标。幸运的是,拉起新的从库通常并不需要停机。从概念上讲,过程如下所示:
可以通过锁定数据库(使其不可用于写入)来使磁盘上的文件保持一致,但是这会违背高可用的目标。幸运的是,拉起新的从库通常并不需要停机。从概念上讲,过程如下所示:
1. 在某个时刻获取主库的一致性快照如果可能而不必锁定整个数据库。大多数数据库都具有这个功能因为它是备份必需的。对于某些场景可能需要第三方工具例如MySQL的innobackupex 【12】。
2. 将快照复制到新的从库节点。
@ -88,19 +88,19 @@
### 处理节点宕机
系统中的任何节点都可能宕机,可能因为意外的故障,也可能由于计划内的维护(例如,重启机器以安装内核安全补丁)。对运维而言,能在系统不中断服务的情况下重启单个节点好处多多。我们的目标是,即使个别节点失效,也能保持整个系统运行,并尽可能控制节点停机带来的影响。
系统中的任何节点都可能宕机,可能因为意外的故障,也可能由于计划内的维护(例如,重启机器以安装内核安全补丁)。对运维而言,能在系统不中断服务的情况下重启单个节点好处多多。我们的目标是,即使个别节点失效,也能保持整个系统运行,并尽可能控制节点停机带来的影响。
如何通过基于主库的复制实现高可用?
如何通过基于主库的复制实现高可用?
#### 从库失效:追赶恢复
在其本地磁盘上,每个从库记录从主库收到的数据变更。如果从库崩溃并重新启动,或者,如果主库和从库之间的网络暂时中断,则比较容易恢复:从库可以从日志中知道,在发生故障之前处理的最后一个事务。因此,从库可以连接到主库,并请求在从库断开连接时发生的所有数据变更。当应用完所有这些变化后,它就赶上了主库,并可以像以前一样继续接收数据变更流。
在其本地磁盘上,每个从库记录从主库收到的数据变更。如果从库崩溃并重新启动,或者,如果主库和从库之间的网络暂时中断,则比较容易恢复:从库可以从日志中知道,在发生故障之前处理的最后一个事务。因此,从库可以连接到主库,并请求在从库断开连接时发生的所有数据变更。当应用完所有这些变化后,它就赶上了主库,并可以像以前一样继续接收数据变更流。
#### 主库失效:故障转移
主库失效处理起来相当棘手:其中一个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。这个过程被称为**故障转移failover**。
主库失效处理起来相当棘手:其中一个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。这个过程被称为**故障转移failover**。
故障转移可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动故障转移过程通常由以下步骤组成:
故障转移可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动故障转移过程通常由以下步骤组成:
1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用**超时Timeout**节点频繁地相互来回传递消息并且如果一个节点在一段时间内例如30秒没有响应就认为它挂了因为计划内维护而故意关闭主库不算
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点controller node**来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个**共识**问题,将在[第9章](ch9.md)详细讨论。
@ -128,7 +128,7 @@
#### 基于语句的复制
在最简单的情况下,主库记录下它执行的每个写入请求(**语句statement**)并将该语句日志发送给其从库。对于关系数据库来说,这意味着每个`INSERT``UPDATE`或`DELETE`语句都被转发给每个从库每个从库解析并执行该SQL语句就像从客户端收到一样。
在最简单的情况下,主库记录下它执行的每个写入请求(**语句statement**)并将该语句日志发送给其从库。对于关系数据库来说,这意味着每个`INSERT``UPDATE`或`DELETE`语句都被转发给每个从库每个从库解析并执行该SQL语句就像从客户端收到一样。
虽然听上去很合理,但有很多问题会搞砸这种复制方式:
@ -138,7 +138,7 @@
的确有办法绕开这些问题 ——例如,当语句被记录时,主库可以用固定的返回值替换任何不确定的函数调用,以便从库获得相同的值。但是由于边缘情况实在太多了,现在通常会选择其他的复制方法。
基于语句的复制在5.1版本前的MySQL中使用。因为它相当紧凑现在有时候也还在用。但现在在默认情况下如果语句中存在任何不确定性MySQL会切换到基于行的复制稍后讨论。 VoltDB使用了基于语句的复制但要求事务必须是确定性的以此来保证安全【15】。
基于语句的复制在5.1版本前的MySQL中使用。因为它相当紧凑现在有时候也还在用。但现在在默认情况下如果语句中存在任何不确定性MySQL会切换到基于行的复制稍后讨论。 VoltDB使用了基于语句的复制但要求事务必须是确定性的以此来保证安全【15】。
#### 传输预写式日志WAL
@ -149,15 +149,15 @@
在任何一种情况下,日志都是包含所有数据库写入的仅追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,主库还可以通过网络将其发送给其从库。
当从库应用这个日志时,它会建立和主库一模一样数据结构的副本。
当从库应用这个日志时,它会建立和主库一模一样数据结构的副本。
PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志记录的数据非常底层WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本通常不可能在主库和从库上运行不同版本的数据库软件。
PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志记录的数据非常底层WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合。如果数据库将其存储格式从一个版本更改为另一个版本通常不可能在主库和从库上运行不同版本的数据库软件。
看上去这可能只是一个微小的实现细节但却可能对运维产生巨大的影响。如果复制协议允许从库使用比主库更新的软件版本则可以先升级从库然后执行故障转移使升级后的节点之一成为新的主库从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配传输WAL经常出现这种情况则此类升级需要停机。
看上去这可能只是一个微小的实现细节但却可能对运维产生巨大的影响。如果复制协议允许从库使用比主库更新的软件版本则可以先升级从库然后执行故障转移使升级后的节点之一成为新的主库从而执行数据库软件的零停机升级。如果复制协议不允许版本不匹配传输WAL经常出现这种情况则此类升级需要停机。
#### 逻辑日志复制(基于行)
另一种方法是,复制和存储引擎使用不同的日志格式,这样可以使复制日志从存储引擎内部分离出来。这种复制日志被称为逻辑日志,以将其与存储引擎的(物理)数据表示区分开来。
另一种方法是,复制和存储引擎使用不同的日志格式,这样可以使复制日志从存储引擎内部分离出来。这种复制日志被称为逻辑日志,以将其与存储引擎的(物理)数据表示区分开来。
关系数据库的逻辑日志通常是以行的粒度描述对数据库表的写入的记录序列:
@ -167,49 +167,49 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
修改多行的事务会生成多个这样的日志记录,后面跟着一条记录,指出事务已经提交。 MySQL的二进制日志当配置为使用基于行的复制时使用这种方法【17】。
由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。
由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。
对于外部应用程序来说逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统如数据这一点很有用例如复制到数据仓库进行离线分析或建立自定义索引和缓存【18】。 这种技术被称为**捕获数据变更change data capture**第11章将重新讲到它。
对于外部应用程序来说逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统如数据这一点很有用例如复制到数据仓库进行离线分析或建立自定义索引和缓存【18】。 这种技术被称为**捕获数据变更change data capture**第11章将重新讲到它。
#### 基于触发器的复制
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果您只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果您需要冲突解决逻辑(参阅“[处理写入冲突](#处理写入冲突)”),则可能需要将复制移动到应用程序层。
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果您只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果您需要冲突解决逻辑(参阅“[处理写入冲突](#处理写入冲突)”),则可能需要将复制移动到应用程序层。
一些工具如Oracle Golden Gate 【19】可以通过读取数据库日志使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能触发器和存储过程。
一些工具如Oracle Golden Gate 【19】可以通过读取数据库日志使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能触发器和存储过程。
触发器允许您注册在数据库系统中发生数据更改写入事务时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中使用外部程序读取这个表再加上任何业务逻辑处理会后将数据变更复制到另一个系统去。例如Databus for Oracle 【20】和Bucardo for Postgres 【21】就是这样工作的。
触发器允许您注册在数据库系统中发生数据更改写入事务时自动执行的自定义应用程序代码。触发器有机会将更改记录到一个单独的表中使用外部程序读取这个表再加上任何业务逻辑处理会后将数据变更复制到另一个系统去。例如Databus for Oracle 【20】和Bucardo for Postgres 【21】就是这样工作的。
基于触发器的复制通常比其他复制方法具有更高的开销,并且比数据库的内置复制更容易出错,也有很多限制。然而由于其灵活性,仍然是很有用的。
基于触发器的复制通常比其他复制方法具有更高的开销,并且比数据库的内置复制更容易出错,也有很多限制。然而由于其灵活性,仍然是很有用的。
## 复制延迟问题
容忍节点故障只是需要复制的一个原因。正如在[第二部分](part-ii.md)的介绍中提到的,另一个原因是可扩展性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更接近用户)。
容忍节点故障只是需要复制的一个原因。正如在[第二部分](part-ii.md)的介绍中提到的,另一个原因是可扩展性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更接近用户)。
基于主库的复制要求所有写入都由单个节点处理但只读查询可以由任何副本处理。所以对于读多写少的场景Web上的常见模式一个有吸引力的选择是创建很多从库并将读请求分散到所有的从库上去。这样能减小主库的负载并允许向最近的副本发送读请求。
基于主库的复制要求所有写入都由单个节点处理但只读查询可以由任何副本处理。所以对于读多写少的场景Web上的常见模式一个有吸引力的选择是创建很多从库并将读请求分散到所有的从库上去。这样能减小主库的负载并允许向最近的副本发送读请求。
在这种扩展体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制——如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
在这种扩展体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制——如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为**最终一致性eventually consistency**[^iii]【22,23】
不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为**最终一致性eventually consistency**[^iii]【22,23】
[^iii]: 道格拉斯·特里Douglas Terry等人创造了术语最终一致性。 【24】 并经由Werner Vogels 【22】推广成为许多NoSQL项目的战吼。 然而不只有NoSQL数据库是最终一致的关系型数据库中的异步复制追随者也有相同的特性。
“最终”一词故意含糊不清:总的来说,副本落后的程度是没有限制的。在正常的操作中,**复制延迟replication lag**,即写入主库到反映至从库之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。
“最终”一词故意含糊不清:总的来说,副本落后的程度是没有限制的。在正常的操作中,**复制延迟replication lag**,即写入主库到反映至从库之间的延迟,可能仅仅是几分之一秒,在实践中并不显眼。但如果系统在接近极限的情况下运行,或网络中存在问题,延迟可以轻而易举地超过几秒,甚至几分钟。
因为滞后时间太长引入的不一致性,可不仅是一个理论问题,更是应用设计中会遇到的真实问题。本节将重点介绍三个由复制延迟问题的例子,并简述解决这些问题的一些方法。
因为滞后时间太长引入的不一致性,可不仅是一个理论问题,更是应用设计中会遇到的真实问题。本节将重点介绍三个由复制延迟问题的例子,并简述解决这些问题的一些方法。
### 读己之写
许多应用程序让用户提交一些数据,然后查看他们提交的内容。可能是用户数据库中的记录,也可能是对讨论主题的评论,或其他类似的内容。提交新数据时,必须将其发送给领导者,但是当用户查看数据时,可以从追随者读取。如果数据经常被查看,但只是偶尔写入,这是非常合适的。
许多应用让用户提交一些数据,然后查看他们提交的内容。可能是用户数据库中的记录,也可能是对讨论主题的评论,或其他类似的内容。提交新数据时,必须将其发送给领导者,但是当用户查看数据时,可以从追随者读取。如果数据经常被查看,但只是偶尔写入,这是非常合适的。
但对于异步复制,问题就来了。如[图5-3](fig5-3.png)所示:如果用户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户而言,看起来好像是刚提交的数据丢失了,用户会不高兴,可以理解。
但对于异步复制,问题就来了。如[图5-3](fig5-3.png)所示:如果用户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户而言,看起来好像是刚提交的数据丢失了,用户会不高兴,可以理解。
![](img/fig5-3.png)
**图5-3 用户写入后从旧副本中读取数据。需要写后读(read-after-write)的一致性来防止这种异常**
在这种情况下,我们需要**读写一致性read-after-write consistency**,也称为**读己之写一致性read-your-writes consistency**【24】。这是一个保证如果用户重新加载页面他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。
在这种情况下,我们需要**读写一致性read-after-write consistency**,也称为**读己之写一致性read-your-writes consistency**【24】。这是一个保证如果用户重新加载页面他们总会看到他们自己提交的任何更新。它不会对其他用户的写入做出承诺其他用户的更新可能稍等才会看到。它保证用户自己的输入已被正确保存。
如何在基于领导者的复制系统中实现读后一致性?有各种可能的技术,这里说一些:
@ -234,17 +234,17 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
### 单调读
从异步从库读取第二个异常例子是,用户可能会遇到**时光倒流moving backward in time**。
从异步从库读取第二个异常例子是,用户可能会遇到**时光倒流moving backward in time**。
如果用户从不同从库进行多次读取,就可能发生这种情况。例如,[图5-4](img/fig5-4.png)显示了用户2345两次进行相同的查询首先查询了一个延迟很小的从库然后是一个延迟较大的从库。 如果用户刷新网页而每个请求被路由到一个随机的服务器这种情况是很有可能的。第一个查询返回最近由用户1234添加的评论但是第二个查询不返回任何东西因为滞后的从库还没有拉取写入内容。在效果上相比第一个查询第二个查询是在更早的时间点来观察系统。如果第一个查询没有返回任何内容那问题并不大因为用户2345可能不知道用户1234最近添加了评论。但如果用户2345先看见用户1234的评论然后又看到它消失那么对于用户2345就很让人头大了。
如果用户从不同从库进行多次读取,就可能发生这种情况。例如,[图5-4](img/fig5-4.png)显示了用户2345两次进行相同的查询首先查询了一个延迟很小的从库然后是一个延迟较大的从库。 如果用户刷新网页而每个请求被路由到一个随机的服务器这种情况是很有可能的。第一个查询返回最近由用户1234添加的评论但是第二个查询不返回任何东西因为滞后的从库还没有拉取写入内容。在效果上相比第一个查询第二个查询是在更早的时间点来观察系统。如果第一个查询没有返回任何内容那问题并不大因为用户2345可能不知道用户1234最近添加了评论。但如果用户2345先看见用户1234的评论然后又看到它消失那么对于用户2345就很让人头大了。
![](img/fig5-4.png)
**图5-4 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。**
**单调读Monotonic reads**【23】是这种异常不会发生的保证。这是一个比**强一致性strong consistency**更弱,但比**最终一致性eventually consistency**更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
**单调读Monotonic reads**【23】是这种异常不会发生的保证。这是一个比**强一致性strong consistency**更弱,但比**最终一致性eventually consistency**更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取不同的用户可以从不同的副本读取。例如可以基于用户ID的散列来选择副本而不是随机选择副本。但是如果该副本失败用户的查询将需要重新路由到另一个副本。
实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取不同的用户可以从不同的副本读取。例如可以基于用户ID的散列来选择副本而不是随机选择副本。但是如果该副本失败用户的查询将需要重新路由到另一个副本。
@ -260,7 +260,7 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
这两句话之间有因果关系Cake夫人听到了Poons先生的问题并回答了这个问题。
现在,想象第三个人正在通过从库来听这个对话。 Cake夫人说的内容是从一个延迟很低的从库读取的但Poons先生所说的内容从库的延迟要大的多见[图5-5](img/fig5-5.png))。 于是,这个观察者会听到以下内容:
现在,想象第三个人正在通过从库来听这个对话。 Cake夫人说的内容是从一个延迟很低的从库读取的但Poons先生所说的内容从库的延迟要大的多见[图5-5](img/fig5-5.png))。 于是,这个观察者会听到以下内容:
> *Mrs. Cake*
> 通常约十秒钟Mr. Poons.
@ -275,43 +275,43 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
**图5-5 如果某些分区的复制速度慢于其他分区,那么观察者在看到问题之前可能会看到答案。**
防止这种异常,需要另一种类型的保证:**一致前缀读consistent prefix reads**【23】。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
防止这种异常,需要另一种类型的保证:**一致前缀读consistent prefix reads**【23】。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
这是**分区partitioned****分片sharded**数据库中的一个特殊问题将在第6章中讨论。如果数据库总是以相同的顺序应用写入则读取总是会看到一致的前缀所以这种异常不会发生。但是在许多分布式数据库中不同的分区独立运行因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
这是**分区partitioned****分片sharded**数据库中的一个特殊问题将在第6章中讨论。如果数据库总是以相同的顺序应用写入则读取总是会看到一致的前缀所以这种异常不会发生。但是在许多分布式数据库中不同的分区独立运行因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“关系与并发”一节中返回这个主题。
一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“关系与并发”一节中返回这个主题。
### 复制延迟的解决方案
在使用最终一致的系统时,如果复制延迟增加到几分钟甚至几小时,则应该考虑应用程序的行为。如果答案是“没问题”,那很好。但如果结果对于用户来说是不好体验,那么设计系统来提供更强的保证是很重要的,例如**写后读**。明明是异步复制却假设复制是同步的,这是很多麻烦的根源。
在使用最终一致的系统时,如果复制延迟增加到几分钟甚至几小时,则应该考虑应用程序的行为。如果答案是“没问题”,那很好。但如果结果对于用户来说是不好体验,那么设计系统来提供更强的保证是很重要的,例如**写后读**。明明是异步复制却假设复制是同步的,这是很多麻烦的根源。
如前所述,应用程序可以提供比底层数据库更强有力的保证,例如通过主库进行某种读取。但在应用程序代码中处理这些问题是复杂的,容易出错。
如前所述,应用程序可以提供比底层数据库更强有力的保证,例如通过主库进行某种读取。但在应用程序代码中处理这些问题是复杂的,容易出错。
如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库“做了正确的事情”,那该多好呀。这就是**事务transaction**存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更假简单。
如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库“做了正确的事情”,那该多好呀。这就是**事务transaction**存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更假简单。
单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务。声称事务在性能和可用性上的代价太高,并断言在可扩展系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。第七章和第九章将回到事务的话题,并讨论一些替代机制。
单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务。声称事务在性能和可用性上的代价太高,并断言在可扩展系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。第七章和第九章将回到事务的话题,并讨论一些替代机制。
## 多主复制
本章到目前为止,我们只考虑使用单个领导者的复制架构。 虽然这是一种常见的方法,但也有一些有趣的选择。
本章到目前为止,我们只考虑使用单个领导者的复制架构。 虽然这是一种常见的方法,但也有一些有趣的选择。
基于领导者的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
基于领导者的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
[^iv]: 如果数据库被分区见第6章每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。
基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为**多领导者配置**(也称多主、多活复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者。
基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为**多领导者配置**(也称多主、多活复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者。
### 多主复制的应用场景
在单个数据中心内部使用多个主库很少是有意义的,因为好处很少超过复杂性的代价。 但在一些情况下,多活配置是也合理的。
在单个数据中心内部使用多个主库很少是有意义的,因为好处很少超过复杂性的代价。 但在一些情况下,多活配置是也合理的。
#### 运维多个数据中心
假如你有一个数据库,副本分散在好几个不同的数据中心(也许这样可以容忍单个数据中心的故障,或地理上更接近用户)。 使用常规的基于领导者的复制设置,主库必须位于其中一个数据中心,且所有写入都必须经过该数据中心。
假如你有一个数据库,副本分散在好几个不同的数据中心(也许这样可以容忍单个数据中心的故障,或地理上更接近用户)。 使用常规的基于领导者的复制设置,主库必须位于其中一个数据中心,且所有写入都必须经过该数据中心。
多领导者配置中可以在每个数据中心都有主库。 [图5-6](img/fig5-6.png)展示了这个架构的样子。 在每个数据中心内使用常规的主从复制;;在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。
多领导者配置中可以在每个数据中心都有主库。 [图5-6](img/fig5-6.png)展示了这个架构的样子。 在每个数据中心内使用常规的主从复制;在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。
![](img/fig5-6.png)
@ -331,37 +331,37 @@ 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](img/fig5-6.png)中“[冲突解决](#冲突解决)”)。本书将在“[处理写入冲突](#处理写入冲突)”中详细讨论这个问题。
尽管多主复制有这些优势,但也有一个很大的缺点:两个不同的数据中心可能会同时修改相同的数据,写冲突是必须解决的(如[图5-6](img/fig5-6.png)中“[冲突解决](#冲突解决)”)。本书将在“[处理写入冲突](#处理写入冲突)”中详细讨论这个问题。
由于多主复制在许多数据库中都属于改装的功能所以常常存在微妙的配置缺陷且经常与其他数据库功能之间出现意外的反应。例如自增主键、触发器、完整性约束等都可能会有麻烦。因此多主复制往往被认为是危险的领域应尽可能避免【28】。
由于多主复制在许多数据库中都属于改装的功能所以常常存在微妙的配置缺陷且经常与其他数据库功能之间出现意外的反应。例如自增主键、触发器、完整性约束等都可能会有麻烦。因此多主复制往往被认为是危险的领域应尽可能避免【28】。
#### 需要离线操作的客户端
多主复制的另一种适用场景是:应用程序在断网之后仍然需要继续工作。
多主复制的另一种适用场景是:应用程序在断网之后仍然需要继续工作。
例如,考虑手机,笔记本电脑和其他设备上的日历应用。无论设备目前是否有互联网连接,你需要能随时查看你的会议(发出读取请求),输入新的会议(发出写入请求)。如果在离线状态下进行任何更改,则设备下次上线时,需要与服务器和其他设备同步。
例如,考虑手机,笔记本电脑和其他设备上的日历应用。无论设备目前是否有互联网连接,你需要能随时查看你的会议(发出读取请求),输入新的会议(发出写入请求)。如果在离线状态下进行任何更改,则设备下次上线时,需要与服务器和其他设备同步。
在这种情况下,每个设备都有一个充当领导者的本地数据库(它接受写请求),并且在所有设备上的日历副本之间同步时,存在异步的多主复制过程。复制延迟可能是几小时甚至几天,具体取决于何时可以访问互联网。
在这种情况下,每个设备都有一个充当领导者的本地数据库(它接受写请求),并且在所有设备上的日历副本之间同步时,存在异步的多主复制过程。复制延迟可能是几小时甚至几天,具体取决于何时可以访问互联网。
从架构的角度来看,这种设置实际上与数据中心之间的多领导者复制类似,每个设备都是一个“数据中心”,而它们之间的网络连接是极度不可靠的。从历史上各类日历同步功能的破烂实现可以看出,想把多活配好是多么困难的一件事。
从架构的角度来看,这种设置实际上与数据中心之间的多领导者复制类似,每个设备都是一个“数据中心”,而它们之间的网络连接是极度不可靠的。从历史上各类日历同步功能的破烂实现可以看出,想把多活配好是多么困难的一件事。
有一些工具旨在使这种多领导者配置更容易。例如CouchDB就是为这种操作模式而设计的【29】。
有一些工具旨在使这种多领导者配置更容易。例如CouchDB就是为这种操作模式而设计的【29】。
#### 协同编辑
实时协作编辑应用程序允许多个人同时编辑文档。例如Etherpad 【30】和Google Docs 【31】允许多人同时编辑文本文档或电子表格该算法在“[自动冲突解决](#自动冲突解决)”中简要讨论。我们通常不会将协作式编辑视为数据库复制问题但与前面提到的离线编辑用例有许多相似之处。当一个用户编辑文档时所做的更改将立即应用到其本地副本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)
@ -369,25 +369,25 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
#### 同步与异步冲突检测
在单主数据库中,第二个写入将被阻塞,并等待第一个写入完成,或中止第二个写入事务,强制用户重试。另一方面,在多活配置中,两个写入都是成功的,并且在稍后的时间点仅仅异步地检测到冲突。那时要求用户解决冲突可能为时已晚。
在单主数据库中,第二个写入将被阻塞,并等待第一个写入完成,或中止第二个写入事务,强制用户重试。另一方面,在多活配置中,两个写入都是成功的,并且在稍后的时间点仅仅异步地检测到冲突。那时要求用户解决冲突可能为时已晚。
原则上,可以使冲突检测同步 - 即等待写入被复制到所有副本,然后再告诉用户写入成功。但是,通过这样做,您将失去多主复制的主要优点:允许每个副本独立接受写入。如果您想要同步冲突检测,那么您可以使用单主程序复制。
原则上,可以使冲突检测同步 - 即等待写入被复制到所有副本,然后再告诉用户写入成功。但是,通过这样做,您将失去多主复制的主要优点:允许每个副本独立接受写入。如果您想要同步冲突检测,那么您可以使用单主程序复制。
#### 避免冲突
处理冲突的最简单的策略就是避免它们如果应用程序可以确保特定记录的所有写入都通过同一个领导者那么冲突就不会发生。由于多领导者复制处理的许多实现冲突相当不好避免冲突是一个经常推荐的方法【34】。
处理冲突的最简单的策略就是避免它们如果应用程序可以确保特定记录的所有写入都通过同一个领导者那么冲突就不会发生。由于多领导者复制处理的许多实现冲突相当不好避免冲突是一个经常推荐的方法【34】。
例如,在用户可以编辑自己的数据的应用程序中,可以确保来自特定用户的请求始终路由到同一数据中心,并使用该数据中心的领导者进行读写。不同的用户可能有不同的“家庭”数据中心(可能根据用户的地理位置选择),但从任何用户的角度来看,配置基本上都是单一的领导者。
例如,在用户可以编辑自己的数据的应用程序中,可以确保来自特定用户的请求始终路由到同一数据中心,并使用该数据中心的领导者进行读写。不同的用户可能有不同的“家庭”数据中心(可能根据用户的地理位置选择),但从任何用户的角度来看,配置基本上都是单一的领导者。
但是,有时您可能需要更改指定的记录的主库——可能是因为一个数据中心出现故障,您需要将流量重新路由到另一个数据中心,或者可能是因为用户已经迁移到另一个位置,现在更接近不同的数据中心。在这种情况下,冲突避免会中断,你必须处理不同主库同时写入的可能性。
但是,有时您可能需要更改指定的记录的主库——可能是因为一个数据中心出现故障,您需要将流量重新路由到另一个数据中心,或者可能是因为用户已经迁移到另一个位置,现在更接近不同的数据中心。在这种情况下,冲突避免会中断,你必须处理不同主库同时写入的可能性。
#### 收敛至一致的状态
单主数据库按顺序应用写操作:如果同一个字段有多个更新,则最后一个写操作将确定该字段的最终值。
单主数据库按顺序应用写操作:如果同一个字段有多个更新,则最后一个写操作将确定该字段的最终值。
在多主配置中,写入顺序没有定义,所以最终值应该是什么并不清楚。在[图5-7](img/fig5-7.png)中在主库1中标题首先更新为B而后更新为C在主库2中首先更新为C然后更新为B。两个顺序都不是“更正确”的。
在多主配置中,写入顺序没有定义,所以最终值应该是什么并不清楚。在[图5-7](img/fig5-7.png)中在主库1中标题首先更新为B而后更新为C在主库2中首先更新为C然后更新为B。两个顺序都不是“更正确”的。
如果每个副本只是按照它看到写入的顺序写入那么数据库最终将处于不一致的状态最终值将是在主库1的C和主库2的B。这是不可接受的每个复制方案都必须确保数据在所有副本中最终都是相同的。因此数据库必须以一种**收敛convergent**的方式解决冲突,这意味着所有副本必须在所有变更复制完成时收敛至一个相同的最终值。
如果每个副本只是按照它看到写入的顺序写入那么数据库最终将处于不一致的状态最终值将是在主库1的C和主库2的B。这是不可接受的每个复制方案都必须确保数据在所有副本中最终都是相同的。因此数据库必须以一种**收敛convergent**的方式解决冲突,这意味着所有副本必须在所有变更复制完成时收敛至一个相同的最终值。
实现冲突合并解决有多种途径:
@ -400,23 +400,23 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
#### 自定义冲突解决逻辑
作为解决冲突最合适的方法可能取决于应用程序,大多数多主复制工具允许使用应用程序代码编写冲突解决逻辑。该代码可以在写入或读取时执行:
作为解决冲突最合适的方法可能取决于应用程序,大多数多主复制工具允许使用应用程序代码编写冲突解决逻辑。该代码可以在写入或读取时执行:
***写时执行***
只要数据库系统检测到复制更改日志中存在冲突就会调用冲突处理程序。例如Bucardo允许您为此编写一段Perl代码。这个处理程序通常不能提示用户——它在后台进程中运行并且必须快速执行。
只要数据库系统检测到复制更改日志中存在冲突就会调用冲突处理程序。例如Bucardo允许您为此编写一段Perl代码。这个处理程序通常不能提示用户——它在后台进程中运行并且必须快速执行。
***读时执行***
当检测到冲突时所有冲突写入被存储。下一次读取数据时会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突并将结果写回数据库。例如CouchDB以这种方式工作。
当检测到冲突时所有冲突写入被存储。下一次读取数据时会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突并将结果写回数据库。例如CouchDB以这种方式工作。
请注意冲突解决通常适用于单个行或文档层面而不是整个事务【36】。因此如果您有一个事务会原子性地进行几次不同的写入请参阅第7章则对于冲突解决而言每个写入仍需分开单独考虑。
请注意冲突解决通常适用于单个行或文档层面而不是整个事务【36】。因此如果您有一个事务会原子性地进行几次不同的写入请参阅第7章则对于冲突解决而言每个写入仍需分开单独考虑。
> #### 题外话:自动冲突解决
>
> 冲突解决规则可能很快变得复杂并且自定义代码可能容易出错。亚马逊是一个经常被引用的例子由于冲突解决处理程序令人惊讶的效果一段时间以来购物车上的冲突解决逻辑将保留添加到购物车的物品但不包括从购物车中移除的物品。因此顾客有时会看到物品重新出现在他们的购物车中即使他们之前已经被移走【37】。
> 冲突解决规则可能很快变得复杂并且自定义代码可能容易出错。亚马逊是一个经常被引用的例子由于冲突解决处理程序令人惊讶的效果一段时间以来购物车上的冲突解决逻辑将保留添加到购物车的物品但不包括从购物车中移除的物品。因此顾客有时会看到物品重新出现在他们的购物车中即使他们之前已经被移走【37】。
>
> 已经有一些有趣的研究来自动解决由于数据修改引起的冲突。有几行研究值得一提:
>
@ -431,97 +431,97 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
#### 什么是冲突?
有些冲突是显而易见的。在[图5-7](img/fig5-7.png)的例子中,两个写操作并发地修改了同一条记录中的同一个字段,并将其设置为两个不同的值。毫无疑问这是一个冲突。
有些冲突是显而易见的。在[图5-7](img/fig5-7.png)的例子中,两个写操作并发地修改了同一条记录中的同一个字段,并将其设置为两个不同的值。毫无疑问这是一个冲突。
其他类型的冲突可能更为微妙,难以发现。例如,考虑一个会议室预订系统:它记录谁腚了哪个时间段的哪个房间。应用需要确保每个房间只有一组人同时预定(即不得有相同房间的重叠预订)。在这种情况下,如果同时为同一个房间创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查可用性,如果两次预订是由两个不同的领导者进行的,则可能会有冲突。
其他类型的冲突可能更为微妙,难以发现。例如,考虑一个会议室预订系统:它记录谁腚了哪个时间段的哪个房间。应用需要确保每个房间只有一组人同时预定(即不得有相同房间的重叠预订)。在这种情况下,如果同时为同一个房间创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查可用性,如果两次预订是由两个不同的领导者进行的,则可能会有冲突。
现在还没有一个现成的答案但在接下来的章节中我们将追溯到对这个问题有很好的理解。我们将在第7章中看到更多的冲突示例在[第12章](ch12.md)中我们将讨论用于检测和解决复制系统中冲突的可扩展方法。
现在还没有一个现成的答案但在接下来的章节中我们将追溯到对这个问题有很好的理解。我们将在第7章中看到更多的冲突示例在[第12章](ch12.md)中我们将讨论用于检测和解决复制系统中冲突的可扩展方法。
### 多主复制拓扑
复制拓扑描述写入从一个节点传播到另一个节点的通信路径。如果你有两个领导者,如[图5-7]()所示只有一个合理的拓扑结构领导者1必须把他所有的写到领导者2反之亦然。有两个以上的领导各种不同的拓扑是可能的。[图5-8]()举例说明了一些例子。
复制拓扑描述写入从一个节点传播到另一个节点的通信路径。如果你有两个领导者,如[图5-7]()所示只有一个合理的拓扑结构领导者1必须把他所有的写到领导者2反之亦然。有两个以上的领导各种不同的拓扑是可能的。[图5-8]()举例说明了一些例子。
![](img/fig5-8.png)
**图5-8 三个可以设置多领导者复制的示例拓扑。**
最普遍的拓扑是全部到全部([图5-8 [c]]()其中每个领导者将其写入每个其他领导。但是也会使用更多受限制的拓扑例如默认情况下MySQL仅支持**环形拓扑circular topology**【34】其中每个节点接收来自一个节点的写入并将这些写入加上自己的任何写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状[^v]。个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。
最普遍的拓扑是全部到全部([图5-8 [c]]()其中每个领导者将其写入每个其他领导。但是也会使用更多受限制的拓扑例如默认情况下MySQL仅支持**环形拓扑circular topology**【34】其中每个节点接收来自一个节点的写入并将这些写入加上自己的任何写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状[^v]。个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。
[^v]: 不要与星型模式混淆(请参阅“[分析模式:星型还是雪花](ch2.md#分析模式:星型还是雪花)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
在圆形和星形拓扑中写入可能需要在到达所有副本之前通过多个节点。因此节点需要转发从其他节点收到的数据更改。为了防止无限复制循环每个节点被赋予一个唯一的标识符并且在复制日志中每个写入都被标记了所有已经通过的节点的标识符【43】。当一个节点收到用自己的标识符标记的数据更改时该数据更改将被忽略因为节点知道它已经被处理。
在圆形和星形拓扑中写入可能需要在到达所有副本之前通过多个节点。因此节点需要转发从其他节点收到的数据更改。为了防止无限复制循环每个节点被赋予一个唯一的标识符并且在复制日志中每个写入都被标记了所有已经通过的节点的标识符【43】。当一个节点收到用自己的标识符标记的数据更改时该数据更改将被忽略因为节点知道它已经被处理。
循环和星型拓扑的问题是,如果只有一个节点发生故障,则可能会中断其他节点之间的复制消息流,导致它们无法通信,直到节点修复。拓扑结构可以重新配置为在发生故障的节点上工作,但在大多数部署中,这种重新配置必须手动完成。更密集连接的拓扑结构(例如全部到全部)的容错性更好,因为它允许消息沿着不同的路径传播,避免单点故障。
循环和星型拓扑的问题是,如果只有一个节点发生故障,则可能会中断其他节点之间的复制消息流,导致它们无法通信,直到节点修复。拓扑结构可以重新配置为在发生故障的节点上工作,但在大多数部署中,这种重新配置必须手动完成。更密集连接的拓扑结构(例如全部到全部)的容错性更好,因为它允许消息沿着不同的路径传播,避免单点故障。
另一方面,全能拓扑也可能有问题。特别是,一些网络链接可能比其他网络链接更快(例如,由于网络拥塞),结果是一些复制消息可能“超过”其他复制消息,如[图5-9](img/fig5-9.png)所示。
另一方面,全能拓扑也可能有问题。特别是,一些网络链接可能比其他网络链接更快(例如,由于网络拥塞),结果是一些复制消息可能“超过”其他复制消息,如[图5-9](img/fig5-9.png)所示。
![](img/fig5-9.png)
**图5-9 使用多主程序复制时,可能会在某些副本中写入错误的顺序。**
在图5-9客户端A向**林登万Leader One**的表中插入一行客户端B在主库3上更新该行。然而主库2可以以不同的顺序接收写入它可以首先接收更新其中从它的角度来看是对数据库中不存在的行的更新并且仅在稍后接收到相应的插入其应该在更新之前
在[图5-9](img/fig5-9.png)客户端A向**林登万Leader One**的表中插入一行客户端B在主库3上更新该行。然而主库2可以以不同的顺序接收写入它可以首先接收更新其中从它的角度来看是对数据库中不存在的行的更新并且仅在稍后接收到相应的插入其应该在更新之前
这是一个因果关系的问题,类似于我们在“[一致前缀读](ch8.md#一致前缀读)”中看到的更新取决于先前的插入所以我们需要确保所有节点先处理插入然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的因为时钟不可能被充分地同步以便在主库2处正确地排序这些事件见[第8章](ch8.md))。
这是一个因果关系的问题,类似于我们在“[一致前缀读](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】。
如果您正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试您的数据库,以确保它确实提供了您认为具有的保证。
如果您正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试您的数据库,以确保它确实提供了您认为具有的保证。
## 无主复制
我们在本章到目前为止所讨论的复制方法 ——单主复制、多主复制——都是这样的想法:客户端向一个主库发送写请求,而数据库系统负责将写入复制到其他副本。主库决定写入的顺序,而从库按相同顺序应用主库的写入。
我们在本章到目前为止所讨论的复制方法 ——单主复制、多主复制——都是这样的想法:客户端向一个主库发送写请求,而数据库系统负责将写入复制到其他副本。主库决定写入的顺序,而从库按相同顺序应用主库的写入。
一些数据存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。最早的一些的复制数据系统是**无领导的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的托管数据库产品它使用了完全不同的体系结构它基于单引导程序复制。
在一些无领导者的实现中,客户端直接将写入发送到到几个副本中,而另一些情况下,一个**协调者coordinator**节点代表客户端进行写入。但与主库数据库不同,协调员不执行特定的写入顺序。我们将会看到,这种设计上的差异对数据库的使用方式有着深远的影响。
在一些无领导者的实现中,客户端直接将写入发送到到几个副本中,而另一些情况下,一个**协调者coordinator**节点代表客户端进行写入。但与主库数据库不同,协调员不执行特定的写入顺序。我们将会看到,这种设计上的差异对数据库的使用方式有着深远的影响。
### 当节点故障时写入数据库
假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于主机的配置中,如果要继续处理写入,则可能需要执行故障切换(参阅「[处理节点宕机](#处理节点宕机)」)。
假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于主机的配置中,如果要继续处理写入,则可能需要执行故障切换(参阅「[处理节点宕机](#处理节点宕机)」)。
另一方面,在无领导配置中,故障切换不存在。[图5-10](img/fig5-10.png)显示了发生了什么事情客户端用户1234并行发送写入到所有三个副本并且两个可用副本接受写入但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的在用户1234已经收到两个确定的响应之后我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
另一方面,在无领导配置中,故障切换不存在。[图5-10](img/fig5-10.png)显示了发生了什么事情客户端用户1234并行发送写入到所有三个副本并且两个可用副本接受写入但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的在用户1234已经收到两个确定的响应之后我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
![](img/fig5-10.png)
**图5-10 仲裁写入,法定读取,并在节点中断后读取修复。**
现在想象一下,不可用的节点重新联机,客户端开始读取它。节点关闭时发生的任何写入都从该节点丢失。因此,如果您从该节点读取数据,则可能会将陈旧(过时)值视为响应。
现在想象一下,不可用的节点重新联机,客户端开始读取它。节点关闭时发生的任何写入都从该节点丢失。因此,如果您从该节点读取数据,则可能会将陈旧(过时)值视为响应。
为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新(参阅“[检测并发写入](#检测并发写入)”)。
为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新(参阅“[检测并发写入](#检测并发写入)”)。
#### 读修复和反熵
复制方案应确保最终将所有数据复制到每个副本。在一个不可用的节点重新联机之后,它如何赶上它错过的写入?
复制方案应确保最终将所有数据复制到每个副本。在一个不可用的节点重新联机之后,它如何赶上它错过的写入?
在Dynamo风格的数据存储中经常使用两种机制
在Dynamo风格的数据存储中经常使用两种机制
***读修复Read repair***
当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在[图5-10](img/fig5-10.png)中用户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***
此外,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。与基于领导者的复制中的复制日志不同,此反熵过程不会以任何特定的顺序复制写入,并且在复制数据之前可能会有显着的延迟。
此外,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。与基于领导者的复制中的复制日志不同,此反熵过程不会以任何特定的顺序复制写入,并且在复制数据之前可能会有显着的延迟。
并不是所有的系统都实现了这两个;例如Voldemort目前没有反熵过程。请注意如果没有反熵过程某些副本中很少读取的值可能会丢失从而降低了持久性因为只有在应用程序读取值时才执行读取修复。
并不是所有的系统都实现了这两个;例如Voldemort目前没有反熵过程。请注意如果没有反熵过程某些副本中很少读取的值可能会丢失从而降低了持久性因为只有在应用程序读取值时才执行读取修复。
#### 读写的法定人数
在[图5-10](img/fig5-10.png)的示例中,我们认为即使仅在三个副本中的两个上进行处理,写入仍然是成功的。如果三个副本中只有一个接受了写入,会怎样?我们能推多远呢?
在[图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]: 有时候这种法定人数被称为严格的法定人数,相对“松散的法定人数”而言(见“[松散法定人数与带提示的接力](#松散法定人数与带提示的接力)”)
在Dynamo风格的数据库中参数nw和r通常是可配置的。一个常见的选择是使n为奇数通常为3或5并设置 $w = r =n + 1/ 2$(向上取整)。但是可以根据需要更改数字。例如,设置$w = n$和$r = 1$的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。
在Dynamo风格的数据库中参数nw和r通常是可配置的。一个常见的选择是使n为奇数通常为3或5并设置 $w = r =n + 1/ 2$(向上取整)。但是可以根据需要更改数字。例如,设置$w = n$和$r = 1$的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。
> 集群中可能有多于n的节点。集群的机器数可能多于副本书目但是任何给定的值只能存储在n个节点上。 这允许对数据集进行分区,从而支持可以放在一个节点上的数据集更大的数据集。 将在第6章回到分区。
>
@ -538,19 +538,19 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
**图5-11 如果$w + r > n$读取r个副本至少有一个r副本必然包含了最近的成功写入**
如果少于所需的w或r节点可用则写入或读取将返回错误。 由于许多原因,节点可能不可用:因为由于执行操作的错误(由于磁盘已满而无法写入)导致节点关闭(崩溃,关闭电源),由于客户端和服务器之间的网络中断 节点,或任何其他原因。 我们只关心节点是否返回了成功的响应,而不需要区分不同类型的错误。
如果少于所需的w或r节点可用则写入或读取将返回错误。 由于许多原因,节点可能不可用:因为由于执行操作的错误(由于磁盘已满而无法写入)导致节点关闭(崩溃,关闭电源),由于客户端和服务器之间的网络中断 节点,或任何其他原因。 我们只关心节点是否返回了成功的响应,而不需要区分不同类型的错误。
### 仲裁一致性的局限性
如果你有n个副本并且你选择w和r使得$w + r> n$,你通常可以期望每个读取返回为一个键写的最近的值。情况就是这样,因为你写的节点集合和你读过的节点集合必须重叠。也就是说,您读取的节点中必须至少有一个具有最新值的节点(如[图5-11](img/fig5-11.png)所示)。
如果你有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个节点但操作成功只需要少量的成功响应。
您也可以将w和r设置为较小的数字以使$w + r≤n$即法定条件不满足。在这种情况下读取和写入操作仍将被发送到n个节点但操作成功只需要少量的成功响应。
较小的w和r更有可能会读取过时的数据因为您的读取更有可能不包含具有最新值的节点。另一方面这种配置允许更低的延迟和更高的可用性如果存在网络中断并且许多副本变得无法访问则可以继续处理读取和写入的机会更大。只有当可达副本的数量低于w或r时数据库才分别变得不可用于写入或读取。
较小的w和r更有可能会读取过时的数据因为您的读取更有可能不包含具有最新值的节点。另一方面这种配置允许更低的延迟和更高的可用性如果存在网络中断并且许多副本变得无法访问则可以继续处理读取和写入的机会更大。只有当可达副本的数量低于w或r时数据库才分别变得不可用于写入或读取。
但是,即使在$w + r> n$的情况下,也可能存在返回陈旧值的边缘情况。这取决于实现,但可能的情况包括:
@ -563,52 +563,52 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数w和r来调整读取陈旧值的概率但把它们当成绝对的保证是不明智的。
尤其是,通常没有得到“[与延迟有关的问题](#)”(读取您的写入,单调读取或一致的前缀读取)中讨论的保证,因此前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要**事务**或**共识**。我们将在[第七章](ch7.md)和[第九章](ch9.md)回到这些话题。
尤其是,通常没有得到“[与延迟有关的问题](#)”(读取您的写入,单调读取或一致的前缀读取)中讨论的保证,因此前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要**事务**或**共识**。我们将在[第七章](ch7.md)和[第九章](ch9.md)回到这些话题。
#### 监控陈旧度
从运维的角度来看,监视你的数据库是否返回最新的结果是很重要的。即使应用可以容忍陈旧的读取,您也需要了解复制的健康状况。如果显着落后,应该提醒您,以便您可以调查原因(例如,网络中的问题或超载节点)。
从运维的角度来看,监视你的数据库是否返回最新的结果是很重要的。即使应用可以容忍陈旧的读取,您也需要了解复制的健康状况。如果显着落后,应该提醒您,以便您可以调查原因(例如,网络中的问题或超载节点)。
对于基于领导者的复制,数据库通常会公开复制滞后的度量标准,您可以将其提供给监视系统。这是可能的,因为写入按照相同的顺序应用于领导者和追随者,并且每个节点在复制日志中具有一个位置(在本地应用的写入次数)。通过从领导者的当前位置中减去随从者的当前位置,您可以测量复制滞后量。
对于基于领导者的复制,数据库通常会公开复制滞后的度量标准,您可以将其提供给监视系统。这是可能的,因为写入按照相同的顺序应用于领导者和追随者,并且每个节点在复制日志中具有一个位置(在本地应用的写入次数)。通过从领导者的当前位置中减去随从者的当前位置,您可以测量复制滞后量。
然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读取修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。
然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读取修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究并根据参数nw和r来预测陈旧读取的预期百分比【48】。不幸的是这还不是很常见的做法但是将过时测量值包含在数据库的标准度量标准中是一件好事。最终的一致性是故意模糊的保证但是对于可操作性来说能够量化“最终”是很重要的。
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究并根据参数nw和r来预测陈旧读取的预期百分比【48】。不幸的是这还不是很常见的做法但是将过时测量值包含在数据库的标准度量标准中是一件好事。最终的一致性是故意模糊的保证但是对于可操作性来说能够量化“最终”是很重要的。
### 松散法定人数与带提示的接力
合理配置的法定人数可以使数据库无需故障切换即可容忍个别节点的故障。也可以容忍个别节点变慢因为请求不必等待所有n个节点响应——当w或r节点响应时它们可以返回。对于需要高可用、低延时、且能够容忍偶尔读到陈旧值的应用场景来说这些特性使无主复制的数据库很有吸引力。
合理配置的法定人数可以使数据库无需故障切换即可容忍个别节点的故障。也可以容忍个别节点变慢因为请求不必等待所有n个节点响应——当w或r节点响应时它们可以返回。对于需要高可用、低延时、且能够容忍偶尔读到陈旧值的应用场景来说这些特性使无主复制的数据库很有吸引力。
然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能能够连接到它们,但是从数据库节点切断的客户端,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于可用节点,因此客户端可能无法达到法定人数。
然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能能够连接到它们,但是从数据库节点切断的客户端,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于可用节点,因此客户端可能无法达到法定人数。
在一个大型的群集中节点数量明显多于n个网络中断期间客户端可能连接到某些数据库节点而不是为了为特定值组成法定人数的节点们。在这种情况下数据库设计人员需要权衡一下
在一个大型的群集中节点数量明显多于n个网络中断期间客户端可能连接到某些数据库节点而不是为了为特定值组成法定人数的节点们。在这种情况下数据库设计人员需要权衡一下
* 将错误返回给我们无法达到w或r节点的法定数量的所有请求是否更好
* 或者我们是否应该接受写入然后将它们写入一些可达的节点但不在n值通常存在的n个节点之间
后者被认为是一个**松散的法定人数sloppy quorum**【37】写和读仍然需要w和r成功的响应但是那些可能包括不在指定的n个“主”节点中的值。比方说如果你把自己锁在房子外面你可能会敲开邻居的门问你是否可以暂时停留在沙发上。
一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的“本地”节点。这就是所谓的**带提示的接力hinted handoff**。 (一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家。)
一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的“本地”节点。这就是所谓的**带提示的接力hinted handoff**。 (一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家。)
松散法定人数提高写入可用性特别有用只要有任何w节点可用数据库就可以接受写入。然而这意味着即使当$w + r> n$时也不能确定读取某个键的最新值因为最新的值可能已经临时写入了n之外的某些节点【47】。
松散法定人数提高写入可用性特别有用只要有任何w节点可用数据库就可以接受写入。然而这意味着即使当$w + r> n$时也不能确定读取某个键的最新值因为最新的值可能已经临时写入了n之外的某些节点【47】。
因此在传统意义上一个松散的法定人数实际上不是一个法定人数。这只是一个保证即数据存储在w节点的地方。不能保证r节点的读取直到提示已经完成。
因此在传统意义上一个松散的法定人数实际上不是一个法定人数。这只是一个保证即数据存储在w节点的地方。不能保证r节点的读取直到提示已经完成。
在所有常见的Dynamo实现中松散法定人数是可选的。在Riak中它们默认是启用的而在Cassandra和Voldemort中它们默认是禁用的【46,49,50】。
在所有常见的Dynamo实现中松散法定人数是可选的。在Riak中它们默认是启用的而在Cassandra和Voldemort中它们默认是禁用的【46,49,50】。
#### 运维多个数据中心
我们先前讨论了跨数据中心复制作为多主复制的用例(参阅“[多主复制](#多主复制)”)。无主复制还适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
我们先前讨论了跨数据中心复制作为多主复制的用例(参阅“[多主复制](#多主复制)”)。无主复制还适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持副本的数量n包括所有数据中心的节点在配置中您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何每个来自客户端的写入都会发送到所有副本但客户端通常只等待来自其本地数据中心内的法定节点的确认从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生尽管配置有一定的灵活性【50,51】。
Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持副本的数量n包括所有数据中心的节点在配置中您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何每个来自客户端的写入都会发送到所有副本但客户端通常只等待来自其本地数据中心内的法定节点的确认从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生尽管配置有一定的灵活性【50,51】。
Riak将客户端和数据库节点之间的所有通信保持在一个数据中心本地因此n描述了一个数据中心内的副本数量。数据库集群之间的跨数据中心复制在后台异步发生其风格类似于多领导者复制【52】。
Riak将客户端和数据库节点之间的所有通信保持在一个数据中心本地因此n描述了一个数据中心内的副本数量。数据库集群之间的跨数据中心复制在后台异步发生其风格类似于多领导者复制【52】。
### 检测并发写入
Dynamo风格的数据库允许多个客户端同时写入相同的Key这意味着即使使用严格的法定人数也会发生冲突。这种情况与多领导者复制相似参阅“[处理写入冲突](#处理写入冲突)”但在Dynamo样式的数据库中在**读修复**或**带提示的接力**期间也可能会产生冲突。
Dynamo风格的数据库允许多个客户端同时写入相同的Key这意味着即使使用严格的法定人数也会发生冲突。这种情况与多领导者复制相似参阅“[处理写入冲突](#处理写入冲突)”但在Dynamo样式的数据库中在**读修复**或**带提示的接力**期间也可能会产生冲突。
问题在于,由于可变的网络延迟和部分故障,事件可能在不同的节点以不同的顺序到达。例如,[图5-12](img/fig5-12.png)显示了两个客户机A和B同时写入三节点数据存储区中的键X
问题在于,由于可变的网络延迟和部分故障,事件可能在不同的节点以不同的顺序到达。例如,[图5-12](img/fig5-12.png)显示了两个客户机A和B同时写入三节点数据存储区中的键X
* 节点 1 接收来自 A 的写入,但由于暂时中断,从不接收来自 B 的写入。
* 节点 2 首先接收来自 A 的写入,然后接收来自 B 的写入。
@ -618,25 +618,25 @@ Dynamo风格的数据库允许多个客户端同时写入相同的Key这意
**图5-12 并发写入Dynamo风格的数据存储没有明确定义的顺序。**
如果每个节点只要接收到来自客户端的写入请求就简单地覆盖了某个键的值,那么节点就会永久地不一致,如[图5-12](img/fig5-12.png)中的最终获取请求所示节点2认为 X 的最终值是 B而其他节点认为值是 A 。
如果每个节点只要接收到来自客户端的写入请求就简单地覆盖了某个键的值,那么节点就会永久地不一致,如[图5-12](img/fig5-12.png)中的最终获取请求所示节点2认为 X 的最终值是 B而其他节点认为值是 A 。
为了最终达成一致,副本应该趋于相同的值。如何做到这一点?有人可能希望复制的数据库能够自动处理,但不幸的是,大多数的实现都很糟糕:如果你想避免丢失数据,你(应用程序开发人员)需要知道很多有关数据库冲突处理的内部信息。
为了最终达成一致,副本应该趋于相同的值。如何做到这一点?有人可能希望复制的数据库能够自动处理,但不幸的是,大多数的实现都很糟糕:如果你想避免丢失数据,你(应用程序开发人员)需要知道很多有关数据库冲突处理的内部信息。
在“[处理写冲突](#处理写入冲突)”一节中已经简要介绍了一些解决冲突的技术。在总结本章之前,让我们来更详细地探讨这个问题。
在“[处理写冲突](#处理写入冲突)”一节中已经简要介绍了一些解决冲突的技术。在总结本章之前,让我们来更详细地探讨这个问题。
#### 最后写入为准(丢弃并发写入)
实现最终融合的一种方法是声明每个副本只需要存储最**“最近”**的值,并允许**“更旧”**的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是“最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
实现最终融合的一种方法是声明每个副本只需要存储最**“最近”**的值,并允许**“更旧”**的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是“最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
正如**“最近”**的引号所表明的,这个想法其实颇具误导性。在[图5-12](img/fig5-12.png)的例子中,当客户端向数据库节点发送写入请求时,客户端都不知道另一个客户端,因此不清楚哪一个先发生了。事实上,说“发生”是没有意义的:我们说写入是**并发concurrent**的,所以它们的顺序是不确定的。
正如**“最近”**的引号所表明的,这个想法其实颇具误导性。在[图5-12](img/fig5-12.png)的例子中,当客户端向数据库节点发送写入请求时,客户端都不知道另一个客户端,因此不清楚哪一个先发生了。事实上,说“发生”是没有意义的:我们说写入是**并发concurrent**的,所以它们的顺序是不确定的。
即使写入没有自然的排序,我们也可以强制任意排序。例如,可以为每个写入附加一个时间戳,挑选最**“最近”**的最大时间戳,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为**最后写入为准LWW, last write wins**是Cassandra 【53】唯一支持的冲突解决方法也是Riak 【35】中的一个可选特征。
即使写入没有自然的排序,我们也可以强制任意排序。例如,可以为每个写入附加一个时间戳,挑选最**“最近”**的最大时间戳,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为**最后写入为准LWW, last write wins**是Cassandra 【53】唯一支持的冲突解决方法也是Riak 【35】中的一个可选特征。
LWW实现了最终收敛的目标但以**持久性**为代价如果同一个Key有多个并发写入即使它们都被报告为客户端成功因为它们被写入 w 个副本其中一个写道会生存下来其他的将被无声丢弃。此外LWW甚至可能会删除不是并发的写入我们将在的“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中讨论。
LWW实现了最终收敛的目标但以**持久性**为代价如果同一个Key有多个并发写入即使它们都被报告为客户端成功因为它们被写入 w 个副本其中一个写道会生存下来其他的将被无声丢弃。此外LWW甚至可能会删除不是并发的写入我们将在的“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中讨论。
有一些情况如缓存其中丢失的写入可能是可以接受的。如果丢失数据不可接受LWW是解决冲突的一个很烂的选择。
有一些情况如缓存其中丢失的写入可能是可以接受的。如果丢失数据不可接受LWW是解决冲突的一个很烂的选择。
与LWW一起使用数据库的唯一安全方法是确保一个键只写入一次然后视为不可变从而避免对同一个密钥进行并发更新。例如推荐使用Cassandra的方法是使用UUID作为键从而为每个写操作提供一个唯一的键【53】。
与LWW一起使用数据库的唯一安全方法是确保一个键只写入一次然后视为不可变从而避免对同一个密钥进行并发更新。例如推荐使用Cassandra的方法是使用UUID作为键从而为每个写操作提供一个唯一的键【53】。
#### “此前发生”的关系和并发
@ -647,23 +647,23 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
如果操作B了解操作A或者依赖于A或者以某种方式构建于操作A之上则操作A在另一个操作B之前发生。在另一个操作之前是否发生一个操作是定义什么并发的关键。事实上我们可以简单地说如果两个操作都不在另一个之前发生那么两个操作是并发的两个操作都不知道另一个【54】。
因此只要有两个操作A和B就有三种可能性A在B之前发生或者B在A之前发生或者A和B并发。我们需要的是一个算法来告诉我们两个操作是否是并发的。如果一个操作发生在另一个操作之前则后面的操作应该覆盖较早的操作但是如果这些操作是并发的则存在需要解决的冲突。
因此只要有两个操作A和B就有三种可能性A在B之前发生或者B在A之前发生或者A和B并发。我们需要的是一个算法来告诉我们两个操作是否是并发的。如果一个操作发生在另一个操作之前则后面的操作应该覆盖较早的操作但是如果这些操作是并发的则存在需要解决的冲突。
> #### 并发性,时间和相对性
>
> 如果两个操作**“同时”**发生,似乎应该称为并发——但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否**同时**发生的,这个问题我们将在[第8章](ch8.md)中详细讨论。
> 如果两个操作**“同时”**发生,似乎应该称为并发——但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否**同时**发生的,这个问题我们将在[第8章](ch8.md)中详细讨论。
>
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】它引入了信息不能比光速更快的思想。因此如果事件之间的时间短于光通过它们之间的距离那么发生一定距离的两个事件不可能相互影响。
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】它引入了信息不能比光速更快的思想。因此如果事件之间的时间短于光通过它们之间的距离那么发生一定距离的两个事件不可能相互影响。
>
> 在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作也可能是**并行的**。例如,如果网络缓慢或中断,两个操作间可能会出现一段时间间隔,且仍然是并发的,因为网络问题阻止一个操作意识到另一个操作的存在。
> 在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作也可能是**并行的**。例如,如果网络缓慢或中断,两个操作间可能会出现一段时间间隔,且仍然是并发的,因为网络问题阻止一个操作意识到另一个操作的存在。
#### 捕获"此前发生"关系
来看一个算法,它确定两个操作是否为并发的,还是一个在另一个之前。为了简单起见,我们从一个只有一个副本的数据库开始。一旦我们已经制定了如何在单个副本上完成这项工作,我们可以将该方法概括为具有多个副本的无领导者数据库。
来看一个算法,它确定两个操作是否为并发的,还是一个在另一个之前。为了简单起见,我们从一个只有一个副本的数据库开始。一旦我们已经制定了如何在单个副本上完成这项工作,我们可以将该方法概括为具有多个副本的无领导者数据库。
[图5-13]()显示了两个客户端同时向同一购物车添加项目。 (如果这样的例子让你觉得太麻烦了,那么可以想象,两个空中交通管制员同时把飞机添加到他们正在跟踪的区域)最初,购物车是空的。在它们之间,客户端向数据库发出五次写入:
@ -677,13 +677,13 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
**图5-13 捕获两个客户端之间的因果关系,同时编辑购物车。**
[图5-13](img/fig5-13.png)中的操作之间的数据流如[图5-14](img/fig5-14.png)所示。 箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。 在这个例子中,客户端永远不会完全掌握服务器上的数据,因为总是有另一个操作同时进行。 但是,旧版本的值最终会被覆盖,并且不会丢失任何写入。
[图5-13](img/fig5-13.png)中的操作之间的数据流如[图5-14](img/fig5-14.png)所示。 箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。 在这个例子中,客户端永远不会完全掌握服务器上的数据,因为总是有另一个操作同时进行。 但是,旧版本的值最终会被覆盖,并且不会丢失任何写入。
![](img/fig5-14.png)
**图5-14 图5-13中的因果依赖关系图。**
请注意,服务器可以通过查看版本号来确定两个操作是否是并发的——它不需要解释该值本身(因此该值可以是任何数据结构)。该算法的工作原理如下:
请注意,服务器可以通过查看版本号来确定两个操作是否是并发的——它不需要解释该值本身(因此该值可以是任何数据结构)。该算法的工作原理如下:
* 服务器为每个键保留一个版本号,每次写入键时都增加版本号,并将新版本号与写入的值一起存储。
* 当客户端读取键时,服务器将返回所有未覆盖的值以及最新的版本号。客户端在写入前必须读取。
@ -694,31 +694,31 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
#### 合并同时写入的值
这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:如果多个操作并发发生,则客户端必须通过合并并发写入的值来擦屁股。 Riak称这些并发值**兄弟siblings**。
这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:如果多个操作并发发生,则客户端必须通过合并并发写入的值来擦屁股。 Riak称这些并发值**兄弟siblings**。
合并兄弟值,本质上是与多领导者复制中的冲突解决相同的问题,我们先前讨论过(参阅“[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中做更聪明的事情。
合并兄弟值,本质上是与多领导者复制中的冲突解决相同的问题,我们先前讨论过(参阅“[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中做更聪明的事情。
以购物车为例,一种合理的合并兄弟方法就是集合求并。在[图5-14](img/fig5-14.png)中,最后的两个兄弟是[牛奶,面粉,鸡蛋,熏肉]和[鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋出现在两个,即使他们每个只写一次。合并的价值可能是像[牛奶,面粉,鸡蛋,培根,火腿],没有重复。
以购物车为例,一种合理的合并兄弟方法就是集合求并。在[图5-14](img/fig5-14.png)中,最后的两个兄弟是[牛奶,面粉,鸡蛋,熏肉]和[鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋出现在两个,即使他们每个只写一次。合并的价值可能是像[牛奶,面粉,鸡蛋,培根,火腿],没有重复。
然而,如果你想让人们也可以从他们的手推车中**删除**东西而不是仅仅添加东西那么把兄弟求并可能不会产生正确的结果如果你合并了两个兄弟手推车并且只在其中一个兄弟值里删掉了它那么被删除的项目会重新出现在兄弟的并集中【37】。为了防止这个问题一个项目在删除时不能简单地从数据库中删除;相反,系统必须留下一个具有合适版本号的标记,以指示合并兄弟时该项目已被删除。这种删除标记被称为**墓碑tombstone**。 (我们之前在“[哈希索引”](ch3.md#哈希索引)中的日志压缩的上下文中看到了墓碑。)
然而,如果你想让人们也可以从他们的手推车中**删除**东西而不是仅仅添加东西那么把兄弟求并可能不会产生正确的结果如果你合并了两个兄弟手推车并且只在其中一个兄弟值里删掉了它那么被删除的项目会重新出现在兄弟的并集中【37】。为了防止这个问题一个项目在删除时不能简单地从数据库中删除;相反,系统必须留下一个具有合适版本号的标记,以指示合并兄弟时该项目已被删除。这种删除标记被称为**墓碑tombstone**。 (我们之前在“[哈希索引”](ch3.md#哈希索引)中的日志压缩的上下文中看到了墓碑。)
因为在应用程序代码中合并兄弟是复杂且容易出错的,所以有一些数据结构被设计出来用于自动执行这种合并,如“[自动冲突解决]()”中讨论的。例如Riak的数据类型支持使用称为CRDT的数据结构家族【38,39,55】可以以合理的方式自动合并兄弟包括保留删除。
因为在应用程序代码中合并兄弟是复杂且容易出错的,所以有一些数据结构被设计出来用于自动执行这种合并,如“[自动冲突解决]()”中讨论的。例如Riak的数据类型支持使用称为CRDT的数据结构家族【38,39,55】可以以合理的方式自动合并兄弟包括保留删除。
#### 版本向量
[图5-13](img/fig5-13.png)中的示例只使用一个副本。如果有没有主库,有多个副本,算法如何改变?
[图5-13](img/fig5-13.png)中的示例只使用一个副本。如果有没有主库,有多个副本,算法如何改变?
[图5-13](img/fig5-13.png)使用单个版本号来捕获操作之间的依赖关系,但是当多个副本并发接受写入时,这是不够的。相反,除了对每个键使用版本号之外,还需要在**每个副本**中版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些值,以及保留哪些值作为兄弟。
[图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](img/fig5-13.png)中的版本号一样,当读取值时,版本向量会从数据库副本发送到客户端,并且随后写入值时需要将其发送回数据库。 Riak将版本向量编码为一个字符串它称为**因果上下文causal context**)。版本向量允许数据库区分覆盖写入和并发写入。
与[图5-13](img/fig5-13.png)中的版本号一样,当读取值时,版本向量会从数据库副本发送到客户端,并且随后写入值时需要将其发送回数据库。 Riak将版本向量编码为一个字符串它称为**因果上下文causal context**)。版本向量允许数据库区分覆盖写入和并发写入。
另外,就像在单个副本的例子中,应用程序可能需要合并兄弟。版本向量结构确保从一个副本读取并随后写回到另一个副本是安全的。这样做可能会创建兄弟,但只要兄弟姐妹合并正确,就不会丢失数据。
另外,就像在单个副本的例子中,应用程序可能需要合并兄弟。版本向量结构确保从一个副本读取并随后写回到另一个副本是安全的。这样做可能会创建兄弟,但只要兄弟姐妹合并正确,就不会丢失数据。
> #### 版本向量和向量时钟
>
> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——请参阅参考资料的细节【57,60,61】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——请参阅参考资料的细节【57,60,61】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
>
## 本章小结
@ -727,56 +727,60 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
***高可用性***
即使在一台机器(或多台机器,或整个数据中心)停机的情况下也能保持系统正常运行
即使在一台机器(或多台机器,或整个数据中心)停机的情况下也能保持系统正常运行
***断开连接的操作***
允许应用程序在网络中断时继续工作
允许应用程序在网络中断时继续工作
***延迟***
将数据放置在距离用户较近的地方,以便用户能够更快地与其交互
将数据放置在距离用户较近的地方,以便用户能够更快地与其交互
***可扩展性***
能够处理比单个机器更高的读取量可以通过对副本进行读取来处理
能够处理比单个机器更高的读取量可以通过对副本进行读取来处理
尽管是一个简单的目标 - 在几台机器上保留相同数据的副本,但复制却是一个非常棘手的问题。它需要仔细考虑并发和所有可能出错的事情,并处理这些故障的后果。至少,我们需要处理不可用的节点和网络中断(甚至不考虑更隐蔽的故障,例如由于软件错误导致的无提示数据损坏)。
我们讨论了复制的三种主要方法:
尽管是一个简单的目标 - 在几台机器上保留相同数据的副本,但复制却是一个非常棘手的问题。它需要仔细考虑并发和所有可能出错的事情,并处理这些故障的后果。至少,我们需要处理不可用的节点和网络中断(甚至不考虑更隐蔽的故障,例如由于软件错误导致的无提示数据损坏)。
我们讨论了复制的三种主要方法:
***单主复制***
客户端将所有写入操作发送到单个节点(领导者),该节点将数据更改事件流发送到其他副本(追随者)。读取可以在任何副本上执行,但从追随者读取可能是陈旧的。
客户端将所有写入操作发送到单个节点(领导者),该节点将数据更改事件流发送到其他副本(追随者)。读取可以在任何副本上执行,但从追随者读取可能是陈旧的。
***多主复制***
客户端发送每个写入到几个领导节点之一,其中任何一个都可以接受写入。领导者将数据更改事件流发送给彼此以及任何跟随者节点。
客户端发送每个写入到几个领导节点之一,其中任何一个都可以接受写入。领导者将数据更改事件流发送给彼此以及任何跟随者节点。
***无主复制***
客户端发送每个写入到几个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。
客户端发送每个写入到几个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。
每种方法都有优点和缺点。单主复制是非常流行的,因为它很容易理解,不需要担心冲突解决。在出现故障节点,网络中断和延迟峰值的情况下,多领导者和无领导者复制可以更加稳健,但代价很难推理,只能提供非常弱的一致性保证。
复制可以是同步的,也可以是异步的,在发生故障时对系统行为有深远的影响。尽管在系统运行平稳时异步复制速度很快,但是在复制滞后增加和服务器故障时要弄清楚会发生什么,这一点很重要。如果一个领导者失败了,并且你推动一个异步更新的追随者成为新的领导者,那么最近承诺的数据可能会丢失。
复制可以是同步的,也可以是异步的,在发生故障时对系统行为有深远的影响。尽管在系统运行平稳时异步复制速度很快,但是在复制滞后增加和服务器故障时要弄清楚会发生什么,这一点很重要。如果一个领导者失败了,并且你推动一个异步更新的追随者成为新的领导者,那么最近承诺的数据可能会丢失。
我们研究了一些可能由复制滞后引起的奇怪效应,我们讨论了一些有助于决定应用程序在复制滞后时的行为的一致性模型:
我们研究了一些可能由复制滞后引起的奇怪效应,我们讨论了一些有助于决定应用程序在复制滞后时的行为的一致性模型:
***写后读***
用户应该总是看到自己提交的数据。
用户应该总是看到自己提交的数据。
***单调读***
当用户在某个时间点看到数据后,他们不应该在较早的时间点看到数据。
当用户在某个时间点看到数据后,他们不应该在较早的时间点看到数据。
***一致前缀读***
用户应该将数据视为具有因果意义的状态:例如,按照正确的顺序查看问题及其答复。
用户应该将数据视为具有因果意义的状态:例如,按照正确的顺序查看问题及其答复。
最后,我们讨论了多领导者和无领导者复制方法所固有的并发问题:因为他们允许多个写入并发发生冲突。我们研究了一个数据库可能使用的算法来确定一个操作是否发生在另一个操作之前,或者它们是否同时发生。我们还谈到了通过合并并发更新来解决冲突的方法。
在下一章中,我们将继续研究分布在多个机器上的数据,通过复制的对应方式:将大数据集分割成分区。
最后,我们讨论了多领导者和无领导者复制方法所固有的并发问题:因为他们允许多个写入并发发生冲突。我们研究了一个数据库可能使用的算法来确定一个操作是否发生在另一个操作之前,或者它们是否同时发生。我们还谈到了通过合并并发更新来解决冲突的方法。
在下一章中,我们将继续研究分布在多个机器上的数据,通过复制的对应方式:将大数据集分割成分区。
@ -914,7 +918,6 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
1. Reinhard Schwarz and Friedemann Mattern: “[Detecting Causal Relationships in Distributed Computations: In Search of the Holy Grail](http://dcg.ethz.ch/lectures/hs08/seminar/papers/mattern4.pdf),” *Distributed Computing*, volume 7, number 3, pages 149174, March 1994. [doi:10.1007/BF02277859](http://dx.doi.org/10.1007/BF02277859)
--------
| 上一章 | 目录 | 下一章 |

187
ch6.md
View file

@ -11,30 +11,30 @@
[TOC]
在[第5章](ch5.md)中,我们讨论了复制——即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行**分区partitions**,也称为**分片sharding**[^i]
在[第5章](ch5.md)中,我们讨论了复制——即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行**分区partitions**,也称为**分片sharding**[^i]
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与**网络分区net splits**无关,这是节点之间网络中的一种故障类型。我们将在[第8章](ch8.md)讨论这些错误。
> ##### 术语澄清
>
> 上文中的**分区(partition)**,在MongoDB,Elasticsearch和Solr Cloud中被称为**分片(shard)**,在HBase中称之为**区域(Region)**Bigtable中则是 **表块tablet**Cassandra和Riak中是**虚节点vnode)**, Couchbase中叫做**虚桶(vBucket)**.但是**分区(partition)** 是约定俗成的叫法。
> 上文中的**分区(partition)**,在MongoDB,Elasticsearch和Solr Cloud中被称为**分片(shard)**,在HBase中称之为**区域(Region)**Bigtable中则是 **表块tablet**Cassandra和Riak中是**虚节点vnode)**, Couchbase中叫做**虚桶(vBucket)**.但是**分区(partition)** 是约定俗成的叫法。
>
通常情况下,每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将进行深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时进行多个分区的操作。
通常情况下,每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将进行深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时进行多个分区的操作。
分区主要是为了**可扩展性**。不同的分区可以放在不共享集群中的不同节点上(参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
分区主要是为了**可扩展性**。不同的分区可以放在不共享集群中的不同节点上(参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
对于在单个分区上运行的查询,每个节点可以独立执行对自己的查询,因此可以通过添加更多的节点来扩大查询吞吐量。大型,复杂的查询可能会跨越多个节点并行处理,尽管这也带来了新的困难。
对于在单个分区上运行的查询,每个节点可以独立执行对自己的查询,因此可以通过添加更多的节点来扩大查询吞吐量。大型,复杂的查询可能会跨越多个节点并行处理,尽管这也带来了新的困难。
分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出最近因为NoSQL数据库和基于Hadoop的数据仓库重新被关注。有些系统是为事务性工作设计的有些系统则用于分析参阅“[事务处理或分析]”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出最近因为NoSQL数据库和基于Hadoop的数据仓库重新被关注。有些系统是为事务性工作设计的有些系统则用于分析参阅“[事务处理或分析]”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论[重新平衡分区](#重新平衡分区),如果想要添加或删除群集中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论[重新平衡分区](#重新平衡分区),如果想要添加或删除群集中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
## 分区与复制
分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。 这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。 这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
一个节点可能存储多个分区。 如果使用主从复制模型,则分区和复制的组合如[图6-1]()所示。 每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
一个节点可能存储多个分区。 如果使用主从复制模型,则分区和复制的组合如[图6-1]()所示。 每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
我们在[第5章](ch5.md)讨论的关于数据库复制的所有内容同样适用于分区的复制。 大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
![](img/fig6-1.png)
@ -43,91 +43,91 @@
## 键值数据的分区
假设你有大量数据并且想要分区,如何决定在哪些节点上存储哪些记录呢?
假设你有大量数据并且想要分区,如何决定在哪些节点上存储哪些记录呢?
分区目标是将数据和查询负载均匀分布在各个节点上。如果每个节点公平分享数据和负载那么理论上10个节点应该能够处理10倍的数据量和10倍的单个节点的读写吞吐量暂时忽略复制
分区目标是将数据和查询负载均匀分布在各个节点上。如果每个节点公平分享数据和负载那么理论上10个节点应该能够处理10倍的数据量和10倍的单个节点的读写吞吐量暂时忽略复制
如果分区是不公平的,一些分区比其他分区有更多的数据或查询,我们称之为**偏斜skew**。数据偏斜的存在使分区效率下降很多。在极端的情况下所有的负载可能压在一个分区上其余9个节点空闲的瓶颈落在这一个繁忙的节点上。不均衡导致的高负载的分区被称为**热点hot spot**。
如果分区是不公平的,一些分区比其他分区有更多的数据或查询,我们称之为**偏斜skew**。数据偏斜的存在使分区效率下降很多。在极端的情况下所有的负载可能压在一个分区上其余9个节点空闲的瓶颈落在这一个繁忙的节点上。不均衡导致的高负载的分区被称为**热点hot spot**。
避免热点最简单的方法是将记录随机分配给节点。这将在所有节点上平均分配数据,但是它有一个很大的缺点:当你试图读取一个特定的值时,你无法知道它在哪个节点上,所以你必须并行地查询所有的节点。
避免热点最简单的方法是将记录随机分配给节点。这将在所有节点上平均分配数据,但是它有一个很大的缺点:当你试图读取一个特定的值时,你无法知道它在哪个节点上,所以你必须并行地查询所有的节点。
我们可以做得更好。现在假设您有一个简单的键值数据模型,其中您总是通过其主键访问记录。例如,在一本老式的纸质百科全书中,你可以通过标题来查找一个条目;由于所有条目按字母顺序排序,因此您可以快速找到您要查找的条目。
我们可以做得更好。现在假设您有一个简单的键值数据模型,其中您总是通过其主键访问记录。例如,在一本老式的纸质百科全书中,你可以通过标题来查找一个条目;由于所有条目按字母顺序排序,因此您可以快速找到您要查找的条目。
### 根据键的范围分区
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸百科全书的卷([图6-2]())。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果您还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸百科全书的卷([图6-2]())。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果您还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
![](img/fig6-2.png)
**图6-2 印刷版百科全书按照关键字范围进行分区**
键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在[图6-2]()中第1卷包含以A和B开头的单词但第12卷则包含以TUVXY和Z开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据分区边界需要依据数据调整。
键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在[图6-2]()中第1卷包含以A和B开头的单词但第12卷则包含以TUVXY和Z开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据分区边界需要依据数据调整。
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在“[重新平衡分区]()”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略以及其开源等价物HBase 【2, 3】RethinkDB和2.4版本之前的MongoDB 【4】。
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在“[重新平衡分区]()”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略以及其开源等价物HBase 【2, 3】RethinkDB和2.4版本之前的MongoDB 【4】。
在每个分区中,我们可以按照一定的顺序保存键(参见“[SSTables和LSM-树]()”)。好处是进行范围扫描非常简单,您可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(参阅“[多列索引](#ch2.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
在每个分区中,我们可以按照一定的顺序保存键(参见“[SSTables和LSM-树]()”)。好处是进行范围扫描非常简单,您可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(参阅“[多列索引](#ch2.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
然而Key Range分区的缺点是某些特定的访问模式会导致热点。 如果主键是时间戳,则分区对应于时间范围,例如,给每天分配一个分区。 不幸的是由于我们在测量发生时将数据从传感器写入数据库因此所有写入操作都会转到同一个分区即今天的分区这样分区可能会因写入而过载而其他分区则处于空闲状态【5】。
然而Key Range分区的缺点是某些特定的访问模式会导致热点。 如果主键是时间戳,则分区对应于时间范围,例如,给每天分配一个分区。 不幸的是由于我们在测量发生时将数据从传感器写入数据库因此所有写入操作都会转到同一个分区即今天的分区这样分区可能会因写入而过载而其他分区则处于空闲状态【5】。
为了避免传感器数据库中的这个问题,需要使用除了时间戳以外的其他东西作为主键的第一个部分。 例如,可以在每个时间戳前添加传感器名称,这样会首先按传感器名称,然后按时间进行分区。 假设有多个传感器同时运行,写入负载将最终均匀分布在不同分区上。 现在,当想要在一个时间范围内获取多个传感器的值时,您需要为每个传感器名称执行一个单独的范围查询。
为了避免传感器数据库中的这个问题,需要使用除了时间戳以外的其他东西作为主键的第一个部分。 例如,可以在每个时间戳前添加传感器名称,这样会首先按传感器名称,然后按时间进行分区。 假设有多个传感器同时运行,写入负载将最终均匀分布在不同分区上。 现在,当想要在一个时间范围内获取多个传感器的值时,您需要为每个传感器名称执行一个单独的范围查询。
### 根据键的散列分区
由于偏斜和热点的风险,许多分布式数据存储使用散列函数来确定给定键的分区。
由于偏斜和热点的风险,许多分布式数据存储使用散列函数来确定给定键的分区。
一个好的散列函数可以将将偏斜的数据均匀分布。假设你有一个32位散列函数,无论何时给定一个新的字符串输入它将返回一个0到$2^{32}$ -1之间的"随机"数。即使输入的字符串非常相似,它们的散列也会均匀分布在这个数字范围内。
一个好的散列函数可以将将偏斜的数据均匀分布。假设你有一个32位散列函数,无论何时给定一个新的字符串输入它将返回一个0到$2^{32}$ -1之间的"随机"数。即使输入的字符串非常相似,它们的散列也会均匀分布在这个数字范围内。
出于分区的目的散列函数不需要多么强壮的加密算法例如Cassandra和MongoDB使用MD5Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数它们用于哈希表但是它们可能不适合分区例如在Java的`Object.hashCode()`和Ruby的`Object#hash`同一个键可能在不同的进程中有不同的哈希值【6】。
出于分区的目的散列函数不需要多么强壮的加密算法例如Cassandra和MongoDB使用MD5Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数它们用于哈希表但是它们可能不适合分区例如在Java的`Object.hashCode()`和Ruby的`Object#hash`同一个键可能在不同的进程中有不同的哈希值【6】。
一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个通过哈希散列落在分区范围内的键将被存储在该分区中。如[图6-3](img/fig6-3.png)所示。
一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个通过哈希散列落在分区范围内的键将被存储在该分区中。如[图6-3](img/fig6-3.png)所示。
![](img/fig6-3.png)
**图6-3 按哈希键分区**
这种技术擅长在分区之间分配键。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时也被称为**一致性哈希consistent hashing**)。
这种技术擅长在分区之间分配键。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时也被称为**一致性哈希consistent hashing**)。
> #### 一致性哈希
>
> 一致性哈希由Karger等人定义。【7】 用于跨互联网级别的缓存系统例如CDN中是一种能均匀分配负载的方法。它使用随机选择的**分区边界partition boundaries**来避免中央控制或分布式一致性的需要。 请注意这里的一致性与复制一致性请参阅第5章或ACID一致性参阅[第7章](ch7.md))无关,而是描述了重新平衡的特定方法。
> 一致性哈希由Karger等人定义。【7】 用于跨互联网级别的缓存系统例如CDN中是一种能均匀分配负载的方法。它使用随机选择的**分区边界partition boundaries**来避免中央控制或分布式一致性的需要。 请注意这里的一致性与复制一致性请参阅第5章或ACID一致性参阅[第7章](ch7.md))无关,而是描述了重新平衡的特定方法。
>
> 正如我们将在“[重新平衡分区](#重新平衡分区)”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然指的是一致性哈希,但是它 往往是不准确的)。 因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区hash partitioning**。
> 正如我们将在“[重新平衡分区](#重新平衡分区)”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然指的是一致性哈希,但是它 往往是不准确的)。 因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区hash partitioning**。
不幸的是通过使用Key散列进行分区我们失去了键范围分区的一个很好的属性高效执行范围查询的能力。曾经相邻的密钥现在分散在所有分区中所以它们之间的顺序就丢失了。在MongoDB中如果您使用了基于散列的分区模式则任何范围查询都必须发送到所有分区【4】。Riak 【9】Couchbase 【10】或Voldemort不支持主键上的范围查询。
不幸的是通过使用Key散列进行分区我们失去了键范围分区的一个很好的属性高效执行范围查询的能力。曾经相邻的密钥现在分散在所有分区中所以它们之间的顺序就丢失了。在MongoDB中如果您使用了基于散列的分区模式则任何范围查询都必须发送到所有分区【4】。Riak 【9】Couchbase 【10】或Voldemort不支持主键上的范围查询。
Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据而其他列则被用作Casssandra的SSTables中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表但如果第一列已经指定了固定值则可以对该键的其他列执行有效的范围扫描。
Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使用由多个列组成的复合主键来声明。键中只有第一列会作为散列的依据而其他列则被用作Casssandra的SSTables中排序数据的连接索引。尽管查询无法在复合主键的第一列中按范围扫表但如果第一列已经指定了固定值则可以对该键的其他列执行有效的范围扫描。
组合索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为`(user_id, update_timestamp)`,那么您可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,对于每个用户,更新按时间戳顺序存储在单个分区上。
组合索引方法为一对多关系提供了一个优雅的数据模型。例如,在社交媒体网站上,一个用户可能会发布很多更新。如果更新的主键被选择为`(user_id, update_timestamp)`,那么您可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。不同的用户可以存储在不同的分区上,对于每个用户,更新按时间戳顺序存储在单个分区上。
### 负载倾斜与消除热点
如前所述,哈希分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。
如前所述,哈希分区可以帮助减少热点。但是,它不能完全避免它们:在极端情况下,所有的读写操作都是针对同一个键的,所有的请求都会被路由到同一个分区。
这种场景也许并不常见但并非闻所未闻例如在社交媒体网站上一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致大量写入同一个键键可能是名人的用户ID或者人们正在评论的动作的ID。哈希策略不起作用因为两个相同ID的哈希值仍然是相同的。
这种场景也许并不常见但并非闻所未闻例如在社交媒体网站上一个拥有数百万追随者的名人用户在做某事时可能会引发一场风暴【14】。这个事件可能导致大量写入同一个键键可能是名人的用户ID或者人们正在评论的动作的ID。哈希策略不起作用因为两个相同ID的哈希值仍然是相同的。
如今大多数数据系统无法自动补偿这种高度偏斜的负载因此应用程序有责任减少偏斜。例如如果一个主键被认为是非常火爆的一个简单的方法是在主键的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将主键分散为100钟不同的主键,从而存储在不同的分区中。
如今大多数数据系统无法自动补偿这种高度偏斜的负载因此应用程序有责任减少偏斜。例如如果一个主键被认为是非常火爆的一个简单的方法是在主键的开始或结尾添加一个随机数。只要一个两位数的十进制随机数就可以将主键分散为100钟不同的主键,从而存储在不同的分区中。
然而将主键进行分割之后任何读取都必须要做额外的工作因为他们必须从所有100个主键分布中读取数据并将其合并。此技术还需要额外的记录只需要对少量热点附加随机数;对于写入吞吐量低的绝大多数主键来是不必要的开销。因此,您还需要一些方法来跟踪哪些键需要被分割。
然而将主键进行分割之后任何读取都必须要做额外的工作因为他们必须从所有100个主键分布中读取数据并将其合并。此技术还需要额外的记录只需要对少量热点附加随机数;对于写入吞吐量低的绝大多数主键来是不必要的开销。因此,您还需要一些方法来跟踪哪些键需要被分割。
也许在将来,数据系统将能够自动检测和补偿偏斜的工作负载;但现在,您需要自己来权衡。
也许在将来,数据系统将能够自动检测和补偿偏斜的工作负载;但现在,您需要自己来权衡。
## 分片与次级索引
到目前为止,我们讨论的分区方案依赖于键值数据模型。如果只通过主键访问记录,我们可以从该键确定分区,并使用它来将读写请求路由到负责该键的分区。
到目前为止,我们讨论的分区方案依赖于键值数据模型。如果只通过主键访问记录,我们可以从该键确定分区,并使用它来将读写请求路由到负责该键的分区。
如果涉及次级索引,情况会变得更加复杂(参考“[其他索引结构]()”。辅助索引通常并不能唯一地标识记录而是一种搜索记录中出现特定值的方式查找用户123的所有操作查找包含词语`hogwash`的所有文章,查找所有颜色为红色的车辆等等。
如果涉及次级索引,情况会变得更加复杂(参考“[其他索引结构]()”。辅助索引通常并不能唯一地标识记录而是一种搜索记录中出现特定值的方式查找用户123的所有操作查找包含词语`hogwash`的所有文章,查找所有颜色为红色的车辆等等。
次级索引是关系型数据库的基础并且在文档数据库中也很普遍。许多键值存储如HBase和Volde-mort为了减少实现的复杂度而放弃了次级索引但是一些如Riak已经开始添加它们因为它们对于数据模型实在是太有用了。并且次级索引也是Solr和Elasticsearch等搜索服务器的基石。
次级索引是关系型数据库的基础并且在文档数据库中也很普遍。许多键值存储如HBase和Volde-mort为了减少实现的复杂度而放弃了次级索引但是一些如Riak已经开始添加它们因为它们对于数据模型实在是太有用了。并且次级索引也是Solr和Elasticsearch等搜索服务器的基石。
次级索引的问题是它们不能整齐地映射到分区。有两种用二级索引对数据库进行分区的方法:**基于文档的分区document-based**和**基于关键词term-based的分区**。
次级索引的问题是它们不能整齐地映射到分区。有两种用二级索引对数据库进行分区的方法:**基于文档的分区document-based**和**基于关键词term-based的分区**。
### 按文档的二级索引
假设你正在经营一个销售二手车的网站(如[图6-4](img/fig6-4.png)所示)。 每个列表都有一个唯一的ID——称之为文档ID——并且用文档ID对数据库进行分区例如分区0中的ID 0到499分区1中的ID 500到999等
假设你正在经营一个销售二手车的网站(如[图6-4](img/fig6-4.png)所示)。 每个列表都有一个唯一的ID——称之为文档ID——并且用文档ID对数据库进行分区例如分区0中的ID 0到499分区1中的ID 500到999等
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段field**,关系数据库中这些是**列column** )。 如果您声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`colorred`的文档ID列表中。
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段field**,关系数据库中这些是**列column** )。 如果您声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`colorred`的文档ID列表中。
[^ii]: 如果数据库仅支持键值模型则你可能会尝试在应用程序代码中创建从值到文档ID的映射来实现辅助索引。 如果沿着这条路线走下去,请万分小心,确保您的索引与底层数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 参见“[多对象事务的需求]()”。
@ -135,35 +135,35 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
**图6-4 按文档分区二级索引**
在这种索引方法中每个分区是完全独立的每个分区维护自己的二级索引仅覆盖该分区中的文档。它不关心存储在其他分区的数据。无论何时您需要写入数据库添加删除或更新文档只需处理包含您正在编写的文档ID的分区即可。出于这个原因**文档分区索引**也被称为**本地索引local index**(而不是将在下一节中描述的**全局索引global index**)。
在这种索引方法中每个分区是完全独立的每个分区维护自己的二级索引仅覆盖该分区中的文档。它不关心存储在其他分区的数据。无论何时您需要写入数据库添加删除或更新文档只需处理包含您正在编写的文档ID的分区即可。出于这个原因**文档分区索引**也被称为**本地索引local index**(而不是将在下一节中描述的**全局索引global index**)。
但是从文档分区索引中读取需要注意除非您对文档ID做了特别的处理否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在[图6-4](img/fig6-4.png)中红色汽车出现在分区0和分区1中。因此如果要搜索红色汽车则需要将查询发送到所有分区并合并所有返回的结果。
但是从文档分区索引中读取需要注意除非您对文档ID做了特别的处理否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在[图6-4](img/fig6-4.png)中红色汽车出现在分区0和分区1中。因此如果要搜索红色汽车则需要将查询发送到所有分区并合并所有返回的结果。
这种查询分区数据库的方法有时被称为**分散/聚集scatter/gather**,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”。然而它被广泛使用MonDBDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案但这并不总是可行尤其是当在单个查询中使用多个二级索引时例如同时需要按颜色和制造商查询
这种查询分区数据库的方法有时被称为**分散/聚集scatter/gather**,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”。然而它被广泛使用MonDBDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案但这并不总是可行尤其是当在单个查询中使用多个二级索引时例如同时需要按颜色和制造商查询
### 根据关键词(Term)的二级索引
我们可以构建一个覆盖所有分区数据的**全局索引**,而不是给每个分区创建自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为瓶颈,违背了分区的目的。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
我们可以构建一个覆盖所有分区数据的**全局索引**,而不是给每个分区创建自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为瓶颈,违背了分区的目的。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
[图6-5](img/fig6-5.png)述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从`a`到`r`的颜色在分区0中`s`到`z`的在分区1。汽车制造商的索引也与之类似分区边界在`f`和`h`之间)。
[图6-5](img/fig6-5.png)述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从`a`到`r`的颜色在分区0中`s`到`z`的在分区1。汽车制造商的索引也与之类似分区边界在`f`和`h`之间)。
![](img/fig6-5.png)
**图6-5 按关键词对二级索引进行分区**
我们将这种索引称为**关键词分区term-partitioned**,因为我们寻找的关键词决定了索引的分区方式。例如,一个关键词可能是:`颜色:红色`。**关键词(Term)** 来源于来自全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。
我们将这种索引称为**关键词分区term-partitioned**,因为我们寻找的关键词决定了索引的分区方式。例如,一个关键词可能是:`颜色:红色`。**关键词(Term)** 来源于来自全文搜索索引(一种特殊的次级索引),指文档中出现的所有单词。
和之前一样,我们可以通过**关键词**本身或者它的散列进行索引分区。根据它本身分区对于范围扫描非常有用(例如对于数字,像汽车的报价),而对关键词的哈希分区提供了负载均衡的能力。
和之前一样,我们可以通过**关键词**本身或者它的散列进行索引分区。根据它本身分区对于范围扫描非常有用(例如对于数字,像汽车的报价),而对关键词的哈希分区提供了负载均衡的能力。
关键词分区的全局索引优于文档分区索引的地方点是它可以使读取更有效率:不需要**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求。全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个关键词可能位于不同的分区或者不同的节点上) 。
关键词分区的全局索引优于文档分区索引的地方点是它可以使读取更有效率:不需要**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求。全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个关键词可能位于不同的分区或者不同的节点上) 。
理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持(请参阅[第7章](ch7.md)和[第9章](ch9.md))。
理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持(请参阅[第7章](ch7.md)和[第9章](ch9.md))。
在实践中,对全局二级索引的更新通常是**异步**的也就是说如果在写入之后不久读取索引刚才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB声称在正常情况下其全局次级索引会在不到一秒的时间内更新但在基础架构出现故障的情况下可能会有延迟【20】。
在实践中,对全局二级索引的更新通常是**异步**的也就是说如果在写入之后不久读取索引刚才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB声称在正常情况下其全局次级索引会在不到一秒的时间内更新但在基础架构出现故障的情况下可能会有延迟【20】。
全局关键词分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库它允许您在本地和全局索引之间进行选择【22】。我们将在[第12章](ch12.md)中涉及实现关键字二级索引的话题。
全局关键词分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库它允许您在本地和全局索引之间进行选择【22】。我们将在[第12章](ch12.md)中涉及实现关键字二级索引的话题。
## 分区再平衡
@ -188,73 +188,73 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
#### 反面教材hash mod N
我们在前面说过([图6-3](img/fig6-3.png)),最好将可能的散列分成不同的范围,并将每个范围分配给一个分区(例如,如果$0≤hash(key)<b_0$则将键分配给分区0如果$b_0 hash(key) <b_1$则分配给分区1
我们在前面说过([图6-3](img/fig6-3.png)),最好将可能的散列分成不同的范围,并将每个范围分配给一个分区(例如,如果$0≤hash(key)<b_0$则将键分配给分区0如果$b_0 hash(key) <b_1$则分配给分区1
也许你想知道为什么我们不使用***mod***(许多编程语言中的%运算符)。例如,`hash(key) mod 10`会返回一个介于0和9之间的数字如果我们将散列写为十进制数散列模10将是最后一个数字。如果我们有10个节点编号为0到9这似乎是将每个键分配给一个节点的简单方法。
也许你想知道为什么我们不使用***mod***(许多编程语言中的%运算符)。例如,`hash(key) mod 10`会返回一个介于0和9之间的数字如果我们将散列写为十进制数散列模10将是最后一个数字。如果我们有10个节点编号为0到9这似乎是将每个键分配给一个节点的简单方法。
模$N$方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设$hash(key)=123456$。如果最初有10个节点那么这个键一开始放在节点6上因为$123456\ mod\ 10 = 6$。当您增长到11个节点时密钥需要移动到节点3$123456\ mod\ 11 = 3$当您增长到12个节点时需要移动到节点0$123456\ mod\ 12 = 0$)。这种频繁的举动使得重新平衡过于昂贵。
模$N$方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设$hash(key)=123456$。如果最初有10个节点那么这个键一开始放在节点6上因为$123456\ mod\ 10 = 6$。当您增长到11个节点时密钥需要移动到节点3$123456\ mod\ 11 = 3$当您增长到12个节点时需要移动到节点0$123456\ mod\ 12 = 0$)。这种频繁的举动使得重新平衡过于昂贵。
我们需要一种只移动必需数据的方法。
我们需要一种只移动必需数据的方法。
#### 固定数量的分区
幸运的是有一个相当简单的解决方案创建比节点更多的分区并为每个节点分配多个分区。例如运行在10个节点的集群上的数据库可能会从一开始就被拆分为1,000个分区因此大约有100个分区被分配给每个节点。
幸运的是有一个相当简单的解决方案创建比节点更多的分区并为每个节点分配多个分区。例如运行在10个节点的集群上的数据库可能会从一开始就被拆分为1,000个分区因此大约有100个分区被分配给每个节点。
现在,如果一个节点被添加到集群中,新节点可以从当前每个节点中**窃取**一些分区,直到分区再次公平分配。这个过程如[图6-6](img/fig6-6.png)所示。如果从集群中删除一个节点,则会发生相反的情况。
现在,如果一个节点被添加到集群中,新节点可以从当前每个节点中**窃取**一些分区,直到分区再次公平分配。这个过程如[图6-6](img/fig6-6.png)所示。如果从集群中删除一个节点,则会发生相反的情况。
只有分区在节点之间的移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所在的节点。这种变更并不是即时的 — 在网络上传输大量的数据需要一些时间 — 所以在传输过程中,原有分区仍然会接受读写操作。
只有分区在节点之间的移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所在的节点。这种变更并不是即时的 — 在网络上传输大量的数据需要一些时间 — 所以在传输过程中,原有分区仍然会接受读写操作。
![](img/fig6-6.png)
**图6-6 将新节点添加到每个节点具有多个分区的数据库群集。**
原则上您甚至可以解决集群中的硬件不匹配问题通过为更强大的节点分配更多的分区可以强制这些节点承载更多的负载。在Riak 【15】Elasticsearch 【24】Couchbase 【10】和Voldemort 【25】中使用了这种再平衡的方法。
原则上您甚至可以解决集群中的硬件不匹配问题通过为更强大的节点分配更多的分区可以强制这些节点承载更多的负载。在Riak 【15】Elasticsearch 【24】Couchbase 【10】和Voldemort 【25】中使用了这种再平衡的方法。
在这种配置中,分区的数量通常在数据库第一次建立时确定,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是您可以拥有的最大节点数量,所以您需要选择足够多的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太大的数字会适得其反。
在这种配置中,分区的数量通常在数据库第一次建立时确定,之后不会改变。虽然原则上可以分割和合并分区(请参阅下一节),但固定数量的分区在操作上更简单,因此许多固定分区数据库选择不实施分区分割。因此,一开始配置的分区数就是您可以拥有的最大节点数量,所以您需要选择足够多的分区以适应未来的增长。但是,每个分区也有管理开销,所以选择太大的数字会适得其反。
如果数据集的总大小难以预估(例如,如果它开始很小,但随着时间的推移可能会变得更大),选择正确的分区数是困难的。由于每个分区包含了总数据量固定比率的数据,因此每个分区的大小与集群中的数据总量成比例增长。如果分区非常大,再平衡和从节点故障恢复变得昂贵。但是,如果分区太小,则会产生太多的开销。当分区大小“恰到好处”的时候才能获得很好的性能,如果分区数量固定,但数据量变动很大,则难以达到最佳性能。
如果数据集的总大小难以预估(例如,如果它开始很小,但随着时间的推移可能会变得更大),选择正确的分区数是困难的。由于每个分区包含了总数据量固定比率的数据,因此每个分区的大小与集群中的数据总量成比例增长。如果分区非常大,再平衡和从节点故障恢复变得昂贵。但是,如果分区太小,则会产生太多的开销。当分区大小“恰到好处”的时候才能获得很好的性能,如果分区数量固定,但数据量变动很大,则难以达到最佳性能。
#### 动态分区
对于使用键范围分区的数据库(参阅“[按键范围分区](#按键范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。
对于使用键范围分区的数据库(参阅“[按键范围分区](#按键范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。
出于这个原因按键的范围进行分区的数据库如HBase和RethinkDB会动态创建分区。当分区增长到超过配置的大小时在HBase上默认值是10GB会被分成两个分区每个分区约占一半的数据【26】。与之相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似参阅“[B树](ch2.md#B树)”)。
出于这个原因按键的范围进行分区的数据库如HBase和RethinkDB会动态创建分区。当分区增长到超过配置的大小时在HBase上默认值是10GB会被分成两个分区每个分区约占一半的数据【26】。与之相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似参阅“[B树](ch2.md#B树)”)。
每个分区分配给一个节点每个节点可以处理多个分区就像固定数量的分区一样。大型分区拆分后可以将其中的一半转移到另一个节点以平衡负载。在HBase中分区文件的传输通过HDFS底层分布式文件系统来实现【3】。
每个分区分配给一个节点每个节点可以处理多个分区就像固定数量的分区一样。大型分区拆分后可以将其中的一半转移到另一个节点以平衡负载。在HBase中分区文件的传输通过HDFS底层分布式文件系统来实现【3】。
动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就足够了,所以开销很小;如果有大量的数据每个分区的大小被限制在一个可配置的最大值【23】。
动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就足够了,所以开销很小;如果有大量的数据每个分区的大小被限制在一个可配置的最大值【23】。
需要注意的是一个空的数据库从一个分区开始因为没有关于在哪里绘制分区边界的先验信息。数据集开始时很小直到达到第一个分区的分割点所有写入操作都必须由单个节点处理而其他节点则处于空闲状态。为了解决这个问题HBase和MongoDB允许在一个空的数据库上配置一组初始分区这被称为**预分割pre-splitting**。在键范围分区的情况中预分割需要提前知道键是如何进行分配的【4,26】。
需要注意的是一个空的数据库从一个分区开始因为没有关于在哪里绘制分区边界的先验信息。数据集开始时很小直到达到第一个分区的分割点所有写入操作都必须由单个节点处理而其他节点则处于空闲状态。为了解决这个问题HBase和MongoDB允许在一个空的数据库上配置一组初始分区这被称为**预分割pre-splitting**。在键范围分区的情况中预分割需要提前知道键是如何进行分配的【4,26】。
动态分区不仅适用于数据的范围分区而且也适用于散列分区。从版本2.4开始MongoDB同时支持范围和哈希分区并且都是进行动态分割分区。
动态分区不仅适用于数据的范围分区而且也适用于散列分区。从版本2.4开始MongoDB同时支持范围和哈希分区并且都是进行动态分割分区。
#### 按节点比例分区
通过动态分区,分区的数量与数据集的大小成正比,因为拆分和合并过程将每个分区的大小保持在固定的最小值和最大值之间。另一方面,对于固定数量的分区,每个分区的大小与数据集的大小成正比。在这两种情况下,分区的数量都与节点的数量无关。
通过动态分区,分区的数量与数据集的大小成正比,因为拆分和合并过程将每个分区的大小保持在固定的最小值和最大值之间。另一方面,对于固定数量的分区,每个分区的大小与数据集的大小成正比。在这两种情况下,分区的数量都与节点的数量无关。
Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比——换句话说每个节点具有固定数量的分区【23,27,28】。在这种情况下每个分区的大小与数据集大小成比例地增长而节点数量保持不变但是当增加节点数时分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储因此这种方法也使每个分区的大小较为稳定。
Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比——换句话说每个节点具有固定数量的分区【23,27,28】。在这种情况下每个分区的大小与数据集大小成比例地增长而节点数量保持不变但是当增加节点数时分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储因此这种方法也使每个分区的大小较为稳定。
当一个新节点加入集群时它随机选择固定数量的现有分区进行拆分然后占有这些拆分分区中每个分区的一半同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割但是平均在更大数量的分区上时在Cassandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种再分配的算法来避免不公平的分割【29】。
当一个新节点加入集群时它随机选择固定数量的现有分区进行拆分然后占有这些拆分分区中每个分区的一半同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割但是平均在更大数量的分区上时在Cassandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种再分配的算法来避免不公平的分割【29】。
随机选择分区边界要求使用基于散列的分区可以从散列函数产生的数字范围中挑选边界。实际上这种方法最符合一致性哈希的原始定义【7】参阅“[一致性哈希](#一致性哈希)”。最新的哈希函数可以在较低元数据开销的情况下达到类似的效果【8】。
随机选择分区边界要求使用基于散列的分区可以从散列函数产生的数字范围中挑选边界。实际上这种方法最符合一致性哈希的原始定义【7】参阅“[一致性哈希](#一致性哈希)”。最新的哈希函数可以在较低元数据开销的情况下达到类似的效果【8】。
### 运维:手动还是自动平衡
关于再平衡有一个重要问题:自动还是手动进行?
关于再平衡有一个重要问题:自动还是手动进行?
在全自动重新平衡系统自动决定何时将分区从一个节点移动到另一个节点无须人工干预和完全手动分区指派给节点由管理员明确配置仅在管理员明确重新配置时才会更改之间有一个权衡。例如CouchbaseRiak和Voldemort会自动生成建议的分区分配但需要管理员提交才能生效。
在全自动重新平衡系统自动决定何时将分区从一个节点移动到另一个节点无须人工干预和完全手动分区指派给节点由管理员明确配置仅在管理员明确重新配置时才会更改之间有一个权衡。例如CouchbaseRiak和Voldemort会自动生成建议的分区分配但需要管理员提交才能生效。
全自动重新平衡可以很方便,因为正常维护的操作工作较少。但是,这可能是不可预测的。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,降低其他请求的性能。
全自动重新平衡可以很方便,因为正常维护的操作工作较少。但是,这可能是不可预测的。再平衡是一个昂贵的操作,因为它需要重新路由请求并将大量数据从一个节点移动到另一个节点。如果没有做好,这个过程可能会使网络或节点负载过重,降低其他请求的性能。
这种自动化与自动故障检测相结合可能十分危险。例如,假设一个节点过载,并且对请求的响应暂时很慢。其他节点得出结论:过载的节点已经死亡,并自动重新平衡集群,使负载离开它。这会对已经超负荷的节点,其他节点和网络造成额外的负载,从而使情况变得更糟,并可能导致级联失败。
这种自动化与自动故障检测相结合可能十分危险。例如,假设一个节点过载,并且对请求的响应暂时很慢。其他节点得出结论:过载的节点已经死亡,并自动重新平衡集群,使负载离开它。这会对已经超负荷的节点,其他节点和网络造成额外的负载,从而使情况变得更糟,并可能导致级联失败。
出于这个原因,再平衡的过程中有人参与是一件好事。这比完全自动的过程慢,但可以帮助防止运维意外。
出于这个原因,再平衡的过程中有人参与是一件好事。这比完全自动的过程慢,但可以帮助防止运维意外。
## 请求路由
现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题当客户想要发出请求时如何知道要连接哪个节点随着分区重新平衡分区对节点的分配也发生变化。为了回答这个问题需要有人知晓这些变化如果我想读或写键“foo”需要连接哪个IP地址和端口号
现在我们已经将数据集分割到多个机器上运行的多个节点上。但是仍然存在一个悬而未决的问题当客户想要发出请求时如何知道要连接哪个节点随着分区重新平衡分区对节点的分配也发生变化。为了回答这个问题需要有人知晓这些变化如果我想读或写键“foo”需要连接哪个IP地址和端口号
这个问题可以概括为 **服务发现(service discovery)** 它不仅限于数据库。任何可通过网络访问的软件都有这个问题特别是如果它的目标是高可用性在多台机器上运行冗余配置。许多公司已经编写了自己的内部服务发现工具其中许多已经作为开源发布【30】。
这个问题可以概括为 **服务发现(service discovery)** 它不仅限于数据库。任何可通过网络访问的软件都有这个问题特别是如果它的目标是高可用性在多台机器上运行冗余配置。许多公司已经编写了自己的内部服务发现工具其中许多已经作为开源发布【30】。
概括来说这个问题有几种不同的方案如图6-7所示:
@ -268,33 +268,33 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比
**图6-7 将请求路由到正确节点的三种不同方式。**
这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,而不是正确处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见[第9章](ch9.md))。
这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,而不是正确处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见[第9章](ch9.md))。
许多分布式数据系统都依赖于一个独立的协调服务比如ZooKeeper来跟踪集群元数据如[图6-8](img/fig6-8.png)所示。 每个节点在ZooKeeper中注册自己ZooKeeper维护分区到节点的可靠映射。 其他参与者如路由层或分区感知客户端可以在ZooKeeper中订阅此信息。 只要分区分配发生的改变或者集群中添加或删除了一个节点ZooKeeper就会通知路由层使路由信息保持最新状态。
许多分布式数据系统都依赖于一个独立的协调服务比如ZooKeeper来跟踪集群元数据如[图6-8](img/fig6-8.png)所示。 每个节点在ZooKeeper中注册自己ZooKeeper维护分区到节点的可靠映射。 其他参与者如路由层或分区感知客户端可以在ZooKeeper中订阅此信息。 只要分区分配发生的改变或者集群中添加或删除了一个节点ZooKeeper就会通知路由层使路由信息保持最新状态。
![](img/fig6-8.png)
**图6-8 使用ZooKeeper跟踪分区分配给节点。**
例如LinkedIn的Espresso使用Helix 【31】进行集群管理依靠ZooKeeper实现了如[图6-8](img/fig6-8.png)所示的路由层。 HBaseSolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构但它依赖于自己的**配置服务器config server** 实现和mongos守护进程作为路由层。
例如LinkedIn的Espresso使用Helix 【31】进行集群管理依靠ZooKeeper实现了如[图6-8](img/fig6-8.png)所示的路由层。 HBaseSolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。 MongoDB具有类似的体系结构但它依赖于自己的**配置服务器config server** 实现和mongos守护进程作为路由层。
Cassandra和Riak采取不同的方法他们在节点之间使用**流言协议gossip protocol** 来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图6-7]()中的方法1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像ZooKeeper这样的外部协调服务的依赖。
Cassandra和Riak采取不同的方法他们在节点之间使用**流言协议gossip protocol** 来传播群集状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图6-7]()中的方法1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像ZooKeeper这样的外部协调服务的依赖。
Couchbase不会自动重新平衡这简化了设计。通常情况下它配置了一个名为moxi的路由层它会从集群节点了解路由变化【32】。
Couchbase不会自动重新平衡这简化了设计。通常情况下它配置了一个名为moxi的路由层它会从集群节点了解路由变化【32】。
当使用路由层或向随机节点发送请求时客户端仍然需要找到要连接的IP地址。这些地址并不像分区的节点分布变化的那么快所以使用DNS通常就足够了。
当使用路由层或向随机节点发送请求时客户端仍然需要找到要连接的IP地址。这些地址并不像分区的节点分布变化的那么快所以使用DNS通常就足够了。
### 执行并行查询
到目前为止,我们只关注读取或写入单个键的非常简单的查询(对于文档分区的二级索引,另外还有分散/聚集查询。这与大多数NoSQL分布式数据存储所支持的访问级别有关。
到目前为止,我们只关注读取或写入单个键的非常简单的查询(对于文档分区的二级索引,另外还有分散/聚集查询。这与大多数NoSQL分布式数据存储所支持的访问级别有关。
然而,通常用于分析的**大规模并行处理MPP, Massively parallel processing** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
然而,通常用于分析的**大规模并行处理MPP, Massively parallel processing** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
数据仓库查询的快速并行执行是一个专门的话题由于分析有很重要的商业意义可以带来很多利益。我们将在第10章讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献【1,33】。
数据仓库查询的快速并行执行是一个专门的话题由于分析有很重要的商业意义可以带来很多利益。我们将在第10章讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献【1,33】。
## 本章小结
在本章中,我们探讨了将大数据集划分成更小的子集的不同方法。数据量非常大的时候,在单台机器上存储和处理不再可行,则分区十分必要。分区的目标是在多台机器上均匀分布数据和查询负载,避免出现热点(负载不成比例的节点)。这需要选择适合于您的数据的分区方案,并在将节点添加到集群或从集群删除时进行再分区。
在本章中,我们探讨了将大数据集划分成更小的子集的不同方法。数据量非常大的时候,在单台机器上存储和处理不再可行,则分区十分必要。分区的目标是在多台机器上均匀分布数据和查询负载,避免出现热点(负载不成比例的节点)。这需要选择适合于您的数据的分区方案,并在将节点添加到集群或从集群删除时进行再分区。
我们讨论了两种主要的分区方法:
@ -306,9 +306,11 @@ Couchbase不会自动重新平衡这简化了设计。通常情况下
***散列分区***
散列函数应用于每个键,分区拥有一定范围的散列。这种方法破坏了键的排序,使得范围查询效率低下,但可以更均匀地分配负载。
散列函数应用于每个键,分区拥有一定范围的散列。这种方法破坏了键的排序,使得范围查询效率低下,但可以更均匀地分配负载。
通过散列进行分区时,通常先提前创建固定数量的分区,为每个节点分配多个分区,并在添加或删除节点时将整个分区从一个节点移动到另一个节点。也可以使用动态分区。
通过散列进行分区时,通常先提前创建固定数量的分区,为每个节点分配多个分区,并在添加或删除节点时将整个分区从一个节点移动到另一个节点。也可以使用动态分区。
两种方法搭配使用也是可行的,例如使用复合主键:使用键的一部分来标识分区,而使用另一部分作为排序顺序。
@ -319,7 +321,7 @@ Couchbase不会自动重新平衡这简化了设计。通常情况下
最后,我们讨论了将查询路由到适当的分区的技术,从简单的分区负载平衡到复杂的并行查询执行引擎。
按照设计,多数情况下每个分区是独立运行的 — 这就是分区数据库可以扩展到多台机器的原因。但是,需要写入多个分区的操作结果可能难以预料:例如,如果写入一个分区成功,但另一个分区失败,会发生什么情况?我们将在下面的章节中讨论这个问题。
按照设计,多数情况下每个分区是独立运行的 — 这就是分区数据库可以扩展到多台机器的原因。但是,需要写入多个分区的操作结果可能难以预料:例如,如果写入一个分区成功,但另一个分区失败,会发生什么情况?我们将在下面的章节中讨论这个问题。
@ -399,7 +401,6 @@ Couchbase不会自动重新平衡这简化了设计。通常情况下
------
| 上一章 | 目录 | 下一章 |

16
ch7.md
View file

@ -2,7 +2,7 @@
![](img/ch7.png)
> 一些作者声称,支持通用的两阶段提交代价太大,会带来性能与可用性的问题。让程序员来处理过度使用事务导致的性能问题,总比缺少事务编程好得多。
> 一些作者声称,支持通用的两阶段提交代价太大,会带来性能与可用性的问题。让程序员来处理过度使用事务导致的性能问题,总比缺少事务编程好得多。
>
> ——James Corbett等人SpannerGoogle的全球分布式数据库2012
@ -21,23 +21,23 @@
为了实现可靠性,系统必须处理这些故障,确保它们不会导致整个系统的灾难性故障。但是实现容错机制工作量巨大。需要仔细考虑所有可能出错的事情,并进行大量的测试,以确保解决方案真正管用。
数十年来,**事务transaction**一直是简化这些问题的首选机制。事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功(**提交commit**)要么失败(**中止abort****回滚rollback**)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不用再担心部分失败的情况了,即某些操作成功,某些失败(无论出于何种原因)。
数十年来,**事务transaction**一直是简化这些问题的首选机制。事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功(**提交commit**)要么失败(**中止abort****回滚rollback**)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不用再担心部分失败的情况了,即某些操作成功,某些失败(无论出于何种原因)。
和事务打交道时间长了,你可能会觉得它显而易见。但我们不应将其视为理所当然。事务不自然法;它们是为了**简化应用编程模型**而创建的。通过使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,因为数据库会替应用处理好这些。(我们称之为**安全保证safety guarantees**)。
和事务打交道时间长了,你可能会觉得它显而易见。但我们不应将其视为理所当然。事务不自然法;它们是为了**简化应用编程模型**而创建的。通过使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,因为数据库会替应用处理好这些。(我们称之为**安全保证safety guarantees**)。
并不是所有的应用都需要事务,有时候弱化事务保证、或完全放弃事务也是有好处的(例如,为了获得更高性能或更高可用性)。一些安全属性也可以在没有事务的情况下实现。
并不是所有的应用都需要事务,有时候弱化事务保证、或完全放弃事务也是有好处的(例如,为了获得更高性能或更高可用性)。一些安全属性也可以在没有事务的情况下实现。
怎样知道你是否需要事务?为了回答这个问题,首先需要确切理解事务可以提供的安全保障,以及它们的代价。尽管乍看事务似乎很简单,但实际上有许多微妙但重要的细节在起作用。
怎样知道你是否需要事务?为了回答这个问题,首先需要确切理解事务可以提供的安全保障,以及它们的代价。尽管乍看事务似乎很简单,但实际上有许多微妙但重要的细节在起作用。
本章将研究许多出错案例,并探索数据库用于防范这些问题的算法。尤其会深入**并发控制**的领域,讨论各种可能发生的竞争条件,以及数据库如何实现**读已提交****快照隔离**和**可串行化**等隔离级别。
本章将研究许多出错案例,并探索数据库用于防范这些问题的算法。尤其会深入**并发控制**的领域,讨论各种可能发生的竞争条件,以及数据库如何实现**读已提交****快照隔离**和**可串行化**等隔离级别。
本章同时适用于单机数据库与分布式数据库;在[第8章](ch8.md)中将重点讨论仅出现在分布式系统中的特殊挑战。
本章同时适用于单机数据库与分布式数据库;在[第8章](ch8.md)中将重点讨论仅出现在分布式系统中的特殊挑战。
## 事务的棘手概念
现今,几乎所有的关系型数据库和一些非关系数据库都支持**事务**。其中大多数遵循IBM System R第一个SQL数据库在1975年引入的风格【1,2,3】。40年里尽管一些实现细节发生了变化但总体思路大同小异MySQLPostgreSQLOracleSQL Server等数据库中的事务支持与System R异乎寻常地相似。
现今,几乎所有的关系型数据库和一些非关系数据库都支持**事务**。其中大多数遵循IBM System R第一个SQL数据库在1975年引入的风格【1,2,3】。40年里尽管一些实现细节发生了变化但总体思路大同小异MySQLPostgreSQLOracleSQL Server等数据库中的事务支持与System R异乎寻常地相似。
2000年以后非关系NoSQL数据库开始普及。它们的目标是通过提供新的数据模型选择参见第2章并通过默认包含复制第5章和分区第6章来改善关系现状。事务是这种运动的主要原因这些新一代数据库中的许多数据库完全放弃了事务或者重新定义了这个词描述比以前理解所更弱的一套保证【4】。

3
ch8.md
View file

@ -327,7 +327,7 @@
在[图8-3]()中当一个写入被复制到其他节点时它会根据发生写入的节点上的时钟时钟标记一个时间戳。在这个例子中时钟同步是非常好的节点1和节点3之间的偏差小于3ms这可能比你在实践中预期的更好。
尽管如此,[图8-3](img/fig8-3.png)中的时间戳却无法正确排列事件:写入`x = 1`的时间戳为42.004秒,但写入`x = 2`的时间戳为42.003秒,即使`x = 2`在稍后出现。当节点2接收到这两个事件时会错误地推断出`x = 1`是最近的值,而写入`x = 2`。效果上表现为客户端B的增量操作会丢失。
尽管如此,[图8-3](img/fig8-3.png)中的时间戳却无法正确排列事件:写入`x = 1`的时间戳为42.004秒,但写入`x = 2`的时间戳为42.003秒,即使`x = 2`在稍后出现。当节点2接收到这两个事件时会错误地推断出`x = 1`是最近的值,而丢弃写入`x = 2`。效果上表现为客户端B的增量操作会丢失。
这种冲突解决策略被称为**最后写入为准LWW**它在多领导者复制和无领导者数据库如Cassandra 【53】和Riak 【54】中被广泛使用参见“[最后写入为准(丢弃并发写入)](#最后写入为准(丢弃并发写入))”一节。有些实现会在客户端而不是服务器上生成时间戳但这并不能改变LWW的基本问题
@ -773,7 +773,6 @@ while(true){
------
| 上一章 | 目录 | 下一章 |

4
ch9.md
View file

@ -1,5 +1,3 @@
# 9. 一致性与共识
![](img/ch9.png)
@ -272,7 +270,7 @@
#### CAP定理
这个问题不仅仅是单主复制和多主复制的后果:任何线性一致的数据库都有这个问题,不管它是如何实现的。这个问题也不仅仅局限于多数据中心部署,而可能发生在任何不可靠的网络上,即使在同一个数据中心内也是如此。问题面临的权衡如下:[^v]
这个问题不仅仅是单主复制和多主复制的后果:任何线性一致的数据库都有这个问题,不管它是如何实现的。这个问题也不仅仅局限于多数据中心部署,而可能发生在任何不可靠的网络上,即使在同一个数据中心内也是如此。问题面临的权衡如下:[^v]
* 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都**不可用unavailable**)。
* 如果应用不需要线性一致性,那么某个副本即使与其他副本断开连接,也可以独立处理请求(例如多主复制)。在这种情况下,应用可以在网络问题前保持可用,但其行为不是线性一致的。