This commit is contained in:
Vonng 2018-03-31 02:03:16 +08:00
parent 849eefcc24
commit 028df0de4b
3 changed files with 60 additions and 64 deletions

View File

@ -106,7 +106,7 @@
| 第六章:分区 | 初翻 | |
| 第七章:事务 | 精翻 60% | Vonng |
| 第八章:分布式系统中的问题 | 初翻 | |
| 第九章:一致性与共识 | 初翻 80% | Vonng |
| 第九章:一致性与共识 | 初翻 | Vonng |
| 第三部分:前言 | 精翻 | |
| 第十章:批处理 | 草翻 | |
| 第十一章:流处理 | 草翻 | |

4
ch2.md
View File

@ -633,9 +633,9 @@ RETURN person.name
对于声明性查询语言来说,典型的情况是,在编写查询语句时,您不需要指定执行细节:查询优化程序会自动选择预测效率最高的策略,因此您可以继续编写其余的应用程序。
### SQL中的图查询
### SQL中的图查询
[例2-2]()建议可以在关系数据库中表示图数据。但是,如果我们把图数据放入关系结构中我们是否也可以使用SQL查询它答案是肯定的但有些困难。在关系数据库中您通常会事先知道在查询中需要哪些连接。在图查询中,您可能需要在找到要查找的顶点之前,遍历可变数量的边。也就是说,连接的数量事先并不确定。
[例2-2]()建议可以在关系数据库中表示图数据。但是如果我们把图数据放入关系结构中我们是否也可以使用SQL查询它答案是肯定的但有些困难。在关系数据库中您通常会事先知道在查询中需要哪些连接。在图查询中您可能需要在找到要查找的顶点之前遍历可变数量的边。也就是说连接的数量事先并不确定。
在我们的例子中这发生在Cypher查询中的`() -[:WITHIN*0..]-> ()`规则中。一个人的`LIVES_IN`边缘可以指向任何类型的位置:街道,城市,地区,地区,国家等。城市可以在一个地区,在一个州内的一个地区,在一个国家内的一个州等等。`LIVES_IN`边可以直接指向你正在查找的位置,或者可以在位置层次结构中删除几个级别。
在Cypher中`WITHIN * 0`表示这个事实非常简洁:意思是“沿着一个`WITHIN`边,零次或多次”。它就像正则表达式中的`*`运算符。

118
ch9.md
View File

