Zookeeper——一致性算法原理与实现

Zookeeper——一致性算法原理与实现,第1张

Zookeeper——一致性算法原理与实现 摘要

本博文主要是的介绍ZAB协议相关原理,同时也将介绍Zookeeper中使用ZAB协议进行集群一致性分析。

一、二阶段提交三阶段提交算法原理

ZooKeeper 在分布式系统环境中主要解决的是分布式一致性问题。为什么会发生数据不一致的问题呢?是因为当网络集群处理来自客户端的请求时,其中的事务性会导致服务器上数据状态的变更。为了保证数据变更请求在整个分布式环境下正确地执行,不会发生异常中断,从而导致请求在某一台服务器执行失败而在集群中其他服务器上执行成功,在整个分布式系统处理数据变更请求的过程中,引入了分布式事务的概念。

1.1 分布式事务

对于事务 *** 作我们并不陌生,最为熟悉的就是数据库事务 *** 作。当多个线程对数据库中的同一个信息进行修改的时候,为保证数据的原子性、一致性、隔离性、持久性,需要进行本地事务性 *** 作。而在分布式的网络环境下,也会面临多个客户端的数据请求服务。在处理数据变更的时候,需要保证在分布式环境下的数据的正确完整,因此在分布式环境下也引入了分布式事务。

1.2 二阶段提交

二阶段提交(Two-phase Commit)简称 2PC ,它是一种实现分布式事务的算法。二阶段提交算法可以保证分布在不同网络节点上的程序或服务按照事务性的方式进行调用。

1.2.1 二阶段提交底层实现

二阶段提交的底层实现主要分成两个阶段,分别是询问阶段和提交阶段。

整个集群服务器被分成一台协调服务器,集群中的其他服务器是被协调的服务器。在二阶段算法的询问阶段,分布式集群服务在接收到来自客户端的请求的时候,首先会通过协调者服务器,针对本次请求能否正常执行向集群中参与处理的服务器发起询问请求。集群服务器在接收到请求的时候,会在本地机器上执行会话 *** 作,并记录执行的相关日志信息,最后将结果返回给协调服务器。

在协调服务器接收到来自集群中其他服务器的反馈信息后,会对信息进行统计。如果集群中的全部机器都能正确执行客户端发送的会话请求,那么协调者服务器就会再次向这些服务器发送提交命令。在集群服务器接收到协调服务器的提交指令后,会根据之前处理该条会话 *** 作的日志记录在本地提交 *** 作,并最终完成数据的修改。

虽然二阶段提交可以有效地保证客户端会话在分布式集群中的事务性,但是该算法自身也有很多问题,主要可以归纳为以下几点:效率问题、单点故障、异常中断。

  • 性能问题:首先,我们先来介绍一下性能问题。如我们上面介绍的二阶段算法,在数据提交的过程中,所有参与处理的服务器都处于阻塞状态,如果其他线程想访问临界区的资源,需要等待该条会话请求在本地执行完成后释放临界区资源。因此,采用二阶段提交算法也会降低程序并发执行的效率。
  • 单点问题:此外,还会发生单点问题。单点问题也叫作单点服务器故障问题,它指的是当作为分布式集群系统的调度服务器发生故障时,整个集群因为缺少协调者而无法进行二阶段提交算法。单点问题也是二阶段提交最大的缺点,因此使用二阶段提交算法的时候通常都会进行一些改良,以满足对系统稳定性的要求。
  • 异常中断:异常中断问题指的是当统计集群中的服务器可以进行事务 *** 作时,协调服务器会向这些处理事务 *** 作的服务器发送 commit 提交请求。如果在这个过程中,其中的一台或几台服务器发生网络故障,无法接收到来自协调服务器的提交请求,导致这些服务器无法完成最终的数据变更,就会造成整个分布式集群出现数据不一致的情况。
1.3 三阶段提交

