DDIA - 第9章 一致性与共识

文章目录

  • 第9章 一致性与共识
    • 1 一致性保证
    • 2 可线性化
      • 2.1 如何达到线性化?
      • 可线性化和可串行化
      • 2.2 线性化的依赖条件
        • 2.2.1 加锁与主节点选举
        • 2.2.2 约束与唯一性保证
        • 2.2.3 跨通道的时间依赖
      • 2.3 实现线性化系统
        • 2.3.1 线性化与quorum
      • 2.4 线性化的代价
      • CAP理论是否有用?
        • 2.4.1 CAP理论
        • 2.4.2 可线性化与网络延迟
    • 3 顺序保证
      • 3.1 顺序与因果关系
        • 3.1.1 因果顺序并非全序
        • 3.1.2 可线性化强于因果一致性
        • 3.1.3 捕获因果关系
      • 3.2 序列号排序
        • 3.2.1 非因果序列发生器
        • 3.2.2 Lamport时间戳
        • 3.2.3 时间戳排序依然不够
      • 3.3 全序关系广播
        • 3.3.1 使用全序关系广播
        • 3.3.2 采用全序关系广播实现线性化存储
        • 3.3.3 采用线性化存储实现全序关系广播
    • 4 分布式事务与共识
      • 共识的不可能性
      • 4.1 原子提交与两阶段提交
        • 4.1.1 从单节点到分布式的原子提交
        • 4.1.2 两阶段提交
        • 4.1.3 系统的承诺
        • 4.1.4 协调者发生故障
        • 4.1.5 三阶段提交
      • 4.2 实践中的分布式事务
        • 4.2.1 Exactly-once消息处理
        • 4.2.2 XA交易
        • 4.2.3 停顿时仍持有锁
        • 4.2.4 从协调者故障中恢复
        • 4.2.5 分布式事务的限制
      • 4.3 支持容错的共识
        • 4.3.1 共识算法与全序广播
        • 4.3.2 主从复制与共识
        • 4.3.3 Epoch和Quorum
        • 4.3.4 共识的局限性
      • 4.4 成员与协调服务
        • 4.4.1 节点任务分配
        • 4.4.2 服务发现
        • 4.4.3 成员服务
    • 小结

信息是激发创新的力量

        本章目标: 分布式环境如何达成一致性与共识。

第9章 一致性与共识

        本章我们将讨论构建容错式分布式系统的相关算法和协议。为了构建容错系统,最好先构建一套通用的抽象机制和与之对应的技术保证,这样只需实现一次,其上的各种应用程序都可以安全地信赖底层的保证。例如,抽象的事务机制可以屏蔽系统内很多复杂的问题,例如发生崩溃、边界条件、磁盘故障等,使得应用层轻松无忧。
        现在继续沿着这个思路,尝试建立可以让分布式应用忽略内部各种问题的抽象机制。例如,分布式系统最重要的抽象之一就是共识:所有的节点就某一项提议达成一致。

1 一致性保证

        大多数多副本的数据库都至少提供了最终的一致性,这意味着如果停止更新数据库,并等待一段时间(长度未知)之后,最终所有读请求会返回相同的内容。换句话说,不一致现象是暂时的,最终会达到一致(假设网络故障最终会被修复)。换言之,最终一致性意味着“收敛”,即预期所有的副本最终会收敛到相同的值。
        分布式一致性模型与我们之前讨论过的多种事务隔离级别有相似之处。虽然存在某些重叠,但总体讲他们有着显著的区别:事务隔离主要是为了处理并发执行事务时的各种临界条件,而分布式一致性则主要是针对延迟和故障等问题来协调副本之间的状态。

  • 我们首先介绍线性化,这是最强的一致性模型,并考察其优缺点
  • 然后,我们讲探讨分布式系统中事件顺序问题,特别是因果关系和全局顺序
  • 最后“分布式事务与共识”小结,我们将探索如何自动提交分布式事务,并最终解决公式问题

2 可线性化

        可线性化基本的想法是让一个系统看起来好像只有一个数据副本,且所有的操作都是原子的。 有了这个保证,应用程序就不需要关心系统内部的多个副本。

2.1 如何达到线性化?

        可线性化背后的基本思想很简单:使系统看起来好像只有一个数据副本。
        就近性保证:一旦新值被写入或读取,所有后续的读都看到的是最新的值,直到被再次覆盖。
        通过记录所有请求和响应的时序,然后检查它们是否可以顺序排列,可以用来测试系统是否可线性化(这里存在额外的计算开销)

