从本地事务到分布式事务,浅谈事务的分布式一致性算法。(2021/09/15更新Paxos算法)

本地事务(数据库事务)与ACID

什么是数据库事务?
事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。
事务是逻辑上的一组操作,要么都执行,要么都不执行。

数据库事务的四大特性
ACID
原子性A:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用
一致性C:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的
隔离性I:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的
持久性D:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

原子性和持久性定义了事务的边界,行为的开始和结束,一致性和隔离性即是对事务中间状态的管理。

ACID的核心是C,大家都是为得到C而提出的不同纬度的限制和规范
A确定一个功能的完整性,D对状态负责,I作为C的等级系数,不同的I策略会出现不同的C。
隔离性I的设定就是对一致性不同程度的破坏,事实上,如果我们顺序对数据进行读写,ACD是完全可以保证的,但这样效率会非常低下。
选择合适的隔离策略是为了在一致性和性能之间平衡,取得最好的综合表现。

一致性作为本地事务的核心,即使从本地事务进化到了分布式事务后,也同样是作为分布式事务的核心,下文的分布式事务,也将会着重讲解一致性。

分布式事务与CAP、BASE理论

什么是分布式事务?

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常一个分布式事务中会涉及对多个数据源或业务系统的操作。

对于本地事务处理或者是集中式的事务处理系统,很显然我们可以采用已经被实践证明很成熟的ACID模型来保证数据的严格一致性。
但是随着分布式事务出现,传统的单机事务模型已经无法胜任。尤其是对于一个高访问量、高并发的互联网分布式系统来说,如果我们期望实现一套严格满足ACID特性的分布式事务,
很可能出现的情况就是在系统的可用性和严格一致性之间出现冲突。故产生了CAP定理。

CAP定理

C一致性:所有节点在同一时间具有相同的数据
A可用性:保证每个请求不管成功或者失败都有响应
P分隔容忍:系统中任意信息的丢失或失败不会影响系统的继续运作
CAP不可能都取,只能取其中两个
如果C是第一需求的话,那么会影响A的性能,因为要数据同步,不然请求结果会有差异,但是数据同步会消耗时间,可用性就会降低
如果A是第一需求的话,那么只要有一个服务在,就能正常接收请求,但是对返回结果便不能保证,原因是在分布式部署的时候,数据一致的过程不可能那么快。
如果同时满足一致性和可用性,就很难保证分区的容错。

同时需要明确的一点是,对于一个分布式系统而言,分区容错性可以说是一个最基本的要求。
因为既然是一个分布式系统,那么分布式系统中的组件必然需要被部署到不同的节点,否则也就无所谓分布式系统了,因此必然出现子网络。
对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。

Zookeeper&Eureka
Zookeeper:满足cp,任何时候对Zookeeper的访问请求都能得到一致的数据结果,同时系统对网络分割具备容错性,但是Zookeeper不能保证每次服务请求都是可达的。
在使用zookeeper获取服务列表时,如果此时的zookeeper集群中的leader宕机了,该集群就要进行leader的选举,又或者zookeeper集群中半数以上服务器节点不可用,那么将无法处理该请求,故zk不能保证服务可用性。

Eureka:满足ap,eureka server采用peer to peer对等通信。这是一种去中心化的架构,无master/slave之分,每一个peer都是对等的。
在这种架构风格中,节点通过彼此互相注册来提高可用性,每个节点都可被视为其他节点的副本。只要有一台eureka还在,就能保证服务可用,只不过查到的信息可能不是最新的。

BASE理论

BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的。
其核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。

Basically Available(基本可用):
基本可用是指分布式系统在出现不可预知故障的时候允许损失部分可用性(绝不等价系统不可用)

Soft state(软状态):
弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步过程存在延时。

Eventually consistent(最终一致性):
最终一致性强调的是系统中的所有的数据副本在经过一段时间的同步后,最终能够达到一个一致的状态。
因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不要实时保证系统数据的强一致性。

BASE理论面向的是大型高可用可扩展的分布式系统,和传统事务的ACID特性是相反的,它完全不同于ACID的强一致性模型,而是提出通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

解决分布式一致性问题--->一致性协议