三阶段提交(Three-phase commit)简称 3PC , 其实是在二阶段算法的基础上进行了优化和改进。如下图所示,在整个三阶段提交的过程中,相比二阶段提交,增加了预提交阶段。

1.3.1 三阶段提交底层实现

预提交阶段:为了保证事务性 *** 作的稳定性,同时避免二阶段提交中因为网络原因造成数据不一致等问题,完成提交准备阶段后,集群中的服务器已经为请求 *** 作做好了准备,协调服务器会向参与的服务器发送预提交请求。集群服务器在接收到预提交请求后,在本地执行事务 *** 作,并将执行结果存储到本地事务日志中,并对该条事务日志进行锁定处理。

提交阶段:在处理完预提交阶段后,集群服务器会返回执行结果到协调服务器,最终,协调服务器会根据返回的结果来判断是否继续执行 *** 作。如果所有参与者服务器返回的都是可以执行事务 *** 作,协调者服务器就会再次发送提交请求到参与者服务器。参与者服务器在接收到来自协调者服务器的提交请求后,在本地正式提交该条事务 *** 作,并在完成事务 *** 作后关闭该条会话处理线程、释放系统资源。当参与者服务器执行完相关的 *** 作时,会再次向协调服务器发送执行结果信息。

协调者服务器在接收到返回的状态信息后会进行处理,如果全部参与者服务器都正确执行,并返回 yes 等状态信息,整个事务性会话请求在服务端的 *** 作就结束了。如果在接收到的信息中,有参与者服务器没有正确执行,则协调者服务器会再次向参与者服务器发送 rollback 回滚事务 *** 作请求,整个集群就退回到之前的状态,这样就避免了数据不一致的问题。

二、ZAB 协议算法

ZooKeeper 最核心的作用就是保证分布式系统的数据一致性,而无论是处理来自客户端的会话请求时,还是集群 Leader 节点发生重新选举时,都会产生数据不一致的情况。为了解决这个问题,ZooKeeper 采用了 ZAB 协议算法。

ZAB 协议算法(Zookeeper Atomic Broadcast ,Zookeeper 原子广播协议)是ZooKeeper 专门设计用来解决集群最终一致性问题的算法,它的两个核心功能点是崩溃恢复和原子广播协议。

在整个 ZAB 协议的底层实现中,ZooKeeper 集群主要采用主从模式的系统架构方式来保证 ZooKeeper 集群系统的一致性。整个实现过程如下图所示,当接收到来自客户端的事务性会话请求后,系统集群采用主服务器来处理该条会话请求,经过主服务器处理的结果会通过网络发送给集群中其他从节点服务器进行数据同步 *** 作。

以ZooKeeper 集群为例,这个 *** 作过程可以概括为:当 ZooKeeper 集群接收到来自客户端的事务性的会话请求后,集群中的其他 Follow 角色服务器会将该请求转发给 Leader 角色服务器进行处理。当 Leader 节点服务器在处理完该条会话请求后,会将结果通过 *** 作日志的方式同步给集群中的 Follow 角色服务器。然后 Follow 角色服务器根据接收到的 *** 作日志,在本地执行相关的数据处理 *** 作,最终完成整个 ZooKeeper 集群对客户端会话的处理工作。

三、崩溃恢复

我们不难看出整个 ZooKeeper 集群处理客户端会话的核心点在一台 Leader 服务器上。所有的业务处理和数据同步 *** 作都要靠 Leader 服务器完成。目前介绍的 ZooKeeper 架构方式而言,极易产生单点问题,即当集群中的 Leader 发生故障的时候,整个集群就会因为缺少 Leader 服务器而无法处理来自客户端的事务性的会话请求。因此,为了解决这个问题。在 ZAB 协议中也设置了处理该问题的崩溃恢复机制。

崩溃恢复机制是保证 ZooKeeper 集群服务高可用的关键。触发 ZooKeeper 集群执行崩溃恢复的事件是集群中的 Leader 节点服务器发生了异常而无法工作,于是 Follow 服务器会通过投票来决定是否选出新的 Leader 节点服务器。