可线性化和可串行化

        可线性化(Linearizability)非常容易与可串行化(Serializability)发生混淆,两个词似乎都在表达类似“可以按顺序排列”的意思。但是它们完全不同,需要仔细区分:

  • 可串行化
            可串行化是事务的隔离属性,其中每个事务可以读写多个对象(行,文档,记录等,)。它用来确保事务执行的结果与串行执行(即每次执行一个事务)的结果完全相同,即使串行执行的顺序可能与事务实际执行顺序不同
  • 可线性化
            可线性化是读写寄存器(单个对象)的最新值保证。它并不要求将操作组合到事务中,因此无法避免写倾斜等问题,除非采取其他额外措施(如实现实体化冲突)

        数据库可以同时支持可串行化与线性化,这种组合又被称为严格的可串行化或者强的单副本可串行化(strong one-copy serializability,strong-1SR)。基于两阶段加锁或者实际以串行执行都是典型的可线性化。
        但是,可串行化的快照隔离则不是线性化的:按照设计,它可以从一致性快照中读取,以避免读、写之间的竞争。一致性快照的要点在于它里面不包括快照点创建时刻之后的写入数据,因此从快照读取肯定不满足线性化。

2.2 线性化的依赖条件

        在有些场景下,线性化对于保证系统正确工作至关重要。

2.2.1 加锁与主节点选举

        不管锁具体如何实现,它必须满足可线性化:所有节点都必须同一哪个节点持有锁,否则就会出现问题。
        提供协调者服务的系统如Apache ZooKeeper和etcd等通常用来实现分布式锁和主节点选举。归根结底,线性化存储服务是所有这些协调服务的基础。

2.2.2 约束与唯一性保证

        唯一性约束在数据库中很常见。硬性的唯一性约束,常见如关系型数据库中主键的约束,则需要线性化保证。其他如外键或属性约束,则并不要求一定线性化。

2.2.3 跨通道的时间依赖

        线性化违例之所以被注意到,是因为系统中存在其他的通信渠道(例如,Alice对Bob发出的声音来传递信息)
        Web服务器和图像调整器通过文件存储和消息队列通信,存在边界条件的可能。之所以出现这个问题是因为Web服务器和调整模块之间存在两个不同的通信通道:文件存储器和消息队列。 如果没有线性化的就近性保证,这两个通道之间存在竞争条件。

2.3 实现线性化系统

  • 主从复制(部分支持可线性化)
            在主从复制的系统中,只有主节点承担数据写入,从节点则在各自节点上维护数据的备份副本。如果从主节点或者同步更新的从节点上读取,则可以满足线性化。但并非每个主从复制的具体数据库实例都是可线性化的,主要是因为它们可能采用了快照隔离的设计,或者实现时存在并发方面的bug
            而从主节点上读取的前提是你确定知道哪个节点是主节点。某节点可能自认为是主节点,但事实并非如此,这个“自以为之”的主节点如果对外提供服务,就会违反线性化。如果使用了异步复制,故障切换过程中甚至可能会丢失一些已提交的写入,结果是同时违反持久性和线性化
  • 共识算法(可线性化)
            即将讨论的一些共识算法,与主从复制机制相似。不过共识协议通常内置一些措施来防止脑裂和过期的副本。正是由于这些专门的设计,共识算法可以安全地实现线性化存储,这些系统包括ZookKeeper和etcd等
  • 多主复制(不可线性化)
            具有多主节点复制的系统通常无法线性化的,主要由于它们同时在多个节点上执行并发写入,并将数据库异步复制到其他节点。因此它们可能会产生冲突的写入,需要额外的解决方案。这类冲突其实正是多副本所引入的结果
  • 无主复制(可能不可线性化)
            对于无主节点复制的系统,有些人认为只要配置法定读取和写入满足(w+r>n)就可以获得“强一致性”。但这完全取决于具体的quorum的配置,以及如何定义强一致性,它可能并不保证线性化
            例如基于墙上时钟的“最后写入获胜”冲突解决方法几乎肯定是非线性化,因为这种时间戳无法保证与实际事件顺序一致(例如由于时钟偏移)。不规范的quorum也会破坏线性化。甚至即使是严格quorum,正如之后即将介绍的,也会发生违背线性化的情况
