From 11087acbc8a42ec2d273703ba219157fcbb76f60 Mon Sep 17 00:00:00 2001 From: Vonng Date: Sun, 3 Jun 2018 21:43:15 +0800 Subject: [PATCH] format fix --- ch5.md | 367 +++++++++++++++++++++++++++++---------------------------- ch6.md | 187 ++++++++++++++--------------- ch7.md | 16 +-- ch8.md | 3 +- ch9.md | 4 +- 5 files changed, 289 insertions(+), 288 deletions(-) diff --git a/ch5.md b/ch5.md index eafb123..45e0d00 100644 --- a/ch5.md +++ b/ch5.md @@ -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版本开始),MySQL,Oracle Data Guard 【2】和SQL Server的AlwaysOn可用性组【3】。 它也被用于一些非关系数据库,包括MongoDB,RethinkDB和Espresso 【4】。 最后,基于领导者的复制并不仅限于数据库:像Kafka 【5】和RabbitMQ高可用队列【6】这样的分布式消息代理也使用它。 某些网络文件系统,例如DRBD这样的块复制设备也与之类似。 +​ 这种复制模式是许多关系数据库的内置功能,如PostgreSQL(从9.0版本开始),MySQL,Oracle Data Guard 【2】和SQL Server的AlwaysOn可用性组【3】。 它也被用于一些非关系数据库,包括MongoDB,RethinkDB和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的托管数据库产品,它使用了完全不同的体系结构:它基于单主程序复制。) Riak,Cassandra和Voldemort是由Dynamo启发的无领导复制模型的开源数据存储,所以这类数据库也被称为*Dynamo风格*。 +​ 一些数据存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。最早的一些的复制数据系统是**无领导的(leaderless)**【1,44】,但是在关系数据库主导的时代,这个想法几乎已被忘却。在亚马逊将其用于其内部的Dynamo系统[^vi]之后,它再一次成为数据库的一种时尚架构【37】。(Dynamo不适用于Amazon以外的用户。 令人困惑的是,AWS提供了一个名为DynamoDB的托管数据库产品,它使用了完全不同的体系结构:它基于单主程序复制。) Riak,Cassandra和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 = 3,w = 2,r = 2$)。只要$w + r> n$,我们期望在读取时获得最新的值,因为r个读取中至少有一个节点是最新的。遵循这些r值,w值的读写称为**法定人数(quorum)**[^vii]的读和写。【44】 ,你可以认为,r和w是有效读写所需的最低票数。 +​ 更一般地说,如果有n个副本,每个写入必须由w节点确认才能被认为是成功的,并且我们必须至少为每个读取查询r个节点。 (在我们的例子中,$n = 3,w = 2,r = 2$)。只要$w + r> n$,我们期望在读取时获得最新的值,因为r个读取中至少有一个节点是最新的。遵循这些r值,w值的读写称为**法定人数(quorum)**[^vii]的读和写。【44】 ,你可以认为,r和w是有效读写所需的最低票数。 [^vii]: 有时候这种法定人数被称为严格的法定人数,相对“松散的法定人数”而言(见“[松散法定人数与带提示的接力](#松散法定人数与带提示的接力)”) -在Dynamo风格的数据库中,参数n,w和r通常是可配置的。一个常见的选择是使n为奇数(通常为3或5)并设置 $w = r =(n + 1)/ 2$(向上取整)。但是可以根据需要更改数字。例如,设置$w = n$和$r = 1$的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。 +​ 在Dynamo风格的数据库中,参数n,w和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)回到这些话题。 #### 监控陈旧度 -从运维的角度来看,监视你的数据库是否返回最新的结果是很重要的。即使应用可以容忍陈旧的读取,您也需要了解复制的健康状况。如果显着落后,应该提醒您,以便您可以调查原因(例如,网络中的问题或超载节点)。 +​ 从运维的角度来看,监视你的数据库是否返回最新的结果是很重要的。即使应用可以容忍陈旧的读取,您也需要了解复制的健康状况。如果显着落后,应该提醒您,以便您可以调查原因(例如,网络中的问题或超载节点)。 -对于基于领导者的复制,数据库通常会公开复制滞后的度量标准,您可以将其提供给监视系统。这是可能的,因为写入按照相同的顺序应用于领导者和追随者,并且每个节点在复制日志中具有一个位置(在本地应用的写入次数)。通过从领导者的当前位置中减去随从者的当前位置,您可以测量复制滞后量。 +​ 对于基于领导者的复制,数据库通常会公开复制滞后的度量标准,您可以将其提供给监视系统。这是可能的,因为写入按照相同的顺序应用于领导者和追随者,并且每个节点在复制日志中具有一个位置(在本地应用的写入次数)。通过从领导者的当前位置中减去随从者的当前位置,您可以测量复制滞后量。 -然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读取修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。 +​ 然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读取修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。 -已经有一些关于衡量无主复制数据库中的复制陈旧度的研究,并根据参数n,w和r来预测陈旧读取的预期百分比【48】。不幸的是,这还不是很常见的做法,但是将过时测量值包含在数据库的标准度量标准中是一件好事。最终的一致性是故意模糊的保证,但是对于可操作性来说,能够量化“最终”是很重要的。 +​ 已经有一些关于衡量无主复制数据库中的复制陈旧度的研究,并根据参数n,w和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 149–174, March 1994. [doi:10.1007/BF02277859](http://dx.doi.org/10.1007/BF02277859) - -------- | 上一章 | 目录 | 下一章 | diff --git a/ch6.md b/ch6.md index a1b7dbc..1ed2a14 100644 --- a/ch6.md +++ b/ch6.md @@ -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卷则包含以T,U,V,X,Y和Z开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据,分区边界需要依据数据调整。 +​ 键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在[图6-2]()中,第1卷包含以A和B开头的单词,但第12卷则包含以T,U,V,X,Y和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使用MD5,Voldemort使用Fowler-Noll-Vo函数。许多编程语言都有内置的简单哈希函数(它们用于哈希表),但是它们可能不适合分区:例如,在Java的`Object.hashCode()`和Ruby的`Object#hash`,同一个键可能在不同的进程中有不同的哈希值【6】。 +​ 出于分区的目的,散列函数不需要多么强壮的加密算法:例如,Cassandra和MongoDB使用MD5,Voldemort使用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]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`color:red`的文档ID列表中。 +​ 你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段(field)**,关系数据库中这些是**列(column)** )。 如果您声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`color:red`的文档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#实践中的百分位点)”)。然而,它被广泛使用:MonDBDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。 +​ 这种查询分区数据库的方法有时被称为**分散/聚集(scatter/gather)**,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”)。然而,它被广泛使用:MonDBDB,Riak 【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) 一些作者声称,支持通用的两阶段提交代价太大,会带来性能与可用性的问题。让程序员来处理过度使用事务导致的性能问题,总比缺少事务编程好得多。 +> ​ 一些作者声称,支持通用的两阶段提交代价太大,会带来性能与可用性的问题。让程序员来处理过度使用事务导致的性能问题,总比缺少事务编程好得多。 > > ​ ——James Corbett等人,Spanner:Google的全球分布式数据库(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年里,尽管一些实现细节发生了变化,但总体思路大同小异:MySQL,PostgreSQL,Oracle,SQL Server等数据库中的事务支持与System R异乎寻常地相似。 +​ 现今,几乎所有的关系型数据库和一些非关系数据库都支持**事务**。其中大多数遵循IBM System R(第一个SQL数据库)在1975年引入的风格【1,2,3】。40年里,尽管一些实现细节发生了变化,但总体思路大同小异:MySQL,PostgreSQL,Oracle,SQL Server等数据库中的事务支持与System R异乎寻常地相似。 2000年以后,非关系(NoSQL)数据库开始普及。它们的目标是通过提供新的数据模型选择(参见第2章),并通过默认包含复制(第5章)和分区(第6章)来改善关系现状。事务是这种运动的主要原因:这些新一代数据库中的许多数据库完全放弃了事务,或者重新定义了这个词,描述比以前理解所更弱的一套保证【4】。 diff --git a/ch8.md b/ch8.md index 7700aa5..43e7daf 100644 --- a/ch8.md +++ b/ch8.md @@ -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){ - ------ | 上一章 | 目录 | 下一章 | diff --git a/ch9.md b/ch9.md index c2df3ea..be2f5f6 100644 --- a/ch9.md +++ b/ch9.md @@ -1,5 +1,3 @@ - - # 9. 一致性与共识 ![](img/ch9.png) @@ -272,7 +270,7 @@ #### CAP定理 -这个问题不仅仅是单主复制和多主复制的后果:任何线性一致的数据库都有这个问题,不管它是如何实现的。这个问题也不仅仅局限于多数据中心部署,而可能发生在任何不可靠的网络上,即使在同一个数据中心内也是如此。问题面临的权衡如下:[^v] +​ 这个问题不仅仅是单主复制和多主复制的后果:任何线性一致的数据库都有这个问题,不管它是如何实现的。这个问题也不仅仅局限于多数据中心部署,而可能发生在任何不可靠的网络上,即使在同一个数据中心内也是如此。问题面临的权衡如下:[^v] * 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都**不可用(unavailable)**)。 * 如果应用不需要线性一致性,那么某个副本即使与其他副本断开连接,也可以独立处理请求(例如多主复制)。在这种情况下,应用可以在网络问题前保持可用,但其行为不是线性一致的。 -- GitLab