在分布式系统中,每一个机器节点虽然都能够明确地知道自己在进行事务操作过程中的结果是成功或失败,但却无法直接获取到其他分布式节点的操作结果。

因此,当一个事务操作需要跨越多个分布式节点的时候,为了保持事务处理的ACID特性,就需要引入一个称为"协调者"的组件来统一调度所有分布式节点的执行逻辑,
这些被调度的分布式节点则被称为"参与者"。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务真正进行提交。

基于这个思想,衍生出了二阶段提交和三阶段提交两种协议。

2PC

2PC(Two-Phase Commit),二阶段提交。为了使基于分布式系统架构下的所有节点在进行事务处理过程中能够保证原子性和一致性而设计的一种算法。
目前,绝大部分关系型数据库都是采用二阶段提交协议来完成分布式事务处理的。
二阶段提交将一个事务的处理过程分为了投票和执行两个阶段,其核心是对每个事务都采用先尝试后提交的处理方式,因此也可以将二阶段提交看作一个强一致性的算法。

阶段一:提交事务请求

  1. 事务询问

    协调者向所有参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的相应。

  2. 执行事务
    各参与者节点执行事务操作,并将undo和redo信息记入事务日志。

  3. 各参与者向协调者反馈事务询问的响应
    如果参与者成功执行了事务操作,那么就反馈给协调者yes相应,表示事务可以执行;
    如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。

二阶段提交协议的阶段一也被称为"投票阶段",即各参与者投票表明是否要继续执行接下来的事务提交操作。

阶段二:执行事务提交

两种可能:
A. 执行事务提交
假如协调者从所有的参与者获得的反馈都是yes响应,那么就会执行事务提交。

  1. 发送提交请求
    协调者向所有参与者节点发出Commit请求

  2. 事务提交
    参与者接收到Commit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。

  3. 反馈事务提交结果
    参与者在完成事务提交之后,向协调者发送Ack消息

  4. 完成事务
    协调者接收到所有参与者反馈的Ack消息后,完成事务。

B. 中断事务
假如任何一个参与者向协调者反馈了No相应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。

  1. 发送回滚请求
    协调者向所有参与者节点发出RollBack请求

  2. 事务回滚
    参与者接收到Rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。

  3. 反馈事务回滚结果
    参与者在完成事务回滚之后,向协调者发送Ack消息

  4. 中断事务
    协调者接收到所有参与者反馈的Ack消息后,完成事务中断。

二阶段提交协议的优缺点
优点:原理简单,实现方便
缺点:

  1. 同步阻塞
    在二阶段提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,也就是说,各个参与者在等待其他参与者响应的过程中,将无法进行其他任何操作。

  2. 单点问题
    协调者的角色在整个二阶段提交协议中起到了非常重要的作用。一旦协调者出现问题,那么整个二阶段提交流程将无法运转,
    更为严重的是,如果协调者是在阶段二中出现问题的话,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作。

  3. 数据不一致
    在阶段二时,当协调者向所有的参与者发送Commit请求之后,发生了局部网络异常或者是协调者尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求。
    于是,这部分收到了Commit请求的参与者就会进行事务的提交,而其他没有收到Commit请求的参与者则无法进行事务提交,于是整个分布式系统便出现了数据不一致现象。

  4. 太过保守
    二阶段提交协议没有设计较为完善的容错机制,任何一个节点的失败都会导致整个事务的失败。

3PC

3PC(Three-Phase commits):3阶段提交,将二阶段提交协议的"提交事务请求"过程一分为二。

阶段一:CanCommit

  1. 事务询问
    协调者向所有的参与者发送一个包含事务内容的CanCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。

  2. 各参与者向协调者反馈事务访问的响应。
    参与者在接收到来自协调者的CanCommit请求后,正常情况下,如果其自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No响应。

阶段二:PreCommit
在阶段二中,协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作,正常情况下,包含两种可能。

A. 执行事务预提交
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务预提交。

  1. 发送预提交请求
    协调者向所有参与者节点发出preCommit的请求,并进入Prepared阶段。

  2. 事务预提交
    参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。

  3. 各参与者向协调者反馈事务执行的响应
    如果参与者成功执行了事务操作,那么就会反馈给协调者Ack响应,同时等待最终的指令:提交或中止。