3.1 投票过程如下

当崩溃恢复机制开始的时候,整个 ZooKeeper 集群的每台 Follow 服务器会发起投票,并同步给集群中的其他 Follow 服务器。在接收到来自集群中的其他 Follow 服务器的投票信息后,集群中的每个 Follow 服务器都会与自身的投票信息进行对比,如果判断新的投票信息更合适,则采用新的投票信息作为自己的投票信息。在集群中的投票信息还没有达到超过半数原则的情况下,再进行新一轮的投票,最终当整个 ZooKeeper 集群中的 Follow 服务器超过半数投出的结果相同的时候,就会产生新的 Leader 服务器。

3.2 选票结构

Leader 节点的过程后,我们来看一下整个投票阶段中的投票信息具有怎样的结构。以 Fast Leader Election 选举的实现方式来讲,如下图所示,一个选票的整体结果可以分为一下六个部分:

  • logicClock:用来记录服务器的投票轮次。logicClock 会从 1 开始计数,每当该台服务经过一轮投票后,logicClock 的数值就会加 1 。
  • state:用来标记当前服务器的状态。在 ZooKeeper 集群中一台服务器具有 LOOKING、FOLLOWING、LEADERING、OBSERVING 这四种状态。
  • self_id:用来表示当前服务器的 ID 信息,该字段在 ZooKeeper 集群中主要用来作为服务器的身份标识符。
  • self_zxid: 当前服务器上所保存的数据的最大事务 ID ,从 0 开始计数。
  • vote_id:投票要被推举的服务器的唯一 ID 。
  • vote_zxid:被推举的服务器上所保存的数据的最大事务 ID ,从 0 开始计数。

当 ZooKeeper 集群需要重新选举出新的 Leader 服务器的时候,就会根据上面介绍的投票信息内容进行对比,以找出最适合的服务器。

3.3 选票筛选

当一台 Follow 服务器接收到网络中的其他 Follow 服务器的投票信息后,是如何进行对比来更新自己的投票信息的。Follow 服务器进行选票对比的过程,如下图所示。

首先,会对比 logicClock 服务器的投票轮次,当 logicClock 相同时,表明两张选票处于相同的投票阶段,并进入下一阶段,否则跳过。接下来再对比 vote_zxid 被选举的服务器 ID 信息,若接收到的外部投票信息中的 vote_zxid 字段较大,则将自己的票中的 vote_zxid 与 vote_myid 更新为收到的票中的 vote_zxid 与 vote_myid ,并广播出去。要是对比的结果相同,则继续对比 vote_myid 被选举服务器上所保存的最大事务 ID ,若外部投票的 vote_myid 比较大,则将自己的票中的 vote_myid 更新为收到的票中的 vote_myid 。 经过这些对比和替换后,最终该台 Follow 服务器会产生新的投票信息,并在下一轮的投票中发送到 ZooKeeper 集群中。 

四、消息广播

在 Leader 节点服务器处理请求后,需要通知集群中的其他角色服务器进行数据同步。ZooKeeper 集群采用消息广播的方式发送通知。

ZooKeeper集群使用原子广播协议进行消息发送,如下图所示。

当要在集群中的其他角色服务器进行数据同步的时候,Leader 服务器将该 *** 作过程封装成一个 Proposal 提交事务,并将其发送给集群中其他需要进行数据同步的服务器。当这些服务器接收到 Leader 服务器的数据同步事务后,会将该条事务能否在本地正常执行的结果反馈给 Leader 服务器,Leader 服务器在接收到其他 Follow 服务器的反馈信息后进行统计,判断是否在集群中执行本次事务 *** 作。 

五、Raft算法原理

Raft是一个用于管理日志一致性的协议。它将分布式一致性分解为多个子问题:Leader选举(Leader election)、日志复制(Log replication)、安全性(Safety)、日志压缩(Log compaction)等。同时,Raft算法使用了更强的假设来减少了需要考虑的状态,使之变的易于理解和实现。

