ZooKeeper的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。可以基于它实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。zk可以保证如下分布式一致性:
顺序一致性
从同一个客户端发起的事务请求,最终会严格按照发起顺序被应用到zk中
原子性
所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,要么整个集群所有机器都成功应用了某一个服务,要么都没有应用。一定不会出现集群中部分机器应用了该事务,另外一部分没有应用的情况。
可靠性
一旦服务端成功应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务对其进行了变更。
实时性
zk保证在一定时间段内,客户端最终一定能从服务端读取到最新的数据状态。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口
组成zk集群的每台机器都会在内存中维护当前服务器状态,每台机器之间都互相保持通信。只要集群中超过一半的机器能够正常工作,整个机器就能对外提供服务。
基本概念
集群角色
Leader:leader服务器为客户端提供读和写服务,全局事务 Id(zxid)只能由 leader分配
Follower:提供读服务(非事务请求),参与Leader选举过程,不参与过半写成功策略
Observer:只提供读服务,不参与Leader选举和写服务
Leader、Follower 和 Observer这三种角色有四种状态:leading、following、observing 和looking
LOOKING:当前Server不知道leader是谁,正在搜寻。
LEADING:当前Server即为选举出来的leader。
FOLLOWING:leader已经选举出来,当前Server与之同步。
OBSERVING:observer的行为在大多数情况下与follower完全一致,但是他们不参加选举和投票,而仅仅接受(observing)选举和投票的结果。
选主过程
zk 选举有两种算法:basic paxos 和fast paxos系统默认采用fast paxos
会话(Session)
数据节点(Znode)
zk将所有数据存储在内存中,数据模型是一颗树(ZNode tree),由/
进行分割,如:/foo/bar
。树中的数据节点称为ZNode。
ZNode可以分为持久节点和临时节点,持久节点指一旦ZNode被创建,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在zk上。临时节点的生命周期和客户端会话绑定,一旦客户端会话失效(而非TCP连接断开),客户端创建的所有临时节点都会被移除。不能基于临时节点创建子节点,即临时节点只能作为叶节点。
版本
对每个ZNode,zk都为其维护一个Stat
的数据结构,其中记录了这个ZNode的三个数据版本,分别是version(当前ZNode版本)、cversion(当前ZNode子节点版本)和aversion(当前ZNode的ACL版本)。
事件监听器(Watcher)
zk允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发时,zk服务端将事件通知到感兴趣的客户端上去。监听器有三个关键点:
- 一次性触发(one-time trigger)客户端设置监听 zk 的事件后,相应事件发生只会触发客户端一次,如果需要再次收到监听事件客户端需要再次设置监听
- 发送到客户端(send to the client)zk 和客户端通过 socket 通信,并且zk 提供了顺序保证,客户端只有先看到监听事件,才会感知到监听的 znode发生了变化
- 被设置 watch 的数据可以具有不同的改变方式
如果客户端通过exists()设置了某个znode节点的监视,但是客户端在此znode节点被创建和删除的时间间隔内与zookeeper服务器失去了联系,该客户端即使稍后重新连接 zookeeper服务器后也得不到事件通知
ACL
zk采用ACL(Access Control Lists)策略来进行权限控制,类似于UNIX文件系统的权限控制。zk定义了五种权限:
- CREATE:创建子节点的权限
- READ:获取节点数据和子节点列表的权限
- WRITE:更新节点数据的权限
- DELETE:删除子节点的权限
- ADMIN:设置节点ACL的权限
ZAB协议
zk没有完全采用Paxos算法,而是使用了一种称为ZAB(ZooKeeper Atomic Broadcast,ZooKeeper原子消息广播协议)的协议作为数据一致性的核心算法。它是为zk专门设计的崩溃可恢复的原子消息广播算法,zk使用一个单一的主进程来接收并处理客户端的所有事务请求,采用ZAB原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上去。
ZAB定义了那些会改变zk服务器数据状态的事务请求处理方式:所有事务请求必须由全局唯一的Leader服务器来协调处理,余下的其他服务器成为Follower服务器。Leader服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器。之后,Leader服务器需要等待所有的Follower的反馈,一旦超过半数Follower服务器进行了正确的反馈,Leader就会再次向所有的Follower服务器分发Commit消息,要求将前一个Proposal进行提交。
崩溃恢复(Recovery 选主)
整个服务框架启动过程中,或者Leader服务器出现网络中断、崩溃推出或重启等异常情况时,ZAB协议进入恢复模式并选举产生新的Leader服务器。产生新的Leader并且集群中过半机器与该Leader完成状态同步后,ZAB协议就会退出恢复模式。其中,状态同步指数据同步,保证集群中存在过半的机器能够与Leader数据状态保持一致。
过半Follower完成和Leader的状态同步后,整个服务框架进入消息广播模式。当一台遵守ZAB协议的服务器启动后加入集群时,如果集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器会自觉进入消息恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。Leader接收到客户端的事务请求后,生成提案并发起一轮广播协议;如果其他服务器收到客户端事务请求,这些非Leader服务器会首先将这个事务请求转发给Leader服务器。
消息广播(Broadcast同步)
ZAB的原子广播协议类似于二阶段提交过程,Leader为每个Follower分配一个单独的队列,收到客户端事务请求后生成Proposal并分配一个全局单调递增的唯一ID(事务ID,ZXID),将Proposal依次放入这些队列中,并根据FIFO策略发送消息。每个Follower收到事务proposal后会首先将其以事务日志的形式写入本地磁盘中,并在成功写入后反馈给Leader一个ACK响应。当Leader收到超过半数Follower的ack后,会广播一个Commit消息给所有Follower以通知其进行事务提交,Leader自身也会完成事务提交,Follower收到commit消息后也会完成事务提交。
事务
在zk中,事务是指能够改变zk服务器状态的操作,包括数据节点的删除和创建,数据节点内容更新和客户端会话创建与失效等操作。对每个事务请求,zk都会为其分配一个全局唯一的事务id,用zxid来表示,通常是一个64位数字。每一个zxid对应一次更新操作,从这些zxid中可以间接地识别出zk处理这些更新操作请求的全局顺序。zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
序列化和通信
Zookeeper 采用Jute 作为序列化协议,虽然使用较少,但序列化/反序列化一直不是 zk 的瓶颈
zk基于TCP 长连接完成客户端和服务器、服务器和服务器之间的通信
数据存储
zk 数据存储分为内存数据存储和磁盘数据存储,DataTree是zk 内存数据存储的核心,它是一个树型数据结构,底层是一个 ConcurrentHashMap,代表了内存中的一份完整的数据。DataNode是数据存储的最小单元,保存了数据内容、ACL 列表和节点状态,并记录了父节点和子节点列表。
ZKDatabase是 zk 的内存数据库,负责管理zk 的所有会话、DataTree存储和事务日志,ZKDatabase会定时向磁盘 dump快照数据,同时在zk 服务器启动的时候,通过磁盘上的事务日志和快照数据文件恢复成一个完整的内存数据库。
zk 实现分布式锁
- 每个线程 lock() 时在锁 znode(持久节点)下创建一个临时顺序节点,这个顺序节点由 zk 内部维护一个递增的节点序号
- 获取锁 znode 下的所有子节点并排序,如果线程创建的节点是第一个子节点,就成功获取锁;如果不是,就监听线程创建节点的前一个节点
- 获取锁的线程释unlock 时删除它创建的节点,其它监听该节点的线程收到通知获取锁
- 如果某个客户端创建的临时节点后宕机或者 fullgc,zk 通过 tcp 保活可以感知到那个客户端宕机,总而自动删除对应的临时顺序节点,相当于自己释放锁