2.3.1 线性化与quorum

        尽管使用了严格的quorum,仍然不满足线性化。 可以选择牺牲性能为代价来满足线性化,但这种方式只能实现线性化读、写操作,但无法支持线性化的“比较和设置”操作,后者需要共识算法的支持。

2.4 线性化的代价

        网络中断迫使在可线性化与可用性之间做出选择。

CAP理论是否有用?

         CAP有时也代表一致性,可用性,分区容错性,系统只能支持其中两个特性。不过,这种理解存在误导性,网络分区是一种故障,不管喜欢还是不喜欢,它都可能发生,所以无法选择或逃避分区的问题。
        在网络正常的时候,系统可以同时保证一致性(线性化)和可用性。而一旦发生了网络故障,必须要么选择线性(一致性),要么可用性。因此,更准确的称呼应该是“网络分区情况下,选择一致还是可用”。高可靠的网络会帮助减少发生的概率,但无法做到彻底避免。
        有必要指出,在CAP的诸多讨论中,术语可用性存在争议,其形式化定理中的可用性与通常意义上的理解有些差别。许多所谓的“高可用性”(容错)系统实际上并不符合CAP对可用性的特殊定义。总之,围绕着CAP有太多的误解与困扰,最后反而无法帮助我们更好地理解系统,所以本人建议最好避免使用CAP。

2.4.1 CAP理论

        只要有不可靠的网络,都会发生违背线性化的风险。我们可以做以下的权衡考虑;

  • 如果应用要求线性化,但由于网络方面的问题,某些副本与其他副本断开连接之后无法继续处理请求,就必须等待网络修复,或者直接返回错误。无论哪种方式,结果是服务不可用
  • 如果应用不要求线性化,那么断开连接之后,每个副本可独立处理请求例如写操作(多主复制)。此时,服务可用,但结果行为不符合线性化

        不要求线性化的应用更能容忍网络故障。正式定义的CAP定理范围很窄,它只考虑了一种一致性模型(即线性化)和一种故障(网络分区,节点处于活动状态但相互断开),而没有考虑网络延迟、节点失败或其他需要折中的情况。分布式系统中还有很多有趣的研究结果,目前CAP已被更精确的研究成果所取代,所以它现在更多的是代表历史上曾经的一个关注热点而已。

2.4.2 可线性化与网络延迟

        虽然线性化是个很有用的保证,但实际上很少有系统真正满足线性化。例如,现代多核CPU上的内存甚至就是非线性化:如果某个CPU核上运行的线程修改一个内存地址,紧接着另一个CPU核上的线程尝试读取,则系统无法保证可以读到刚刚写入的值,除非使用了内存屏障或fence指令。
        出现这种现象的原因是每个CPU核都有自己独立的cache和寄存器。内存访问首先进入cache系统,所有修改默认会异步地刷新到主存。由于访问cache比访问主存要快得多,所以这样的异步刷新特性对于现代CPU的性能至关重要。但是,这就导致出现了多个数据副本(一个在主存,另外几个在不同级别的cache中)。而副本更新是异步方式,无法保证线性化。
        Attiya和Welch证明如果想要满足线性化,那么读、写请求的响应时间至少与网络延迟成正比。虽然没有足够快的线性化算法,但弱一致性模型的性能则快得多,这种取舍对于延迟敏感的系统非常重要。

3 顺序保证

        事实证明,排序、可线性化与共识之间存在着某种深刻的联系。

3.1 顺序与因果关系

        之所以反复出现“顺序”问题,其中的一个原因是它有助于保持因果关系。
        因果关系对所发生的事件施加了某种排序:发送消息先于收到消息;问题出现在答案之前等,或者就像在现实生活中一样,一件事情会导致另一件事情:一个节点根据读取的数据做出决定,然后写入结果,另一个节点读取写入的结果再写入新的内容,诸如此类。 这些因果关系的依赖链条定义了系统中的因果顺序,即某件事应该发生另一间事情之前。
        如果系统服从因果关系所规定的顺序,我们称之为因果一致性。例如,快照隔离提供了因果一致性:当从数据库中读数据时,如果查询到了某些数据,也一定能看到触发该数据的前序事件(假设期间没有发生删除事件)

