Zookeeper
Zookeeper是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等。
Zookeeper提供了一个类似于Linux文件系统的树形结构
(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),
同时提供了对于每个节点的监控与通知机制。
据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
分布式应用程序可以基于Zookeeper实现
- 数据发布/订阅
- 负载均衡
- 命名服务
- 分布式协调/通知
- 集群管理
Master选举- 分布式锁和分布式队列
- 等
Zookeeper保证了如下分布式一致性特性:
- 顺序一致性
- 原子性
- 单一视图
- 可靠性
- 实时性(最终一致性)
客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的zookeeper机器来处理。
对于写请求,这些请求会同时发给其他zookeeper机器并且达成一致后,请求才会返回成功。
因此,随着zookeeper的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。
有序性是zookeeper中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,
这个时间戳称为zxid(Zookeeper Transaction Id)。
而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个zookeeper最新的zxid。
Zookeeper 文件系统
Zookeeper提供一个多层级的节点命名空间(节点称为znode)。与文件系统不同的是,这些节点都可
以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。
Zookeeper为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得Zookeeper
不能用于存放大量的数据,每个节点的存放数据上限为1M。
Zookeeper 通知机制
Zookeeper允许客户端对服务端的某个znode注册一个watcher监听事件,当服务端的一些指定事件触发了这个watcher,
服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据watcher通知状态和事件类型做出业务上的改变。
大致分为三个步骤:
- 客户端注册
watcher- 调用
getData、getChildren、exist三个API ,传入watcher对象。 - 标记请求
request,封装watcher到WatchRegistration。 - 封装成
Packet对象,发服务端发送request。 - 收到服务端响应后,将
watcher注册到ZKWatcherManager中进行管理。 - 请求返回,完成注册。
- 调用
- 服务端处理
watcher- 服务端接收
watcher并存储 watcher触发,调用process方法来触发watcher。
- 服务端接收
- 客户端回调
watcher- 客户端
SendThread线程接收事件通知,交由EventThread线程回调watcher。 - 客户端的
watcher机制同样是一次性的,一旦被触发后,该watcher就失效了。
- 客户端
总结:客户端会对某个znode建立一个watcher事件,当该znode发生变化时,
这些客户端会收到Zookeeper的通知,然后客户端可以根据znode变化来做出业务上的改变等。
Zookeeper 通知机制的特点
- 一次性触发数据发生改变时,一个
watcher event会被发送到客户端,但是客户端只会收到一次这样的信息。 watcher event异步发送watcher的通知事件从服务端发送到客户端是异步的,这就存在一个问题,- 不同的客户端和服务器之间通过
socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件, - 由于
Zookeeper本身提供了ordering guarantee,即客户端监听事件后,才会感知它所监视znode发生了变化。 - 所以我们使用
Zookeeper不能期望能够监控到节点每次的变化。 Zookeeper只能保证最终的一致性,而无法保证强一致性。
- 不同的客户端和服务器之间通过
- 数据监视
Zookeeper有数据监视和子数据监视getData()、exists()设置数据监视,getchildren()设置了子节点监视。 - 注册
watcher,getData、exists、getChildren - 触发
watcher,create、delete、setData setData()会触发znode上设置的data watch(如果set成功的话)。- 一个成功的
create()操作会触发被创建的znode上的数据watch,以及其父节点上的child watch。 - 一个成功的
delete()操作将会同时触发一个znode的data watch和child watch(因为这样就没有子节点了),同时也会触发其父节点的child watch。
- 一个成功的
- 当一个客户端连接到一个新的服务器上时,
watch将会被以任意会话事件触发。- 当与一个服务器失去连接的时候,是无法接收到
watch的。而当客户端重新连接时,如果需要的话,所有先前注册过的watch,都会被重新注册。通常这是完全透明的。 - 有在一个特殊情况下,
watch可能会丢失:对于一个未创建 的znode的exist watch,如果在客户端断开连接期间被创建了, 并且随后在客户端连接上之前又删除了,这种情况下,这个watch事件可能会被丢失。
- 当与一个服务器失去连接的时候,是无法接收到
watch是轻量级的,其实就是本地JVM的Callback,服务器端只是存了是否有设置了watcher的布尔类型。
Zookeeper节点ZNode和相关属性
ZNode有两种类型 :
- 持久的(PERSISTENT):客户端和服务器端断开连接后,创建的节点不删除(默认)。
- 短暂的(EPHEMERAL):客户端和服务器端断开连接后,创建的节点自己删除。
ZNode有四种形式:
- PERSISTENT-持久节点
- 客户端与
Zookeeper断开连接后,除非手动删除,否则节点一直存在于Zookeeper上
- 客户端与
- PERSISTENT_SEQUENTIAL-持久顺序节点
- 基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
- EPHEMERAL-临时节点
- 临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与
Zookeeper连接断开不一定会话失效), 那么这个客户端创建的所有临时节点都会被移除。
- 临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与
- EPHEMERAL_SEQUENTIAL-临时顺序节点
- 基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
创建ZNode时设置顺序标识,ZNode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护。
节点属性
znode节点不仅可以存储数据,还有一些其他特别的属性。
| 节点属性 | 注解 |
|---|---|
| cZxid | 该数据节点被创建时的事务Id |
| ctime | 该数据节点创建时间 |
| mZxid | 该数据节点被修改时最新的事物Id |
| mtime | 该数据节点最后修改时间 |
| pZxid | 当前节点的父级节点事务Id |
| cversion | 子节点版本号(子节点修改次数,每修改一次值+1递增) |
| dataVersion | 当前节点版本号(每修改一次值+1递增) |
| aclVersion | 当前节点acl版本号(节点被修改acl权限,每修改一次值+1递增) |
| ephemeralOwner | 临时节点标示,当前节点如果是临时节点,则存储的创建者的会话id(sessionId),如果不是,那么值=0 |
| dataLength | 当前节点所存储的数据长度 |
| numChildren | 当前节点下子节点的个数 |
Zookeeper 集群中的角色
Zookeeper集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色中的一种
Leader- 一个
Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follower及Observer间的心跳。 - 所有的写操作必须要通过
Leader完成再由Leader将写操作广播给其它服务器。 只要有超过半数节点(不包括observer节点)写入成功,该写请求就会被提交(类2PC协议)。
- 一个
Follower- 一个
Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳, Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。
- 一个
Observer- 与
Follower类似,但是无投票权。 Zookeeper需保证高可用和强一致性,为了支持更多的客户端,需要增加更多ServerServer增多,投票阶段延迟增大,影响性能
- 引入
Observer,Observer不参与投票Observers接受客户端的连接,并将写请求转发给leader节点- 加入更多
Observer节点,提高伸缩性,同时不影响吞吐率。
- 与
Zookeeper 集群中Server工作状态
LOOKING- 寻找
Leader状态;当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态
- 寻找
FOLLOWING- 跟随者状态;表明当前服务器角色是
Follower
- 跟随者状态;表明当前服务器角色是
LEADING- 领导者状态;表明当前服务器角色是
Leader
- 领导者状态;表明当前服务器角色是
OBSERVING- 观察者状态;表明当前服务器角色是
Observer
- 观察者状态;表明当前服务器角色是
Zookeeper 集群中服务器之间通信
Leader服务器会和每一个Follower/Observer服务器都建立TCP连接,
同时为每个Follower/Observer都创建一个叫做LearnerHandler的实体。
LearnerHandler主要负责Leader和Follower/Observer之间的网络通讯,包括数据同步,请求转发和proposal提议的投票等。
Leader服务器保存了所有Follower/Observer的LearnerHandler。
ZAB 协议
Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。
实现这个机制的协议叫做ZAB协议,它也是分布式共识算法的一种。
ZAB协议有两种模式,它们分别是恢复模式和广播模式。
事务编号Zxid(事务请求计数器 + epoch)
在ZAB(ZooKeeper Atomic Broadcast,ZooKeeper原子消息广播协议)协议的事务编号Zxid设计中,
Zxid是一个64位的数字,其中低32位是一个简单的单调递增的计数器,针对客户端每一个事务请求,计数器加1。
而高32位则代表Leader周期epoch的编号,每个当选产生一个新的Leader服务器,
就会从这个Leader服务器上取出其本地日志中最大事务的Zxid,并从中读取epoch值,然后加1,
以此作为新的epoch,并将低32位从0开始计数。
Zxid(Transaction id)类似于RDBMS中的事务ID,用于标识一次更新操作的Proposal(提议)ID。
为了保证顺序性,该id必须单调递增。
epoch
epoch:可以理解为当前集群所处的年代或者周期,每个Leader就像皇帝,都有自己的年号,
所以每次改朝换代,leader变更之后,都会在前一个年代的基础上加1。
这样就算旧的Leader崩溃恢复之后,也没有人听他的了,因为Follower只听从当前年代的Leader的命令。
ZAB 协议有两种模式-恢复模式(选主)、广播模式(同步)
ZAB协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。
当服务启动或者在领导者崩溃后,ZAB就进入了恢复模式,当领导者被选举出来,
且大多数Server完成了和Leader的状态同步以后,恢复模式就结束了。
状态同步保证了Leader和Server具有相同的系统状态。
ZAB协议4阶段
Leader election(选举阶段-选出准Leader)- 节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准
Leader。 - 只有到达广播阶段(
broadcast)准leader才会成为真正的leader。 - 这一阶段的目的是就是为了选出一个准
leader,然后进入下一个阶段。
- 节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准
Discovery(发现阶段-接受提议、生成epoch、接受epoch)- 在这个阶段,
Followers跟准Leader进行通信,同步Followers最近接收的事务提议。 - 这个一阶段的主要目的是发现当前大多数节点接收的最新提议,并且准
Leader生成新的epoch, 让Followers接受,更新它们的accepted Epoch - 一个
Follower只会连接一个Leader,如果有一个节点f认为另一个Follower p是Leader,f在尝试连接p时会被拒绝,f被拒绝之后,就会进入重新选举阶段。
- 在这个阶段,
Synchronization(同步阶段-同步Follower副本)- 同步阶段主要是利用
Leader前一阶段获得的最新提议历史,同步集群中所有的副本。 - 只有当大多数节点都同步完成,准
Leader才会成为真正的Leader。 Follower只会接收Zxid比自己的lastZxid大的提议。
- 同步阶段主要是利用
Broadcast(广播阶段-Leader消息广播)- 到了这个阶段,
Zookeeper集群才能正式对外提供事务服务,并且Leader可以进行消息广播。 - 同时如果有新的节点加入,还需要对新节点进行同步。
- 到了这个阶段,
ZAB提交事务并不像2PC一样需要全部Follower都ACK,只需要得到超过半数的节点的ACK就可以了。
ZAB协议JAVA实现(FLE-发现阶段和同步合并为Recovery Phase(恢复阶段))
协议的Java版本实现跟上面的定义有些不同,选举阶段使用的是Fast Leader Election(FLE),它包含了选举的发现职责。
因为FLE会选举拥有最新提议历史的节点作为Leader,这样就省去了发现最新提议的步骤。
实际的实现将发现阶段和同步合并为Recovery Phase(恢复阶段)。
所以,ZAB的实现只有三个阶段:Fast Leader Election、Recovery Phase、Broadcast Phase。
投票机制
每个Server首先给自己投票,然后用自己的选票和其他Server选票对比,权重大的胜出,使用权重较大的更新自身选票箱。
选举过程如下:
- 每个
Server启动以后都询问其它的Server它要投票给谁。对于其他Server的询问,Server每次根据自己的状态都回复自己推荐的Leader的id和上一次处理事务的Zxid(系统启动时每个Server都会推荐自己) - 收到所有
Server回复以后,就计算出Zxid最大的哪个Server,并将这个Server相关信息设置成下一次要投票的Server。 - 计算这过程中获得票数最多的的
Server为获胜者,如果获胜者的票数超过半数,则改Server被选为Leader。 否则,继续这个过程,直到Leader被选举出来 - 选举出来后,
Leader就会开始等待Server的连接 Follower连接Leader,将最大的Zxid发送给LeaderLeader根据Follower的Zxid确定同步点,至此选举阶段完成。- 选举阶段完成
Leader同步后通知Follower已经成为uptoDate状态 Follower收到uptoDate消息后,又可以重新接受客户端的请求进行服务了
假设目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动
选择举过程如下:
- 服务器
1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。 - 服务器
2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出, 但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。 - 服务器
3启动,给自己投票,同时与之前启动的服务器1,2交换信息, 由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数, 所以服务器3成为领导者,服务器1,2成为小弟。 - 服务器
4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息, 虽然服务器4的编号大,但服务器3已经成为领导者,所以服务器4只能成为小弟。 - 服务器
5启动,同服务器4一样流程,最后成为小弟。