Raft将系统中的角色分为领导者(Leader)、跟从者(Follower)和候选者(Candidate):

  • Leader:接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。

  • Follower:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。

  • Candidate:Leader选举过程中的临时角色。

Raft要求系统在任意时刻最多只有一个Leader,正常工作期间只有Leader和Followers。Raft算法将时间分为一个个的任期(term),每一个term的开始都是Leader选举。在成功选举Leader之后,Leader会在整个term内管理整个集群。如果Leader选举失败,该term就会因为没有Leader而结束。

Term:Raft 算法将时间划分成为任意不同长度的任期(term)。任期用连续的数字进行表示。每一个任期的开始都是一次选举(election),一个或多个候选人会试图成为领导人。如果一个候选人赢得了选举,它就会在该任期的剩余时间担任领导人。在某些情况下,选票会被瓜分,有可能没有选出领导人,那么,将会开始另一个任期,并且立刻开始下一次选举。Raft 算法保证在给定的一个任期最多只有一个领导人。

Raft 算法中服务器节点之间通信使用远程过程调用(RPC),并且基本的一致性算法只需要两种类型的 RPC,为了在服务器之间传输快照增加了第三种 RPC。

  • RequestVote RPC:候选人在选举期间发起。

  • AppendEntries RPC:领导人发起的一种心跳机制,复制日志也在该命令中完成。

  • InstallSnapshot RPC: 领导者使用该RPC来发送快照给太落后的追随者。

5.1 Leader选举

Raft 使用心跳(heartbeat)触发Leader选举。当服务器启动时,初始化为Follower。Leader向所有Followers周期性发送heartbeat。如果Follower在选举超时时间内没有收到Leader的heartbeat,就会等待一段随机的时间后发起一次Leader选举。

每一个follower都有一个时钟,是一个随机的值,表示的是follower等待成为leader的时间,谁的时钟先跑完,则发起leader选举。

Follower将其当前term加一然后转换为Candidate。它首先给自己投票并且给集群中的其他服务器发送 RequestVote RPC。结果有以下三种情况:

  • 赢得了多数的选票,成功选举为Leader;

  • 收到了Leader的消息,表示有其它服务器已经抢先当选了Leader;

  • 没有服务器赢得多数的选票,Leader选举失败,等待选举时间超时后发起下一次选举。

在Raft协议中,所有的日志条目都只会从Leader节点往Follower节点写入,且Leader节点上的日志只会增加,绝对不会删除或者覆盖。

这意味着Leader节点必须包含所有已经提交的日志,即能被选举为Leader的节点一定需要包含所有的已经提交的日志。因为日志只会从Leader向Follower传输,所以如果被选举出的Leader缺少已经Commit的日志,那么这些已经提交的日志就会丢失,显然这是不符合要求的。

这就是Leader选举的限制:能被选举成为Leader的节点,一定包含了所有已经提交的日志条目。

5.2 日志复制(保证数据一致性) 5.2.1 日志复制的过程

Leader选出后,就开始接收客户端的请求。Leader把请求作为日志条目(Log entries)加入到它的日志中,然后并行的向其他服务器发起 AppendEntries RPC复制日志条目。当这条日志被复制到大多数服务器上,Leader将这条日志应用到它的状态机并向客户端返回执行结果。

  • 客户端的每一个请求都包含被复制状态机执行的指令。

  • leader把这个指令作为一条新的日志条目添加到日志中,然后并行发起 RPC 给其他的服务器,让他们复制这条信息。

  • 假如这条日志被安全的复制,领导人就应用这条日志到自己的状态机中,并返回给客户端。

  • 如果 follower 宕机或者运行缓慢或者丢包,leader会不断的重试,直到所有的 follower 最终都复制了所有的日志条目。

简而言之,leader选举的过程是:1、增加term号;2、给自己投票;3、重置选举超时计时器;4、发送请求投票的RPC给其它节点。

5.2.2 日志的组成

