ZAB思考
前言
ZAB是专门为Zookeeper设计的分布式协调协议,其全称是Zookeeper Atomic Broadcast(Zookeeper原子广播协议) 。
在本篇文章中,我不会特别详细地去讲ZAB的详细内容和步骤,而是会列出一些我觉得很重要的点或者是理解ZAB的脉络,然后从这些点展开来讲。在看文章之前建议先看下本文的参考博客。
脉络
ZAB的大致内容
- ZAB的出现是为了解决Zookeeper内部的一致性问题,它主要分为两大部分,崩溃恢复和原子广播。
- 与Paxos不同,在ZAB中,每个进程最终是会有主备的关系的,在ZAB内部被称为Leader和Follower。与很多其他的主从架构一样,对于系统的写操作,都是最终会由Leader来统一处理,而对于读操作,Follower可以单独来处理。而正因为这个原因,Zookeeper在一定程度上丧失了强一致性,后面具体解释。
ZAB的效果
- 使用单一进程来处理写请求,并把请求以proposal的方式广播到所有的follower上去;
- 保证了全局事务的有序性。因为利用单进程来处理写请求,那么就可以在处理写请求的时候为每个请求生成一个独特的id,在后续的广播或者崩溃恢复的过程中利用这个id来保证所有副本上的全局事务的有序性;
- 当leader挂掉的时候,可以利用ZAB的崩溃恢复中的选举部分选举新的leader。
ZAB原理
发现,同步,广播
- 发现:要求zookeeper集群必须选举出一个 Leader 进程,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower节点进行通信。
- 同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是提现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
- 广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。
其实用通俗一点的话说,理解ZAB最重要的就是要知道,ZAB是利用单一的leader节点来作为写请求的入口,而且leader和所有的follower互相之间都知道彼此的存在,这样就可以在每个节点内部维护一份全量的节点名单。而leader在获取写请求后,会把请求包装成proposal发送给所有的follower做备份,与2PC不一样的是,只要有超过一半的follower返回了ACK,那么leader就会向所有的follower发送Commit,并且自己也会做commit。
内容
消息广播
ZAB之所以对2PC做出了改进,不需要所有的follower作出ack后才会提出commit,是因为这样可以大大减小阻塞的时间。
在真实的应用中,客户端会随机连接到Zookeeper的任意节点,这样客户端的写请求就可能发送到leader节点或者是follower。但是大致区别是不大的,本质上都是一个2PC(改进版的)过程。
写leader节点:
写follower节点:
根据上图总的来说,写Leader主要有:
- 客户端向Leader发起写请求
- Leader将写请求以Proposal的形式发给所有Follower并等待ACK
- Follower收到Leader的Proposal后返回ACK
- Leader得到过半数的ACK(Leader对自己默认有一个ACK)后向所有的Follower和Observer发送Commmit
- Leader将处理结果返回给客户端
而写follower只是在写leader的第一步之前加了一步转发给leader而已。
另外值得一提的是,leader服务器与每个follower之间都有一个单独的队列进行收发消息,使用队列消息可以做到异步解耦。leader和follower之间只要往队列中发送了消息即可。如果使用同步方式容易引起阻塞。性能上要下降很多。
崩溃恢复
既然ZK内部发生了崩溃,那么是如何恢复数据,保证数据的一致性呢?在广播中我们可以知道是通过类似2PC的过程来做的,但是既然ZK内部Leader崩溃了,那么便无法像原子广播一样直接采取这种方式来做。在Leader崩溃后,一般来说会有两种情况:
- leader在提出proposal时未提交之前崩溃,则经过崩溃恢复之后,新选举的leader一定不能是刚才的leader。因为这个leader存在未提交的proposal。
- leader在发送commit消息之后,崩溃。即消息已经发送到队列中。经过崩溃恢复之后,参与选举的follower服务器(刚才崩溃的leader有可能已经恢复运行,也属于follower节点范畴)中有的节点已经是消费了队列中所有的commit消息。即该follower节点将会被选举为最新的leader。剩下动作就是数据同步过程。
其实总的来说就是一种是在commit前,一种在commit后,区别核心在于follower是否有机会执行commit。
为了解决这个问题,ZAB提出了崩溃恢复的两个要求:
- 确保leader提交的proposal必须被所有的follower服务器提交;
- 确保丢弃已经被leader提出的但没有提交的proposal。
为了解决这个问题,ZAB采用的策略是重新进行选举,然后利用特殊的机制同步新的leader和follower之间的数据,这样保证新的leader和follower之间的数据一致性。总的来说就是三个阶段:1. 选举; 2. 恢复; 3. 广播。
选举
ZAB利用了特殊的被称为Fast Leader Election的选举策略。
简单的来说,在Zookeeper内部的zxid是一个64位的变量,其中前32位被称为epoch或者logicalClock,它的含义是代表这是该服务器发起的第多少轮投票;而低32位则代表这是当前epoch下第多少次的写请求。在参考1和3中有详细的分析过程。
选举的流程大致如下:
- follower发现leader崩溃,这时就会把自己的epoch+1;
- 初始化投票(投给自己)
- 接受外部投票,投票更新规则如下:
- 判断选举轮次(epoch)
- 若 epoch 相等,选 zxid 最大的
- 若epoch和zxid都一样,那么选择serverid最大的(myid最大)
- 统计选票,如果有过半选择了自己,那么自己成为leader
- 更新状态。Looking===>Leading/Following
具体的过程可以看参考中的第1,2篇。
同步
选举完后的leader只是准leader,只有当准leader和follower完成了数据的同步后才能成为真正的leader。
之前说过ZAB对崩溃恢复有两个要求,在同步过程中为了满足这两个要求做了下面的操作:
- 首先leader会和follower交换最大的commit的zxid,如果发现follower有部分zxid没有commit,那么会把commit这些事务的任务放入各个follower的队列中,这样保证了所有leader的commit都提交了;
- 如果follower的zxid有大于leader的zxid的,那么leader会发送trunc命令让follower删除这些zxid对应的事务。这个就保证了未被commit的消息会被丢弃。
广播
这个过程和之前的原子广播一致。
思考与问题
- ZAB和Paxos
zookeeper之所以没有采用Paxos,是因为Paxos无法保证全局有序,并且因为在Paxos中,第一阶段会一直竞争acceptor的promise,这个阶段很容易发生活锁,造成效率低下。因此zookeeper引入了leader的概念,并用leader的选举,恢复等(即崩溃恢复)保证系统的高可用。在崩溃恢复的过程中,leader的选举、同步与paxos有一定程度的相似。
- Zookeeper是不保证读一致性的
如文章开头部分所说,因为在消息的原子广播过程中,只要得到了大部分的follower的ack就会让follower做commit。而客户端是可以任意连接zookeeper集群的任意节点,如果客户端连接到的节点还没有给leader返回ack,但是大部分的节点都给leader返回了ack并做了commit,那么这时候客户端读取的便不是最新的状态。可以参考4了解详细分析。
- zookeeper内部follower是怎么知道leader挂了的?
有几点还没有完全理解,如果一个follower和其他所有follower和leader都失去了联系,它会做什么操作?或者说zookeeper在什么情况下知道leader挂了,特别是follower没办法和其他所有follower联系的情况下,比如集群分裂成两部分,少的那部分会怎么做?在什么时候把following状态改成looking状态?