概述
ZooKeeper Atomic Broadcast,ZooKeeper原子消息广播协议。ZAB协议是为分布式协调服务ZK专门设计的一种支持崩溃恢复的原子广播协议。ZK主要依赖ZAB协议来实现分布式数据的最终一致性,基于该协议,ZK实现一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
集群角色
- Leader:同一时间集群总只允许有一个Leader,提供对客户端的读写功能,负责将数据同步至各个节点;
- Follower:提供对客户端读功能,写请求则转发给Leader处理,当Leader崩溃失联之后参与Leader选举;
- Observer:与Follower不同的是但不参与Leader选举
运行时状态
对应于org.apache.zookeeper.server.quorum.QuorumPeer.ServerState
枚举类,ZAB协议的Java实现都是在这个包下面,下文不再写全路径名:
public static enum ServerState {
LOOKING,
FOLLOWING,
LEADING,
OBSERVING;
}
进程的可能状态:
- LOOKING:Leader选举状态,正在寻找Leader
- FOLLOWING:当前节点是Follower。与Leader服务器保持同步状态
- LEADING:当前节点是Leader,作为主进程领导状态
- OBSERVING:observer角色
ZAB状态
ZK给ZAB定义的4种状态,反应ZK从选举到对外提供服务的过程中的四个步骤。状态枚举源码:
public enum ZabState {
ELECTION,
DISCOVERY,
SYNCHRONIZATION,
BROADCAST
}
- ELECTION:集群进入选举状态,此过程会选出一个节点作为Leader角色;
- DISCOVERY:连接上Leader,响应Leader心跳,并且检测Leader的角色是否更改,通过此步骤之后选举出的Leader才能执行真正职务;
- SYNCHRONIZATION:整个集群都确认Leader之后,将会把Leader的数据同步到各个节点,保证整个集群的数据一致性;
- BROADCAST:过渡到广播状态,集群开始对外提供服务。
状态切换
-
启动时的状态转换
- 所有进程的初始状态都是LOOKING状态,此时不存在Leader
- 接下来,进程会试图选举出来一个新的Leader,Leader切换为LEADING状态,其它进程发现已经选举出新的Leader,那么它就会切换到FOLLOWING状态,并开始与Leader保持同步
- 处于FOLLOWING状态的进程称为Follower,LEADING状态的进程称为Leader
- 当Leader崩溃或者放弃领导地位时,其余Follower进程就会切换到LOOKING状态开始新一轮的Leader选举
-
运行过程中的状态转换
- 一个Follower只能和一个Leader保持同步,Leader进程和所有的Follower进程之间通过心跳监测机制来感知彼此的情况
- 若Leader能够在超时时间内正常的收到心跳检测,那么Follower就会一直与该Leader保持连接
- 如果在指定时间内Leader无法从过半的Follower进程那里接收到心跳检测,或者TCP连接断开,那么Leader会放弃当前周期的领导,并转换为LOOKING状态;其他的Follower也会选择放弃这个Leader,同时转换为LOOKING状态,之后会进行新一轮的Leader选举。
Epoch
Epoch指当前集群的周期号(年代号),集群的每次Leader变更都会产生一个新的周期号,周期号的产生规则是在上一个周期号的基础上加1,这样当之前的Leader崩溃恢复后会发现自己的周期号比当前的周期号小,说明此时集群已经产生新的Leader,旧的Leader会再次以Follower的角色加入集群。
Proposal
提议,源码为Leader.Proposal
,是Leader的内部类,由Leader发起选举的提议:
public static class Proposal extends SyncedLearnerTracker {
private QuorumPacket packet;
protected Request request;
// 省略构造方法和getter
public long getZxid() {
return this.packet.getZxid();
}
}
Vote
投票,源码:
public class Vote {
// 默认值为0
private final int version;
private final long id;
private final long zxid;
// 默认值-1
private final long electionEpoch;
// 默认值-1
private final long peerEpoch;
// 默认值Looking
private final QuorumPeer.ServerState state;
// 省略构造方法和getter
}
Zxid
该协议主要通过唯一的事务编号Zxid(ZooKeeper Transaction id)保障集群状态的唯一性。Zxid与RDBMS中的事务id类似,用于标识一次提议的id;为了保证顺序性,Zxid必须单调递增,保证全局有序。
Zxid指ZAB协议的事务编号,一个64位的数字,低32位存储的是一个简单的单调递增的计数器,针对客户端的每一个事务请求,计数器都加1。高32位存储的是Leader的周期号Epoch。每次选举产生一个新的Leader时,该Leader都会从当前服务器的日志中取出最大事务的Zxid,获取其中高32位的Epoch值并加1,以此作为新的Epoch,并将低32位重置为0,重新开始计数。
这样设计的好处是旧的Leader宕机后重启,它不会被选举为Leader,因为此时它的Zxid肯定小于当前的新Leader。当旧的Leader作为Follower接入新的Leader后,新的Leader会让它将所有的拥有旧的epoch号的未被COMMIT的Proposal清除。
模式
ZAB协议有两种模式,分别是崩溃恢复模式(集群选主)和消息广播模式(数据同步)。
崩溃恢复
Recovery,当集群启动、集群重启、网络中断、Leader崩溃后,集群将开始选主,该过程为崩溃恢复模式。当选举产生新的Leader服务器,同时集群中已经有超过半数(ZK里定义为quorum,下同)的Follower机器与该Leader服务器完成状态(数据)同步之后,ZAB协议会退出恢复模式,即进入消息广播模式。
在ZAB协议中,为保证程序的正确运⾏,整个恢复过程结束后需要选举出⼀个新的Leader服务器。Leader选举算法不仅仅需要让Leader⾃身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速感知到选举产⽣出来的新Leader服务器。
消息广播
Boardcast,广播的过程实际上是一个简化的二阶段提交过程。当Leader被选举出来后,Leader将最新的集群状态广播给其他Follower,该过程为广播模式。
当一台遵守ZAB协议的服务器启动后加入到集群中,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么加入的服务器会自觉的进入数据恢复模式:找到Leader所在的服务器,并与其进⾏数据同步,数据同步完成后参与到消息⼴播流程中。
消息广播过程:
- 客户端发起写请求
- Leader将客户端请求信息转化为事务Proposal,同时为每个Proposal分配一个事务ID
- Leader为每个Follower单独分配一个FIFO的队列,将需要广播的Proposal依次放入到队列中
- Follower接收到Proposal后,首先将其以事务日志的方式写入到本地磁盘中,写入成功后给Leader反馈一个ACK响应
- Leader接收到半数以上Follower的ACK响应后,即认为消息发送成功,可以发送Commit消息
- Leader向所有Follower广播Commit消息,同时自身也会完成事务提交。Follower接收到Commit消息后也会完成事务的提交
数据一致性
ZAB协议规定:如果⼀个事务Proposal在⼀台机器上被处理成功,则在所有的机器上都被处理成功,哪怕机器出现故障崩溃。
数据同步:所有正常运行的服务器要么成为Leader,要么成为Follower并和Leader保持同步。
- 完成Leader选举(新的Leader具有最高的zxid)之后,在正式开始⼯作(接收客户端请求)之前,Leader服务器会⾸先确认事务⽇志中的所有Proposal是否已经被集群中过半的机器提交,即是否完成数据同步。
- Leader服务器需要确保所有的Follower服务器能够接收到每⼀条事务Proposal,并且能够正确地将所有已经提交的事务Proposal应⽤到内存数据中。等到Follower服务器将所有其尚未同步的事务Proposal都从Leader服务器上同步过来并成功应⽤到本地数据库中后,Leader服务器就会将该Follower服务器加⼊到真正的可⽤Follower列表中,并开始之后的其他流程。
ZAB的选举出来的Leader必须满足条件:
能够确保提交已经被Leader提交的事务Proposal,同时丢弃已经被跳过的事务Proposal。即:
- 新选举出来的Leader不能包含未提交的Proposal。
- 新选举的Leader节点中含有最大的zxid。
选举
4个阶段
ZAB的四个阶段:Leader Election领导选举、Discovery发现、Synchronization同步、Broadcast广播。
选举阶段
在集群选举开始时,所有节点都处于选举阶段。当某一个节点的票数超过半数节点后,该节点将被推选为准Leader。选举阶段的目的就是产生一个准Leader。只有到达广播阶段后,准Leader才会成为真正的Leader。
发现阶段
各个Follower开始和准Leader进行通信,同步Follower最近接收的事务提议。这时,准Leader会产生一个新的Epoch,并尝试让其他Follower接收该Epoch后再更新到本地,即更新acceptedEpoch。
发现阶段的一个Follower只会连接一个Leader,如果节点1认为节点2是Leader,则当节点1尝试连接节点2时,如果连接被拒绝,则集群会进入重新选举阶段。发现阶段的主要目的是发现当前大多数节点接收的最新提议。
同步阶段
主要是将Leader在前一阶段获得的最新提议信息同步到集群中所有的副本。只有当quorum都同步完成时,准Leader才会成为真正的Leader。Follower只会接收Zxid比自己的lastZxid大的提议。同步阶段完成后集群选主的操作才完成,新的Leader将产生。
广播阶段
ZK集群开始正式对外提供事务服务,这时Leader进行消息广播,将其上的状态通知到其他Follower,如果后续有新的节点加入,则Leader会对新节点进行状态的同步。ZAB提交事务并不像2PC一样需要全部Follower都Ack,只需要得到quorum的Ack就可以。
ZAB选举过程
每个Server首先都提议自己是Leader,并为自己投票,然后将投票结果与其他Server的选票进行对比,权重大的胜出,使用权重较大的选票更新自身的选票箱,具体选举过程:
- 每个Server启动以后都询问其他Server给谁投票,其他Server根据自己的状态回复自己推荐的Leader并返回对应的Leader id和Zxid。在集群初次启动时,每个Server都会推荐自己为Leader
- 当Server收到所有其他Server的回复后,计算出Zxid最大的Server,并将该Server设置成下一次要投票推荐的Server
- 计算过程中票数最多的Server将成为获胜者,如果获胜者的票数超过集群个数的一半,则该Server将被推选为Leader。否则,继续投票,直到Leader被选举出来
- Leader等待其他Server连接
- Follower连接Leader,将最大的Zxid发送给Leader
- Leader根据Follower的Zxid确定同步点,至此,选举阶段完成。
在选举阶段完成后,Leader通知其他Follower集群已经成为Uptodate状态,Follower收到Uptodate消息后,接收Client的请求并开始对外提供服务。
Java实现
ZAB协议的Java实现与其定义略有不同,在实际实现时,选举阶段采用Fast Leader Election模式。在该模式下,节点首先向所有Server提议自己要成为Leader,当其他Server收到提议以后,判断Epoch信息并接收对方的提议,然后向对方发送接收提议完成的消息。在Java的实现过程中将发现阶段和同步阶段合并为恢复阶段。因此,ZAB协议的Java实现只有3个阶段:Fast Leader Election、Recovery和Broadcast。
Fast Leader Election
简称FLE,是Java实现版的选举机制,对应源码为FastLeaderElection implements Election
。Election是一个接口,有两个方法:
public interface Election {
Vote lookForLeader() throws InterruptedException;
void shutdown();
}
选举过程核心在lookForLeader()
方法。
FLE会选举拥有最新提议的历史节点(其lastZxid最大)作为Leader,故而可省去发现最新提议的步骤。
成为leader的条件?
- 选epoch最大的
- epoch相等,选zxid最大的
- epoch和zxid都相等,选
server_id
最大的,即zoo.cfg
中配置的myid
节点在选举开始时,都默认投票给自己,当接收其他节点的选票时,会根据上面的Leader条件判断并且更改自己的选票,然后重新发送选票给其他节点。当有一个节点的得票超过半数,该节点会设置自己的状态为Leading,其他节点会设置自己的状态为Following。
Recovery Phase
将发现阶段和同步阶段合并为恢复阶段。这一阶段Follower发送他们的lastZxid给Leader,Leader根据lastZxid决定如何同步数据。这里的实现跟前面的阶段3有所不同:Follower收到TRUNC指令会终止L.lastCommitedZxid
之后的Proposal,收到DIFF指令会接收新的Proposal。
拓展
ZAB对比二阶段提交
ZAB协议的消息广播使用原子广播协议, 类似一个二阶段提交的过程 ,但又有所不同:
- 二阶段提交中,需要所有参与者反馈ACK后再发送Commit请求。要求所有参与者要么成功,要么失败。这样会产生严重的阻塞问题
- ZAB协议中,Leader等待半数以上的Follower成功反馈ACK即可,不需要收到全部的Follower反馈ACK
ZAB与Paxos的区别
相同点:
- 都存在一个类似Leader进程的角色,由其负责协调多个Follower进程的运行
- Leader进程都会等待超过半数的Follower作出正确的反馈后,才会将一个提议进行提交
- 在ZAB中,每个Proposal中都包含一个epoch值,用来代表当前Leader周期,在Paxos中同样存在这样的一个表示叫Ballot
不同点:
- Paxos算法中,新选举产生的主进程会进行两个阶段的工作;第一阶段称为读阶段:新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第二阶段称为写阶段:当前主进程开始提出自己的提议。
- ZAB协议在Paxos基础上添加同步阶段。此阶段,新的Leader会确保存在过半的Follower已经提交之前Leader周期中的所有事务Proposal。此阶段的引入能够有效保证,Leader在新的周期中提出事务Proposal之前,所有的进程都已经完成对之前所有事务Proposal的提交。
本质区别在于两者的设计目的不一样:ZAB协议主要用于构建一个高可用的分布式数据主备系统,而Paxos算法则用于构建一个分布式的一致性状态机系统。
问答
主从架构下,Leader崩溃,数据一致性怎么保证?
Leader崩溃后,集群会选出新的Leader,进入恢复阶段,新Leader具有所有已经提交的提议,因此它会保证让Followers同步已提交的提议,丢弃未提交的提议(以Leader的记录为准),保证整个集群的数据一致性。
选举Leader时,整个集群无法处理写请求的,如何快速进行leader选举?
通过FLE实现的,Leader选举只需要超过半数的节点投票即可,这样不需要等待所有节点的选票,能够尽早选出Leader。
参考
ZAB协议的那些事
ZAB协议
ZooKeeper是强一致的吗