finish chapter 5

This commit is contained in:
Yin Gang 2021-08-03 20:58:26 +08:00
parent b52750bf23
commit ecc0462c3f

107
ch5.md
View File

@ -169,7 +169,7 @@
由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。
对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统(如数据)这一点很有用例如复制到数据仓库进行离线分析或建立自定义索引和缓存【18】。 这种技术被称为 **数据变更捕获change data capture**第11章将重新讲到它。
对于外部应用程序来说逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统这一点很有用例如复制到数据仓库进行离线分析或建立自定义索引和缓存【18】。 这种技术被称为 **数据变更捕获change data capture**第11章将重新讲到它。
#### 基于触发器的复制
@ -191,7 +191,7 @@
在这种伸缩体系结构中,只需添加更多的追随者,就可以提高只读请求的服务容量。但是,这种方法实际上只适用于异步复制——如果尝试同步复制到所有追随者,则单个节点故障或网络中断将使整个系统无法写入。而且越多的节点越有可能会被关闭,所以完全同步的配置是非常不可靠的。
不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为 **最终一致性eventually consistency**[^iii]【22,23】
不幸的是,当应用程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为 **最终一致性eventual consistency**[^iii]【22,23】
[^iii]: 道格拉斯·特里Douglas Terry等人创造了术语最终一致性。 【24】 并经由Werner Vogels 【22】推广成为许多NoSQL项目的口号。 然而不只有NoSQL数据库是最终一致的关系型数据库中的异步复制追随者也有相同的特性。
@ -242,7 +242,7 @@
**图5-4 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。**
**单调读Monotonic reads**【23】保证这种异常不会发生。这是一个比 **强一致性strong consistency** 更弱,但比 **最终一致性eventually consistency** 更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
**单调读Monotonic reads**【23】保证这种异常不会发生。这是一个比 **强一致性strong consistency** 更弱,但比 **最终一致性eventual consistency** 更强的保证。当读取数据时,您可能会看到一个旧值;单调读取仅意味着如果一个用户顺序地进行多次读取,则他们不会看到时间后退,即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
实现单调读取的一种方式是确保每个用户总是从同一个副本进行读取不同的用户可以从不同的副本读取。例如可以基于用户ID的散列来选择副本而不是随机选择副本。但是如果该副本失败用户的查询将需要重新路由到另一个副本。
@ -279,7 +279,7 @@
这是**分区partitioned****分片sharded**数据库中的一个特殊问题将在第6章中讨论。如果数据库总是以相同的顺序应用写入则读取总是会看到一致的前缀所以这种异常不会发生。但是在许多分布式数据库中不同的分区独立运行因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“关系与并发”一节中返回这个主题。
一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“[“此前发生”的关系和并发](#“此前发生”的关系和并发)”一节中返回这个主题。
### 复制延迟的解决方案
@ -297,7 +297,7 @@
本章到目前为止,我们只考虑使用单个领导者的复制架构。 虽然这是一种常见的方法,但也有一些有趣的选择。
基于领导者的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
基于领导者的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它[^iv]。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
[^iv]: 如果数据库被分区见第6章每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。
@ -333,7 +333,7 @@
有些数据库默认情况下支持多主配置但使用外部工具实现也很常见例如用于MySQL的Tungsten Replicator 【26】用于PostgreSQL的BDR【27】以及用于Oracle的GoldenGate 【19】。
尽管多主复制有这些优势,但也有一个很大的缺点:两个不同的数据中心可能会同时修改相同的数据,写冲突是必须解决的(如[图5-6](img/fig5-6.png)中“[冲突解决](#冲突解决)”)。本书将在“[处理写入冲突](#处理写入冲突)”中详细讨论这个问题。
尽管多主复制有这些优势,但也有一个很大的缺点:两个不同的数据中心可能会同时修改相同的数据,写冲突是必须解决的(如[图5-6](img/fig5-6.png)中“冲突解决conflict resolution”)。本书将在“[处理写入冲突](#处理写入冲突)”中详细讨论这个问题。
由于多主复制在许多数据库中都属于改装的功能所以常常存在微妙的配置缺陷且经常与其他数据库功能之间出现意外的反应。例如自增主键、触发器、完整性约束等都可能会有麻烦。因此多主复制往往被认为是危险的领域应尽可能避免【28】。
@ -375,7 +375,7 @@
#### 避免冲突
处理冲突的最简单的策略就是避免它们:如果应用程序可以确保特定记录的所有写入都通过同一个领导者,那么冲突就不会发生。由于多领导者复制处理的许多实现冲突相当不好避免冲突是一个经常推荐的方法【34】。
处理冲突的最简单的策略就是避免它们:如果应用程序可以确保特定记录的所有写入都通过同一个领导者,那么冲突就不会发生。由于许多的多领导者复制实现在处理冲突时处理得相当不好避免冲突是一个经常推荐的方法【34】。
例如,在用户可以编辑自己的数据的应用程序中,可以确保来自特定用户的请求始终路由到同一数据中心,并使用该数据中心的领导者进行读写。不同的用户可能有不同的“家庭”数据中心(可能根据用户的地理位置选择),但从任何用户的角度来看,配置基本上都是单一的领导者。
@ -410,11 +410,11 @@
当检测到冲突时所有冲突写入被存储。下一次读取数据时会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突并将结果写回数据库。例如CouchDB以这种方式工作。
请注意冲突解决通常适用于单个行或文档层面而不是整个事务【36】。因此如果您有一个事务会原子性地进行几次不同的写入请参阅第7章对于冲突解决而言,每个写入仍需分开单独考虑。
请注意冲突解决通常适用于单个行或文档层面而不是整个事务【36】。因此如果您有一个事务会原子性地进行几次不同的写入请参阅[第7章](ch7.md),对于冲突解决而言,每个写入仍需分开单独考虑。
> #### 题外话:自动冲突解决
> #### 自动冲突解决
>
> 冲突解决规则可能很快变得复杂并且自定义代码可能容易出错。亚马逊是一个经常被引用的例子由于冲突解决处理程序令人意外的效果一段时间以来购物车上的冲突解决逻辑将保留添加到购物车的物品但不包括从购物车中移除的物品。因此顾客有时会看到物品重新出现在他们的购物车中即使他们之前已经被移走【37】。
>
@ -435,7 +435,7 @@
其他类型的冲突可能更为微妙,难以发现。例如,考虑一个会议室预订系统:它记录谁订了哪个时间段的哪个房间。应用需要确保每个房间只有一组人同时预定(即不得有相同房间的重叠预订)。在这种情况下,如果同时为同一个房间创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查可用性,如果两次预订是由两个不同的领导者进行的,则可能会有冲突。
现在还没有一个现成的答案但在接下来的章节中我们将更好地了解这个问题。我们将在第7章中看到更多的冲突示例在[第12章](ch12.md)中我们将讨论用于检测和解决复制系统中冲突的可伸缩方法。
现在还没有一个现成的答案,但在接下来的章节中,我们将更好地了解这个问题。我们将在[第7章](ch7.md)中看到更多的冲突示例,在[第12章](ch12.md)中我们将讨论用于检测和解决复制系统中冲突的可伸缩方法。
@ -449,7 +449,7 @@
最普遍的拓扑是全部到全部([图5-8 [c]]()其中每个领导者将其写入每个其他领导。但是也会使用更多受限制的拓扑例如默认情况下MySQL仅支持**环形拓扑circular topology**【34】其中每个节点接收来自一个节点的写入并将这些写入加上自己的任何写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状[^v]。一个指定的根节点将写入转发给所有其他节点。星型拓扑可以推广到树。
[^v]: 不要与星型模式混淆(请参阅“[分析模式:星型还是雪花](ch2.md#分析模式:星型还是雪花)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
[^v]: 不要与星型模式混淆(请参阅“[星型和雪花型:分析模式](ch2.md#星型和雪花型:分析模式)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
在圆形和星形拓扑中写入可能需要在到达所有副本之前通过多个节点。因此节点需要转发从其他节点收到的数据更改。为了防止无限复制循环每个节点被赋予一个唯一的标识符并且在复制日志中每个写入都被标记了所有已经过的节点的标识符【43】。当一个节点收到用自己的标识符标记的数据更改时该数据更改将被忽略因为节点知道它已经被处理过。
@ -461,9 +461,9 @@
**图5-9 使用多主程序复制时,可能会在某些副本中写入错误的顺序。**
在[图5-9](img/fig5-9.png)中客户端A向主库1的表中插入一行客户端B在主库3上更新该行。然而主库2可以以不同的顺序接收写入它可以首先接收更新其中,从它的角度来看,是对数据库中不存在的行的更新),并且仅在稍后接收到相应的插入(其应该在更新之前)。
在[图5-9](img/fig5-9.png)中客户端A向主库1的表中插入一行客户端B在主库3上更新该行。然而主库2可以以不同的顺序接收写入它可以首先接收更新从它的角度来看是对数据库中不存在的行的更新并且仅在稍后接收到相应的插入其应该在更新之前
这是一个因果关系的问题,类似于我们在“[一致前缀读](ch8.md#一致前缀读)”中看到的更新取决于先前的插入所以我们需要确保所有节点先处理插入然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的因为时钟不可能被充分地同步以便在主库2处正确地排序这些事件见[第8章](ch8.md))。
这是一个因果关系的问题,类似于我们在“[一致前缀读](#一致前缀读)”中看到的更新取决于先前的插入所以我们需要确保所有节点先处理插入然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的因为时钟不可能被充分地同步以便在主库2处正确地排序这些事件见[第8章](ch8.md))。
要正确排序这些事件,可以使用一种称为 **版本向量version vectors** 的技术,本章稍后将讨论这种技术(参阅“[检测并发写入](#检测并发写入)”。然而冲突检测技术在许多多领导者复制系统中执行得不好。例如在撰写本文时PostgreSQL BDR不提供写入的因果排序【27】而Tungsten Replicator for MySQL甚至不尝试检测冲突【34】。
@ -477,7 +477,7 @@
一些数据存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。最早的一些的复制数据系统是**无领导的leaderless**【1,44】但是在关系数据库主导的时代这个想法几乎已被忘却。在亚马逊将其用于其内部的Dynamo系统[^vi]之后它再一次成为数据库的一种时尚架构【37】。 RiakCassandra和Voldemort是由Dynamo启发的无领导复制模型的开源数据存储所以这类数据库也被称为*Dynamo风格*。
[^vi]: Dynamo不适用于Amazon以外的用户。 令人困惑的是AWS提供了一个名为DynamoDB的托管数据库产品它使用了完全不同的体系结构它基于单引导程序复制。
[^vi]: Dynamo不适用于Amazon以外的用户。 令人困惑的是AWS提供了一个名为DynamoDB的托管数据库产品它使用了完全不同的体系结构它基于单领导者复制。
在一些无领导者的实现中,客户端直接将写入发送到到几个副本中,而另一些情况下,一个 **协调者coordinator** 节点代表客户端进行写入。但与主库数据库不同,协调者不执行特定的写入顺序。我们将会看到,这种设计上的差异对数据库的使用方式有着深远的影响。
@ -523,7 +523,7 @@
在Dynamo风格的数据库中参数nw和r通常是可配置的。一个常见的选择是使n为奇数通常为3或5并设置 $w = r =n + 1/ 2$(向上取整)。但是可以根据需要更改数字。例如,设置$w = n$和$r = 1$的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。
> 集群中可能有多于n的节点。集群的机器数可能多于副本数目但是任何给定的值只能存储在n个节点上。 这允许对数据集进行分区,从而支持可以放在一个节点上的数据集更大的数据集。 将在第6章回到分区。
> 集群中可能有多于n的节点。集群的机器数可能多于副本数目但是任何给定的值只能存储在n个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在[第6章](ch6.md)继续讨论分区。
>
法定人数条件$w + r> n$允许系统容忍不可用的节点,如下所示:
@ -538,13 +538,13 @@
**图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】。
@ -555,50 +555,50 @@
但是,即使在$w + r> n$的情况下,也可能存在返回陈旧值的边缘情况。这取决于实现,但可能的情况包括:
* 如果使用宽松的法定人数(见“[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”w个写入和r个读取落在完全不同的节点上因此r节点和w之间不再保证有重叠节点【46】。
* 如果两个写入同时发生,不清楚哪一个先发生。在这种情况下,唯一安全的解决方案是合并并发写入(请参阅第171页的“处理写入冲突”)。如果根据时间戳(最后写入胜利)挑选出一个胜者,则由于时钟偏差[35],写入可能会丢失。我们将返回“[检测并发写入](#检测并发写入)”中的此主题。
* 如果两个写入同时发生,不清楚哪一个先发生。在这种情况下,唯一安全的解决方案是合并并发写入(请参阅“[处理写入冲突](#处理写入冲突)”)。如果根据时间戳(最后写入胜利)挑选出一个胜者,则由于时钟偏差[35],写入可能会丢失。我们将在“[检测并发写入](#检测并发写入)”继续讨论此话题。
* 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
* 如果写操作在某些副本上成功而在其他节点上失败例如因为某些节点上的磁盘已满在小于w个副本上写入成功。所以整体判定写入失败但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败后续的读取仍然可能会读取这次失败写入的值【47】。
* 如果携带新值的节点失败需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复则存储新值的副本数可能会低于w从而打破法定人数条件。
* 即使一切工作正常,有时也会不幸地出现关于**时序timing** 的边缘情况,我们将在第334页上的“[线性化和法定人数](ch9.md#线性化和法定人数)”中看到这点。
* 即使一切工作正常,有时也会不幸地出现关于**时序timing** 的边缘情况,我们将在**“[线性一致性和法定人数](ch9.md#线性一致性和法定人数)”中看到这点。
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数w和r来调整读取陈旧值的概率但把它们当成绝对的保证是不明智的。
尤其是,通常没有得到“[与延迟有关的问题](#)”(读取您的写入,单调读取或一致的前缀读取)中讨论的保证,因此前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要**事务**或**共识**。我们将在[第七章](ch7.md)和[第九章](ch9.md)回到这些话题。
尤其是,因为通常没有得到“[复制延迟问题](#复制延迟问题)”中讨论的保证(读己之写,单调读,一致前缀读),前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要**事务**或**共识**。我们将在[第七章](ch7.md)和[第九章](ch9.md)回到这些话题。
#### 监控陈旧度
从运维的角度来看,监视你的数据库是否返回最新的结果是很重要的。即使应用可以容忍陈旧的读取,您也需要了解复制的健康状况。如果显著落后,应该提醒您,以便您可以调查原因(例如,网络中的问题或超载节点)。
对于基于领导者的复制,数据库通常会公开复制滞后的度量标准,您可以将其提供给监视系统。这是可能的,因为写入按照相同的顺序应用于领导者和追随者,并且每个节点在复制日志中具有一个位置(在本地应用的写入数)。通过从领导者的当前位置中减去随者的当前位置,您可以测量复制滞后量。
对于基于领导者的复制,数据库通常会公开复制滞后的度量标准,您可以将其提供给监视系统。这是可能的,因为写入按照相同的顺序应用于领导者和追随者,并且每个节点在复制日志中具有一个位置(在本地应用的写入数)。通过从领导者的当前位置中减去随者的当前位置,您可以测量复制滞后量。
然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究并根据参数nw和r来预测陈旧读取的预期百分比【48】。不幸的是这还不是很常见的做法但是将过时测量值包含在数据库的标准度量标准中是一件好事。最终的一致性是故意模糊的保证,但是对于可操作性来说,能够量化“最终”是很重要的。
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究并根据参数nw和r来预测陈旧读取的预期百分比【48】。不幸的是这还不是很常见的做法但是将陈旧测量值包含在数据库的度量标准集中是一件好事。最终一致性是一种有意模糊的保证,但是从可操作性角度来说,能够量化“最终”是很重要的。
### 宽松的法定人数与提示移交
合理配置的法定人数可以使数据库无需故障切换即可容忍个别节点的故障。也可以容忍个别节点变慢因为请求不必等待所有n个节点响应——当w或r节点响应时它们可以返回。对于需要高可用、低延时、且能够容忍偶尔读到陈旧值的应用场景来说这些特性使无主复制的数据库很有吸引力。
然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能能够连接到它们,但是从数据库节点切断的客户端,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于可用节点,因此客户端可能无法达到法定人数。
然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能能够连接到它们,但是从数据库节点切断的客户端来看,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于w或r因此客户端不再能达到法定人数。
在一个大型的群集中节点数量明显多于n个网络中断期间客户端可能连接到某些数据库节点,而不是为了为特定值组成法定人数的节点们。在这种情况下,数据库设计人员需要权衡一下:
在一个大型的群集中节点数量明显多于n个网络中断期间客户端可能仍能连接到一些数据库节点,但又不足以组成一个特定值的法定人数。在这种情况下,数据库设计人员需要权衡一下:
* 将错误返回给我们无法达到w或r节点的法定数量的所有请求是否更好
* 或者我们是否应该接受写入,然后将它们写入一些可达的节点,但不在这些值通常存在的n个节点之间
* 对于所有无法达到w或r节点法定人数的请求是否返回错误是更好的
* 或者我们是否应该接受写入,然后将它们写入一些可达的节点,但不在这些值通常所存在的n个节点上
后者被认为是一个**宽松的法定人数sloppy quorum**【37】写和读仍然需要w和r成功的响应是那些可能包括不在指定的n个“主”节点中的值。比方说,如果你把自己锁在房子外面,你可能会敲开邻居的门,问你是否可以暂时停留在沙发上。
后者被认为是一个**宽松的法定人数sloppy quorum**【37】写和读仍然需要w和r成功的响应这些响应可能来自不在指定的n个“主”节点中的其它节点。比方说,如果你把自己锁在房子外面,你可能会敲开邻居的门,问你是否可以暂时在沙发上。
一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的“本地”节点。这就是所谓的**提示移交hinted handoff**。 (一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家。)
一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的“”节点。这就是所谓的**提示移交hinted handoff**。 (一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家。)
宽松的法定人数对写入可用性的提高特别有用只要有任何w节点可用数据库就可以接受写入。然而这意味着即使当$w + r> n$时也不能确定读取某个键的最新值因为最新的值可能已经临时写入了n之外的某些节点【47】。
因此在传统意义上一个宽松的法定人数实际上不是一个法定人数。这只是一个保证即数据存储在w节点的地方。不能保证r节点的读取直到提示已经完成。
因此在传统意义上一个宽松的法定人数实际上不是一个法定人数。这只是一个保证即数据存储在w节点的地方。不能保证r节点的读取直到提示移交已经完成。
在所有常见的Dynamo实现中宽松的法定人数是可选的。在Riak中它们默认是启用的而在Cassandra和Voldemort中它们默认是禁用的【46,49,50】。
#### 运维多个数据中心
我们先前讨论了跨数据中心复制作为多主复制的用例(参阅“[多主复制](#多主复制)”)。无主复制适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
我们先前讨论了跨数据中心复制作为多主复制的用例(参阅“[多主复制](#多主复制)”)。无主复制适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持副本的数量n包括所有数据中心的节点在配置中您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何每个来自客户端的写入都会发送到所有副本但客户端通常只等待来自其本地数据中心内的法定节点的确认从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生尽管配置有一定的灵活性【50,51】。
@ -610,7 +610,7 @@
问题在于,由于可变的网络延迟和部分故障,事件可能在不同的节点以不同的顺序到达。例如,[图5-12](img/fig5-12.png)显示了两个客户机A和B同时写入三节点数据存储区中的键X
* 节点 1 接收来自 A 的写入,但由于暂时中断,从不接收来自 B 的写入。
* 节点 1 接收来自 A 的写入,但由于暂时中断,未接收到来自 B 的写入。
* 节点 2 首先接收来自 A 的写入,然后接收来自 B 的写入。
* 节点 3 首先接收来自 B 的写入,然后从 A 写入。
@ -622,7 +622,7 @@
为了最终达成一致,副本应该趋于相同的值。如何做到这一点?有人可能希望复制的数据库能够自动处理,但不幸的是,大多数的实现都很糟糕:如果你想避免丢失数据,你(应用程序开发人员)需要知道很多有关数据库冲突处理的内部信息。
在“[处理写冲突](#处理写入冲突)”一节中已经简要介绍了一些解决冲突的技术。在总结本章之前,让我们来更详细地探讨这个问题。
在“[处理写冲突](#处理写入冲突)”一节中已经简要介绍了一些解决冲突的技术。在总结本章之前,让我们来更详细地探讨这个问题。
#### 最后写入胜利(丢弃并发写入)
@ -632,18 +632,18 @@
即使写入没有自然的排序,我们也可以强制任意排序。例如,可以为每个写入附加一个时间戳,挑选最 **“最近”** 的最大时间戳,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为 **最后写入胜利LWW, last write wins**是Cassandra 【53】唯一支持的冲突解决方法也是Riak 【35】中的一个可选特征。
LWW实现了最终收敛的目标但以**持久性**为代价如果同一个Key有多个并发写入即使它们都被报告为客户端成功(因为它们被写入 w 个副本),但只有一个写入将存活而其他写入将被静默丢弃。此外LWW甚至可能会删除不是并发的写入我们将在的“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中讨论。
LWW实现了最终收敛的目标但以**持久性**为代价如果同一个Key有多个并发写入即使它们报告给客户端的都是成功(因为它们被写入 w 个副本),也只有一个写入将存活而其他写入将被静默丢弃。此外LWW甚至可能会删除不是并发的写入我们将在的“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中讨论。
有一些情况如缓存其中丢失的写入可能是可以接受的。如果丢失数据不可接受LWW是解决冲突的一个很烂的选择。
与LWW一起使用数据库的唯一安全方法是确保一个键只写入一次然后视为不可变从而避免对同一个密钥进行并发更新。例如Cassandra推荐使用的方法是使用UUID作为键从而为每个写操作提供一个唯一的键【53】。
与LWW一起使用数据库的唯一安全方法是确保一个键只写入一次然后视为不可变从而避免对同一个进行并发更新。例如Cassandra推荐使用的方法是使用UUID作为键从而为每个写操作提供一个唯一的键【53】。
#### “此前发生”的关系和并发
我们如何判断两个操作是否是并发的?为了建立一个直觉,让我们看看一些例子:
* 在[图5-9](fig5-9.png)中两个写入不是并发的A的插入发生在B的增之前因为B递增的值是A插入的值。换句话说B的操作建立在A的操作上所以B的操作必须有后来发生。我们也可以说B是 **因果依赖causally dependent** 于A
* 另一方面,[图5-12](fig5-12.png)中的两个写入是并发的:当每个客户端启动操作时,它不知道另一个客户端也正在执行操作同样的Key。因此,操作之间不存在因果关系。
* 在[图5-9](fig5-9.png)中两个写入不是并发的A的插入发生在B的增之前因为B递增的值是A插入的值。换句话说B的操作建立在A的操作上所以B的操作必须有后来发生。我们也可以说B是 **因果依赖causally dependent** 于A
* 另一方面,[图5-12](fig5-12.png)中的两个写入是并发的:当每个客户端启动操作时,它不知道另一个客户端也正在执行操作同样的。因此,操作之间不存在因果关系。
如果操作B了解操作A或者依赖于A或者以某种方式构建于操作A之上则操作A在另一个操作B之前发生。在另一个操作之前是否发生一个操作是定义什么并发的关键。事实上我们可以简单地说如果两个操作都不在另一个之前发生那么两个操作是并发的两个操作都不知道另一个【54】。
@ -655,15 +655,15 @@
>
> 如果两个操作 **“同时”** 发生,似乎应该称为并发——但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否**同时**发生的,这个问题我们将在[第8章](ch8.md)中详细讨论。
>
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】它引入了信息不能比光速更快的思想。因此如果事件之间的时间短于光通过它们之间的距离,那么发生一定距离的两个事件不可能相互影响。
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】它引入了信息不能比光速更快的思想。因此如果两个事件发生的时间差小于光通过它们之间的距离所需要的时间,那么这两个事件不可能相互影响。
>
> 在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作也可能是**并行的**。例如,如果网络缓慢或中断,两个操作间可能会出现一段时间间隔,仍然是并发的,因为网络问题阻止一个操作意识到另一个操作的存在。
> 在计算机系统中,即使光速原则上允许一个操作影响另一个操作,但两个操作也可能是**并行的**。例如,如果网络缓慢或中断,两个操作间可能会出现一段时间间隔,仍然是并发的,因为网络问题阻止一个操作意识到另一个操作的存在。
#### 捕获"此前发生"关系
来看一个算法,它确定两个操作是否为并发的,还是一个在另一个之前。为了简单起见,我们从一个只有一个副本的数据库开始。一旦我们已经制定了如何在单个副本上完成这项工作,我们可以将该方法概括为具有多个副本的无领导者数据库。
来看一个算法,它确定两个操作是否为并发的,还是一个在另一个之前。为了简单起见,我们从一个只有一个副本的数据库开始。一旦我们已经制定了如何在单个副本上完成这项工作,我们可以将该方法推广到具有多个副本的无领导者数据库。
[图5-13]()显示了两个客户端同时向同一购物车添加项目。 (如果这样的例子让你觉得太麻烦了,那么可以想象,两个空中交通管制员同时把飞机添加到他们正在跟踪的区域)最初,购物车是空的。在它们之间,客户端向数据库发出五次写入:
@ -687,10 +687,10 @@
* 服务器为每个键保留一个版本号,每次写入键时都增加版本号,并将新版本号与写入的值一起存储。
* 当客户端读取键时,服务器将返回所有未覆盖的值以及最新的版本号。客户端在写入前必须读取。
* 客户端写入键时,必须包含之前读取的版本号,并且必须将之前读取的所有值合并在一起。 来自写入请求的响应可以像读取一样,返回所有当前值,这使得我们可以像购物车示例那样连接多个写入。)
* 当服务器接收到具有特定版本号的写入时,它可以覆盖该版本号或更低版本的所有值(因为它知道它们已经被合并到新的值中),但是它必须保持所有值更高版本号(因为这些值与传入的写入同时发生)。
* 客户端写入键时,必须包含之前读取的版本号,并且必须将之前读取的所有值合并在一起。 针对写入请求的响应可以像读取请求一样,返回所有当前值,这使得我们可以像购物车示例那样将多个写入串联起来。)
* 当服务器接收到具有特定版本号的写入时,它可以覆盖该版本号或更低版本的所有值(因为它知道它们已经被合并到新的值中),但是它必须用更高的版本号来保存所有值(因为这些值与随后的写入是并发的)。
当一个写入包含前一次读取的版本号时,它会告诉我们写入的哪一种状态。如果在不包含版本号的情况下进行写操作,则与所有其他写操作并发,因此它不会覆盖任何内容 —— 只会在随后的读取中作为其中一个值返回。
当一个写入包含前一次读取的版本号时,它会告诉我们写入是基于之前的哪一种状态。如果在不包含版本号的情况下进行写操作,则与所有其他写操作并发,因此它不会覆盖任何内容 —— 只会在随后的读取中作为其中一个值返回。
#### 合并同时写入的值
@ -698,11 +698,11 @@
合并兄弟值,本质上是与多领导者复制中的冲突解决相同的问题,我们先前讨论过(参阅“[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中做更聪明的事情。
以购物车为例,一种合理的合并兄弟方法就是集合求并。在[图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】可以以合理的方式自动合并兄弟包括保留删除。
#### 版本向量
@ -710,7 +710,7 @@
[图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**)。版本向量允许数据库区分覆盖写入和并发写入。
@ -718,7 +718,7 @@
> #### 版本向量和向量时钟
>
> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——请参阅参考资料的细节【57,60,61】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
> 版本向量有时也被称为矢量时钟,即使它们不完全相同。 差别很微妙——细节请参阅参考资料【57,60,61】。 简而言之,在比较副本的状态时,版本向量是正确的数据结构。
>
## 本章小结
@ -743,7 +743,7 @@
尽管是一个简单的目标 - 在几台机器上保留相同数据的副本,但复制却是一个非常棘手的问题。它需要仔细考虑并发和所有可能出错的事情,并处理这些故障的后果。至少,我们需要处理不可用的节点和网络中断(甚至不考虑更隐蔽的故障,例如由于软件错误导致的无提示数据损坏)。
尽管是一个简单的目标 - 在几台机器上保留相同数据的副本,但复制却是一个非常棘手的问题。它需要仔细考虑并发和所有可能出错的事情,并处理这些故障的后果。至少,我们需要处理不可用的节点和网络中断(甚至不考虑更隐蔽的故障,例如由于软件错误导致的静默数据损坏)。
我们讨论了复制的三种主要方法:
@ -758,11 +758,12 @@
***无主复制***
客户端发送每个写入到几个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。
每种方法都有优点和缺点。单主复制是非常流行的,因为它很容易理解,不需要担心冲突解决。在出现故障节点,网络中断和延迟峰值的情况下,多领导者和无领导者复制可以更加稳健,但以更难以推理并仅提供非常弱的一致性保证为代价。
复制可以是同步的,也可以是异步的,在发生故障时对系统行为有深远的影响。尽管在系统运行平稳时异步复制速度很快,但是在复制滞后增加和服务器故障时要弄清楚会发生什么,这一点很重要。如果一个领导者失败了,并且你推动一个异步更新的追随者成为新的领导者,那么最近承诺的数据可能会丢失。
复制可以是同步的,也可以是异步的,在发生故障时对系统行为有深远的影响。尽管在系统运行平稳时异步复制速度很快,但是在复制滞后增加和服务器故障时要弄清楚会发生什么,这一点很重要。如果一个领导者失败了,并且你提升了一个异步更新的追随者成为新的领导者,那么最近提交的数据可能会丢失。
我们研究了一些可能由复制滞后引起的奇怪效应,我们讨论了一些有助于决定应用程序在复制滞后时的行为的一致性模型:
我们研究了一些可能由复制滞后引起的奇怪效应,我们讨论了一些有助于决定应用程序在复制滞后时的行为的一致性模型:
***写后读***
@ -770,7 +771,7 @@
***单调读***
用户在一个时间点看到数据后,他们不应该在某个早期时间点看到数据。
用户在一个时间点看到数据后,他们不应该在某个更早的时间点看到数据。
***一致前缀读***
@ -778,9 +779,9 @@
最后,我们讨论了多领导者和无领导者复制方法所固有的并发问题:因为他们允许多个写入并发发生冲突。我们研究了一个数据库可能使用的算法来确定一个操作是否发生在另一个操作之前,或者它们是否同时发生。我们还谈到了通过合并并发更新来解决冲突的方法。
最后,我们讨论了多领导者和无领导者复制方法所固有的并发问题:因为他们允许多个写入并发发生,这可能会导致冲突。我们研究了一个数据库可能使用的算法来确定一个操作是否发生在另一个操作之前,或者它们是否同时发生。我们还谈到了通过合并并发更新来解决冲突的方法。
在下一章中,我们将继续研究分布在多个机器上的数据,通过复制的对应方式:将大数据集分割成分区。
在下一章中,我们将继续研究分布在多个机器上的数据,通过复制的同僚:将大数据集分割成分区。