日志由有序编号(log index)的日志条目组成。每个日志条目包含它被创建时的任期号(term)和用于状态机执行的命令。如果一个日志条目被复制到大多数服务器上,就被认为可以提交(commit)了。

上图显示,共有 8 条日志,提交了 7 条。提交的日志都将通过状态机持久化到磁盘,防止宕机。

5.2.3 日志的一致性

日志复制的两条保证

  • 如果不同日志中的两个条目有着相同的索引和任期号,则它们所存储的命令是相同的(原因:leader 最多在一个任期里的一个日志索引位置创建一条日志条目,日志条目在日志的位置从来不会改变)。
  • 如果不同日志中的两个条目有着相同的索引和任期号,则它们之前的所有条目都是完全一样的(原因:每次 RPC 发送附加日志时,leader 会把这条日志条目的前面的日志的下标和任期号一起发送给 follower,如果 follower 发现和自己的日志不匹配,那么就拒绝接受这条日志,这个称之为一致性检查)。

日志的不正常情况

  • 一般情况下,Leader和Followers的日志保持一致,因此 AppendEntries 一致性检查通常不会失败。然而,Leader崩溃可能会导致日志不一致:旧的Leader可能没有完全复制完日志中的所有条目。

下图阐述了一些Followers可能和新的Leader日志不同的情况。一个Follower可能会丢失掉Leader上的一些条目,也有可能包含一些Leader没有的条目,也有可能两者都会发生。丢失的或者多出来的条目可能会持续多个任期。

如何保证日志的正常复制

  • Leader通过强制Followers复制它的日志来处理日志的不一致,Followers上的不一致的日志会被Leader的日志覆盖。Leader为了使Followers的日志同自己的一致,Leader需要找到Followers同它的日志一致的地方,然后覆盖Followers在该位置之后的条目。
  •   具体的 *** 作是:Leader会从后往前试,每次AppendEntries失败后尝试前一个日志条目,直到成功找到每个Follower的日志一致位置点(基于上述的两条保证),然后向后逐条覆盖Followers在该位置之后的条目。

总结一下就是:当 leader 和 follower 日志冲突的时候,leader 将校验 follower 最后一条日志是否和 leader 匹配,如果不匹配,将递减查询,直到匹配,匹配后,删除冲突的日志。这样就实现了主从日志的一致性。

5.3 安全性
  • 拥有最新的已提交的log entry的Follower才有资格成为leader。

  • Leader只能推进commit index来提交当前term的已经复制到大多数服务器上的日志,旧term日志的提交要等到提交当前term的日志来间接提交(log index 小于 commit index的日志被间接提交)。

5.4 日志压缩

在实际的系统中,不能让日志无限增长,否则系统重启时需要花很长的时间进行回放,从而影响可用性。Raft采用对整个系统进行snapshot来解决,snapshot之前的日志都可以丢弃(以前的数据已经落盘了)。

每个副本独立的对自己的系统状态进行snapshot,并且只能对已经提交的日志记录进行snapshot。

Snapshot中包含以下内容

  • 日志元数据,最后一条已提交的 log entry的 log index和term。这两个值在snapshot之后的第一条log entry的AppendEntries RPC的完整性检查的时候会被用上。

  • 系统当前状态。

当Leader要发给某个日志落后太多的Follower的log entry被丢弃,Leader会将snapshot发给Follower。或者当新加进一台机器时,也会发送snapshot给它。发送snapshot使用InstalledSnapshot RPC。

做snapshot既不要做的太频繁,否则消耗磁盘带宽, 也不要做的太不频繁,否则一旦节点重启需要回放大量日志,影响可用性。推荐当日志达到某个固定的大小做一次snapshot。

做一次snapshot可能耗时过长,会影响正常日志同步。可以通过使用copy-on-write技术避免snapshot过程影响正常日志同步。

5.5 成员变更