3.1.1 因果顺序并非全序

        全序关系 支持任何两个元素之间进行比较,即对于任意两个元素,总是可以指出哪个更大,哪个更小。全序和偏序的差异也会体现在不同的数据库一致性模型中:

  • 可线性化
            在一个可线性化的系统中,存在全序操作关系。关系的行为就好像只有一个数据副本,且每个操作都是原子的,这意味着对于任何两个操作,我们总是可以指出哪个操作在先
  • 因果关系
            如果两个操作都没有发生在对方之前,那么这两个操作是并发关系。换言之,如果两个事件是因果关系(一个发生在另一个之前),那么这两个事件可以被排序;而并发的事件则无法排序比较。这表明因果关系至少可以定义为偏序,而非全序
            并发意味着时间线会出现分支和合并,而不同分支上的操作无法直接比较。
3.1.2 可线性化强于因果一致性

        那么因果序和可串行化之间是什么关系呢?答案是可线性化一定意味着因果关系:任何可线性化的系统都将正确地保证因果关系。
        线性化并非是保证因果关系的唯一途径,还有其他方法使得系统可以满足因果一致性而免于线性化所带来的性能问题。事实上,因果一致性可以认为是,不会由于网络延迟而显著影响性能,又能对网络故障提供容错的最强的一致性模型。

3.1.3 捕获因果关系

        为保持因果关系,需要知道哪个操作发生在前。为了确定请求的因果依赖关系,我们需要一些手段来描述系统中节点所知道的“知识”。

3.2 序列号排序

        虽然因果关系很重要,但实际上跟踪所有的因果关系不切实际。更好的办法是 :我们可以使用序列号或时间戳来排序事件。
        特别是,我们可以按照与因果关系一致的顺序来创建序列号:保证如果操作A发生在B之前,那么A一定在全序中出现在B之前(即A的序列号更小)。并行操作的序列可能是任意的。这样的全局排序可以捕获所有的因果信息,同时也强加了比因果关系更为严格的顺序性。

3.2.1 非因果序列发生器

        如果系统不存在这样唯一的主节点(例如可能是多主或者无主类型的数据库),如何产生序列号就不是那么简单了。

  • 每个节点都独立产生自己的一组序列号
  • 可以把墙上时间戳信息(物理时钟)附加到每个操作上
  • 可以预先分配序列号的区间范围

        三种方法都存在的问题:所产生的序列号与因果关系并不严格一致。 所有这些序列号发生器都无法保证正确捕获跨界点操作的顺序,因而存在因果关系方面的问题。

  • 每个节点可能有不同的处理速度,如每秒请求数。因此,某个节点产生偶数而另一个产生奇数,偶数的计数器产生速度可能落后于奇数的计数器,反之亦然。这样就无法准确地知道哪个操作在先
  • 物理时钟的时间戳会受到时钟偏移的影响,也可能导致与实际因果关系不一致
  • 对于区间分配器,一个操作可能被赋予从1001到2000之间的某个序列号,而后发生的操作则路由到另一个节点,拿到了某个1到1000之间的序列号,导致与因果序不一致
3.2.2 Lamport时间戳

        兰伯特时间戳(Lamport timestamp) 可以产生与因果关系一致的序列号,可以保证全序与因果关系一致。
        首先每个节点都有一个唯一的标识符,且每个节点都有一个计数器来记录各自已处理的请求总数。Lamport时间戳是一个值对(计数器,节点ID)。两个节点可能会有相同的计数器值,但时间戳中还包含节点ID信息,因此可以确保每个时间戳都是唯一的。
        Lamport时间戳与物理墙上时钟并不存在直接对应关系,但它可以保证全序:给定两个Lamport时间戳,计数器较大那个时间戳大;如计数器值刚好相同,则节点ID越大,时间戳越大。每个节点以及每个客户端都跟踪迄今为止所见到的最大计数器值,并在每个请求中附带该最大计数器值。当节点收到某个请求(或者回复)时,如果发现请求内嵌的最大计数器值大于节点自身的计数器值,则它立即把自己的计数器修改为最大值。
        只要把最大计数器值嵌入到每一个请求中,该方案可以确保Lamport时间戳与因果关系一致,而请求的因果依赖性一定会保证后发生的请求得到更大的时间戳。Lamport时间戳由于版本向量之处在于它更加紧凑和高效。

3.2.3 时间戳排序依然不够

        虽然Lamport时间戳定义了与因果序列一致的全序关系,但还不足以解决实际分布式系统中许多常见的问题。这里问题的关键是:只有在收集了所有的请求信息之后,才能清楚这些请求之间的全序关系。
        总而言之,为了实现像用户名唯一性约束这样的目标,仅仅对操作进行全序排列还是不够的,还需要知道这些操作是否发生、何时确定等。假如能够在创建用户名时,已经确定知道了没有其他节点正在执行相同用户名的创建,你大可以直接安全返回创建成功。