B. 中断事务
假如任何一个参与者向协调者反馈了No相应,或者再等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。

  1. 发送中断请求
    协调者向所有参与者节点发出abort请求

  2. 中断事务
    无论是收到来自协调者的abort请求,或者是在等待协调者请求过程中出现超时,参与者都会中断事务。

阶段三:doCommit
该阶段将进行真正的事务提交,会存在以下两种可能的情况

A. 执行提交

  1. 发送提交请求
    进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的Ack相应,那么它将从"预提交"状态转换到"提交"状态,并向所有的参与者发送doCommit请求。

  2. 事务提交
    参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。

  3. 反馈事务提交结果
    参与者在完成事务提交之后,向协调者发送Ack消息

  4. 完成事务
    协调者接收到所有参与者反馈的Ack消息后,完成事务。

B. 中断事务
进入这一阶段,假设协调者处于正常工作状态,并且有任何一个参与者向协调者反馈了No相应,或者再等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。

  1. 发送中断请求
    协调者向所有参与者节点发送abort请求

  2. 事务回滚
    参与者接收到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。

  3. 反馈事务回滚结果
    参与者在完成事务回滚之后,向协调者发送Ack消息

  4. 中断事务
    协调者接收到所有参与者反馈的Ack消息后,事务中断

一旦进入阶段三,可能会存在以下两种故障。

  • 协调者出现问题。

  • 协调者和参与者之间的网络出现故障。

无论出现哪种情况,最终都会导致参与者无法及时接收到来自协调者的doCommit或是abort请求,针对这样的异常情况,参与者都会在等待超时之后,继续进行事务提交。

优缺点:
优点
相较于二阶段提交协议,三阶段提交协议最大的优点就是降低了参与者的阻塞反馈,并且能够在出现单点故障后继续达成一致。
缺点
在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。

Paxos算法:目前公认的解决分布式一致性问题最有效的算法之一。

Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,
并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。

在一个Paxos系统中,首先将所有节点划分为Proposer(提议者),Acceptor(接受者),和Learner(学习者)(注意:每个节点都可以身兼数职)

  • 每个参与者以任意的速度执行,可能会因为出错而停止,也可能会重启。同时,即使一个提案被选定后,所有的参与者也都有可能失败或重启,因此除非那些失败或重启的参与者可以记录某些信息,否则将无法确定最终的值
  • 消息在传输过程中可能会出现不可预知的延迟,也可能会重复或丢失,但是消息不会被损坏,即消息内容不会被篡改

一个Acceptor可能会收到来自Proposer的两种请求,分别是Prepare请求和Accept请求,对这两类请求做出的响应的条件分别如下。

  • Prepare请求:Acceptor可以在任何时候响应一个Prepare请求。
  • Accept请求:在不违背Accept现有承诺的前提下,可以任意响应Accept请求。

一个完整的Paxos算法流程分为三个阶段:

  1. Prepare准备阶段
    Proposer向多个Acceptor发出Propose请求Promise(承诺)
    (Proposer选择一个提案编号Mn,然后向Acceptor的某个超过半数的子集成员发送编号为Mn的Prepare请求)
    Acceptor针对收到的Propose请求进行Promise(承诺)
    (如果一个Acceptor收到一个编号为Mn的Prepare请求,且编号Mn大于该Acceptor已经响应的所有Prepare请求的编号,那么它就会将它已经批准过的最大编号的提案作为响应反馈给Proposer,同时该Acceptor会承诺不再批准任何编号小于Mn的提案)

  2. Accept接受阶段
    Proposer收到多数Acceptor承诺的Promise后,向Acceptor发出Propose请求
    (如果Proposer收到来自半数以上的Acceptor对于其发出的编号为Mn的Prepare请求的响应,那么它就会发送一个针对[Mn,Vn]提案的Accept请求给Acceptor。注意,Vn的值就是收到的响应中编号最大的提案的值,如果响应中不包含任何提案,那么它就是任意值。)
    Acceptor针对收到的Propose请求进行Accept处理
    (如果Acceptor收到这个针对[Mn,Vn]提案的Accept请求,只要该Acceptor尚未对编号大于Mn的Prepare请求做出响应,它就可以通过这个提案。)

  3. Learn学习阶段
    Proposer将形成的决议发送给所有Learners

