zookeeper文件数据结构
zookeeper维护了一种类似文件系统的数据结构:
节点类型:
-
PERSISTENT:持久化目录节点
客户端与服务端断开连接后,该节点不会被清除,除非手动删除节点,不然会一直存在。 -
PERSISTENT_SEQUENTIAL:持久化顺序编号目录节点
创建节点时,zookeeper会给该节点自己标上序号,其他的与PERSISTENT一样。 -
EPHEMERAL:临时目录节点
客户端与服务端断开连接后,节点会被清除。 -
EPHEMERAL_SEQUENTIAL:临时顺序编号目录节点
创建节点时,zookeeper会给该节点自己标上序号,其他的与EPHEMERAL一样。 -
Container:容器节点
如果该节点没有子节点,则会删除该节点。定时任务默认60s检查一次。 -
TTL 节点
创建该节点后,过了指定的时间后,zookeeper会自动清楚该节点。
默认情况下是禁止的,需要配置 zookeeper.extendedTypesEnabled=true开启。
监听通知机制
在zookeeper,事件监听可分为三种:
- 监听目录
会监听目录的变化,如目录的增、删,而不是节点的值的变化。
命令:ls -w /path - 监听节点
会监听节点的变化,就是节点的值的变化,节点的路径变化不会监听。
命令:get -w /path - 监听目录的递归子节点
对目录的所有子节点以及节点的值进行监听,如果子节点发生目录结构的变化(增、删)或者节点的值进行改变,都会监听到。这种情况下,对于每个子节点的目录变化都会响应一次。
命令:ls -R -w /path
需要注意的是,对于节点或目录的监听都是一次性的,如果需要保持持续监听,需要在响应后再执行一次监听命令。
zookeeper使用场景
分布式锁
这种方式在并发高的情况下,性能会很低,因为当监听的节点被删除时,zookeeper会通知所有的客户端,收到通知的客户端会同时去竞争,但是只会有一个客户端获取到锁,这就是羊群效应。
想要避免的话,就要采取下面这种办法:
获取锁的过程:
- 客户端尝试在/lock下创建一个临时顺序节点
- 判断在/lock下创建的节点是否是最小的节点;
如果是,则成功获取到锁;
如果不是,则监听前面的节点。当前面的节点被删除时,会收到通知,再重复2步骤。 - 删除创建的临时节点,后面的客户端收到通知。
类似的用法还可以实现读写锁:
读写加锁时,要判断前面的节点是否有写锁,如果有写锁,则监听最近的写锁节点;
写锁加锁时,要判断前面的节点是否有加锁,如果有加锁,则监听最近的加锁节点。
注册中心
ZAB协议
ZAB 协议全称:Zookeeper Atomic Broadcast(Zookeeper 原子广播协议)。
整个Zookeeper 就是一个分布式一致性算法的实现,底层协议用的是ZAB协议。
ZAB协议是为分布式协调服务 zookeeper 专门设计的,支持 崩溃恢复 的 原子广播协议。
基于该协议,zookeeper实现了一种 主备模式 的系统架构来保持集群间数据的一致性。
下面开始介绍 消息广播 和 崩溃恢复。
消息广播
整个 消息广播过程如下:
- 客户端往集群leader发送一条写命令;
- leader收到命令后:
2.1 将写命令封装成一个事务proposal,放入一个队列里,发送给所有follower;
2.2 leader写本地事务文件;
2.3 leader给自己发送一个ack信号; - follower收到leader发过来的proposal后,会以事务日志的方式存入本地磁盘,然后给leader发送一个ack信号;
- leader收到半数以上的follower的ack信号后,认为消息发送成功,然后发送commit信号给所有的follower,同时完成自己的事务提交;
- follower收到commit信号,完成事务提交。
有一些需要注意的地方:
- Leader收到客户端的请求后,会将请求封装成一个proposal事务,并给proposal事务分配一个全局唯一的递增ID,称为事务ID(ZXID),zookeeper要保证事务必须按照顺序依次执行,所以把它放入一个先进先出的队列中;
- 为了保证数据的一致性,只有Leader能处理客户端的写请求,Follower只能处理读请求,就算Follower收到了写请求,也会转发给Leader;
- ZAB协议规定了一个事务在一台机器中commit了,也就认为这个事务在所有的机器都能成功,即使这台机器崩溃了。
崩溃恢复
按照上诉的过程,不得不思考两个问题:
1.如果leader把proposal发给所有leader后,如果leader挂了怎么办?
2.如果follow在收到ack后提交了自己的事务,发送给commit时leader挂了怎么办?
针对这两个问题,ZAB会有两种行动原则:
1.ZAB协议会丢弃那些只在leader提出,但还没commit的事务;
2.ZAB协议会保证那些在leader中commit的事务在所有的机器中都commit。
所以,在进行leader的崩溃恢复时,ZAB协议会确保选出来的leader具有以下两个条件:
1.不能包含未提交的proposal;
2.含有集群中最大的事务ID(ZXID),这样的目的是保证新选出来的leader含有所以已提交的proposal。
数据同步
崩溃恢复完成后,leader会确保事务被过半数的follower提交了,即完成了数据同步。当follower同步完所有的数据后,就会加入到可用服务列表当中。
ZXID
ZXID被设计成了一个64位的数字。
低32位是一个简单的递增计数器,每当完成一个客户端的事务proposal请求,该值就会+1;
高32位代表了从本地事务日志取出的最大事务的ZXID,并从该值解析出epoch,epoch代表了该机器的选举周期,每进行一轮选举,epoch+1。
当完成一轮选举后,除了epoch会加1,低32位的事务id会清零,然后从0开始计数。
通过这种巧妙的设计,高32位代表了每一代leader的唯一性,低32位代表了每一代leader中事务的唯一性。简化了数据恢复流程。