P2P结构与Quorum机制------《Designing Data-Intensive Applications》读书笔记8

前文涉及到了很多与Leader相关的算法,大家有木有想过,王侯将相,宁有种乎,既然Leader这么麻烦,干脆还是采用P2P模型吧,来个大家平等的架构。本篇需要和大家探讨的就是多副本下实现民主政治的Quorum机制。至于它是怎么样解决我们在前文提及的各种问题的,接着这篇文章我们继续聊聊~~

1. No-Leader机制

有些数据存储系统放弃了Leader的机制,允许任何副本直接接受用户的写操作。(如Amazon的Dynamo,FaceBook的Cassandra,虽然最终FaceBook放弃了Cassandra转而支持Hbase,但是Uber的强势介入让Cassandra后来在开源社区大放异彩。) 每个接受到客户端写请求的节点会转换为一个协调器节点,而协调器节点不强制执行特定的写入顺序。正是这种设计上的差异对数据库的使用方式与数据模型产生了深远的影响。

多副本的读写

No-Leader机制是怎么样消除Leader这个角色的存在的呢?答案也很简单:多副本读写。接下来我们来看一个栗子:

假设我们在数据系统之中采用了三副本的结构,如下图所示:User 1234 并行地将所有的副本发送给三个存储节点,并且两个节点可以接受副本的写入,但是其中一个节点不在线,所以副本写入失败。所以在三个副本中有两个副本确认写入成功了:在User 1234收到两个OK响应之后,User就认为写入操作是成功的,忽略了一个副本写入失败。(当然,不是简单的就不管这个写入失败了,后续会有修复机制来补齐这个副本的数据

P2P结构与Quorum机制------《Designing Data-Intensive Applications》读书笔记8_第1张图片
image.png

现在假设User 2345开始读取新写入的数据。由于一个节点写入失败了,所以User 234 可能会得到过期的值作为响应。为了解决这个问题,当User 从数据系统之中读取数据时,它不只是将请求发送到一个副本,而是将读取请求并行地发送到多个副本节点。User可以从不同节点获得不同的响应,即来自其他节点的最新值和另一个节点的过期值。这里通过了版本号用于确定哪个值是更新的值。

副本修复

No-Leader机制导致了数据系统之中可能存在大量过期的值,所以一个节点怎么来修复自身的副本来获取最新值的过程我们就称之为副本修复,No-Leader机制也是通过这样的方式来达到最终一致性的。通常会有这样几种方式:

  • 读修复
    当用户并行读取多个节点时,它可以获取到其他过期的值的响应。所以用户会发现其中有些节点拥有过期的值,这时用户可以主动将新值写入该节点。这种方法称之为读修复

  • 反熵过程(其实是一个物理学概念)
    每个数据存储节点都会有一个后台进程,不断的比对自己的副本与其他节点副本的差异,发现自己拥有过期的值之后,会主动修复自己过期的副本。与基于写入顺序日志不同,这种反熵过程不以任何特定的顺序复制写操作,并且在复制数据之前可能会有显著的延迟。

2. Quorum机制

上文之中提及的例子在三个副本中的两个之上写入成功,我们认为写操作成功了。但是如果三个副本只有的一个副本写入成功了?这时的写操作是否是成功的呢?

答案是否定的?这里其实就是简单的鸽巢原理,这里我不做数学证明了,大家有兴趣的可以自行证明一下。
假设有n个副本,每次写操作必须由w个节点确认为成功,每个读操作读取r个节点。(在上文的例子中,n=3,w=2,r=2)。只要w + r > n,如果读和写操作的总次数大于n,那么读和写操作必然至少有一个副本是相同的,也就是读操作必然可以读到最新写操作的数据。这被我们称之为:Quorum机制,每次读写都需要达到法定人数。

通常 n、w和r通常是可配置的,根据您的需要来修改这些数字。一个常见的选择是使n为奇数(通常为3或5),并设置w=r=(n + 1)/ 2 。如下图所示,如果w < n,如果有n - w个节点不可用,我们仍然可以处理写操作。同样的如果r<n,如果有n - r个节点不可用,我们仍然可以处理读操作。而如果小于所需的w或r节点可用,则写或读操作就会返回错误。

  • n=3,w=2,r=2,我们可以容忍一个不可用的节点。
  • n=5,w=3,r=3,我们可以容忍两个不可用的节点。
P2P结构与Quorum机制------《Designing Data-Intensive Applications》读书笔记8_第2张图片
Quorum机制保证了一定能读到最新的副本

高可用与Hinted handoff

Quorum机制实现了最终一致性的模型,但是在可用性上还是有一些极端情况,没法很好处理。如:出现网络抖动时,但是可能系统仍然有许多正常工作的节点。但是副本应该被写入的n个节点发生网络问题,导致了会少于w或r个成功的读写操作,由于不能达到法定的人数,读写操作都会失败。所以这时候数据库系统的设计者面临权衡取舍,能不能通过一些机制,实现更好的可用性呢?

所以这种情况下,我们就可以利用Hinted handoff了(原谅我翻译不好)。这种方式是怎么样实现的呢?写和读操作仍然需要w和r成功的响应,但是可以不强制一定要写如指定的n个节点 (这个涉及到一致性哈希,数据分布的知识,暂时要是理解不了,我后续会有专门的专题来写这个内容,可以先放一放。) 打个比方说,如果你把自己锁在门外,你可能会敲邻居的门,问你是否可以暂时呆在他们的沙发上,一旦你找到钥匙了,你就自己回家了。所以其他节点可以暂存本应该放在另一个节点上的副本,一旦网络中断被修复,其他节点就会把副本转交给主人节点。

所以这种模式既保证了不违反Quorum机制,也大大提高了系统的可用性,被No-leader数据系统广泛采用。

3 写入冲突与Quorum机制

同样的Quorum机制的设计本身就可以允许并发读写操作,并容忍网络中断与高峰延迟。但是这也必然会带来一致性问题,我们来看下面这个例子:

如图所示,有两个Client A与B,同时写入关键字X在一个三副本的数据存储系统之中。Node 1接收来自A的写入,但由于网络中断而从未接收来自B的写入。Node 2首先接收来自A的写入,然后接收B写入。而Node 3则是首先接收来自B的写入,然后接收A的写入。Node 2认为X的最终值是B,而其他Node认为最终值是A.


P2P结构与Quorum机制------《Designing Data-Intensive Applications》读书笔记8_第3张图片
并发写导致副本冲突

在这样的场景下如何仲裁写入结果成为了一个大问题,思路和我们之前提到的类型:

  • Last Write Win
    我们可以为每个写操作附加一个时间戳,选择最大的时间戳作为最新的值,并丢弃任何具有早期时间戳的写操作的值。这种冲突解决算法,称为Last Write Win。这种情况要求每个写操作具有幂等性,否则会出现写丢失的情况,如何能保证不出现依赖的写丢失呢?

  • 合并“happens-before”关系
    每当有两个操作A和B时,有三种可能:A发生在B之前,B发生在A之前,A或B是并发的。我们需要的是一个算法,告诉我们两个操作是否并发。如果一个操作在另一个操作之前发生,那么后面的操作应该覆盖前面的操作,但是如果操作是并行的,那么我们需要解决一个冲突。怎么样去捕获并合并“happen-before”的关系呢?可以在服务器节点维护一个版本号,每次写操作时递增版本号,并将新版本号存储在写入的值中。

    • 客户端
      当客户端读取一个键时,服务节点会返回所有未被覆盖的值,以及最新版本号。当客户端需要写一个键时,它必须包含从先前读取中的版本号,并且它必须合并它在前面读取中接收到的所有值。
    • 服务器
      当服务器接收到具有特定版本号的写入时,它可以覆盖该版本号或以下的所有值,因为它知道已经合并到新值,但必须保留所有值具有更高版本号。
  • 版本向量

合并“happen-before"使用一个单一的版本号来捕捉操作之间的依赖关系,但这不足以解决当有多个副本并行写入的情况。相反,我们需要使用每个副本的版本号以及每个键。每个副本在处理写时递增自己的版本号,并跟踪从其他副本中看到的版本号。此信息指示要覆盖哪些值以及作为兄弟版本保存着哪些值。而所有副本的版本号的集合称为版本向量。版本向量从数据节点发送给客户端,所以版本向量让我们可以区分覆盖写与发并行写操作。

4. 小结

好了,到此为止我们终于总结完整了分布式系统之中的副本机制。从Leader-Follower 机制到多Leader机制,最后到No-Leader的机制,并且详细总结了各个机制的实现细节与优缺点,希望大家阅读完之后也能有所收获。

你可能感兴趣的:(P2P结构与Quorum机制------《Designing Data-Intensive Applications》读书笔记8)