Paxos算法流程

  1. Prepare:Proposer生成全局唯一且递增的Proposal ID,向所有Acceptor发送Propose请求,这里无需携带提案内容,只携带Proposal ID即可

  2. Promise:Acceptor收到Propose请求后,做出”两个承诺,一个应答“

  • 不再接受Proposal ID小于等于(注意:这里是<=)当前请求的Propose请求。
  • 不再接受Proposal ID小于(注意:这里是<)当前请求的Accept请求。
  • 不违背以前做出的承诺下,回复已经Accept过的提案中Proposal ID最大的那个提案的Value和Proposal ID,没有则返回空值。
  1. Propose:Proposer收到多数Acceptor的Promise应答后,从应答中选择Proposal ID最大的提案的Value,作为本次要发起的提案。如果所有应答的提案Value均为空值,则可以自己随意决定提案Value,然后携带当前ProposalID,向所有Acceptor发送Propose请求。

  2. Accep:Acceptor收到Propose请求后,在不违背自己之前做出的承诺下,接受并持久化当前Proposal ID和提案Value。

  3. Learn:Proposer收到多数Acceptor的Accept后,决议形成,将形成的决议发送给所有Learner。

Paxos算法极端情况下的缺陷:
在网络复杂的情况下,一个应用Paxos算法的分布式系统,可能很久无法收敛,甚至陷入活锁的情况。

为了保证Paxos算法流程的可持续性,以免陷入死循环,就必须选择一个主Proposer,并规定只有主Proposer才能提出议案。

ZAB协议

ZAB(Zookeeper Atomic Broadcast),Zookeeper原子消息广播协议,Zookeeper数据一致性的核心算法
ZAB协议是为ZK专门设计的一种支持崩溃服务恢复的原子广播协议。并未完全采用Paxos算法,是特别为Zookeeper设计的崩溃可恢复的原子消息广播算法。

基于该协议,Zookeeper实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性。
具体的,Zookeeper使用一个单一的主线程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上去。
ZAB协议的这个主备模型架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更,因此能够很好的处理客户端大量的并发请求。

同时,ZAB协议必须能够保证一个全局的变更序列被顺序应用,也就是说,ZAB协议需要保证如果一个状态变更已经被处理了,那么所有其依赖的状态变更都应该已经被提前处理掉了。

最后,ZAB协议需要做到在当前主进程出现崩溃退出或重启现象时,Zookeeper依然能够正常工作。

ZAB协议的核心:
所有的事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,而余下的其他服务器则成为Follower服务器。
Leader服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器。
之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交。

ZAB协议的两种基本模式:崩溃恢复、消息广播

ZAB协议介绍

ZAB协议包括两种基本的模式,分别是崩溃恢复和消息广播。

  1. 当整个服务框架在启动过程中,或者是当Leader服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB协议就会进入恢复模式并选举产生新的Leader服务器。
    当选举产生了新的leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式。
    其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据状态保持一致。

  2. 当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。
    当一台同样遵守ZAB协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。

  3. 由于Zookeeper只允许唯一的一个Leader服务器来进行事务请求的处理。Leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;
    如果集群中的其他机器接收到客户端的事务请求,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。

  4. 当Leader服务器出现崩溃退出或机器重启,亦或者是集群中已经不存在过半的服务器与该Leader服务器保持正常通信时,
    那么在重新开始新一轮的原子广播事务操作之前,所有进程首先会使用崩溃恢复协议来使彼此达到一个一致的状态,于是整个ZAB流程就会从消息广播模式进入到崩溃恢复模式。

  5. 一个机器要成为新的Leader,必须获得过半进程的支持,同时由于每个进程都有可能会崩溃,因此,在ZAB协议运行过程中,前后会出现多个Leader,并且每个进程也有可能会多次成为Leader。
    进入崩溃恢复模式后,只要集群中存在过半的服务器能够彼此进行正常通信,那么就可以产生一个新的Leader并重新进入消息广播模式。

ZAB与Paxos算法的联系和区别

联系:

  1. 两者都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程的运行。
  2. Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交。
  3. 在ZAB协议中,每个Proposal中都包含了一个epoch值,用来代表当前的Leader周期,在Paxos算法中,同样存在这样一个标识,只是名字变成了Ballot。