我们先将成员变更请求当成普通的写请求,由领导者得到多数节点响应后,每个节点提交成员变更日志,将从旧成员配置(Cold)切换到新成员配置(Cnew)。但每个节点提交成员变更日志的时刻可能不同,这将造成各个服务器切换配置的时刻也不同,这就有可能选出两个领导者,破坏安全性。

考虑以下这种情况:集群配额从 3 台机器变成了 5 台,可能存在这样的一个时间点,两个不同的领导者在同一个任期里都可以被选举成功(双主问题),一个是通过旧的配置,一个通过新的配置。简而言之,成员变更存在的问题是增加或者减少的成员太多了,导致旧成员组和新成员组没有交集,因此出现了双主。

5.5.1 解决方案之一阶段成员变更

Raft解决方法是每次成员变更只允许增加或删除一个成员(如果要变更多个成员,连续变更多次)。

六、Paxos算法原理

在分布式一致性问题的解决方案中,Paxos 算法可以说是目前最为优秀的。很多方案,包括我们学习的 ZooKeeper 的 ZAB 协议算法都是在其基础上改进和演变过来的。

Paxos 算法是基于消息传递的分布式一致性算法,很多大型的网络技术公司和开源框架都采用Paxos 算法作为其各自的底层解决方案,比如 Chubby 、 Megastore 以及 MySQL Group Replication 。 Paxos 算法运行在服务器发生宕机故障的时候,能够保证数据的完整性,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复,保证服务的高可用性。

6.1 Paxos算法底层实现

保证分布式系统下数据的一致性 *** 作,本质是协调运行在不同的网络服务器上的线程服务,使这些服务就某一个特定的数据执行一致性的变更 *** 作。在整个 Paxos 算法的实现过程中,将参与算法的集群中的全部服务器,分成三种角色:提议者(Proposer)、决策者(Acceptor)、决策学习者(Learner)。

先来看看三种角色的具体分工。

  • 提议者(Proposer):提出提案(Proposal)。Proposal 信息包括提案编号(Proposal ID)和提议的值(Value)。
  • 决策者(Acceptor):参与决策,回应 Proposers 的提案。收到 Proposal 后可以接受提案,若 Proposal 获得超过半数 Acceptors 的许可,则称该 Proposal 被批准。
  • 决策学习者:不参与决策,从 Proposers/Acceptors 学习最新达成一致的提案(Value)。

经过我们之前对 ZooKeeper 的学习,相信对 Paxos 算法的集群角色划分并不陌生。而与 ZAB 协议算法不同的是,在 Paxos 算法中,当处理来自客户端的事务性会话请求的过程时,首先会触发一个或多个服务器进程,就本次会话的处理发起提案。当该提案通过网络发送到集群中的其他角色服务器后,这些服务器会就该会话在本地的执行情况反馈给发起提案的服务器。发起提案的服务器会在接收到这些反馈信息后进行统计,当集群中超过半数的服务器认可该条事务性的客户端会话 *** 作后,认为该客户端会话可以在本地执行 *** 作。

Paxos 算法针对事务性会话的处理投票过程与 ZAB 协议十分相似,但不同的是,对于采用 ZAB 协议的 ZooKeeper 集群中发起投票的机器,所采用的是在集群中运行的一台 Leader 角色服务器。而 Paxos 算法则采用多副本的处理方式,即存在多个副本,每个副本分别包含提案者、决策者以及学习者。下图演示了三种角色的服务器之间的关系。

6.2 事务处理过程

