ZooKeeper 应该是一个比较常见的开源工具,加上与分布式系统渊源很深,我们其实可以“望文生义”,大致猜测一下 ZooKeeper 的作用,因为 ZooKeeper 的英文直译是“动物园管理员”,动物园里的动物就像分布式系统里的各个子系统一样,混乱且难以管理,所以 ZooKeeper 大概就是拿来管理分布式系统的,让其可控可用。

事实上,我们的猜测是正确的,ZooKeeper 的主要作用就是拿来做分布式系统管理的。具体的作用主要包括:

  1. 分布式协调(系统间信息异步通信)
  2. 分布式锁(zk 分布式锁)
  3. 元数据/配置信息管理(注册中心)
  4. 高可用(主从选举)

ZooKeeper 能实现这么多的功能自然离不开底层数据结构的支持。ZooKeeper 是一个树形结构,每一个节点(znode)拥有唯一的路径 path,客户端基于 path 上传节点数据,ZooKeeper 收到后会实时通知对该路径进行监听的客户端。
树形结构示意图

znode 有 4 种类型,持久节点、持久序号节点、临时节点、临时序号节点。

节点类型英文 节点类型中文
PERSISTENT 持久节点
PERSISTENT_SEQUENTIAL 持久序号节点
EPHEMERAL 临时节点(不可在拥有子节点)
EPHEMERAL_SEQUENTIAL 临时序号节点(不可在拥有子节点)

持久节点:哪怕客户端断开连接,也一直存在。
临时节点:只要客户端断开连接,节点就没了。
顺序节点:就是在创建节点的时候自动添加全局递增的序号
ZooKeeper 分布式锁的实现就是基于临时顺序节点来实现的,加锁的时候,创建一个临时顺序节点。

每一个 znode 结构包含如下属性:

  1. path:唯一路径
  2. childNode:子节点
  3. stat:状态属性
  4. type:节点类型

我们可以使用命令查看 znode 的属性:

stat /cluster

属性列表如下:

属性 描述
cZxid = 0x75 #创建节点的事务 ID
ctime = Tue Oct 20 16:49:19 CST 2020 #创建时间
mZxid = 0x75 #修改节点的事务 ID
mtime = Tue Oct 20 16:49:19 CST 2020 #最后修改时间
pZxid = 0x76 #子节点变更的事物ID
cversion = 1 #这表示对此znode的子节点进行的更改次数(不包括子节点)
dataVersion = 0 # 数据版本,变更次数
aclVersion = 0 #权限版本,变更次数
ephemeralOwner = 0x0 #临时节点所属会话ID
dataLength = 0 #数据长度
numChildren = 1 #子节点数(不包括子子节点)

ZooKeeper 特点:

  • 顺序写:集群中只有一台机器可以写,所有机器都可以读,所有写请求都会分配一个zk集群全局的唯一递增编号,zxid,保证各种客户端发起的写请求都是有顺序的
  • 数据一致性:任何一台zk机器收到了写请求之后都会同步给其他机器,保证数据的强一致,你连接到任何一台zk机器看到的数据都是一致的
  • 高性能:每台zk机器都在内存维护数据,所以zk集群绝对是高并发高性能的,如果你让zk部署在高配置物理机上,一个3台机器的zk集群抗下每秒几万请求没有问题
  • 高可用:哪怕集群中挂掉不超过一半的机器,都能保证可用,数据不会丢失,3台机器可以挂1台,5台机器可以挂2台
  • 高并发:高性能决定的,只要基于纯内存数据结构来处理,并发能力是很高的,只有一台机器进行写,但是高配置的物理机,比如16核32G,写入几万QPS,读,所有机器都可以读,3台机器的话,起码可以支撑十几万QPS

ZooKeeper 3 种角色:
通常来说ZooKeeper集群里有三种角色的机器。

  • Leader:集群启动自动选举一个Leader出来,只有Leader是可以写的。
  • Follower:Follower 只能同步数据和提供数据的读取,Leader 挂了,Follower 可以继续选举出来Leader。
  • Observer:只能读,但是Observer不参与选举

ZooKeeper最核心的一个机制:Watcher监听回调
就是客户端可以对 Znode 进行 Watcher 监听,然后 Znode 改变的时候回调客户端。

在整个zk的架构和工作原理中,有一个非常关键的环节,就是zk集群的数据同步是用什么协议做的?其实用的是特别设计的ZAB协议,ZooKeeper Atomic Broadcast,就是ZooKeeper原子广播协议。

ZAB的核心思想介绍:主从同步机制和崩溃恢复机制
两种角色:Leader 和 Follower,只有 Leader 可以接受写操作,Leader 和 Follower 都可以读。
流程:Leader 收到事务请求,转换为事务 Proposal(提议)同步给所有的 Follower,超过半数的 Follower 都说收到事务 Proposal 了(返回 ack),Leader 再给所有的 Follower 发一个Commit 消息,让所有 Follower 提交事务。

崩溃恢复:
zk集群启动的时候,进入恢复模式,选举一个leader出来,然后leader等待集群中过半的follower跟他进行数据同步,只要过半follower完成数据同步,接着就退出恢复模式,可以对外提供服务了

集群启动:恢复模式,leader选举(过半机器选举机制) + 数据同步
消息写入:消息广播模式,leader采用2PC模式的过半写机制,给follower进行同步
崩溃恢复:恢复模式,leader/follower宕机,只要剩余机器超过一半,集群宕机不超过一半的机器,就可以选举新的leader,数据同步

采用了2PC两阶段提交思想的ZAB消息广播流程:
每一个消息广播的时候,都是2PC思想走的,先是发起事务Proposal的广播,就是事务提议,仅仅只是个提议而已,各个follower返回ack,过半follower都ack了,就直接发起commit消息到全部follower上去,让大家提交。
发起一个事务proposal之前,leader会分配一个全局唯一递增的事务id,zxid,通过这个可以严格保证顺序。
leader会为每个follower创建一个队列,里面放入要发送给follower的事务proposal,这是保证了一个同步的顺序性。
每个follower收到一个事务proposal之后,就需要立即写入本地磁盘日志中,写入成功之后就可以保证数据不会丢失了,然后返回一个ack给leader,然后过半follower都返回了ack,leader推送commit消息给全部follower。
leader自己也会进行commit操作,commit之后,就意味这个数据可以被读取到了。

ZooKeeper到底是强一致性还是最终一致性:
明显,ZAB协议机制,zk一定不是强一致性,是最终一致,官方的说法是顺序一致性。因为leader一定会保证所有的proposal同步到follower上都是按照顺序来走的,起码顺序不会乱。

使用 Observer 节点来扩展 ZooKeeper 集群:
Observer节点是不参与leader选举的,他也不参与ZAB协议同步时候的过半follower ack的那个环节,他只是单纯的接收数据,同步数据,可能数据存在一定的不一致的问题,但是是只读的。
zk集群无论多少台机器,只能是一个leader进行写,单机写入最多每秒上万QPS,这是没法扩展的,所以zk是适合写少的场景。
但是读呢?follower起码有2个或者4个,读你起码可以有每秒几万QPS,没问题,那如果读请求更多呢?此时你可以引入Observer节点,他就只是同步数据,提供读服务,可以无限的扩展机器。