在Paxos算法中,一个新选举产生的主进程会进行两个阶段的工作。第一阶段被称为读阶段,在这个阶段中,这个新的主进程会通过和所有其他进程进行通信的方式来收集上一个主进程提出的提案,并将它们提交。第二阶段被称为写阶段,在这个阶段,当前主进程开始提出它自己的提案。
在Paxos算法设计的基础上,ZAB协议额外添加了一个同步阶段。在同步阶段之前,ZAB协议也存在一个和Paxos算法中的读阶段非常类似的过程,称为发现(Discovery)阶段。
在同步阶段中,新的Leader会确保存在过半的Follower已经提交了之前Leader周期中的所有事务Proposal。
这一同步阶段的引入,能够有效地保证Leader在新的周期中提出事务Proposal之前,所有的进程都已经完成了对之前所有事务Proposal的提交。
一旦完成同步阶段后,那么ZAB就会执行和Paxos算法类似的写阶段。

ZAB协议和Paxos算法的本质区别在于,两者的设计目标不太一样。
ZAB协议主要用于构建一个高可用的分布式数据主备系统。
Paxos算法则是用于构建一个分布式的一致性状态机系统。

Raft算法

参考原文一文搞懂Raft算法https://www.cnblogs.com/xybaby/p/10124083.html
下文只做笔者个人的一些总结摘要。

Raft是工程上使用较为广泛的强一致性,去中心化,高可用的分布式协议。Raft是一个共识算法,所谓共识,就是多个节点对某个事情达成一致的看法,即使是在部分节点故障、网络延时,网络分割的情况下。
在分布式系统中,共识算法更多用于提高系统的容错性,比如分布式存储中的复制集(replication)。

Raft的工作原理

Raft会选举出Leader,leader完全负责replicated log的管理。
Leader负责接受所有客户端更新请求,然后复制到follower节点,并在"安全"的时候执行这些请求。
如果Leader故障,followers会重新选举出新的leader。

由此可得Raft涉及的两个子问题:leader选举、log复制。

leader选举

raft协议中,一个节点任一时刻处于以下三个状态之一:leader、follower、candidate

所有节点启动时都是follower状态,在一段时间内如果没有收到来自leader的心跳,从follower切换到candidate,发起选举,如果收到majority的造成票(含自己的一票)则切换到leader状态,如果发现其他节点比自己更新,则主动切换到follower。

总之,系统中最多只有一个leader,如果在一段时间里发现没有leader,则大家通过选举-投票选出leader。leader会不停的给follower发心跳消息,表明自己的存活状态。
如果leader故障,那么follower会转换成candidate,重新选出leader。

从上面可以看出,哪个节点做leader是大家投票选举出来的,每个leader工作一段时间,然后选出新的leader继续负责。
这跟民主社会的选举很像,每一届新的履职期称之为一届任期,在raft协议中,也是这样的,对应的术语叫term。
term(任期)以 election(选举)开始,然后就是一段或长或短的稳定工作期(normal Operation)同时,任期是递增的,这就充当了逻辑时钟的作用;
若没有选举出leader就结束了,会发起新的选举。

如果follower在election timeout内没有收到来自leader的心跳,(也许此时还没有选出leader,大家都在等;也许leader挂了;也许只是leader与该follower之间网络故障),则会主动发起选举。
选举的过程如下:

  1. 增加节点本地的 current term ,切换到candidate状态

  2. 投自己一票

  3. 并行给其他节点发送 RequestVote RPCs

  4. 等待其他节点的回复
    在这个过程中,根据来自其他节点的消息,可能出现三种结果

    • 收到majority的投票(含自己的一票),则赢得选举,成为leader
    • 被告知别人已当选,那么自行切换到follower
    • 一段时间内没有收到majority投票,则保持candidate状态,重新发出选举

    第一种情况,赢得了选举之后,新的leader会立刻给所有节点发消息,广而告之,避免其余节点触发新的选举。在这里,先回到投票者的视角,投票者如何决定是否给一个选举请求投票呢,有以下约束:
    + 在任一任期内,单个节点最多只能投一票
    + 候选人知道的信息不能比自己的少(这一部分,后面介绍log replication和safety的时候会详细介绍)
    + first-come-first-served 先来先得

    第二种情况,比如有三个节点A B C。A B同时发起选举,而A的选举消息先到达C,C给A投了一票,当B的消息到达C时,已经不能满足上面提到的第一个约束,即C不会给B投票,而A和B显然都不会给对方投票。
    A胜出之后,会给B,C发心跳消息,节点B发现节点A的term不低于自己的term,知道有已经有Leader了,于是转换成follower。

    第三种情况,没有任何节点获得majority投票,即出现平票(split vote)情况。
    如果出现平票(split vote)的情况,那么就延长了系统不可用的时间(没有leader是不能处理客户端写请求的),因此raft引入了randomized election timeouts来尽量避免平票情况。
    同时,leader-based 共识算法中,节点的数目都是奇数个,尽量保证majority的出现