3.3 全序关系广播

        如果程序只运行在一个CPU核上,可以非常简单地定义出操作的全序关系,即在单核上执行的顺序。
        主从复制的主要挑战在于,如何扩展系统的吞吐量使之突破单一主节点的限制,以及如何处理主节点失效时的故障切换。 在分布式系统研究文献中,这些问题被称为全序关系广播原子广播
        全序关系广播通常指节点之间交换消息的某种协议。非正式的定义要求满足两个基本安全属性:

  • 可靠发送
            没有消息丢失,如果消息发送到了某一个节点,则它一定要发送到所有节点
  • 严格有序
            消息总是以相同的顺序发送给每个节点
3.3.1 使用全序关系广播

        全序关系广播与共识之间有着密切关系,可以使用全序关系广播来实现可串行化事务,
        全序关系广播另一个要点是顺序在发送消息时已经确定,如果消息发送成功,节点不允许追溯地将某条消息插入到先前的某个位置。这一点使得全序关系广播比基于时间戳排序要求更强。

3.3.2 采用全序关系广播实现线性化存储

        在一个可线性化的系统中有全序操作集合。可线性化与全序关系广播不是完全相同的,但两者之间有着密切的联系。
        全序关系广播是基于异步模型:保证消息以固定的顺序可靠地发送,但是不保证消息何时发送成功(因此某个接收者可能明显落后于其他接收者)。而可线性化则强调就近性:读取时保证能够看到最新的写入值。
        可以通过使用全序关系广播以追加日志的方式来实现线性化的原子比较-设置操作。
        此过程可确保线性化写入,但它却无法保证线性化读取,即从异步日志更新的存储中读取数据时,可能是旧值。顺序一致性(时间线一致性),它弱于线性化保证。

3.3.3 采用线性化存储实现全序关系广播

        可以证明,线性化的原子比较-设置(或自增)寄存器与全序关系广播二者都等价于共识问题。
        最简单的方式是假设有一个线性化的寄存器来存储一个计数,然后使其支持原子自增-读取操作或者原子比较-设置操作。

4 分布式事务与共识

        有很多重要的场景都需要集群节点达成某种一致:

  • 主节点选举
  • 原子事务提交

共识的不可能性

        FLP表明如果节点存在可能崩溃的风险,则不存在总是能够达成共识的稳定算法。(重要的理论意义,基于异步系统模型而做的证明,这是一个非常受限的模型,它假定确定性算法都不能使用任何时钟或超时机制)
        

4.1 原子提交与两阶段提交

        原子性可以防止失败的事务破坏系统,避免形成部分成功夹杂着部分失败。

4.1.1 从单节点到分布式的原子提交

        单节点上吗,事务提交非常依赖于数据持久写入磁盘的顺序关系:先写入数据,然后再提交记录。事务提交(或中止)的关键点在于磁盘完成日志记录的时刻:在完成日志记录写之前如果发生了崩溃,则事务需要中止;如果再日志写入完成之后,即使发生崩溃,事务也被安全提交。
        向所有节点简单地发送一个提交请求,然后各个节点独立执行事务提交是绝对不够的。这样做很容易发生部分节点提交成功,而其他一些节点发生失败,从而违反了原子性保证:

  • 某些节点可能会检测到违反约束或有冲突,因而决定中止,而其他节点则可能成功提交
  • 某些提交请求可能在网络中丢失,最终由于超时而中止,而其他提交请求则顺利通过
  • 某些节点可能在日志记录写入之前发生崩溃,然后在回复时回滚,而其他节点则成功提交

        事务提交不可撤销,不能事后再改变主意(在提交之后再追溯去中止)。当然已提交事务的效果可以被之后一笔新的事务来抵消掉,即补偿性事务。 不过,从数据库的角度来看,前后两个事务完全相互独立。类似这种跨事务的正确性需要由应用层来负责。

4.1.2 两阶段提交

        两阶段提交(two-phase commit,2PC)是一种在多节点之间实现事务原子提交的算法,用来确保所有节点要么全部提交,要么全部中止。
        不要混淆2PC和2PL 两阶段提交(2PC)和两阶段加锁(2PL)是两个完全不同的事情。2PC在分布式数据库中负责原子提交,而2PL则提供可串行化的隔离。
        2PC引入了单节点事务所没有的一个新组件:协调者(也称为事务管理器)。 2PC事务从应用程序在多个数据库节点上执行数据读/写开始。