介绍完 Paxos 算法中的服务器角色和投票的处理过程后,接下来我们再来看一下 Paxos 针对一次提案是如何处理的。如下图所示,整个提案的处理过程可以分为三个阶段,分别是提案准备阶段、事务处理阶段、数据同步阶段。我们分别介绍一下这三个阶段的底层处理逻辑。

 

  • 提案准备阶段:该阶段是整个 Paxos 算法的最初阶段,所有接收到的来自客户端的事务性会话在执行之前,整个集群中的 Proposer 角色服务器或者节点,需要将会话发送给 Acceptor 决策者服务器。在 Acceptor 服务器接收到该条询问信息后,需要返回 Promise ,承诺可以执行 *** 作信息给 Proposer 角色服务器。
  • 事务处理阶段:在经过提案准备阶段,确认该条事务性的会话 *** 作可以在集群中正常执行后,Proposer 提案服务器会再次向 Acceptor 决策者服务器发送 propose 提交请求。Acceptor 决策者服务器在接收到该 propose 请求后,在本地执行该条事务性的会话 *** 作。
  • 数据同步阶段:在完成了事务处理阶段的 *** 作后,整个集群中对该条事务性会话的数据变更已经在 Acceptor 决策者服务器上执行完成,当整个集群中有超过半数的 Acceptor 决策者服务器都成功执行后,Paxos 算法将针对本次执行结果形成一个决议,并发送给 Learner 服务器。当 Learner 服务器接收到该条决议信息后,会同步 Acceptor 决策者服务器上的数据信息,最终完成该条事务性会话在整个集群中的处理。
6.3 Paxos和ZAB算法的区别

相同之处是,在执行事务行会话的处理中,两种算法最开始都需要一台服务器或者线程针对该会话,在集群中发起提案或是投票。只有当集群中的过半数服务器对该提案投票通过后,才能执行接下来的处理。

而 Paxos 算法与 ZAB 协议不同的是,Paxos 算法的发起者可以是一个或多个。当集群中的 Acceptor 服务器中的大多数可以执行会话请求后,提议者服务器只负责发送提交指令,事务的执行实际发生在 Acceptor 服务器。这与 ZooKeeper 服务器上事务的执行发生在 Leader 服务器上不同。Paxos 算法在数据同步阶段,是多台 Acceptor 服务器作为数据源同步给集群中的多台 Learner 服务器,而 ZooKeeper 则是单台 Leader 服务器作为数据源同步给集群中的其他角色服务器。

七、ZooKeeper中二阶段提交算法的实现 7.1 提交请求

二阶段提交的本质是协调和处理 ZooKeeper 集群中的服务器,使它们在处理事务性会话请求的过程中能保证数据一致性。如果把执行在 ZooKeeper 集群中各个服务器上的事务会话处理 *** 作分别看作不同的函数,那么整个一致性的处理逻辑就相当于包裹这些函数的事务。而在单机环境中处理事务的逻辑是,包含在事务中的所有函数要么全部成功执行,要么全部都不执行。

不同的是,在分布式环境中,处理事务请求的各个函数是分布在不同的网络服务器上的线程,无法像在单机环境下一样,做到当事务中的某一个环节发生异常的时候,回滚包裹在整个事务中的 *** 作。因此,分布式环境中处理事务 *** 作的时候,一般的算法不会要求全部集群中的机器都成功执行 *** 作,如果有其中一个函数执行异常,那么整个事务就会把所有函数的执行结果回滚到执行前的状态,也就是无论是正确执行的函数,还是执行异常的函数,各自所做的对数据和程序状态的变更都将被删除。

7.2 执行请求

看完提交请求的处理过程后,我们再来看一下在执行请求时 ZooKeeper 的底层实现过程。

ZooKeeper 集群中的 Leader 服务器对该条事务性会话 *** 作是否能够在 Follow 服务器上执行,向集群中的 Follow 服务器发起 Proposal 请求。

这里请你注意,与我们之前介绍的二阶段提交不同的是,在 ZooKeeper 的实现中并没有中断提交的逻辑。集群中的 Follow 服务器在接收到上述 Proposal 请求后,只有两种处理情况:

第一种情况:ZooKeeper 集群中的 Follow 服务器能够正确执行 *** 作,并向 ZooKeeper 集群中的 Leader 反馈执行结果。

第二种情况:无法正确执行该条 Proposal *** 作,直接抛弃该条请求。