log复制

当有了leader,系统应该进入对外工作期了。客户端的一切请求来发送到leader,leader来调度这些并发请求的顺序,并且保证leader与followers状态的一致性。
raft中的做法是,将这些请求以及执行顺序告知followers。leader和followers以相同的顺序来执行这些请求,保证状态一致。

共识算法的实现一般是基于复制状态机(Replicated state machines),何为复制状态机:
简单来说:相同的初识状态 + 相同的输入 = 相同的结束状态。引文中有一个很重要的词deterministic,就是说不同节点要以相同且确定性的函数来处理输入,而不要引入一下不确定的值,比如本地时间等。
如何保证所有节点 get the same inputs in the same order,使用replicated log是一个很不错的注意,log具有持久化、保序的特点,是大多数分布式系统的基石。

因此,可以这么说,在raft中,leader将客户端请求(command)封装到一个个log entry,将这些log entries复制(replicate)到所有follower节点,然后大家按相同顺序应用(apply)log entry中的command,则状态肯定是一致的。

请求完整流程如下:
当系统(leader)收到一个来自客户端的写请求,到返回给客户端,整个过程从leader的视角来看会经历以下步骤:

  1. leader append log entry
  2. leader issue AppendEntries RPC in parallel
  3. leader wait for majority response
  4. leader apply entry to state machine
  5. leader reply to client
  6. leader notify follower apply log
    可以看到日志的提交过程有点类似两阶段提交(2PC),不过与2PC的区别在于,leader只需要大多数(majority)节点的回复即可,这样只要超过一半节点处于工作状态则系统就是可用的。

raft算法为了保证高可用,并不是强一致性,而是最终一致性,leader会不断尝试给follower发log entries,直到所有节点的log entries都相同。

在上面的流程中,leader只需要日志被复制到大多数节点即可向客户端返回,一旦向客户端返回成功消息,那么系统就必须保证log(其实是log所包含的command)在任何异常的情况下都不会发生回滚。
这里有两个词:commit(committed),apply(applied),前者是指日志被复制到了大多数节点后日志的状态;而后者则是节点将日志应用到状态机,真正影响到节点状态。

safety

衡量一个分布式算法,有许多属性,如
safety:nothing bad happens
liveness:something good eventually happens

在任何系统模型下,都需要满足safety属性,即在任何情况下,系统都不能出现不可逆的错误,也不能向客户端返回错误的内容。
raft保证被复制到大多数节点的日志不会被回滚,就是safety属性。而raft最终会让所有节点状态一致,这属于liveness。

为了在任何异常情况下系统不出错,即满足safety属性,对leader election,log replication两个子问题有诸多约束

leader election约束:

  • 同一任期内最多只能投一票,先来先得
  • 选举人必须比自己知道的更多(比较term,log index)

log replication约束:

  • 一个log被复制到大多数节点,就是committed,保证不会回滚
  • leader一定包含最新的committed log,因此leader只会追加日志,不会删除覆盖日志
  • 不同节点,某个位置上日志相同,那么这个位置之前的所有日志一定是相同的
  • Raft never commits log entries from previous terms by counting replicas.

参考文献

《从PAXOS到ZOOKEEPER分布式一致性原理与实践》

posted @ 2021-04-20 09:53  cos晓风残月  阅读(510)  评论(0编辑  收藏  举报