前言

ZooKeeper是一个提供高可用,一致性,高性能的保证读写顺序的存储系统。ZAB协议为ZooKeeper专门设计的一种支持数据一致性的原子广播协议。

演示环境

$ uname -a
Darwin 18.6. Darwin Kernel Version 18.6.: Thu Apr :: PDT ; root:xnu-4903.261.~/RELEASE_X86_64 x86_64

安装

brew cask install java
brew install zookeeper

配置

这里演示的是在同一台机器部署3个ZooKeeper进程的伪集群。

$ cat /usr/local/etc/zookeeper/zoo1.cfg
tickTime=
initLimit=
syncLimit=
dataDir=/usr/local/var/run/zookeeper/data1
clientPort=
server.=localhost::
server.=localhost::
server.=localhost::7888
$ echo "1" > /usr/local/var/run/zookeeper/data1/myid
  • tickTime ZooKeeper中使用的基本时间单元,以毫秒为单位,默认是2000。它用来调节心跳和超时。
  • initLimit 默认值是10,即tickTime属性值的10倍。表示允许follower连接并同步到leader的最大时间。如果ZooKeeper管理的数据量很大的话可以增加这个值。
  • syncLimit 默认值是5,即tickTime属性值的5倍。表示lead和follower进行心跳检测的最大延迟时间。如果在设置的时间内follower无法于leader进行通信,那么follower将会被丢弃。
  • dataDir ZooKeeper用来存储内存数据库快照的目录,并且除非指定其他目录,否则数据库更新的事务日志也会存储在该目录。可以配置dataLogDir指定ZooKeeper事务日志的存储目录。
  • clientPort 服务器监听客户端连接的端口,默认是2181。
  • server.id = host:port1:port2 集群模式中用于感知其他机器,每一行代表一个ZooKeeper实例配置。id被成为Server ID用来表示实例在集群中的序号。同时需要将每个实例的ID写入dataDir的myid文件中。port1用于数据同步。port2用于选举。

集群中第2个实例的配置为:

$ cat /usr/local/etc/zookeeper/zoo2.cfg
tickTime=
initLimit=
syncLimit=
dataDir=/usr/local/var/run/zookeeper/data2
clientPort=
server.=localhost::
server.=localhost::
server.=localhost::
$ cat /usr/local/var/run/zookeeper/data2/myid

集群中第3个示例的配置为:

$ cat /usr/local/etc/zookeeper/zoo3.cfg
tickTime=
initLimit=
syncLimit=
dataDir=/usr/local/var/run/zookeeper/data3
clientPort=
server.=localhost::
server.=localhost::
server.=localhost::
$ cat /usr/local/var/run/zookeeper/data3/myid

启动集群:

$ zkServer start /usr/local/etc/zookeeper/zoo1.cfg
ZooKeeper JMX enabled by default
Using config: /usr/local/etc/zookeeper/zoo1.cfg
Starting zookeeper ... STARTED
$ zkServer start /usr/local/etc/zookeeper/zoo2.cfg
ZooKeeper JMX enabled by default
Using config: /usr/local/etc/zookeeper/zoo2.cfg
Starting zookeeper ... STARTED
$ zkServer start /usr/local/etc/zookeeper/zoo3.cfg
ZooKeeper JMX enabled by default
Using config: /usr/local/etc/zookeeper/zoo3.cfg
Starting zookeeper ... STARTED
$ zkServer status /usr/local/etc/zookeeper/zoo1.cfg
ZooKeeper JMX enabled by default
Using config: /usr/local/etc/zookeeper/zoo1.cfg
Mode: follower
$ zkServer status /usr/local/etc/zookeeper/zoo2.cfg
ZooKeeper JMX enabled by default
Using config: /usr/local/etc/zookeeper/zoo2.cfg
Mode: leader
$ zkServer status /usr/local/etc/zookeeper/zoo3.cfg
ZooKeeper JMX enabled by default
Using config: /usr/local/etc/zookeeper/zoo3.cfg
Mode: follower

从上面的状态检查可以看出,第二实例是leader,其他两个实例是follower。

操作

下面我将演示在集群中读写节点。

$ zkCli -server localhost:
Connecting to localhost:
Welcome to ZooKeeper!
JLine support is enabled WATCHER:: WatchedEvent state:SyncConnected type:None path:null
[zk: localhost:(CONNECTED) ] create /test "i am test"
Created /test
[zk: localhost:(CONNECTED) ] get /test
i am test
cZxid = 0x200000002
ctime = Tue Jul :: CST
mZxid = 0x200000002
mtime = Tue Jul :: CST
pZxid = 0x200000002
cversion =
dataVersion =
aclVersion =
ephemeralOwner = 0x0
dataLength =
numChildren =

在实例2中创建/test节点,接下来在实例3中也能读取到该节点,说明ZooKeeper集群数据的一致性。

ZooKeeper语义保证