ZooKeeper 集群的这种执行逻辑,最终导致无须等 待所有服务器都执行完成并反馈,集群中的 Leader 服务器只需要接收到集群中过半数的 Follow 服务器成功执行的反馈信息, ZooKeeper 集群中的 Leader 服务器最终会统计 Follow 服务器反馈的信息,当超过半数以上服务器可以正确执行 *** 作后,整个 ZooKeeper 集群就可以进入执行事务提交 *** 作。

7.3 源码的实现

从源码层面来讲,ZooKeeper 在实现整个二阶段提交算法的过程中,可以分为 Leader 服务器端的发起 Proposal *** 作和 Follow 服务器端的执行反馈 *** 作。

我们先来看看,在 ZooKeeper 集群中的 Leader 是如何向其他 Follow 服务器发送 Proposal 请求的呢?

如下面的代码所示, ZooKeeper 通过 SendAckRequestProcessor 类发送 Proposal 来提交请求。这个类首先继承了 RequestProcessor 类,但是它不是处理来自客户端的请求信息,而是用来处理向 Follow 服务器发送的 Proposal 请求信息。它在内部通过 processRequest 函数来判断,责任链中传递请求 *** 作是否是数据同步 *** 作:如果判断是 OpCode.sync *** 作(也就是数据同步 *** 作),就通过 learner.writePacket 方法把 Proposal 请求向集群中的所有 Follow 服务器进行发送。

public class SendAckRequestProcessor implements RequestProcessor, Flushable { 

  public void processRequest(Request si) { 

    if(si.type != OpCode.sync){ 

        QuorumPacket qp = new QuorumPacket(Leader.ACK, si.getHdr().getZxid(), null, 

            null); 

        try { 

            learner.writePacket(qp, false); 

        } catch (IOException e) { 

            LOG.warn("Closing connection to leader, exception during packet send", e); 

            try { 

                if (!learner.sock.isClosed()) { 

                    learner.sock.close(); 

                } 

            } catch (IOException e1) { 

                // Nothing to do, we are shutting things down, so an exception here is irrelevant 

                LOG.debug("Ignoring error closing the connection", e1); 

            } 

        } 

    } 

} 

} 

在介绍完 ZooKeeper 集群中的 Leader 服务器发送 Proposal 的底层实现过程后,接下来我们再来学习一下 Follow 服务端在接收到 Leader 服务器发送的 Proposal 后的整个处理逻辑。

如下面的代码所示,这在 Follow 服务器端是通过 ProposalRequestProcessor 来完成处理的。ProposalRequestProcessor 构造函数中首先初始化了 Leader 服务器、下一个请求处理器,以及负责反馈执行结果给 Leader 服务器的 AckRequestProcessor 处理器。

public ProposalRequestProcessor(LeaderZooKeeperServer zks, 

        RequestProcessor nextProcessor) { 

    this.zks = zks; 

    this.nextProcessor = nextProcessor; 

    AckRequestProcessor ackProcessor = new AckRequestProcessor(zks.getLeader()); 

    syncProcessor = new SyncRequestProcessor(zks, ackProcessor); 

} 

 接下来,我们进入到 AckRequestProcessor 函数的内部,来看一下 Follow 服务器是如何反馈处理结果给 Leader 服务器的。

如下面的代码所示, AckRequestProcessor 类同样也继承了 RequestProcessor,从中可以看出在 ZooKeeper 中处理 Leader 服务器的 Proposal 时,是将该 Proposal 请求当作网络中的一条会话请求来处理的。整个处理的逻辑实现也是按照处理链模式设计实现的,在 AckRequestProcessor 类的内部通过 processRequest 函数,来向集群中的 Leader 服务器发送 ack 反馈信息。

class AckRequestProcessor implements RequestProcessor { 

public void processRequest(Request request) { 

    QuorumPeer self = leader.self; 

    if(self != null) 

        leader.processAck(self.getId(), request.zxid, null); 

    else 

        LOG.error("Null QuorumPeer"); 

}} 
博文参考

RAFT算法详解_青萍之末的博客-CSDN博客_raft算法

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5683228.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存