Martin Kleppmann 写了一本很棒的书,叫做Designing Data-Intensive Applications。
在这本书中,马丁非常详细地描述了所有这些概念。
让我在这里引用一些相关讨论的摘录:
图 5-13 中的示例仅使用了一个副本。当有多个副本但没有领导者时,算法如何变化?
图 5-13 使用单个版本号来捕获操作之间的依赖关系,但是当有多个副本同时接受写入时,这还不够。相反,我们需要使用版本号每个副本以及每个键。每个副本在处理写入时都会增加自己的版本号,并且还会跟踪它从其他每个副本中看到的版本号。此信息指示要覆盖哪些值以及将哪些值保留为同级。
所有副本的版本号集合称为版本向量[56]。这个想法的一些变体正在使用中,但最有趣的可能是虚线版本向量[57],用于 Riak 2.0 [58, 59]。我们不会详细介绍,但它的工作方式与我们在购物车示例中看到的非常相似。
与图 5-13 中的版本号一样,版本向量在读取值时从数据库副本发送到客户端,并且在随后写入值时需要将其发送回数据库。 (Riak 将版本向量编码为它调用的字符串因果关系.) 版本向量允许数据库区分覆盖和并发写入。
此外,就像在单副本示例中一样,应用程序可能需要合并兄弟姐妹。版本向量结构确保从一个副本读取并随后写回另一个副本是安全的。这样做可能会导致创建同级,但只要正确合并同级,就不会丢失任何数据。
版本向量和向量时钟
版本向量有时也称为矢量时钟,尽管它们并不完全相同。差异是微妙的——请参阅参考资料了解详细信息 [57, 60, 61]。简而言之,在比较副本的状态时,版本向量是要使用的正确数据结构。
在图 5-10 的示例中,我们认为写入是成功的,即使它只在三分之二的副本上进行了处理。如果只有三分之一的副本接受了写入怎么办?我们能把它推到多远?
如果我们知道每个成功的写入都保证存在于至少三分之二的副本上,这意味着最多有一个副本可能是陈旧的。因此,如果我们从至少两个副本中读取,我们可以确定这两个副本中至少有一个是最新的。如果第三个副本出现故障或响应缓慢,读取仍然可以继续返回最新值。
更一般地说,如果有n副本,每次写入都必须由w节点被认为是成功的,我们必须至少查询r每次读取的节点。 (在我们的例子中,n= 3,w= 2,r= 2.) 只要w+r>n,我们希望在读取时得到一个最新的值,因为至少有一个r我们正在读取的节点必须是最新的。遵守这些的读写r和w值称为仲裁读取和写入 [44]。你可以想到r和w作为读取或写入有效所需的最小投票数。
在 Dynamo 风格的数据库中,参数n,w, 和r通常是可配置的。一个常见的选择是n奇数(通常为 3 或 5)并设置w=r= (n+ 1) / 2(四舍五入)。但是,您可以根据需要更改数字。例如,写入少而读取多的工作负载可能会受益于设置w=n和r= 1。这使得读取速度更快,但缺点是只有一个故障节点会导致所有数据库写入失败。
可能有超过n集群中的节点,但任何给定值仅存储在n节点。这允许对数据集进行分区,支持大于您在一个节点上容纳的数据集。我们将在第 6 章回到分区。
法定人数条件,w+r>n,允许系统容忍不可用的节点,如下所示:
- 如果w<n,如果节点不可用,我们仍然可以处理写入。
- 如果r<n,如果节点不可用,我们仍然可以处理读取。
- 有n= 3,w= 2,r= 2 我们可以容忍一个不可用的节点。
- 有n= 5,w= 3,r= 3 我们可以容忍两个不可用的节点。这种情况如图 5-11 所示。
- 通常,读取和写入始终并行发送到所有 n 个副本。参数 w 和 r 决定了我们等待多少个节点——即,在我们认为读取或写入成功之前,有多少 n 个节点需要报告成功。
图 5-11。如果w+r>n, 至少有一个r您读取的副本必须看到最近的成功写入。
如果少于要求w或者r节点可用,写入或读取返回错误。一个节点不可用的原因有很多:因为节点宕机(崩溃、掉电)、由于执行操作错误(由于磁盘已满而无法写入)、由于客户端和客户端之间的网络中断节点,或出于任何其他原因。我们只关心节点是否返回成功响应,不需要区分不同类型的故障。
分布式交易和共识
共识是分布式计算中最重要和最根本的问题之一。从表面上看,这似乎很简单:非正式地,目标只是让几个节点就某事达成一致.你可能认为这不应该太难。不幸的是,许多损坏的系统是在错误地认为这个问题很容易解决的基础上构建的。
尽管共识非常重要,但关于它的部分却出现在本书的后面,因为这个话题非常微妙,要了解其中的微妙之处需要一些先决知识。即使在学术研究界,对共识的理解也是在几十年的过程中逐渐形成的,在此过程中存在许多误解。现在我们已经讨论了复制(第 5 章)、事务(第 7 章)、系统模型(第 8 章)、线性化和全订单广播(本章),我们终于准备好解决共识问题了。
在许多情况下,节点达成一致很重要。例如:
领袖选举
在具有单领导者复制的数据库中,所有节点都需要就哪个节点是领导者达成一致。如果某些节点由于网络故障而无法与其他节点通信,则领导位置可能会出现竞争。在这种情况下,共识对于避免错误的故障转移很重要,这会导致两个节点都认为自己是领导者的脑裂情况(请参阅第 156 页的“处理节点中断”)。如果有两个领导者,他们都会接受写入并且他们的数据会分歧,导致不一致和数据丢失。
原子提交
在支持跨多个节点或分区的事务的数据库中,我们遇到事务可能在某些节点上失败但在其他节点上成功的问题。如果我们想保持事务的原子性(在 ACID 的意义上;参见第 223 页的“原子性”),我们必须让所有节点就事务的结果达成一致:要么他们都中止/回滚(如果出现任何问题) 或者他们都承诺(如果没有出错的话)。这种共识实例被称为原子提交问题。
共识的不可能性
您可能听说过 FLP 结果 [68](以作者 Fischer、Lynch 和 Paterson 的名字命名),它证明了如果存在节点崩溃的风险,没有一种算法总是能够达成共识。在分布式系统中,我们必须假设节点可能会崩溃,因此不可能达成可靠的共识。然而,我们在这里讨论达成共识的算法。这里发生了什么?
答案是 FLP 结果在异步系统模型中得到证明(参见第 306 页的“系统模型和现实”),这是一个非常严格的模型,它假定了一个不能使用任何时钟或超时的确定性算法。如果允许该算法使用超时,或其他识别可疑崩溃节点的方式(即使怀疑有时是错误的),那么共识就可以解决[67]。即使只是允许算法使用随机数也足以绕过不可能的结果[69]。
因此,尽管关于不可能达成共识的 FLP 结果在理论上具有重要意义,但分布式系统在实践中通常可以达成共识。
在本节中,我们将首先更详细地研究原子提交问题。特别是,我们将讨论两阶段提交(2PC) 算法,这是解决原子提交的最常用方法,在各种数据库、消息传递系统和应用程序服务器中实现。事实证明,2PC 是一种共识算法——但不是一个很好的算法 [70, 71]。
通过向 2PC 学习,我们将朝着更好的共识算法前进,例如 ZooKeeper (Zab) 和 etcd (Raft) 中使用的算法。
进一步阅读