@ -792,96 +792,94 @@ XA事务解决了保持多个参与者数据系统相互一致的现实的
请记住全序广播要求将消息按照相同的顺序恰好传递一次准确传送到所有节点。如果仔细思考这相当于进行了几轮共识在每一轮中节点提议下一条要发送的消息然后决定在全序中下一条要发送的消息【67】。
所以,全序广播相当于重复进行多轮共识(每共识决定与一次消息传递相对应):
所以,全序广播相当于重复进行多轮共识(每共识决定与一次消息传递相对应):
* 由于一致同意属性,所有节点决定以相同的顺序传递相同的消息。
* 由于**一致同意**属性,所有节点决定以相同的顺序传递相同的消息。
* 由于完整性属性,消息不重复。
* 由于有效性属性,消息不会被破坏,也不是凭空制造的
* 由于终止属性,消息不会丢失。
* 由于**完整性**属性,消息不重复。
* 由于**有效性**属性,消息不会被损坏,也不能凭空编造
* 由于**终止**属性,消息不会丢失。
已加密的复制Raft和Zab直接执行全序广播因为这样做比重复一轮一次一致的共识更有效。在Paxos的情况下这种优化被称为Multi-Paxos。
视图戳复制Raft和Zab直接实现了全序广播因为这样做比重复**一次一值one value a time**的共识更高效。在Paxos的情况下这种优化被称为Multi-Paxos。
#### 单领导者复制和共识
在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给领导者,并以相同的顺序将他们应用到追随者,从而使副本保持最新状态。这不是基本上全序广播?我们怎么不用担心[第五章](ch5.md)的共识
在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在[第五章](ch5.md)里一点都没担心过共识问题呢
答案取决于如何选择领导者。如果领导人是由你的运维团队人员手动选择和配置的,那么你基本上拥有独裁类型的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为领导者。这样的制度在实践中效果很好,但是无法达到共识的**终止**属性,因为它需要人为干预才能取得进展。
答案取决于如何选择领导者。如果主库是由运维人员手动选择和配置的,那么你实际上拥有一种**独裁类型**的“共识算法”:只有一个节点被允许接受写入(即决定写入复制日志的顺序),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主库。这样的系统在实践中可以表现良好,但它无法满足共识的**终止**属性,因为它需要人为干预才能取得**进展**
一些数据库执行自动领导者选举和故障转移,如果旧领导者失败,则促使追随者成为新的领导者(参见“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们更接近容错的全面命令播出,从而达成共识。
一些数据库会自动执行领导者选举和故障转移,如果旧主库失效,会提拔一个从库为新主库(参见“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们向容错的全序广播更进一步,从而达成共识。
但是有一个问题。我们之前曾经讨论过脑裂的问题,并且说过所有的节点都需要同意领导者是谁,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全序广播就像单主复制,单主复制需要领导,那么...
但是有一个问题。我们之前曾经讨论过脑裂的问题,并且说过所有的节点都需要同意是谁领导,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,选出一位领导者需要共识。但如果这里描述的共识算法实际上是全序广播算法,并且全序广播就像单主复制,单主复制需要一个领导,那么...
看来要选出一个领导者,我们首先需要一个领导者。要解决共识问题,首先要解决共识问题。我们如何摆脱这个难题?
这样看来要选出一个领导者,我们首先需要一个领导者。要解决共识问题,我们首先需要解决共识问题。我们如何跳出这个先有鸡还是先有蛋的问题?
#### 时代编号和法定人数
迄今为止所讨论的所有共识协议在内部都以某种形式使用领导者但是并不能保证领导者是独一无二的。相反他们可以做出较弱的保证协议定义了一个纪元号码称为Paxos中的选票号码视图戳复制中的视图号码以及Raft中的术语号码并确保在每个纪元中领导者是唯一的。
迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个**时代编号epoch number**在Paxos中称为**投票编号ballot number**,视图戳复制中的**视图编号view number**以及Raft中的**任期号码term number**),并确保在每个时代中,领导者都是唯一的。
当现在的领导被认为是死的时候,就会在节点之间开始投票选出一个新领导。这次选举被赋予了一个递增的时代号码,因此时代号码是完全有序的,单调递增的。如果在两个不同的时代,两个不同的领导者之间有冲突(也许是因为前一个领导者实际上并没有死亡),那么具有更高时代的领导者就占上风了
次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的时代编号,因此时代编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高时代编号的领导说了算
在任何领导人被允许决定任何事情之前,必须首先检查是否没有其他具有较高时代的领导者,这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下?回想一下在“[真理是由多数所定义](ch8.md#真理是由多数所定义)”中:一个节点不一定能相信自己的判断——因为只有节点自己认为它是领导者,并不一定意味着其他节点接受它作为它们的领导者。
在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高时代编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在“[真理在多数人手中](ch8.md#真理在多数人手中)”中提到的:一个节点不一定能相信自己的判断—— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。
相反,它必须从节点法定人数中获取选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成
相反,它必须从**法定人数quorum**的节点中获取选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常但不总是由多数节点组成【105】。只有在没有意识到任何带有更高时代编号的领导者的情况下一个节点才会投票赞成提议
因此,我们有两轮投票:一次是选一位领导人二是投票领导人的提议。关键的看法是这两票的法定人数必须重叠如果一个提案的投票成功至少有一个投票的节点也必须参加最近的领导人选举【105】。因此如果一个提案的投票没有显示任何更高的时代那么现在的领导者就可以得出这样的结论没有一个更高时代的领袖选举发生了因此可以确定它仍然是领导。然后它可以安全地决定提出的价值
因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的**法定人群**必须相互**重叠overlap**如果一个提案的表决通过则至少得有一个参与投票的节点也必须参加过最近的领导者选举【105】。因此如果在一个提案的表决过程中没有出现更高的时代编号。那么现任领导者就可以得出这样的结论没有发生过更高时代的领导选举因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定
个投票过程看起来很像两阶段提交。最大的区别是在2PC中协调者不是选出来的而容错协议算法只需要大部分节点的投票而2PC则要求每个参与者都做“是”的投票。而且共识算法定义了一个恢复过程通过这个过程节点可以在选举出新的领导者之后进入一个一致的状态确保总是满足安全属性。这些差异是共识算法的正确性和容错性的关键。
一投票过程表面上看起来很像两阶段提交。最大的区别在于2PC中协调者不是由选举产生的而且2PC则要求**所有**参与者都投赞成票,而容错共识算法只需要多数节点的投票。而且,共识算法还定义了一个恢复过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保始终能满足安全属性。这些区别正是共识算法正确性和容错性的关键。
#### 共识的局限性
共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全序广播,因此它们也可以容错的方式实现线性一致性的原子操作(参见“[使用全序广播实现线性一致性存储](#使用全序广播实现线性一致性存储)”)。
共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此也可以它们也可以以一种容错的方式实现线性一致的原子操作(参见“[使用全序广播实现线性一致性存储](#使用全序广播实现线性一致性存储)”)。
尽管如此,它们并没有到处使用,因为它的好处是有代价的。
尽管如此,它们并不是在所有地方都用上了,因为好处总是有代价的。
节点在决定之前对节点进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失——但是为了获得更好的性能,许多人选择接受这种风险。
节点在做出决定之前对提议进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。
共识系总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余三个为大多数),或者至少有五个节点来容忍两个节点发生故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(参阅“[线性一致性的代价](#线性一致性的代价)”)。
共识系总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(参阅“[线性一致性的代价](#线性一致性的代价)”)。
大多数一致性算法假定一组参与投票的节点,这意味着你不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。
大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点。共识算法的**动态成员扩展dynamic membership extension**允许集群中的节点集随时间推移而变化,但是它们比静态成员算法要难理解得多。
共识系统通常依靠超时来检测失败的节点。在网络延迟高度变化的环境中,特别是在地理上分布的系统中,经常发生一个节点错误地认为由于暂时的网络问题,导致失败的原因。虽然这个错误不会损害安全属性,但频繁的领导者选举会导致糟糕的表现,因为系统最终会花费更多的时间来选择领导者而不是做任何有用的工作
共识系统通常依靠超时来检测失效的节点。在网络延迟高度变化的环境中,特别是在地理上散布的系统中,经常发生一个节点由于暂时的网络问题,错误地认为领导者已经失效。虽然这种错误不会损害安全属性,但频繁的领导者选举会导致糟糕的性能表现,因系统最后可能花在权力倾扎上的时间要比花在建设性工作的多得多
有时共识算法对网络问题特别敏感。例如Raft已被证明存在不愉快的边缘情况【106】如果整个网络工作正常除了一个特定的网络连接一直不可靠Raft可以进入领导层不断在两个节点之间弹跳的情况或者目前的领导者不断被迫辞职所以这个制度从来没有取得进展。其他一致性算法也存在类似的问题,而对不可靠网络更具鲁棒性设计算法仍然是一个开放的研究问题。
有时共识算法对网络问题特别敏感。例如Raft已被证明存在让人不悦的极端情况【106】如果整个网络工作正常但只有一条特定的网络连接一直不可靠Raft可能会进入领导频繁二人转的局面或者当前领导者不断被迫辞职以致系统实质上毫无进展。其他一致性算法也存在类似的问题,而设计能健壮应对不可靠网络的算法仍然是一个开放的研究问题。
### 成员与协调服务
像ZooKeeper或etcd这样的项目通常被描述为“分布式键值存储”或“协调配置服务”。这种服务的API看起来非常像数据库你可以读写给定键的值并遍历键。所以如果他们基本上是数据库的话,他们为什么要全力实施一个共识算法呢?是什么使他们不同于任何其他类型的数据库?
像ZooKeeper或etcd这样的项目通常被描述为“分布式键值存储”或“协调配置服务”。这种服务的API看起来非常像数据库你可以读写给定键的值并遍历键。所以如果它们基本上算是数据库的话,为什么它们要把工夫全花在实现一个共识算法上呢?是什么使它们区别于其他任意类型的数据库?
为了理解这一点,简单探讨如何使用像ZooKeeper这样的服务是有帮助的。作为应用程序开发人员你很少需要直接使用ZooKeeper因为它实际上不适合作为通用数据库。更有可能的是通过其他项目间接依赖它例如HBaseHadoop YARNOpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从中得到什么?
为了理解这一点,简单了解如何使用ZooKeeper这类服务是很有帮助的。作为应用开发人员你很少需要直接使用ZooKeeper因为它实际上不适合当成通用数据库来用。更有可能的是你会通过其他项目间接依赖它例如HBaseHadoop YARNOpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从它那里得到了什么?
ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据虽然它们仍然写入磁盘以保持持久性),所以你不希望在这里存储所有的应用程序的数据。使用容错全序广播算法在所有节点上复制少量的数据。正如前面所讨论的那样,全序广播就是数据库复制所需要的:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以保持副本之间的一致性
ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据虽然它们仍然会写入磁盘以保证持久性),所以你不会想着把所有应用数据放到这里。这些少量数据会通过容错的全序广播算法复制到所有节点上。正如前面所讨论的那样,数据库复制需要的就是全序广播:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以使副本之间保持一致
ZooKeeper模仿Google的Chubby锁服务【14,98】不仅实现了全序广播因此也实现了共识而且还构建了一组有趣的其他特性这些特性在构建分布式系统时变得特别有用
ZooKeeper模仿Google的Chubby锁服务【14,98】不仅实现了全序广播因此也实现了共识而且还构建了一组有趣的其他特性这些特性在构建分布式系统时变得特别有用
***线性一致性的原子操作***
使用原子CAS操作可以实现锁如果多个节点同时尝试执行相同的操作只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(参阅“[进程暂停](ch8.md#进程暂停)”)。
使用原子CAS操作可以实现锁如果多个节点同时尝试执行相同的操作只有一个节点会成功。共识协议保证了操作的原子性和线性一致性即使节点发生故障或网络在任意时刻中断。分布式锁通常以**租约lease**的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(参阅“[进程暂停](ch8.md#进程暂停)”)。
***操作的全序***
***操作的全序排序***
如“页首301和锁定”中所述当某个资源受到锁定或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作为每个操作提供一个单调递增的事务ID`zxid`)和版本号(`cversion`来提供这个功能【15】。
如“[领导者与锁定](ch8.md#领导者与锁定)”中所述,当某个资源受到锁或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。 ZooKeeper通过全局排序操作来提供这个功能为每个操作提供一个单调递增的事务ID`zxid`)和版本号(`cversion`【15】。
***失效检测***
客户端在ZooKeeper服务器上维护一个长期会话,客户端和服务器周期性地交换心跳来检查另一个节点是否还活着。即使连接暂时中断或者ZooKeeper节点失会话仍保持活动状态。但是如果心跳停止持续时间超过会话超时ZooKeeper会声明该会话已经死亡。当会话超时ZooKeeper调用这些临时节点会话持有的任何锁都可以配置为自动释放。
客户端在ZooKeeper服务器上维护一个长期会话客户端和服务器周期性地交换心跳来检查节点是否还活着。即使连接暂时中断或者ZooKeeper节点失会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时ZooKeeper会宣告该会话已死亡。当会话超时ZooKeeper调用这些临时节点会话持有的任何锁都可以配置为自动释放ZooKeeper称之为**临时节点ephemeral nodes**
***变更通知***
一个客户端不仅可以读取其他客户端创建的锁和值还可以监视其中的更改。因此客户端可以找出另一个客户端何时加入集群基于它写入ZooKeeper的值还是另一个客户端发生故障因为其会话超时并且其临时节点消失。通过订阅通知客户避免了不得不经常轮询以找出变化。
在这些特征中只有线性一致性的原子操作才需要达成共识。但是这些功能的结合使得像ZooKeeper这样的系统在分布式协调中非常有用。
客户端不仅可以读取其他客户端创建的锁和值还可以监听它们的变更。因此客户端可以知道另一个客户端何时加入集群基于新客户端写入ZooKeeper的值或发生故障因其会话超时而其临时节点消失。通过订阅通知客户端不用再通过频繁轮询的方式来找出变更。
在这些功能中只有线性一致的原子操作才真的需要共识。但正是这些功能的组合使得像ZooKeeper这样的系统在分布式协调中非常有用。
#### 将工作分配给节点
ZooKeeper/Chubby模型运行良好的一个例子是如果你有几个流程或服务的实例,并且需要选择其中一个实例作为主库或首要。如果领导失败,其他节点之一应该接管。这对于单主数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的
ZooKeeper/Chubby模型运行良好的一个例子是如果你有几个进程实例或服务,需要选择其中一个实例作为主库或首选服务。如果领导者失败,其他节点之一应该接管。这对单主数据库当然非常实用,但对作业调度程序和类似的有状态系统也很好用
另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式参与者系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失败时,其他节点需要接管失败节点的工作。
另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式Actor系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。
型的任务可以通过在ZooKeeper中明智地使用原子操作各种节点和通知来实现。如果正确完成这种方法允许应用程序自动从故障中恢复无需人工干预。尽管Apache Curator 【17】等库已经出现在ZooKeeper客户端API的顶层提供了更高层的工具但这样做并不容易但它仍然比尝试从头开始实现必要的一致性算法要好得多成绩不佳【107】。
这类任务可以通过在ZooKeeper中明智地使用原子操作临时节点与通知来实现。如果设计得当这种方法允许应用自动从故障中恢复而无需人工干预。不过这并不容易尽管已经有不少在ZooKeeper客户端API基础之上提供更高层工具的库例如Apache Curator 【17】。但它仍然要比尝试从头实现必要的共识算法要好得多这样的尝试鲜有成功记录【107】。
应用最初只能在单个节点上运行但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数投票将是非常低效的。相反ZooKeeper在固定数量的节点通常是三到五个上运行并在这些节点之间执行其多数票同时支持潜在的大量客户端。因此ZooKeeper提供了一种将协调节点共识操作排序和故障检测的一些工作“外包”到外部服务的方式。
@ -909,57 +907,55 @@ ZooKeeper和它的小伙伴们可以看作是成员服务研究的悠久历史
## 本章小结
在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性一致性因为易于理解而变得很吸引人——它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。
在本章中,我们从几个不同的角度审视了关于一致性与共识的话题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使多副本数据看起来好像只有一个副本一样,并使其上所有操作都原子性地生效。虽然线性一致性因为简单易懂而很吸引人 —— 它使数据库表现的好像单线程程序中的一个变量一样,但它有着速度缓慢的缺点,特别是在网络延迟很大的环境中。
我们还探讨了因果性,因果性对系统中的事件进行了排序(根据因和过,以及什么发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些东西可以是并发的,所以版本历史就像是一个不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,并且对网络问题的敏感性要低得多。
我们还探讨了因果性,因果性对系统中的事件施加了顺序(什么发生在什么之前,基于因与果)。与线性一致不同,线性一致性将所有操作放在单一的全序时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些事件可以是**并发**的,所以版本历史就像是一条不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,而且对网络问题的敏感性要低得多。
是,即使我们捕捉到因果顺序(例如使用兰伯特时间戳),有些事情也不能以这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们需要**共识**。
即使捕获到因果顺序(例如使用兰伯特时间戳),我们发现有些事情也不能通过这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”一节的例子中,我们需要确保用户名是唯一的,并拒绝同一用户名的其他并发注册。如果一个节点要通过注册,则需要知道其他的节点没有在并发抢注同一用户名的过程中。这个问题引领我们走向**共识**。
我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,很广泛的一系列问题实际上都可以归结为共识问题,并且彼此是等价的(从这个意义上来讲,如果你有一个问题的解决方案,你可以容易地将它转换为其他问题的解决方案)。这种等价的问题包括:
我们看到,达成共识意味着以这样一种方式决定某件事:所有节点一致同意所做决定,且这一决定不可撤销。通过深入挖掘,结果我们发现很广泛的一系列问题实际上都可以归结为共识问题,并且彼此等价(从这个意义上来讲,如果你有其中之一的解决方案,就可以轻易将它转换为其他问题的解决方案)。这些等价的问题包括:
***线性一致性的CAS寄存器***
寄存器需要基于当前值是否等于操作中给定的参数,自动决定是否设置其值。
寄存器需要基于当前值是否等于操作给出的参数,原子地**决定**是否设置新值。
***原子事务提交***
数据库必须决定是否提交或中止分布式事务。
数据库必须**决定**是否提交或中止分布式事务。
***全序广播***
消息传递系统必须决定传递消息的顺序。
消息系统必须**决定**传递消息的顺序。
***锁和租约***
当几个客户争抢锁或租约时,决定哪个客户端成功获得锁。
当几个客户争抢锁或租约时,由锁来**决定**哪个客户端成功获得锁。
***成员/协调服务***
给定故障检测器(例如,超时),系统必须决定哪些节点处于活动状态,哪些应该被认为是死的,因为它们的会话超时
给定某种故障检测器(例如超时),系统必须**决定**哪些节点活着,哪些节点因为会话超时需要被宣告死亡
***唯一性约束***
当多个事务同时尝试使用相同的键创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。
当多个事务同时尝试使用相同的键创建冲突记录时,约束必须**决定**哪一个被允许,哪些因为违反约束而失败。
如果你只有一个节点,或者你愿意将决策的权能分配给单个节点,所有这些事都很简单。这就是在单领导者数据库中发生的事情:所有决策权归属于领导者,这就是为什么这样的数据库能够提供线性一致的操作,唯一性约束,完全有序的复制日志,以及更多。
但如果该领导者失效,或者如果网络中断导致领导者不可达,这样的系统就无法取得任何进展。应对这种情况可以有三种方法:
如果你只有一个节点,或者你愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。
1. 等待领导者恢复接受系统将在这段时间阻塞的事实。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全达成共识因为它不能满足**终止**属性的要求:如果领导者续命失败,系统可能会永久阻塞。
2. 人工故障切换,让人类选择一个新的领导者节点,并重新配置系统使之生效,许多关系型数据库都采用这种方方式。这是一种来自“天意”的共识 —— 由计算机系统之外的运维人员做出决定。故障切换的速度受到人类行动速度的限制,通常要比计算机慢(得多)。
3. 使用算法自动选择一个新的领导者。这种方法需要一种共识算法使用成熟的算法来正确处理恶劣的网络条件是明智之举【107】。
但是,如果单个领导失败,或者如果网络中断导致主库不可达,这样的系统变得无法取得进展。处理这种情况有三种方法:
尽管单领导者数据库可以提供线性一致性,且无需对每个写操作都执行共识算法,但共识对于保持及变更领导权仍然是必须的。因此从某种意义上说,使用单个领导者不过是“缓兵之计”:共识仍然是需要的,只是在另一个地方,而且没那么频繁。好消息是,容错的共识算法与容错的共识系统是存在的,我们在本章中简要地讨论了它们。
1. 等待领导者恢复同时系统将阻止接受新请求。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全解决共识因为它不能满足终止属性的要求如果领导者没有恢复系统可能会被永久封锁。
2. 通过让人类选择一个新的领导者节点,并重新配置系统,执行手动故障切换来使用它。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识——计算机系统之外的运维人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。
3. 使用算法自动选择一个新的领导。这种方法需要一个共识算法建议使用经过验证的算法来正确处理不利的网络条件【107】。
像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色虽说使用不易但总比自己去开发一个能经受[第8章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识并且希望它能容错使用一个类似ZooKeeper的东西是明智之举。
尽管单主数据库可以提供线性一致性,而不需要在每个写操作上执行共识算法,但对于保持领导权和领导权的变更,共识仍然是需要的。因此从某种意义上说,使用单个领导者不过是“缓兵之计”:共识还是需要的,只是在不同的地方,而且没那么频繁。好消息是,容错算法和共识系统是存在的,我们在本章中简要地讨论了它们
尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)正是不同领导者之间没有达成共识的结果,但也这并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。
像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色虽说用起来并不容易但总比自己去开发一个能经受[第8章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要做的是一个可以归结为共识的东西并且希望它能容错使用一个类似ZooKeeper的东西是明智之举
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于指导这一领域的实践有着极其重要的价值:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到反直觉的分布式系统缺陷。如果你有时间,这些参考资料值得探索
尽管如此,并不是每个系统都需要共识:例如,无主复制和多主复制系统通常不会使用全局共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间没有达成共识的结果,但也许没关系:也许我们只需要简单地在没有线性一致性的环境下应对,并学会更好地与具有分支和合并版本历史的数据打交道。
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到违反直觉的方法,其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。
这里就到了本书[第二部分](part-ii.md)的末尾,第二部介绍了复制([第5章](ch5.md)),分区([第6章](ch6.md)),事务([第7章](ch7.md)),分布式系统故障模型([第8章](ch8.md))以及最后的一致性和共识([第9章](ch9.md))。现在我们已经奠定了坚实的理论基础,在[第三部分](part-iii.md)我们将再次转向更实用的系统,并讨论如何从异构的组件积木块中构建强大的应用。
这里已经到了本书[第二部分](part-ii.md)的末尾,第二部介绍了复制([第5章](ch5.md)),分区([第6章](ch6.md)),事务([第7章](ch7.md)),分布式系统的故障模型([第8章](ch8.md))以及最后的一致性与共识([第9章](ch9.md))。现在我们已经奠定了扎实的理论基础,我们将在[第三部分](part-iii.md)再次转向更实际的系统,并讨论如何使用异构的组件积木块构建强大的应用。