4.1.3 系统的承诺

        该协议有两个关键的“不归路”:首先,当参与者投票“是”时,它做出了肯定提交的承诺(尽管还取决于其他的参与者的投票,协调者才能做出最后决定)。其次,协调者做出了提交(或者放弃)的决定,这个决定也是不可撤销。正是这两个承诺确保了2PC的原子性(而单节点源自提交其实是将两个事件合二为一,写入事务日志即提交)。

4.1.4 协调者发生故障

        2PC能够顺利完成的唯一方法是等待协调者恢复。

4.1.5 三阶段提交

        两阶段提交也被称为阻塞式原子提交协议,因为2PC可能在等待协调者恢复时卡住。通常,非阻塞式原子提交 依赖于一个完美的故障检测器,即有一个非常可靠的机制可以判断出节点是否已经崩溃。
        3PC假定一个有界的网络延迟和节点在规定时间内响应。考虑到目前大多数具有无线网络延迟和进程暂停的实际情况,它无法保证原子性。

4.2 实践中的分布式事务

        分布式事务,尤其是那些通过两阶段提交所实现的事务,声誉混杂。分布式事务的某些实现存在严重的性能问题,两阶段提交性能下降的主要原因式为了防崩溃恢而做的磁盘I/O(fsync)以及额外的网络往返开销。
        两种截然不同的分布式事务概念:

  • 数据库内部的分布式事务
            某些分布式数据库支持跨数据库节点的内部事务
  • 异构分布式事务
            在异构分布式事务中,存在两种或两种以上不同的参与者实现技术

        数据库内部事务由于不必考虑与其他系统的兼容,因此可以使用任何形式的内部协议并采取有针对性的优化。因此,数据库内部的分布式事务往往可行且工作不错,但异构环境的事务则充满了挑战。

4.2.1 Exactly-once消息处理

        异构的分布式事务旨在无缝集成多种不同的系统。需要指出,只有在所受影响的系统都使用相同的原子提交协议的前提下,这种分布式事务才是可行的。

4.2.2 XA交易

        X/Open XA(eXtended Architecture,XA)是异构环境下实施两阶段提交的一个工业标准。XA并不是一个网络协议,而是一个与事务协调者进行通信的C API。

4.2.3 停顿时仍持有锁

        数据库事务通常持有待修改行的行级独占锁,用以防止脏写。此外,如果要使用可串行化的隔离,则两阶段锁的数据库还会对事务曾经读取的行持有读-共享锁。在事务提交(或中止)之前,数据库都不会释放这些锁。

4.2.4 从协调者故障中恢复

        协调者崩溃之后最终可能会出现恢复失败,唯一的出路只能是让管理员手动决定究竟是执行提交还是回滚。
        许多XA的实现都支持某种紧急避险措施称之为启发式决策:这样参与者节点可以在紧急情况下单方面做出决定,放弃或者继续那些停顿的事务,而不需要等到协调者发出指令。需要说明的是,这里的启发式其实是可能破坏原子性的委婉说法,它的确违背了两阶段提交所做出的承诺。 因此,这种启发式决策只是为了应急,不能作为常规手段来使用。

4.2.5 分布式事务的限制

        XA事务解决了多个参与者之间如何达成一致这样一个非常现实而重要的问题。特别是,核心的事务协调者本身就是一种数据库(存储事务的投票结果),因此需要和其他重要的数据库一样格外小心。

4.3 支持容错的共识

        共识问题通常形式化描述如下:一个或多个节点可以提议某些值,由共识算法来决定最终值。 在这个描述中,共识算法必须满足以下性质:

  • 协商一致性(Uniform agreement)
            所有的节点都接受相同的决议
  • 诚实性(Integrity)
            所有节点不能反悔,即对一项提议不能由两次决定
  • 合法性(Validity)
            如果决定了值v,则v一定是由某个节点所提倡的
  • 可中止性(Termination)
            节点如果不崩溃则最终一定可以达成决议

        协商一致性和诚实性属性定义了共识的核心思想:决定一致的结果,一旦决定,就不能改变。有效性属性主要是为了排除一些无意义的方案:例如,无论什么建议,都可以有一个总是为空(NULL)的决定,虽然可以满足一致性和诚实性,但没有任何实际效果。

