mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
progress: ch9 section 1
* fix README.md * add credit
This commit is contained in:
parent
9766f0ad13
commit
a7aa9464c3
38
README.md
38
README.md
@ -3,16 +3,9 @@
|
||||
- 作者: [Martin Kleppmann](https://martin.kleppmann.com)
|
||||
- 原书名称:[《Designing Data-Intensive Application》](http://shop.oreilly.com/product/0636920032175.do)
|
||||
- 译者:[冯若航]( http://vonng.com/about) (fengruohang@outlook.com )
|
||||
- Gitbook地址:[ddia-cn](https://www.gitbook.com/book/vonng/ddia-cn)(需要科学上网)
|
||||
- 建议使用[Typora](https://www.typora.io)或Gitbook以获取最佳阅读体验。
|
||||
|
||||
-------------
|
||||
|
||||
## 法律声明
|
||||
|
||||
译者纯粹出于学习目的与个人兴趣翻译,本译文只供学习研究参考之用,不得公开传播发行或用于商业用途。有能力阅读英文书籍者请购买正版支持。
|
||||
|
||||
译者保留对译文的署名权,其他权利以原作者和出版社的主张为准,侵删。
|
||||
|
||||
## 译序
|
||||
|
||||
> 不懂数据库的全栈工程师不是好架构师
|
||||
@ -82,9 +75,7 @@
|
||||
|
||||
* 机翻:只在乎结构:梳理文章结构、图片、引用、备注。
|
||||
* 初翻:保证经完全理解本章内容,人工修复显著的错误,重新组织语言。
|
||||
* 精翻:阅读相关领域文献书籍,确定术语的最终译法,修复格式瑕疵,着力信达雅。
|
||||
|
||||
通常机翻一章1个小时左右,初翻一章6小时,精翻一章三到五天。
|
||||
* 精校:阅读相关领域文献书籍,确定术语的最终译法,修复格式瑕疵,着力信达雅。
|
||||
|
||||
精翻可以看,机翻基本没法看,初翻对于业内人士能凑合看。
|
||||
|
||||
@ -97,9 +88,9 @@
|
||||
| 第三章:存储与检索 | 初翻 | |
|
||||
| 第四章:编码与演化 | 初翻 | |
|
||||
| 第二部分:分布式数据——概览 | 精翻 | |
|
||||
| 第五章:复制 | 精翻 30% | |
|
||||
| 第五章:复制 | 精翻 30% | Vonng |
|
||||
| 第六章:分片 | 初翻 | |
|
||||
| 第七章:事务 | 精翻 60% | |
|
||||
| 第七章:事务 | 精翻 60% | Vonng |
|
||||
| 第八章:分布式系统中的问题 | 初翻 | |
|
||||
| 第九章:一致性与共识 | 初翻 30% | Vonng |
|
||||
| 第三部分:前言 | 精翻 | |
|
||||
@ -110,17 +101,30 @@
|
||||
| 后记 | 机翻 | |
|
||||
|
||||
|
||||
计划在3月25日前完成所有章节的初翻。
|
||||
计划在3月内完成所有章节的初翻。
|
||||
|
||||
|
||||
|
||||
## CONTRIBUTION
|
||||
|
||||
欢迎贡献,初翻后的章节,接受ISSUE指正。
|
||||
|
||||
贡献者需要同意[法律声明](#法律声明)所叙内容,翻译请提前联系以免冲突。
|
||||
|
||||
有人建议拉个群,也许发布更新通知或者同步翻译进度吧
|
||||
欢迎贡献,初翻后的章节,接受ISSUE指正。参与翻译前请先锁定相应章节。
|
||||
|
||||
整章的翻译、精校请使用PR,将列入署名,少量词法、语法、译法问题请使用ISSUE,将列入致谢。
|
||||
|
||||
All contribution will give proper credit. 贡献者需要同意[法律声明](#法律声明)所叙内容。
|
||||
|
||||
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
|
||||
2. [第一章语法标点修正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
|
||||
|
||||
## 法律声明
|
||||
|
||||
译者纯粹出于学习目的与个人兴趣翻译,本译文只供学习研究参考之用,不得公开传播发行或用于商业用途。有能力阅读英文书籍者请购买正版支持。
|
||||
|
||||
译者保留对译文的署名权,其他权利以原作者和出版社的主张为准。
|
||||
|
||||
## 译读者交流微信群
|
||||
|
||||
![](img/ddia-wexin.JPG)
|
||||
|
||||
|
265
ch9.md
265
ch9.md
@ -47,40 +47,40 @@
|
||||
|
||||
本章涵盖了广泛的话题,但我们将会看到这些领域实际上是紧密联系在一起的:
|
||||
|
||||
* 首先看一下常用的**最强一致性模型**之一,**线性化(linearizability)**,并考察其优缺点。
|
||||
* 首先看一下常用的**最强一致性模型**之一,**线性一致性(linearizability)**,并考察其优缺点。
|
||||
* 然后我们将检查分布式系统中[**事件顺序**](#顺序保证)的问题,特别是因果关系和全局顺序的问题。
|
||||
* 在第三部分(“[分布式事务和共识](#分布式事务和共识)”)中将探讨如何自动提交分布式事务,这将最终引导我们解决共识问题。
|
||||
|
||||
|
||||
|
||||
|
||||
## 线性化
|
||||
## 线性一致性
|
||||
|
||||
在**最终一致**的数据库,如果你在同一时刻问两个不同副本相同的问题,可能会得到两个不同的答案。这很让人困惑。如果数据库可以提供只有一个副本的假象(即,只有一个数据副本),那么事情就简单太多了。那么每个客户端都会有相同的数据视图,且不必担心复制滞后了。
|
||||
|
||||
这就是**线性化(linearizability)**背后的想法【6】(也称为**原子一致性(atomic consistency)**【7】,**强一致性(strong consistency)**,**立即一致性(immediate consistency)**或**外部一致性(external consistency )**【8】)。线性化的精确定义相当微妙,我们将在本节的剩余部分探讨它。但是基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子的。有了这个保证,即使实际中可能有多个副本,应用也不需要担心它们。
|
||||
这就是**线性一致性(linearizability)**背后的想法【6】(也称为**原子一致性(atomic consistency)**【7】,**强一致性(strong consistency)**,**立即一致性(immediate consistency)**或**外部一致性(external consistency )**【8】)。线性一致性的精确定义相当微妙,我们将在本节的剩余部分探讨它。但是基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子的。有了这个保证,即使实际中可能有多个副本,应用也不需要担心它们。
|
||||
|
||||
在一个线性化系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。维护数据的单个副本的错觉是指,系统能保障读到的值是最近的,最新的,不是来自一个陈旧的缓存或副本。换句话说,线性化是一个**新鲜度保证(recency guarantee)**。为了阐明这个想法,我们来看看一个不可线性化系统的例子。
|
||||
在一个线性一致性系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。维护数据的单个副本的错觉是指,系统能保障读到的值是最近的,最新的,不是来自一个陈旧的缓存或副本。换句话说,线性一致性是一个**新鲜度保证(recency guarantee)**。为了阐明这个想法,我们来看看一个非线性一致系统的例子。
|
||||
|
||||
![](img/fig9-1.png)
|
||||
|
||||
**图9-1 这个系统是非线性化的,导致球迷迷惑**
|
||||
**图9-1 这个系统是非线性一致的,导致了球迷的困惑**
|
||||
|
||||
[图9-1](img/fig9-1.png)展示了一个非线性化的,关于体育网站的例子【9】。Alice和Bob正坐在同一个房间里,都盯着各自的手机,关注着2014年FIFA世界杯决赛的结果。在最后得分公布后,Alice刷新页面,看到宣布了获胜者,并兴奋地告诉Bob。Bob难以置信地刷新了自己的手机,但他的请求路由到了一个落后的数据库副本上,手机显示比赛仍在进行。
|
||||
[图9-1](img/fig9-1.png)展示了一个非线性一致性的,关于体育网站的例子【9】。Alice和Bob正坐在同一个房间里,都盯着各自的手机,关注着2014年FIFA世界杯决赛的结果。在最后得分公布后,Alice刷新页面,看到宣布了获胜者,并兴奋地告诉Bob。Bob难以置信地刷新了自己的手机,但他的请求路由到了一个落后的数据库副本上,手机显示比赛仍在进行。
|
||||
|
||||
如果Alice和Bob在同一时间刷新并获得了两个不同的查询结果,也许就没有那么令人惊讶了。因为他们不知道服务器处理他们请求的精确时刻。然而Bob是在听到Alice惊呼最后得分**之后**,点击了刷新按钮(启动了他的查询),因此他希望查询结果至少与爱丽丝一样新鲜。但他的查询返回了陈旧结果,这一事实违背了线性化的要求。
|
||||
如果Alice和Bob在同一时间刷新并获得了两个不同的查询结果,也许就没有那么令人惊讶了。因为他们不知道服务器处理他们请求的精确时刻。然而Bob是在听到Alice惊呼最后得分**之后**,点击了刷新按钮(启动了他的查询),因此他希望查询结果至少与爱丽丝一样新鲜。但他的查询返回了陈旧结果,这一事实违背了线性一致性的要求。
|
||||
|
||||
### 什么使得系统线性一致?
|
||||
|
||||
线性化背后的基本思想很简单:使系统看起来好像只有一个数据副本。然而确切来讲,实际上有更多要操心的地方。为了更好地理解线性化,让我们看看更多的例子。
|
||||
线性一致性背后的基本思想很简单:使系统看起来好像只有一个数据副本。然而确切来讲,实际上有更多要操心的地方。为了更好地理解线性一致性,让我们再看几个例子。
|
||||
|
||||
[图9-2](img/fig9-2.png)显示了三个客户端在线性化数据库中同时读写相同的键`x`。在分布式系统文献中,`x`被称为**寄存器(register)**,例如它可以是键值存储中的一个**键**,关系数据库中的一**行**或文档数据库中的一个**文档**。
|
||||
[图9-2](img/fig9-2.png)显示了三个客户端在线性一致数据库中同时读写相同的键`x`。在分布式系统文献中,`x`被称为**寄存器(register)**,例如它可以是键值存储中的一个**键**,关系数据库中的一**行**或文档数据库中的一个**文档**。
|
||||
|
||||
![](img/fig9-2.png)
|
||||
|
||||
**图9-2 如果读取请求与写入请求并发,则可能会返回旧值或新值**
|
||||
|
||||
为了简单起见,[图9-2](img/fig9-2.png)仅显示了来自客户端的请求,而不是数据库的内部。每个小节都是由客户端发出的请求,其中小节的开始是发送请求的时间,小节的结尾是客户端收到响应的时间。由于可变的网络延迟,客户端不知道恰恰在数据库处理它的请求时——它只知道它在发送请求和接收响应的客户端之间的某个时间已经发生了。[^i]
|
||||
为了简单起见,[图9-2](img/fig9-2.png)采用了用户请求的视角,而不是数据库内部的视角。每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间——只知道它发生在发送请求和接收响应的之间的某个时刻。[^i]
|
||||
|
||||
[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(请参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和NTP产生的实时逼近。
|
||||
|
||||
@ -89,195 +89,196 @@
|
||||
* $ read(x)⇒v$表示客户端请求读取寄存器`x`的值,数据库返回值`v`。
|
||||
|
||||
|
||||
* $write(x,v)⇒r$表示客户端请求将寄存器`x`设置为值`v`,数据库返回响应`r`(可能正确,可能错误误)。
|
||||
* $write(x,v)⇒r$表示客户端请求将寄存器`x`设置为值`v`,数据库返回响应`r`(可能正确,可能错误)。
|
||||
|
||||
在[图9-2](img/fig9-2.png)中,x的值最初为0,客户端C执行写请求将其设置为1。发生这种情况时,客户端A和B反复轮询数据库以读取最新值。 A和B可能会为他们的阅读请求得到什么回应?
|
||||
在[图9-2](img/fig9-2.png)中,x的值最初为`0`,客户端C执行写请求将其设置为`1`。发生这种情况时,客户端A和B反复轮询数据库以读取最新值。 A和B的请求可能会收到怎样的响应?
|
||||
|
||||
* 客户端A的第一个读取操作在写入开始之前完成,因此必须返回旧值0。
|
||||
* 客户端A的最后一次读操作是在写操作完成后开始的,所以如果数据库是线性化的,它必须定期返回新的值1:我们知道写操作必须在写操作开始和结束之间的某个时间被处理,并且读取操作必须在读取操作的开始和结束之间的某个时间进行处理。如果在写入结束后开始读取,则必须在写入之后处理读取,因此它必须看到写入的新值。
|
||||
* 与写操作时间重叠的任何读操作可能会返回0或1,因为我们不知道写操作在处理读操作时是否生效。这些操作与写入同时发生。
|
||||
* 客户端A的第一个读操作,完成于写操作开始之前,因此必须返回旧值`0`。
|
||||
* 客户端A的最后一个读操作,开始于写操作完成之后。如果数据库是线性一致性的,它必然返回新值`1`:因为读操作和写操作一定是在其各自的起止区间内的某个时刻被处理。如果在写入结束后开始读取,则必须在写入之后处理读取,因此它必须看到写入的新值。
|
||||
* 与写操作在时间上重叠的任何读操作,可能会返回0或1,因为我们不知道读取时,写操作是否已经生效。这些操作是**并发(concurrent)**的。
|
||||
|
||||
但是,这还不足以完全描述线性化:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这不是我们所期望的仿真“单一数据副本”的系统。[^ii]
|
||||
但是,这还不足以完全描述线性一致性:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这不是我们所期望的仿真“单一数据副本”的系统。[^ii]
|
||||
|
||||
[^ii]: 如果读取(与写入同时发生时)可能返回旧值或新值,则称该寄存器为**常规寄存器(regular register)**【7,25】
|
||||
|
||||
为了使系统可线性化,我们需要添加另一个约束,如**图9-3**所示
|
||||
为了使系统线性一致,我们需要添加另一个约束,如**图9-3**所示
|
||||
|
||||
![](img/fig9-3.png)
|
||||
**图9-3 任何一个读取返回新值后,所有后续读取(在相同或其他客户端上)也必须返回新值。**
|
||||
|
||||
在一个可线性化的系统中,我们可以想象,在x的值自动翻转从0到1的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值1,即使写操作尚未完成,所有后续读取也必须返回新值。
|
||||
在一个线性一致的系统中,我们可以想象,在x的值自动翻转从0到1的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值1,即使写操作尚未完成,所有后续读取也必须返回新值。
|
||||
|
||||
[图9-3](img/fig9-3.png)中的箭头说明了这个时序依赖关系。客户端A是第一个读取新的值1的位置。在A的读取返回之后,B开始新的读取。由于B的读取严格在A的读取之后发生,因此即使C的写入仍在进行中,也必须返回1。 (与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同:在Alice读取新值之后,Bob也希望读取新的值。)
|
||||
[图9-3](img/fig9-3.png)中的箭头说明了这个时序依赖关系。客户端A是第一个读取新的值1的位置。在A的读取返回之后,B开始新的读取。由于B的读取严格在发生于A的读取之后,因此即使C的写入仍在进行中,也必须返回1。 (与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同:在Alice读取新值之后,Bob也希望读取新的值。)
|
||||
|
||||
我们可以进一步细化这个时序图,以可视化每个操作在某个时间点以原子方式生效。[图9-4](img/fig9-4.png)显示了一个更复杂的例子【10】。
|
||||
我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图9-4](img/fig9-4.png)显示了一个更复杂的例子【10】。
|
||||
|
||||
在[图9-4]()中,除了读写之外,还增加了第三种类型的操作:
|
||||
|
||||
* $cas(x, v_{old}, v_{new})$⇒r表示客户端请求进行原子[**比较与设置**](ch7.md#比较并设置(CAS))操作。如果寄存器$x$的当前值等于$v_{old}$,则应该原子地设置为$v_{new}$。如果$x≠vold$,则操作应该保持寄存器不变并返回一个错误。 $r$是数据库的响应(正确或错误)。
|
||||
* $cas(x, v_{old}, v_{new})$⇒r表示客户端请求进行原子性的[**比较与设置**](ch7.md#比较并设置(CAS))操作。如果寄存器$x$的当前值等于$v_{old}$,则应该原子地设置为$v_{new}$。如果$x≠vold$,则操作应该保持寄存器不变并返回一个错误。 $r$是数据库的响应(正确或错误)。
|
||||
|
||||
[图9-4]()中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的小节内部)。这些标记按顺序连接,结果必须是一个有效的寄存器读写序列(每次读取都必须返回最近一次写入设置的值)。
|
||||
[图9-4]()中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。
|
||||
|
||||
线性化的要求是,连接操作标记的线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新近性保证:一旦新的值被写入或读取,所有后续的读取都会看到写入的值,直到它被再次覆盖。
|
||||
线性一致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新鲜性保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。
|
||||
|
||||
![](img/fig9-4.png)
|
||||
|
||||
**图9-4 可视化读取和写入看起来已经生效的时间点。 B的最后读取不是可线性化的**
|
||||
**图9-4 可视化读取和写入看起来已经生效的时间点。 B的最后读取不是线性一致性的**
|
||||
|
||||
[图9-4]()中有一些有趣的细节需要指出:
|
||||
|
||||
* 第一个客户端B发送一个读取x的请求,然后客户端D发送一个请求将x设置为0,然后客户端A发送请求将x设置为1.不过,返回到B的读取值为1(写入的值由A)。这是可以的:这意味着数据库首先处理D的写入,然后是A的写入,最后是B的读取。虽然这不是发送请求的顺序,但这是一个可以接受的顺序,因为这三个请求是并发的。也许B的读取请求在网络上略有延迟,所以在两次写入之后它只能到达数据库。
|
||||
* 第一个客户端B发送一个读取`x`的请求,然后客户端D发送一个请求将`x`设置为`0`,然后客户端A发送请求将`x`设置为`1`。尽管如此,返回到B的读取值为`1`(由A写入的值)。这是可以的:这意味着数据库首先处理D的写入,然后是A的写入,最后是B的读取。虽然这不是请求发送的顺序,但这是一个可以接受的顺序,因为这三个请求是并发的。也许B的读请求在网络上略有延迟,所以它在两次写入之后才到达数据库。
|
||||
|
||||
* 在客户端A从数据库收到响应之前,客户端B的读取返回1,表示写入值1已成功。这也是可以的:这并不意味着在写入之前读取了值,这只是意味着从数据库到客户端A的正确响应在网络中稍微延迟。
|
||||
* 在客户端A从数据库收到响应之前,客户端B的读取返回`1`,表示写入值`1`已成功。这也是可以的:这并不意味着在写之前读到了值,这只是意味着从数据库到客户端A的正确响应在网络中略有延迟。
|
||||
|
||||
* 此模型不承担任何事务隔离:另一个客户端可能随时更改值。例如,C首先读取1,然后读取2,因为两次读取之间的值由B更改。可以使用原子**比较并设置(cas)**操作来检查该值是否未被另一客户端同时更改:B和C的cas请求成功,但是D的cas请求失败(在数据库处理它时, x的值不再是0)。
|
||||
* 此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C首先读取`1`,然后读取`2`,因为两次读取之间的值由B更改。可以使用原子**比较并设置(cas)**操作来检查该值是否未被另一客户端同时更改:B和C的**cas**请求成功,但是D的**cas**请求失败(在数据库处理它时,`x`的值不再是`0`)。
|
||||
|
||||
* 客户B的最后读取(阴影栏中)不是线性化的。 该操作与C的cas写操作同时发生,它将x从2更新为4.在没有其他请求的情况下,B的读取返回2是可以的。然而,在B的读取开始之前,客户端A已经读取了新的值4 ,因此不允许B读取比A更旧的值。再次,与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同。
|
||||
* 客户B的最后一次读取(阴影条柱中)不是线性一致性的。 该操作与C的**cas**写操作并发(它将`x`从`2`更新为`4`)。在没有其他请求的情况下,B的读取返回`2`是可以的。然而,在B的读取开始之前,客户端A已经读取了新的值4 ,因此不允许B读取比A更旧的值。再次,与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同。
|
||||
|
||||
这就是线性化背后的直觉。 正式的定义【6】更准确地描述了它。 通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,测试一个系统的行为是否可线性化是可能的(尽管在计算上是昂贵的)【11】。
|
||||
这就是线性一致性背后的直觉。 正式的定义【6】更准确地描述了它。 通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,测试一个系统的行为是否线性一致性是可能的(尽管在计算上是昂贵的)【11】。
|
||||
|
||||
|
||||
|
||||
> ### 线性化与可序列化
|
||||
> ### 线性一致性与可序列化
|
||||
>
|
||||
> **线性化**容易和[**可序列化**](ch7.md#可序列化)相混淆,因为两个词似乎都是类似“可以按顺序排列”的东西。但它们是两种完全不同的保证,区分两者非常重要:
|
||||
> **线性一致性**容易和[**可序列化**](ch7.md#可序列化)相混淆,因为两个词似乎都是类似“可以按顺序排列”的东西。但它们是两种完全不同的保证,区分两者非常重要:
|
||||
>
|
||||
> ***可序列化***
|
||||
>
|
||||
> **可序列化(Serializability)**是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)——参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为与如果他们已经按照一定的连续顺序执行(每个事务在下一个事务开始之前运行完成)。该顺序与事务实际执行的顺序不同【12】。
|
||||
> **可序列化(Serializability)**是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)——参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照**某种**顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。【12】。
|
||||
>
|
||||
> ***线性化***
|
||||
> ***线性一致性***
|
||||
>
|
||||
> **线性化(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合到事务中,因此它不会阻止写偏差等问题(请参阅“[写偏差和幻读](ch7.md#写偏差和幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。
|
||||
> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写偏差等问题(请参阅“[写偏差和幻读](ch7.md#写偏差和幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。
|
||||
>
|
||||
> 一个数据库可以提供可串行性和线性化,这种组合被称为严格的可串行性或强的**单拷贝可串行性(strong-1SR)**【4,13】。基于两阶段锁定的串行化实现(请参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是可线性化的。
|
||||
> 一个数据库可以提供可串行性和线性一致性,这种组合被称为严格的可串行性或强的**单副本强可串行性(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(请参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是线性一致性的。
|
||||
>
|
||||
> 但是,可序列化的快照隔离(请参见“[可序列化的快照隔离(SSI)](ch7.md#可序列化的快照隔离(SSI))”)不是线性化的:按照设计,它可以从一致的快照中进行读取,以避免锁定读者和写者之间的争用。一致性快照的要点在于,它不包括比快照更新的写入,因此从快照读取不是线性化的。
|
||||
> 但是,可序列化的快照隔离(请参见“[可序列化的快照隔离(SSI)](ch7.md#可序列化的快照隔离(SSI))”)不是线性一致性的:按照设计,它可以从一致的快照中进行读取,以避免锁定读者和写者之间的争用。一致性快照的要点就在于**它不会包括比快照更新的写入**,因此从快照读取不是线性一致性的。
|
||||
|
||||
|
||||
|
||||
### 依赖线性一致性
|
||||
|
||||
线性化在什么情况下有用?观看体育比赛的最后得分可能是一个轻率的例子:过了几秒钟的结果不可能在这种情况下造成任何真正的伤害。然而,线性化对于使系统正确工作的一个重要条件是有一些领域。
|
||||
线性一致性在什么情况下有用?观看体育比赛的最后得分可能是一个轻率的例子:过了几秒钟的结果不可能在这种情况下造成任何真正的伤害。然而对于少数领域,线性一致性是系统正确工作的一个重要条件。
|
||||
|
||||
#### 锁定和领导选举
|
||||
|
||||
一个使用单主复制的系统需要确保确实只有一个领导而不是几个(脑裂)。选择领导者的一种方法是使用一个锁:每个启动的节点试图获得锁,而成功的则成为领导者【14】。不管这个锁是如何实现的,它必须是可线性化的:所有节点必须同意哪个节点拥有锁;否则就没用了。
|
||||
一个使用单主复制的系统,需要确保领导真的只有一个,而不是几个(脑裂)。一种选择领导者的方法是使用锁:每个节点在启动时尝试获取锁,成功者成为领导者【14】。不管这个锁是如何实现的,它必须是线性一致的:所有节点必须就哪个节点拥有锁达成一致,否则就没用了。
|
||||
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。他们使用一致性算法以容错的方式实现可线性化的操作(我们在本章后面讨论这样的算法,在“[容错共识](#容错共识)”中)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选择(例如,请参阅第301页上的“[领导者和锁](#领导者和锁)”中的屏蔽问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性化存储服务是这些协调任务的基础。
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选择(例如,请参阅第301页上的“[领导者和锁](#领导者和锁)”中的屏蔽问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
|
||||
|
||||
[^iii]: 严格地说,ZooKeeper和etcd提供可线性化的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。您可以选择请求线性化读取:etcd调用这个法定读取【16】,而在ZooKeeper中,您需要在读取【15】之前调用`sync()`。请参阅第350页上的“[使用全局顺序广播实现线性存储](#)使用全局顺序广播实现线性存储”。
|
||||
[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。您可以选择请求线性一致性读取:etcd调用这个法定读取【16】,而在ZooKeeper中,您需要在读取【15】之前调用`sync()`。请参阅第350页上的“[使用全局顺序广播实现线性存储](#使用全局顺序广播实现线性存储)”。
|
||||
|
||||
分布式锁定也在一些分布式数据库(如Oracle Real Application Clusters(RAC)【18】)中以更细化的级别使用。 RAC对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些可线性化的锁处于事务执行的关键路径上,RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
|
||||
分布式锁也在一些分布式数据库(如Oracle Real Application Clusters(RAC)【18】)中以更细的粒度使用。 RAC对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上,RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
|
||||
|
||||
#### 限制和唯一性保证
|
||||
#### 约束和唯一性保证
|
||||
|
||||
唯一性约束在数据库中很常见:例如,用户名或电子邮件地址必须唯一标识一个用户,而在文件存储服务中,不能有两个具有相同路径和文件名的文件。如果要在写入数据时强制执行此约束(例如,如果两个人试图同时创建一个具有相同名称的用户或文件,其中一个将返回一个错误),则需要线性化。
|
||||
唯一性约束在数据库中很常见:例如,用户名或电子邮件地址必须唯一标识一个用户,而在文件存储服务中,不能有两个具有相同路径和文件名的文件。如果要在写入数据时强制执行此约束(例如,如果两个人试图同时创建一个具有相同名称的用户或文件,其中一个将返回一个错误),则需要线性一致性。
|
||||
|
||||
这种情况实际上类似于一个锁:当一个用户注册你的服务时,你可以想到他们获得了他们所选用户名的“锁定”。该操作也非常类似于原子比较和设置,将用户名设置为声明它的用户的ID,前提是用户名尚未被使用。
|
||||
这种情况实际上类似于一个锁:当一个用户注册你的服务时,可以认为他们获得了所选用户名的“锁定”。该操作与原子性的比较与设置非常相似:将用户名赋予声明它的用户,前提是用户名尚未被使用。
|
||||
|
||||
如果你想要确保银行账户余额永远不会为负数,或者你没有出售比仓库里的库存更多的物品,或者两个人不同时在同一个位置一个航班或一个剧院。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。
|
||||
如果想要确保银行账户余额永远不会为负数,或者不会出售比仓库里的库存更多的物品,或者两个人不会都预定了航班或剧院里同一时间的同一个位置。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。
|
||||
|
||||
在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,您可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性化,我们将在第524页的“[及时性和完整性]()”中讨论这种松散解释的约束。
|
||||
在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,您可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在第524页的“[及时性和完整性]()”中讨论这种松散解释的约束。
|
||||
|
||||
然而,一个硬性的唯一性约束,比如你通常在关系数据库中发现的约束,需要线性化。其他类型的约束,如外键或属性约束,可以在不需要线性化的情况下实现【19】。
|
||||
然而,一个硬性的唯一性约束(关系型数据库中常见的那种)需要线性一致性。其他类型的约束,如外键或属性约束,可以在不需要线性一致性的情况下实现【19】。
|
||||
|
||||
#### 跨渠道的时间依赖性
|
||||
#### 跨信道的时间依赖性
|
||||
|
||||
注意[图9-1](img/fig9-1.png)中的一个细节:如果Alice没有惊呼得分,Bob就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面,并最终看到最后的分数。由于系统中存在额外信道(Alice对Bob的耳朵的声音),线性化破坏才被注意到。
|
||||
注意[图9-1](img/fig9-1.png)中的一个细节:如果Alice没有惊呼得分,Bob就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面,并最终看到最后的分数。由于系统中存在额外的信道(Alice的声音传到了Bob的耳朵中),线性一致性的违背才被注意到。
|
||||
|
||||
计算机系统也会出现类似的情况。例如,假设您有一个用户可以上传照片的网站,而且后台进程会调整照片大小来降低分辨率,从而加快下载速度(缩略图)。该系统的体系结构和数据流如[图9-5](img/fig9-5.png)所示。
|
||||
计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如[图9-5](img/fig9-5.png)所示。
|
||||
|
||||
需要明确指示图像大小调整器执行大小调整作业,并且通过消息队列将此指令从Web服务器发送到大小调整器(请参阅[第11章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息中间件都是为小消息而设计的,而且照片的大小可能是几兆字节。相反,首先将照片写入文件存储服务,写入完成后,将调整器的指令放置在队列中。
|
||||
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(请参阅[第11章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将缩放器的指令放入消息队列。
|
||||
![](img/fig9-5.png)
|
||||
**图9-5 Web服务器和图像调整器通过文件存储和消息队列进行通信,打开竞争条件的可能性。**
|
||||
|
||||
如果文件存储服务是可线性化的,那么这个系统应该可以正常工作。如果它不是线性化的,则存在争用条件的风险:消息队列([图9-5](img/fig9-5.png)中的步骤3和4)可能比存储服务内部的复制更快。在这种情况下,当调整器读取图像(步骤5)时,可能会看到图像的旧版本,或者什么都没有。如果它处理旧版本的图像,则文件存储中的全尺寸和调整大小的图像永久不一致。
|
||||
如果文件存储服务是线性一致的,那么这个系统应该可以正常工作。如果它不是线性一致的,则存在竞争条件的风险:消息队列([图9-5](img/fig9-5.png)中的步骤3和4)可能比存储服务内部的复制更快。在这种情况下,当缩放器读取图像(步骤5)时,可能会看到图像的旧版本,或者什么都没有。如果它处理的是旧版本的图像,则文件存储中的全尺寸图和略缩图就产生了永久性的不一致。
|
||||
|
||||
出现这个问题是因为Web服务器和调整器之间存在两个不同的通信通道:文件存储器和消息队列。没有线性化的新近性保证,这两个通道之间的竞争条件是可能的。这种情况类似于[图9-1](img/fig9-1.png),两个通信渠道之间也存在竞争状态:数据库复制和Alice口中与Bob耳朵之间的真实音频通道。
|
||||
出现这个问题是因为Web服务器和缩放器之间存在两个不同的信道:文件存储与消息队列。没有线性一致性的新鲜性保证,这两个信道之间的竞争条件是可能的。这种情况类似于[图9-1](img/fig9-1.png),数据库复制与Alice的嘴到Bob耳朵之间的真人音频信道之间也存在竞争条件。
|
||||
|
||||
线性化并不是避免这种竞争条件的唯一方法,但它是最容易理解的。如果您控制附加的通信通道(例如在消息队列的情况下,而不是在Alice和Bob的情况下),则可以使用与我们在“[读己之写](ch5.md#读己之写)”额外的复杂性的成本。
|
||||
线性一致性并不是避免这种竞争条件的唯一方法,但它是最容易理解的。如果你可以控制额外信道(例如消息队列的例子,而不是在Alice和Bob的例子),则可以使用在“[读己之写](ch5.md#读己之写)”讨论过的备选方法,不过会有额外的复杂度代价。
|
||||
|
||||
### 实现线性一致的系统
|
||||
|
||||
现在我们已经看了几个线性化有用的例子,让我们考虑一下如何实现一个提供可线性化语义的系统。
|
||||
我们已经见到了几个线性一致性有用的例子,让我们思考一下,如何实现一个提供线性一致语义的系统。
|
||||
|
||||
由于线性化本质上意味着“表现得好像只有一个数据副本,而且所有的操作都是原子的”,所以最简单的答案就是只使用一个数据副本。但是,这种方法将无法处理错误:如果持有该副本的节点失败,数据将会丢失,或者至少无法访问,直到节点重新启动。
|
||||
由于线性一致性本质上意味着“表现得好像只有一个数据副本,而且所有的操作都是原子的”,所以最简单的答案就是,真的只用一个数据副本。但是这种方法无法容错:如果持有该副本的节点失效,数据将会丢失,或者至少无法访问,直到节点重新启动。
|
||||
|
||||
使系统容错最常用的方法是使用复制。我们再来回顾第5章中的复制方法,并比较它们是否可以线性化:
|
||||
使系统容错最常用的方法是使用复制。我们再来回顾[第5章](ch5.md)中的复制方法,并比较它们是否可以满足线性一致性:
|
||||
|
||||
***单主复制(可能可线性化)***
|
||||
***单主复制(可能线性一致)***
|
||||
|
||||
在具有单引导程序复制功能的系统中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),领导者具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果你从领导读取数据,或者从同步更新的追随者读取数据,他们有可能是线性化的[^iv]。然而,并不是每个单独的领导者数据库都是可以线性化的,无论是通过设计(例如,因为它使用快照隔离)或者由于并发错误【10】。
|
||||
在具有单主复制功能的系统中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能(protential)**是线性一致性的[^iv]。然而,并不是每个单主数据库都是实际线性一致性的,无论是通过设计(例如,因为使用快照隔离)还是并发错误【10】。
|
||||
|
||||
[^iv]: 对单领域数据库进行分区(分片),以便每个分区有一个单独的领导者,不会影响线性化,因为它只是单一对象的保证。 交叉分区事务是一个不同的问题(请参阅“[分布式事务和共识](#分布式事务和共识)”(第357页))。
|
||||
[^iv]: 对单领域数据库进行分区(分片),以便每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(参阅“[分布式事务和共识](#分布式事务和共识)”)。
|
||||
|
||||
使用领导进行阅读依赖于你确定领导是谁的假设。正如在“[真理被多数人定义](ch8.md#真理被多数人定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此 - 如果妄想的领导者继续为请求提供服务,可能违反线性化【20】。使用异步复制,故障转移甚至可能会丢失提交的写入(参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这违反了持久性和线性化。
|
||||
从主库读取依赖一个假设,你确定领导是谁。正如在“[真理在多数人手中](ch8.md#真理被多数人定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此——如果具有错觉的领导者继续为请求提供服务,可能违反线性一致性【20】。使用异步复制,故障转移时甚至可能会丢失已提交的写入(参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性。
|
||||
|
||||
***共识算法(可线性化)***
|
||||
***共识算法(线性一致)***
|
||||
|
||||
一些共识算法,我们将在本章后面讨论,与单引导者复制相似。然而,共识协议包含措施,以防止裂脑和陈旧的复制品。由于这些细节,协调算法可以安全地实现线性化存储。例如,Zookeeper 【21】和etcd 【22】就是这样工作的。
|
||||
一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。由于这些细节,协调算法可以安全地实现线性一致性存储。例如,Zookeeper 【21】和etcd 【22】就是这样工作的。
|
||||
|
||||
***多主复制(不可线性化)***
|
||||
***多主复制(非线性一致)***
|
||||
|
||||
具有多引导程序复制的系统通常不是线性化的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。由于这个原因,它们可能会产生冲突的写入,需要解析(请参阅第171页的“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是缺乏单一数据副本的人为因素。
|
||||
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生冲突的写入,需要解析(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是因为缺少单一数据副本人为产生的。
|
||||
|
||||
***无主复制(可能不是线性化的)***
|
||||
***无主复制(也许不是线性一致的)***
|
||||
|
||||
对于无领导者复制的系统(Dynamo风格;参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定读写($w + r> n$)可以获得“强一致性”。根据法定人数的确切配置,取决于您如何界定强一致性,这是不正确的。
|
||||
对于无领导者复制的系统(Dynamo风格;参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定人数读写($w + r> n$)可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。
|
||||
|
||||
基于时钟(例如,在Cassandra中;参见第291页上的“[依赖于同步时钟](#依赖于同步时钟)”)的“最后写入胜利”冲突解决方法几乎是非线性的,因为时钟时间戳不能保证与时间戳一致由于时钟歪斜而导致的实际事件排序。不规范的法定人数(第183页的“[马虎法定人数和暗示交接](#马虎法定人数和暗示交接)”)也破坏了线性化的可能性。即使是严格的法定人数,非线性行为也是可能的,如下一节所示。
|
||||
基于时钟(例如,在Cassandra中;参见“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。松散的法定人数(第183页的“[松散法定人数与暗示接力](#马虎法定人数和暗示交接)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也是可能的,如下一节所示。
|
||||
|
||||
#### 线性化和法定人数
|
||||
#### 线性一致性和法定人数
|
||||
|
||||
直觉上,严格的仲裁读写应该是在Dynamo风格的模型中可线性化的。但是,当我们有可变的网络延迟时,就有可能存在竞争条件,如[图9-6](img/fig9-6.png)所示。
|
||||
直觉上在Dynamo风格的模型中,严格的法定人数读写应该是线性一致性的。但是当我们有可变的网络延迟时,就可能存在竞争条件,如[图9-6](img/fig9-6.png)所示。
|
||||
|
||||
![](img/fig9-6.png)
|
||||
|
||||
**图9-6 尽管使用严格的法定人数,仍然是非线性的执行。**
|
||||
在[图9-6](img/fig9-6.png)中,x的初始值为0,写入客户端通过向所有三个副本($n = 3, w = 3$)发送写入将x更新为1。同时,客户端A从两个节点($r = 2$)的仲裁中读取数据,并在其中一个节点上看到新值1。同时与写操作相关,客户端B从两个节点的不同仲裁中读取,并从两个节点中取回旧值0。
|
||||
**图9-6 非线性一致的执行,尽管使用了严格的法定人数**
|
||||
|
||||
仲裁条件满足($w + r> n$),但是这个执行是不可排队的:B的请求在A的请求完成后开始,但是B返回旧值,而A返回新值。 (这又是爱丽丝和鲍勃的情况了[图9-1]())
|
||||
在[图9-6](img/fig9-6.png)中,$x$ 的初始值为0,写入客户端通过向所有三个副本($n = 3, w = 3$)发送写入将 $x$ 更新为1。客户端A并发地从两个节点组成的法定人群($r = 2$)中读取数据,并在其中一个节点上看到新值1。客户端B也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值0。
|
||||
|
||||
有趣的是,可以使Dynamo风格的定额以可降低的性能为代价线性化:读取器必须在将结果返回给应用程序之前,同步执行读取修复(请参阅“修复和反熵”一节第178页) ,并且作者必须在发送写入之前读取法定数量的最新状态【24,25】。然而,由于性能损失,Riak不执行同步读取修复【26】。 Cassandra确实等待读取修复以完成读取问题【27】,但是如果由于使用了最后写入赢得冲突解决方案而导致同一个关键字有多个并发写入,则它将失去线性化。
|
||||
仲裁条件满足($w + r> n$),但是这个执行是非线性一致的:B的请求在A的请求完成后开始,但是B返回旧值,而A返回新值。 (又一次,如同Alice和Bob的例子 [图9-1]())
|
||||
|
||||
而且这种方式只能实现线性化的读写操作。线性化的比较和设置操作不能,因为它需要一个一致的算法【28】。
|
||||
有趣的是,通过牺牲性能,可以使Dynamo风格的法定人数线性化:读取者必须在将结果返回给应用程序之前,同步执行读取修复(请参阅“[读时修复与反熵过程](ch5.md#读时修复与反熵过程)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak不执行同步读取修复【26】。 Cassandra在进行法定人数读取时,**确实**在等待读取修复完成【27】;但是由于使用了最后写入为准的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。
|
||||
|
||||
总而言之,假设无Dynamo风格复制的无领导者系统不能提供线性化性是最安全的。
|
||||
而且,这种方式只能实现线性一致的读写;不能实现线性一致的比较和设置操作,因为它需要一个共识算法【28】。
|
||||
|
||||
总而言之,最安全的做法是:假设采用Dynamo风格无主复制的系统不能提供线性一致性。
|
||||
|
||||
|
||||
|
||||
### 线性一致性的代价
|
||||
|
||||
由于一些复制方法可以提供线性化,其他复制方法则不能,因此更深入地探讨线性化的优缺点是很有趣的。
|
||||
由于一些复制方法可以提供线性一致性,其他复制方法则不能,因此更深入地探讨线性一致性的优缺点是很有趣的。
|
||||
|
||||
我们已经在第五章讨论了不同复制方法的一些用例。例如,我们看到多领导者复制通常是多数据中心复制的理想选择(参阅“[多数据中心操作](ch5.md#多数据中心操作)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。
|
||||
|
||||
![](img/fig9-7.png)
|
||||
|
||||
**图9-7 网络中断迫使在线性化和可用性之间做出选择。**
|
||||
**图9-7 网络中断迫使在线性一致性和可用性之间做出选择。**
|
||||
|
||||
考虑如果两个数据中心之间发生网络中断,会发生什么情况。我们假设每个数据中心内的网络正在工作,客户端可以访问数据中心,但数据中心不能互相连接。
|
||||
|
||||
使用多领导者数据库,每个数据中心都可以继续正常运行:由于从一个数据中心写入的数据是异步复制到另一个数据中心的,所以在恢复网络连接时,只需排队并交换数据。
|
||||
|
||||
另一方面,如果使用单主复制,则主库必须位于其中一个数据中心。任何写入和任何可线性读取都必须发送给leader,因此,对于连接到跟随者数据中心的任何客户端,这些读取和写入请求必须通过网络同步发送到leader数据中心。
|
||||
另一方面,如果使用单主复制,则主库必须位于其中一个数据中心。任何写入和任何线性读取都必须发送给leader,因此,对于连接到跟随者数据中心的任何客户端,这些读取和写入请求必须通过网络同步发送到leader数据中心。
|
||||
|
||||
如果数据中心之间的网络在单引导程序设置中被中断,则连接到跟随者数据中心的客户端不能联系领导者,因此他们不能对数据库进行任何写入,也不能进行任何线性化读取。他们仍然可以从追随者读取,但他们可能是陈旧(非线性)。如果应用程序需要可线性读写,则网络中断将导致应用程序在不能联系领导者的数据中心中变得不可用。
|
||||
如果数据中心之间的网络在单引导程序设置中被中断,则连接到跟随者数据中心的客户端不能联系领导者,因此他们不能对数据库进行任何写入,也不能进行任何线性一致性读取。他们仍然可以从追随者读取,但他们可能是陈旧(非线性)。如果应用程序需要线性读写,则网络中断将导致应用程序在不能联系领导者的数据中心中变得不可用。
|
||||
如果客户端可以直接连接到领导者数据中心,这不是问题,因为应用程序在那里继续正常工作。但是只能访问下一个数据中心的客户端将会经历停机,直到网络链接被修复。
|
||||
|
||||
#### CAP定理
|
||||
|
||||
这个问题不仅仅是单领导者和多领导者复制的结果:任何可线性化的数据库都有这个问题,不管它是如何实现的。这个问题也不是特定于多数据中心部署,而是可能发生在任何不可靠的网络上,即使在一个数据中心内也是如此。权衡如下:[^v]
|
||||
这个问题不仅仅是单领导者和多领导者复制的结果:任何线性一致性的数据库都有这个问题,不管它是如何实现的。这个问题也不是特定于多数据中心部署,而是可能发生在任何不可靠的网络上,即使在一个数据中心内也是如此。权衡如下:[^v]
|
||||
|
||||
* 如果您的应用程序需要线性化,并且由于网络问题某些副本与其他副本断开连接,则某些副本在断开连接时无法处理请求:它们必须等待,直到网络问题得到解决,或返回错误(无论哪种方式,他们变得不可用)。
|
||||
* 如果您的应用程序不需要线性化,那么即使它与其他副本(如多引导程序)断开连接,也可以以每个副本可独立处理请求的方式进行写入。在这种情况下,应用程序可以在网络问题面前保持可用,但其行为不可线性化。
|
||||
* 如果您的应用程序需要线性一致性,并且由于网络问题某些副本与其他副本断开连接,则某些副本在断开连接时无法处理请求:它们必须等待,直到网络问题得到解决,或返回错误(无论哪种方式,他们变得不可用)。
|
||||
* 如果您的应用程序不需要线性一致性,那么即使它与其他副本(如多引导程序)断开连接,也可以以每个副本可独立处理请求的方式进行写入。在这种情况下,应用程序可以在网络问题面前保持可用,但其行为不线性一致性。
|
||||
|
||||
[^v]: 这两种选择有时分别称为CP(在网络分区下一致但不可用)和AP(在网络分区下可用但不一致)。 但是,这种分类方案存在一些缺陷【9】,所以最好避免。
|
||||
|
||||
因此,不需要线性化的应用可以更容忍网络问题。这种见解通常被称为CAP定理【29,30,31,32】,由Eric Brewer于2000年命名,尽管自20世纪70年代以来,分布式数据库的设计者已经知道了这种权衡【33,34,35,36】。
|
||||
因此,不需要线性一致性的应用可以更容忍网络问题。这种见解通常被称为CAP定理【29,30,31,32】,由Eric Brewer于2000年命名,尽管自20世纪70年代以来,分布式数据库的设计者已经知道了这种权衡【33,34,35,36】。
|
||||
|
||||
CAP最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。当时,许多分布式数据库侧重于在具有共享存储的机器集群上提供可线性化的语义【18】,并鼓励数据库工程师探索更广泛的分布式无共享系统的设计空间,这更适合于实施大规模的网络服务【37】。 CAP值得赞扬,因为这种文化转变——见证了自2000年代中期以来新的数据库技术的爆炸式增长(被称为NoSQL)。
|
||||
CAP最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。当时,许多分布式数据库侧重于在具有共享存储的机器集群上提供线性一致性的语义【18】,并鼓励数据库工程师探索更广泛的分布式无共享系统的设计空间,这更适合于实施大规模的网络服务【37】。 CAP值得赞扬,因为这种文化转变——见证了自2000年代中期以来新的数据库技术的爆炸式增长(被称为NoSQL)。
|
||||
|
||||
> ### CAP定理没有帮助
|
||||
>
|
||||
@ -285,33 +286,33 @@ CAP最初是作为一个经验法则提出的,没有准确的定义,目的
|
||||
>
|
||||
> 只能选择从3中挑选出2个。不幸的是,这样做是误导的【32】,因为网络分区是一种错误,所以它们不是你所拥有的一个选择:他们会发生,不管你喜欢还是不喜欢【38】。
|
||||
>
|
||||
> 在网络正常工作的时候,系统可以提供一致性(线性化)和总体可用性。发生网络故障时,您必须选择线性或总可用性。因此,一个更好的表达CAP的方法可以是一致的,或者在分区时可用【39】。一个更可靠的网络需要减少这个选择,但是在某些时候选择是不可避免的。
|
||||
> 在网络正常工作的时候,系统可以提供一致性(线性一致性)和总体可用性。发生网络故障时,您必须选择线性或总可用性。因此,一个更好的表达CAP的方法可以是一致的,或者在分区时可用【39】。一个更可靠的网络需要减少这个选择,但是在某些时候选择是不可避免的。
|
||||
>
|
||||
> 在CAP的讨论中,术语可用性有几个相互矛盾的定义,形式化作为一个定理【30】并不符合其通常的含义【40】。许多所谓的“高可用”(容错)系统实际上不符合CAP对可用性的特殊定义。总而言之,围绕着CAP有很多误解和困惑,并不能帮助我们更好地理解系统,所以最好避免使用CAP。
|
||||
|
||||
正式定义的CAP定理【30】的范围很窄,它只考虑一个一致性模型(即线性化)和一种故障(网络分区[^vi]或活动但彼此断开的节点)。它没有说任何有关网络延迟,死亡节点或其他权衡的事情。 因此,尽管CAP在历史上具有影响力,但对于设计系统来说,它没有实际价值【9,40】。
|
||||
正式定义的CAP定理【30】的范围很窄,它只考虑一个一致性模型(即线性一致性)和一种故障(网络分区[^vi]或活动但彼此断开的节点)。它没有说任何有关网络延迟,死亡节点或其他权衡的事情。 因此,尽管CAP在历史上具有影响力,但对于设计系统来说,它没有实际价值【9,40】。
|
||||
|
||||
在分布式系统中有更多有趣的不可能的结果【41】,并且CAP已经被更精确的结果所取代【2,42】,所以它现在基本上是历史感兴趣的。
|
||||
|
||||
[^vi]: 正如第279页的“实践中的网络故障”中所讨论的,本书使用分区来指将大数据集精细分解成小数据集(分片;参见第6章)。相比之下,网络分区是特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于是CAP的P,所以在这种情况下我们不能避免混淆。
|
||||
|
||||
#### 线性化和网络延迟
|
||||
#### 线性一致性和网络延迟
|
||||
|
||||
虽然线性化是一个有用的保证,但实际上很少有系统实际上是线性化的。例如,现代多核CPU上的RAM甚至不可线性化【43】:如果一个CPU内核上运行的线程写入内存地址,而另一个CPU内核上的线程不久后读取相同的地址,保证读取第一个线程写入的值(除非使用了内存屏障或栅栏【44】)。
|
||||
虽然线性一致性是一个有用的保证,但实际上很少有系统实际上是线性一致性的。例如,现代多核CPU上的RAM甚至不线性一致性【43】:如果一个CPU内核上运行的线程写入内存地址,而另一个CPU内核上的线程不久后读取相同的地址,保证读取第一个线程写入的值(除非使用了内存屏障或栅栏【44】)。
|
||||
|
||||
这种行为的原因是每个CPU内核都有自己的内存缓存和存储缓冲区。内存访问首先进入缓存默认情况下,任何更改异步写出到主内存。由于在缓存中访问数据比进入主内存要快【45】,所以这个特性对于现代CPU的良好性能是至关重要的。但是,现在有几个数据副本(一个在主内存中,另外几个在不同的高速缓存中),而且这些副本是异步更新的,因此线性化会丢失。
|
||||
这种行为的原因是每个CPU内核都有自己的内存缓存和存储缓冲区。内存访问首先进入缓存默认情况下,任何更改异步写出到主内存。由于在缓存中访问数据比进入主内存要快【45】,所以这个特性对于现代CPU的良好性能是至关重要的。但是,现在有几个数据副本(一个在主内存中,另外几个在不同的高速缓存中),而且这些副本是异步更新的,因此线性一致性会丢失。
|
||||
|
||||
为什么要做这个交换?使用CAP定理来证明多核内存一致性模型是没有意义的:在一台计算机中,我们通常假定可靠的通信,并且我们不希望一个CPU内核能够继续正常的操作与电脑的其他部分断开连接。降低线性化的原因是性能,而不是容错。
|
||||
为什么要做这个交换?使用CAP定理来证明多核内存一致性模型是没有意义的:在一台计算机中,我们通常假定可靠的通信,并且我们不希望一个CPU内核能够继续正常的操作与电脑的其他部分断开连接。降低线性一致性的原因是性能,而不是容错。
|
||||
|
||||
许多分布式数据库也是如此,它们选择不提供线性保证:它们主要是为了提高性能,而不是为了容错【46】。线性化速度很慢 - 这一直是事实,不仅在网络故障期间。
|
||||
许多分布式数据库也是如此,它们选择不提供线性保证:它们主要是为了提高性能,而不是为了容错【46】。线性一致性速度很慢 - 这一直是事实,不仅在网络故障期间。
|
||||
|
||||
我们不能找到一个更有效的线性化存储实现吗?看来答案是否定的:Attiya和Welch 【47】证明,如果你想要线性化,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(请参见“[超时和无限延迟](ch8.md#超时和无限延迟)”(第267页)),可线性读写的响应时间不可避免地会很高。线性化算法不存在,但是一致性较弱的模型可以更快,所以这种权衡对于延迟敏感的系统是很重要的。在第12章中,我们将讨论一些避免线性化而不牺牲正确性的方法。
|
||||
我们不能找到一个更有效的线性一致性存储实现吗?看来答案是否定的:Attiya和Welch 【47】证明,如果你想要线性一致性,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(请参见“[超时和无限延迟](ch8.md#超时和无限延迟)”(第267页)),线性读写的响应时间不可避免地会很高。线性一致性算法不存在,但是一致性较弱的模型可以更快,所以这种权衡对于延迟敏感的系统是很重要的。在第12章中,我们将讨论一些避免线性一致性而不牺牲正确性的方法。
|
||||
|
||||
|
||||
|
||||
## 顺序保证
|
||||
|
||||
我们之前曾经说过,线性化寄存器的行为就好像只有一个数据拷贝一样,而且每一个操作在某个时间点似乎都是原子地生效的。这个定义意味着操作按照一定的顺序执行。我们通过按照它们似乎已经执行的顺序加入操作来说明[图9-4]()中的顺序。
|
||||
我们之前曾经说过,线性一致性寄存器的行为就好像只有一个数据拷贝一样,而且每一个操作在某个时间点似乎都是原子地生效的。这个定义意味着操作按照一定的顺序执行。我们通过按照它们似乎已经执行的顺序加入操作来说明[图9-4]()中的顺序。
|
||||
|
||||
订购在本书中一直是反复出现的主题,这表明这可能是一个重要的基本概念。让我们简要回顾一下我们在其中讨论过的其他一些情况:
|
||||
|
||||
@ -319,7 +320,7 @@ CAP最初是作为一个经验法则提出的,没有准确的定义,目的
|
||||
* 我们在[第7章](ch7.md)中讨论的可序列化是关于确保事务按照某种顺序执行的行为。它可以通过以该序列字面执行事务来实现,或者通过允许并行执行,同时防止序列化冲突(通过锁定或中止)来实现。
|
||||
* 我们在[第8章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(参阅“[依赖于同步时钟](ch8.md#依赖于同步时钟)”)是另一种将顺序引入无序世界的尝试,例如确定两个写入中的哪一个稍后发生。
|
||||
|
||||
事实证明,排序,线性化和共识之间有着深刻的联系。尽管这个概念比本书的其他部分更具理论性和抽象性,但对于澄清我们对什么是系统可以做什么和不可以做什么而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
|
||||
事实证明,排序,线性一致性和共识之间有着深刻的联系。尽管这个概念比本书的其他部分更具理论性和抽象性,但对于澄清我们对什么是系统可以做什么和不可以做什么而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
|
||||
|
||||
### 顺序与因果
|
||||
|
||||
@ -346,9 +347,9 @@ CAP最初是作为一个经验法则提出的,没有准确的定义,目的
|
||||
|
||||
全局顺序和局部顺序之间的差异反映在不同的数据库一致性模型中:
|
||||
|
||||
***线性化***
|
||||
***线性一致性***
|
||||
|
||||
在一个可线性化的系统中,我们有一个总的操作顺序:如果系统的行为就好像只有一个数据副本,并且每个操作都是原子的,这意味着对于任何两个操作,我们总是可以说哪个操作先发生。这个总的排序在[图9-4](img/fig9-4.png)中以时间线表示。
|
||||
在一个线性一致性的系统中,我们有一个总的操作顺序:如果系统的行为就好像只有一个数据副本,并且每个操作都是原子的,这意味着对于任何两个操作,我们总是可以说哪个操作先发生。这个总的排序在[图9-4](img/fig9-4.png)中以时间线表示。
|
||||
|
||||
***因果关系***
|
||||
|
||||
@ -360,15 +361,15 @@ CAP最初是作为一个经验法则提出的,没有准确的定义,目的
|
||||
|
||||
如果您熟悉像Git这样的分布式版本控制系统,那么它们的版本历史非常类似于因果关系图。通常,一个提交会以一条直线进行,但是有时您会得到分支(当多个人同时在一个项目上工作时),并且在这些创建的提交合并时创建合并。
|
||||
|
||||
#### 线性化强于因果一致性
|
||||
#### 线性一致性强于因果一致性
|
||||
|
||||
那么因果顺序和线性化之间的关系是什么?答案是线性化意味着因果关系:任何可线性化的系统都能正确保存注意力【7】。特别是,如果系统中有多个通信通道(如图9-5中的消息队列和文件存储服务),线性可确保因果关系被自动保留,而系统不必做任何特殊的事情(如通过不同部件之间的时间戳)。
|
||||
那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性意味着因果关系:任何线性一致性的系统都能正确保存注意力【7】。特别是,如果系统中有多个通信通道(如图9-5中的消息队列和文件存储服务),线性可确保因果关系被自动保留,而系统不必做任何特殊的事情(如通过不同部件之间的时间戳)。
|
||||
|
||||
线性化确保因果关系的事实使线性化系统变得简单易懂,更具吸引力。然而,正如第335页的“线性可用性的成本”中所讨论的,使系统可线性化可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果地理位置分散的话)。出于这个原因,一些分布式数据系统已经放弃了线性化,这使得它们可以获得更好的性能,但却使它们难以工作。
|
||||
线性一致性确保因果关系的事实使线性一致性系统变得简单易懂,更具吸引力。然而,正如第335页的“线性可用性的成本”中所讨论的,使系统线性一致性可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果地理位置分散的话)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,这使得它们可以获得更好的性能,但却使它们难以工作。
|
||||
|
||||
好消息是中间地带是可能的。线性化不是保持因果关系的唯一途径 - 还有其他方法。一个系统可以在原因上是一致的,不会造成使其线性化的性能命中(特别是CAP定理不适用)。事实上,因果一致性是最强可能的一致性模型,不会由于网络延迟而减慢,并且在网络故障时仍然可用【2,42】。
|
||||
好消息是中间地带是可能的。线性一致性不是保持因果关系的唯一途径 - 还有其他方法。一个系统可以在原因上是一致的,不会造成使其线性一致性的性能命中(特别是CAP定理不适用)。事实上,因果一致性是最强可能的一致性模型,不会由于网络延迟而减慢,并且在网络故障时仍然可用【2,42】。
|
||||
|
||||
在许多情况下,似乎需要线性化的系统实际上只需要确定因果一致性,这可以更有效地实施。基于这种观察,研究人员正在探索新的数据库来保存因果关系,其性能和可用性特征与最终一致的系统类似【49,50,51】。
|
||||
在许多情况下,似乎需要线性一致性的系统实际上只需要确定因果一致性,这可以更有效地实施。基于这种观察,研究人员正在探索新的数据库来保存因果关系,其性能和可用性特征与最终一致的系统类似【49,50,51】。
|
||||
由于这项研究是相当新的,其中没有很多已经进入生产系统,仍然有挑战需要克服【52,53】。但是,这是未来系统的一个有利的方向。
|
||||
|
||||
#### 捕获因果关系
|
||||
@ -463,7 +464,7 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检
|
||||
|
||||
如前所述,单引导程序复制通过选择一个节点作为引导程序来确定操作的总顺序,并对引导程序上的单个CPU核心上的所有操作进行排序。接下来的挑战是如果吞吐量大于单个领导者可以处理的情况下如何扩展系统,以及如果领导者失败(参见第156页的“[处理节点宕机](#处理节点宕机)”),如何处理故障转移。在分布式系统文献中,这个问题被称为全序广播或原子广播[^ix]【25,57,58】。
|
||||
|
||||
[^ix]: “原子广播”这个术语是传统的,但是它是非常混乱的,因为它与原子的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性化存储)。 总的顺序组播是另一个同义词。
|
||||
[^ix]: “原子广播”这个术语是传统的,但是它是非常混乱的,因为它与原子的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致性存储)。 总的顺序组播是另一个同义词。
|
||||
|
||||
> #### 顺序保证的范围
|
||||
>
|
||||
@ -494,19 +495,19 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检
|
||||
|
||||
全面订购广播对于实施提供防护令牌的锁定服务也很有用(请参见第294页的“防护令牌”)。每个获取锁的请求都作为消息添加到日志中,并且所有消息都按它们在日志中出现的顺序依次编号。序列号可以作为一个击剑标记,因为它是单调递增的。在ZooKeeper中,这个序列号被称为`zxid` 【15】。
|
||||
|
||||
#### 使用全序广播实现线性化的存储
|
||||
#### 使用全序广播实现线性一致性的存储
|
||||
|
||||
如图9-4所示,在可线性化的系统中,有一个操作的总顺序。这是否意味着线性化与总顺序播放相同?不完全,但两者之间有密切的联系[^x]。
|
||||
如图9-4所示,在线性一致性的系统中,有一个操作的总顺序。这是否意味着线性一致性与总顺序播放相同?不完全,但两者之间有密切的联系[^x]。
|
||||
|
||||
[^x]: 从形式上讲,可线性读写寄存器是一个“更容易”的问题。 总顺序广播等同于共识【67】,在异步崩溃停止模型【68】中没有确定性的解决方案,而可线性化的读写寄存器可以在同一系统模型中实现【23,24,25】。 然而,支持原子操作,如比较和设置,或者在寄存器中增加和获取,使得它相当于共识【28】。 因此,共识问题和可线性化的注册问题密切相关。
|
||||
[^x]: 从形式上讲,线性读写寄存器是一个“更容易”的问题。 总顺序广播等同于共识【67】,在异步崩溃停止模型【68】中没有确定性的解决方案,而线性一致性的读写寄存器可以在同一系统模型中实现【23,24,25】。 然而,支持原子操作,如比较和设置,或者在寄存器中增加和获取,使得它相当于共识【28】。 因此,共识问题和线性一致性的注册问题密切相关。
|
||||
|
||||
全部顺序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被传送(所以一个接收者可能落后于其他接收者)。相比之下,线性化是最近的保证:读取保证看到写入的最新值。
|
||||
全部顺序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被传送(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是最近的保证:读取保证看到写入的最新值。
|
||||
|
||||
但是,如果您有全面的顺序广播,则可以在其上构建线性化存储。例如,您可以确保用户名唯一标识用户帐户。
|
||||
但是,如果您有全面的顺序广播,则可以在其上构建线性一致性存储。例如,您可以确保用户名唯一标识用户帐户。
|
||||
|
||||
想象一下,对于每一个可能的用户名,你都可以拥有一个带有原子比较和设置操作的线性化寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的注册表执行比较设置操作,在前一个注册值为空的情况下,将其设置为用户账号。如果多个用户试图同时获取相同的用户名,则只有一个比较和设置操作会成功,因为其他用户将看到非空值(由于线性化)。
|
||||
想象一下,对于每一个可能的用户名,你都可以拥有一个带有原子比较和设置操作的线性一致性寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的注册表执行比较设置操作,在前一个注册值为空的情况下,将其设置为用户账号。如果多个用户试图同时获取相同的用户名,则只有一个比较和设置操作会成功,因为其他用户将看到非空值(由于线性一致性)。
|
||||
|
||||
您可以通过使用全部命令广播作为仅追加日志【62,63】来执行如下可线性化的比较和设置操作:
|
||||
您可以通过使用全部命令广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作:
|
||||
|
||||
1. 在日志中添加一条消息,暂时指明您要声明的用户名。
|
||||
2. 阅读日志,并等待你附加的信息被传回给你。[^xi]
|
||||
@ -516,27 +517,27 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检
|
||||
|
||||
由于日志条目以相同顺序传递到所有节点,因此如果有多个并发写入,则所有节点将首先同意哪个节点。选择第一个冲突的写入作为胜利者,并中止后面的写入,确保所有节点都同意写入是提交还是中止。一个类似的方法可以用来在一个日志之上实现可序列化的多对象事务【62】。
|
||||
|
||||
虽然此过程确保可线性写入,但不能保证线性化读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 (具体来说,这里描述的过程提供了顺序一致性【47,64】,有时也称为时间线一致性【65,66】,这比线性化要弱一些)。为了使读取线性化,有几个选项:
|
||||
虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 (具体来说,这里描述的过程提供了顺序一致性【47,64】,有时也称为时间线一致性【65,66】,这比线性一致性要弱一些)。为了使读取线性一致性,有几个选项:
|
||||
|
||||
* 您可以通过附加消息,读取日志以及在消息被传回给您时执行实际读取来对日志进行排序。消息在日志中的位置因此定义了读取发生的时间点。 (法定读取etcd的工作有点像这样【16】。)
|
||||
* 如果日志允许以线性方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置的所有条目传送给您,然后执行读取。 (这是Zookeeper的`sync()`操作背后的思想【15】)。
|
||||
* 您可以从写入时同步更新的副本进行读取,因此可以确保最新。 (这种技术用于链式复制【63】;另请参阅第155页上的“复制研究”。)
|
||||
|
||||
#### 使用线性化存储实现总顺序广播
|
||||
#### 使用线性一致性存储实现总顺序广播
|
||||
|
||||
最后一节介绍了如何从全部命令广播中构建一个线性化的比较和设置操作。我们也可以把它转过来,假设我们有可线性化的存储,并展示如何从它构建全部命令播放。
|
||||
最后一节介绍了如何从全部命令广播中构建一个线性一致性的比较和设置操作。我们也可以把它转过来,假设我们有线性一致性的存储,并展示如何从它构建全部命令播放。
|
||||
|
||||
最简单的方法是假设你有一个线性化的寄存器来存储一个整数,并且有一个原子增量和获取操作【28】。或者,原子比较和设置操作也可以完成这项工作。
|
||||
最简单的方法是假设你有一个线性一致性的寄存器来存储一个整数,并且有一个原子增量和获取操作【28】。或者,原子比较和设置操作也可以完成这项工作。
|
||||
|
||||
该算法很简单:对于每个要通过全部顺序广播发送的消息,您将递增并获取可线性化的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,您可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。
|
||||
该算法很简单:对于每个要通过全部顺序广播发送的消息,您将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,您可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。
|
||||
|
||||
请注意,与Lamport时间戳不同,您通过递增可线性化寄存器获得的数字形成一个没有间隙的序列。因此,如果一个节点已经发送了消息4并且接收到序列号为6的传入消息,则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此
|
||||
请注意,与Lamport时间戳不同,您通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此,如果一个节点已经发送了消息4并且接收到序列号为6的传入消息,则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此
|
||||
|
||||
与Lamport时间戳 - 事实上,这是总顺序广播和时间戳订购之间的关键区别。
|
||||
|
||||
使用原子增量和获取操作来创建线性化整数有多困难?像往常一样,如果事情从来没有失败过,那很容易:你可以把它保存在一个节点的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失败时恢复该值【59】。一般来说,如果你对可线性化序列号的产生者认真思考,你不可避免地会得出一个一致的算法。
|
||||
使用原子增量和获取操作来创建线性一致性整数有多困难?像往常一样,如果事情从来没有失败过,那很容易:你可以把它保存在一个节点的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失败时恢复该值【59】。一般来说,如果你对线性一致性序列号的产生者认真思考,你不可避免地会得出一个一致的算法。
|
||||
|
||||
这并非巧合:可以证明,线性化的比较和设置(或增量和取得)寄存器和全部命令广播都相当于【28,67】。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察力!
|
||||
这并非巧合:可以证明,线性一致性的比较和设置(或增量和取得)寄存器和全部命令广播都相当于【28,67】。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察力!
|
||||
|
||||
现在是时候正面处理共识问题了,我们将在本章的其余部分进行讨论。
|
||||
|
||||
@ -545,7 +546,7 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检
|
||||
## 分布式事务与共识
|
||||
|
||||
共识是分布式计算中最重要也是最基本的问题之一。从表面上看,似乎很简单:非正式地说,目标只是让几个节点达成一致。你可能会认为这不应该太难。不幸的是,许多破损的系统已经被误认为这个问题很容易解决。
|
||||
虽然共识是非常重要的,但关于它的部分在本书的后半部分已经出现了,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术研究界,对共识的理解也只是在几十年的时间内逐渐显现出来,一路上有许多误解。现在我们已经讨论了复制(第5章),事务(第7章),系统模型(第8章),线性化以及总播放(本章),我们终于准备好解决共识问题了。
|
||||
虽然共识是非常重要的,但关于它的部分在本书的后半部分已经出现了,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术研究界,对共识的理解也只是在几十年的时间内逐渐显现出来,一路上有许多误解。现在我们已经讨论了复制(第5章),事务(第7章),系统模型(第8章),线性一致性以及总播放(本章),我们终于准备好解决共识问题了。
|
||||
|
||||
在节点达成一致的情况下,有许多情况是很重要的。例如:
|
||||
|
||||
@ -820,13 +821,13 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
|
||||
|
||||
#### 共识的局限性
|
||||
|
||||
共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全部的命令广播,因此它们也可以容错的方式实现线性化的原子操作(参见第350页的“使用全部命令广播实现线性化存储”)。
|
||||
共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全部的命令广播,因此它们也可以容错的方式实现线性一致性的原子操作(参见第350页的“使用全部命令广播实现线性一致性存储”)。
|
||||
|
||||
尽管如此,它们并没有到处使用,因为它的好处是有代价的。
|
||||
|
||||
节点在决定之前对节点进行投票的过程是一种同步复制。如第153页的“同步与异步复制”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失 - 但是为了获得更好的性能,许多人选择接受这种风险。
|
||||
|
||||
共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“可线性化的成本”(第295页))。
|
||||
共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“线性一致性的成本”(第295页))。
|
||||
|
||||
大多数一致性算法假定一组参与投票的节点,这意味着您不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。
|
||||
|
||||
@ -844,9 +845,9 @@ ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据(
|
||||
|
||||
ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全面的命令广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用:
|
||||
|
||||
***线性化的原子操作***
|
||||
***线性一致性的原子操作***
|
||||
|
||||
使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性化的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(请参阅第295页上的“进程暂停”)。
|
||||
使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(请参阅第295页上的“进程暂停”)。
|
||||
|
||||
***操作的总排序***
|
||||
|
||||
@ -860,7 +861,7 @@ ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全面的
|
||||
|
||||
一个客户端不仅可以读取其他客户端创建的锁和值,还可以监视其中的更改。因此,客户端可以找出另一个客户端何时加入集群(基于它写入ZooKeeper的值),还是另一个客户端发生故障(因为其会话超时并且其临时节点消失)。通过订阅通知,客户避免了不得不经常轮询以找出变化。
|
||||
|
||||
在这些特征中,只有线性化的原子操作才需要达成共识。但是,这些功能的结合使得像ZooKeeper这样的系统在分布式协调中非常有用。
|
||||
在这些特征中,只有线性一致性的原子操作才需要达成共识。但是,这些功能的结合使得像ZooKeeper这样的系统在分布式协调中非常有用。
|
||||
|
||||
|
||||
|
||||
@ -880,9 +881,9 @@ ZooKeeper/Chubby模型运行良好的一个例子是,如果您有几个流程
|
||||
|
||||
ZooKeeper,etcd和Consul也经常用于服务发现——也就是找出你需要连接到哪个IP地址才能到达特定的服务。在云数据中心环境中,虚拟机连续来去常见,您通常不会提前知道您的服务的IP地址。相反,您可以配置您的服务,使其在启动时注册服务注册表中的网络端点,然后可以由其他服务找到它们。
|
||||
|
||||
但是,服务发现是否需要达成共识还不太清楚。 DNS是查找服务名称的IP地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从DNS读取是绝对不可线性化的,如果DNS查询的结果有点陈旧,通常不会有问题【109】。 DNS对网络中断的可靠性和可靠性更为重要。
|
||||
但是,服务发现是否需要达成共识还不太清楚。 DNS是查找服务名称的IP地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从DNS读取是绝对不线性一致性的,如果DNS查询的结果有点陈旧,通常不会有问题【109】。 DNS对网络中断的可靠性和可靠性更为重要。
|
||||
|
||||
尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果您的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性化的读取请求。
|
||||
尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果您的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性一致性的读取请求。
|
||||
|
||||
#### 成员服务
|
||||
|
||||
@ -898,15 +899,15 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
|
||||
|
||||
## 本章小结
|
||||
|
||||
在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性化(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性化因为易于理解而变得很吸引人 - 它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。
|
||||
在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性一致性因为易于理解而变得很吸引人 - 它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。
|
||||
|
||||
我们还探讨了因果关系,这个因果关系对系统中的事件进行了排序(根据原因和结果发生在什么之前)。与线性化不同,线性化将所有操作放在单一的完全有序的时间线中,因果性为我们提供了一个较弱的一致性模型:有些东西可以是并发的,所以版本历史就像是一个分支和合并的时间线。因果一致性不具备线性化的协调开销,并且对网络问题的敏感性要低得多。
|
||||
我们还探讨了因果关系,这个因果关系对系统中的事件进行了排序(根据原因和结果发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的完全有序的时间线中,因果性为我们提供了一个较弱的一致性模型:有些东西可以是并发的,所以版本历史就像是一个分支和合并的时间线。因果一致性不具备线性一致性的协调开销,并且对网络问题的敏感性要低得多。
|
||||
|
||||
但是,即使我们捕捉到因果顺序(例如使用Lamport时间戳),我们也看到有些事情不能以这种方式实现:在“时间戳排序不够充分”的第347页中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们达成共识。
|
||||
|
||||
我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,广泛的问题实际上可以归结为共识,并且彼此是等价的(从这个意义上说,如果你有一个解决方案,你可以很容易地将它转换成解决方案之一其他)。这种等同的问题包括:
|
||||
|
||||
***线性化的比较和设置寄存器***
|
||||
***线性一致性的比较和设置寄存器***
|
||||
|
||||
寄存器需要基于当前值是否等于操作中给定的参数,自动决定是否设置其值。
|
||||
|
||||
@ -930,7 +931,7 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
|
||||
|
||||
当多个事务同时尝试使用相同的密钥创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。
|
||||
|
||||
如果您只有一个节点,或者您愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性化操作,唯一性约束,完全有序的复制日志等等。
|
||||
如果您只有一个节点,或者您愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。
|
||||
|
||||
但是,如果单个领导失败,或者如果网络中断导致领导不可达,则这样的系统变得无法取得进展。处理这种情况有三种方法:
|
||||
|
||||
@ -938,11 +939,11 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
|
||||
2. 通过让人类选择一个新的领导者节点并重新配置系统来使用它来手动故障切换。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识 - 计算机系统之外的操作人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。
|
||||
3. 使用算法自动选择一个新的领导。这种方法需要一个一致的算法,建议使用经过验证的算法来正确处理不利的网络条件【107】。
|
||||
|
||||
尽管一个单独的领导者数据库可以提供线性化,而不需要在每个写作上执行一致的算法,但是仍然需要达成共识以保持领导力和领导力的改变。因此,从某种意义上说,有一个领导者只是“把罐子放在路上”:共识还是需要的,只是在一个不同的地方,而不是频繁的。好消息是,容错算法和共识系统存在,我们在本章中简要地讨论它们。
|
||||
尽管一个单独的领导者数据库可以提供线性一致性,而不需要在每个写作上执行一致的算法,但是仍然需要达成共识以保持领导力和领导力的改变。因此,从某种意义上说,有一个领导者只是“把罐子放在路上”:共识还是需要的,只是在一个不同的地方,而不是频繁的。好消息是,容错算法和共识系统存在,我们在本章中简要地讨论它们。
|
||||
|
||||
像ZooKeeper这样的工具在提供应用程序可以使用的“外包”协议,故障检测和会员服务方面起着重要的作用。使用起来并不容易,但比开发自己的算法要好得多,可以承受第8章讨论的所有问题。如果你发现自己想要做一个可以归结为一致的东西,而且你想要它要容错,那么建议使用类似ZooKeeper的东西。
|
||||
|
||||
尽管如此,并不是每个系统都需要达成共识:例如,无领导者和多领导者复制系统通常不会使用全球共识。这些系统中出现的冲突(参见第171页的“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性化的东西,学会更好地工作具有分支和合并版本历史记录的数据。
|
||||
尽管如此,并不是每个系统都需要达成共识:例如,无领导者和多领导者复制系统通常不会使用全球共识。这些系统中出现的冲突(参见第171页的“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性一致性的东西,学会更好地工作具有分支和合并版本历史记录的数据。
|
||||
|
||||
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,不可以做什么,帮助我们找到违反直觉的方法其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user