ZooKeeper简单高效,同时提供以下语义保证,从而使我们可以利用这些特性提供复杂的服务:

  • 顺序性:客户端发起的更新请求会按发送顺序被应用到ZooKeeper上。
  • 原子性:更新操作要么成功要么失败,不会出现中间状态。
  • 可靠性:一个更新操作一旦被接受即不会意外丢失,除非被其他更新操作覆盖。
  • 最终一致性:写操作最终(而非立即)会对客户端可见。

ZooKeeper Watch 机制

所有对ZooKeeper的读操作都可以附带一个Watch。一旦相应的数据有变化,该Watch即被触发。

Watch有以下特点:

  • 主动推送:Watch被触发时,由ZooKeeper服务器主动将更新推送给客户端,而不需要客户端轮询。
  • 一次性:数据变化时Watch只会被触发一次。如果客户端想得到后续更新的通知,必须要在Watch被触发后重新再注册一个Watch。
  • 顺序性:如果多个更新触发了多个Watch,那Watch被触发的顺序与更新的顺序一致。
  • 可见性:如果一个客户端在读请求中附带Watch,Watch被触发的同时再次读取数据,客户端在得到Watch消息之前肯定不可能看到更新后的数据。换句话说,更新通知先于更新结果。

ZAB协议

为了保证写操作的一致性与可用性,ZooKeeper在paxos的基础上设计了一种名为原子广播(ZAB)的支持崩溃恢复的一致性协议。基于该协议,ZooKeeper实现了一种主从模式的系统架构来保持集群中各个副本之间的数据一致性。

根据ZAB协议,所有的写操作都必须通过leader来完成,leader写入本地日志后再复制到所有的follower节点。如果客户端对follower/observer发起写请求,follower/observer会将请求转发到leader,然后由leader处理完成后再将结果转发回follower/observer发送给客户端。

ZAB协议分为广播模式崩溃恢复模式

leader处理写请求(广播模式)的步骤为:

1.leader为事务请求生成唯一的事务ID(ZXID),ZAB协议会将每个事务Proposal按照ZXID的先后顺序来进行排序和处理。

2.将Proposal发送给follower并等待follower回复ACK。

3.leader收到超过半数的ACK(leader默认对自己有一个ACK)后向所有的follower/observer发送commit,同时leader自身也会完成commit。

4.将处理结果返回给客户端。

上述过程成为ZooKeeper的两阶段提交。

崩溃恢复:

当leader实例宕机崩溃,或者因为网络原因导致其与过半的follower都失去联系,那么就会进入崩溃恢复阶段。

leader宕机或者与过半的follower失联都会导致leader重新选举(选举算法文章后面会介绍)。选举结束后会紧着进入数据崩溃恢复,以保证数据一致性,也就是数据同步。需要保证已经commit的事务被所有服务器都提交,同时需要丢弃那些只在leader服务器提交的事务。所以选出来的新leader要拥有集群中最高编号的ZXID。在新的leader选举出来后就会进行数据同步工作,leader会将那些没有被follower同步的事务以Proposal消息的形式发送给follower,并在每个Proposal消息后面紧跟着发送一个commit消息,表示该事务已经被提交。然后follower服务器会将同步过来的事务Proposal都成功应用到本地数据库。

ZXID是个64位的无符号整形,高32位是epoch,代表leader选择周期,低32位是累加计数,每一轮选举后该计数会清零。leader服务器没产生一个事务,ZXID的低32位就会加一,没完成一次leader选举,就会将ZXID的高32位加一。这样做是为了保证新leader生成的ZXID肯定是大于旧leader之前产生的ZXID。

领导选举算法

服务器状态:

  • LOOKING 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举。
  • FOLLOWING 跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁。
  • LEADING 领导者状态。表明当前服务器状态是leader,它会维护与Follower间的心跳。
  • OBSERVING 观察者状态。表明当前服务器角色是Observer,与Follower不同是不参与选举,也不参与集群写操作的投票。

选票数据结构:

每个服务器在进行领导者选举是,会发送如下关键信息:

  • logicClock 每个服务器都会维护一个自增的整数,名为logicClock,它表示这是该服务器第几轮发起的投票。
  • state 表示该服务器当前的状态。
  • self_id 当前服务器的myid
  • self_zxid 当前服务器上所保存的数据的最大zxid
  • vote_id 被推举的服务器的myid
  • vote_zxid 被推举的服务器上所保存的数据的最大zxid

(快速领导者选举算法)选票PK:

选票PK是基于(logicClock,self_id, self_zxid) 与 (vote_logicClock, vote_self_id, vote_self_zxid)对比:
先比较logicClock,如果相等再比较zxid,如果zxid相等,再比较myid。最后如果vote大于自身,则改票,也投vote。

总结

文章一开始演示ZooKeeper的部署和操作给读者一个直观感受,然后介绍了ZooKeeper的ZAB协议和领导者选举原理。

参考

https://cwiki.apache.org/confluence/display/ZOOKEEPER/ProjectDescription

https://dbaplus.cn/news-141-1875-1.html