4.3.1 共识算法与全序广播

        最著名的容错式共识算法包括VSR,Paxos,Raft和Zab。这些算法大部分其实并不是直接使用上述的形式化模型(提议并决定某个值,同时满足上面4个属性)。相反,他们是决定了一系列值,然后采用全序关系广播算法。

4.3.2 主从复制与共识

        如果主节点是由运营人员手动选择和配置的,那基本上就是一个独裁性质的“一致性算法”:只允许一个节点接受写入(并决定复制日志中的写入顺序),如果该节点发生故障,系统将无法写入,直到操作人员再手动配置新的节点成为主节点。 这样的方案也能在实践中很好地发挥作用,但它需要人为干预才你甭取得进展,不满足共识的可终止性。

4.3.3 Epoch和Quorum

        目前所讨论的所有共识协议在其内部都使用了某种形式的主节点,虽然主节点并不是固定的。相反,他们都采用了一种弱化的保证:协议定义了一个世代编号,并且在每个世代里,主节点是唯一确定的。

4.3.4 共识的局限性

        共识算法对于分布式系统来说绝对是一个巨大的突破,它为一切不确定的系统带来了明确的安全属性(一致性,完整性和有效性),此外它还可以支持容错(只要大多数节点还在工作和服务可达)。公式可以提供全序关系广播,以容错的方式实现线性化的原子操作。
        代价:

  • 在达成一致性决议之前,节点投票的过程是一个同步复制过程
  • 共识体系需要严格的多数节点才能运行
  • 多数共识算法假定一组固定参与投票的节点集,这意味着不能动态添加或删除节点
  • 共识系统通常依靠超时机制来检测节点失效
  • 共识算法往往对网络问题特别敏感

4.4 成员与协调服务

        ZooKeep或etcd这样的项目通常被称为“分布式键值存储”或“协调与配置服务”。ZooKeeper和etcd主要针对保存少量、可完全载入内存的数据(虽然它们最终仍要写入磁盘以支持持久化)而设计,所以不要用它们保存大量的数据。

  • 线性化的原子操作
  • 操作全序
  • 故障检测
  • 更改通知
4.4.1 节点任务分配

        对于作业调度系统(或类似的有状态服务)非常有用,或者对于一些分区资源(可以是数据库,消息流,文件存储,分布式actor system等),需要决定将哪个分区分配给哪个节点。
        通常情况下,ZooKeeper所管理的数据变化非常缓慢,类似“分区7的主节点在10.1.1.23”这样的信息,其变化频率往往在分钟甚至是小时级别。它不适合保存那些应用实时运行的状态数据,后者可能每秒差生数千甚至百万次更改。如果这样,应该考虑使用其他工具(Apache BookKeeper)

4.4.2 服务发现

        ZooKeeper、etcd和Consul还经常用于服务发现。

4.4.3 成员服务

        ZooKeeper等还可以看作是成员服务范畴的一部分。

小结

        线性化(一种流行的一致性模型):其目标是使多副本对外看卡来好像单一副本,然后所有操作以原子方式运行,就像一个单线程程序操作变量一样。 主要问题在于性能,特别是在网络延迟较大的环境中。
        因果关系对事件进行了某种排序(根据事件发生的原因-结果依赖关系)。线性化是将所有的操作都挡在唯一的、全局有序时间线上,而因果性则不同,它为我们提供了一个弱一致性模型:允许存在某些并发事件,所以版本历史是一个包含多个分支与合并的时间线。因果一致性避免了线性化昂贵的协调开销,且对网络延迟的敏感性要低很多。
        共识意味着就某一项提议,所有节点做出一致的决定,而且决定不可撤销。 等价的共识问题包括:

  • 可线性化的比较-设置寄存器
  • 原子事务提交
  • 全序广播
  • 锁与租约
  • 成员/协调服务
  • 唯一性约束

        基于主从复制的主节点发生故障,或者出现网络中断而导致主节点不可达,这样的系统就会陷入停顿状态。处理该问题的三种基本思路:

  1. 系统服务停止,并等待主节点恢复
  2. 人为介入来选择新的主节点,并重新配置系统使之生效
  3. 采用算法来自动选择新的主节点

        ZooKeeper等工具以一种类似外包方式为应用提供了重要的共识服务、故障检测和成员服务。

你可能感兴趣的:(java,数据库,分布式)