简介
Zookeeper(源码)是用Java实现的一个分布式协调服务,可用于分布式锁,分布式领导选举,配置管理,服务发现等。
Zookeeper提供了一个类似于Linux文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制。
- 名字由来 the tao of Zookeeper
众所周知,外国人喜欢给用一个动物作为吉祥物,在IT界也不例外。比如,负责大数据工作的Hadoop是一个黄色的大象;负责数据仓库的Hive是一个虚拟蜂巢;负责数据分析的Apache Pig是一头聪明的猪;负责管理web容器的tomcat是一只公猫……那好,负责分布式协调工作的角色就叫ZooKeeper(动物园饲养员)吧。
分布式协调组件:
- Chubby:Google自用,完全实现paxos算法,不开源
- Zookeeper:雅虎模仿Chubby开发的,并以开源的形式捐献给了Apache;使用zab协议(paxos算法的变种)
数据模型
ZooKeeper的数据结构,跟Unix文件系统非常类似,可以看做是一颗树,每个节点叫做ZNode,结构图如下:
Znode的引用方式是路径引用,类似于文件系统路径:
/backend/zk。这样的层级结构,让每个Znode节点都拥有唯一的路径,就像命名空间一样对不同信息作出清晰的隔离。
数据持久化
与文件系统不同的是,Znode节点都可以设置关联的数据。(linux只有文件节点可以关联数据,目录节点不可以)
zk为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得zk不能用于存放大量的数据,每个节点的存放数据上限为1M。可见,zk最适于读多写少且轻量级数据(默认设置下单个dataNode限制为1MB大小)的应用场景。
数据仅存储在内存是很不安全的,zk提供了两种持久化机制:
-
事务日志
- zk把
执行的命令以日志形式保存在dataLogDir指定的路径中的文件中(如果没有指定 dataLogDir,则按dataDir指定的路径)。
- zk把
-
数据快照
- zk会在一定的时间间隔内做一次内存数据的快照,把该时刻的内存数据保存在快照文件 中。
这两种持久化方式默认都开启,保障数据在不丢失的情况下能快速恢复(在恢复时先恢复快照文件中的数据到内存中,再用日志文件中的数据做增量恢复,这样的恢复速度更快)。
Znode
节点信息
zk中的znode,包含了四部分核心数据:
-
data: 该节点存储的数据
-
acl:权限,定义了什么样的用户能够操作这个节点,且能够进行怎样的操作。
- c: create 创建权限,允许在该节点下创建子节点
- w:write 更新权限,允许更新该节点的数据
- r:read 读取权限,允许读取该节点的内容以及子节点的列表信息
- d:delete 删除权限,允许删除该节点的子节点
- a:admin 管理者权限,允许对该节点进行acl权限设置
-
stat:描述当前znode的元数据
-
czxid: 创建节点的事务ID
-
mzxid:修改节点的事务ID
-
pzxid:添加和删除子节点的事务ID
-
ctime:节点创建的时间
-
mtime: 节点最近修改的时间
-
版本:每更新一次数据,版本会+1
- version: 当前数据节点数据内容的版本号
- cversion: 当前数据子节点的版本号
- aversion: 当前数据节点ACL变更版本号
-
ephemeralOwner: 如果当前节点是临时节点,该值是当前节点所有者的session id。如果节点不是临时节点,则该值为零。
-
-
children: 当前节点的子节点
节点类型
- PERSISTENT 持久节点: 创建出的节点,在会话结束后依然存在。
- PERSISTENT_SEQUENTIAL 持久序号节点: 创建出的节点,根据先后顺序,会在节点之后带上一个数值,越后执行数值越大,适用于分布式锁的应用场景- 单调递增
- EPHEMERAL 临时节点:临时节点是在会话结束后,自动被删除的,通过这个特性,zk可以实现服务注册与发现的效果。那么临时节点是如何维持心跳呢?
-
EPHEMERAL_SEQUENTIAL 临时序号节点:跟持久序号节点相同,适用于临时的分布式锁。
-
Container节点(3.5.3版本新增):Container容器节点,当容器中没有任何子节点,该容器节点会被zk定期删除(60s)。
-
TTL节点:可以指定节点的到期时间,到期后被zk定时删除。只能通过系统配zookeeper.extendedTypesEnabled=true 开启
使用-CLI(命令行界面)
安装(略)
cloud.bytedance.net/zk/service
Help
[zkshell: 1] help
# a sample one
[zkshell: 2] h
ZooKeeper -server host:port cmd args
addauth scheme auth
close
config [-c] [-w] [-s]
connect host:port
create [-s] [-e] [-c] [-t ttl] path [data] [acl]
delete [-v version] path
deleteall path
delquota [-n|-b|-N|-B] path
get [-s] [-w] path
getAcl [-s] path
getAllChildrenNumber path
getEphemerals path
history
listquota path
ls [-s] [-w] [-R] path
printwatches on|off
quit
reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
redo cmdno
removewatches path [-c|-d|-a] [-l]
set [-s] [-v version] path data
setAcl [-s] [-v version] [-R] path acl
setquota -n|-b|-N|-B val path
stat [-w] path
sync path
version
创建节点
用给定的路径创建一个znode。语法如下:
create /path data
持久节点
默认情况下,所有znode都是持久的。
[zk: localhost:2181(CONNECTED) 0] create /FirstZnode “Myfirstzookeeper-app"
Created /FirstZnode
持久序号节点
要创建序号节点,请添加flag:-s
[zk: localhost:2181(CONNECTED) 2] create -s /FirstZnode “second-data"
Created /FirstZnode0000000023
临时节点
要创建临时节点,请添加flag:-e
[zk: localhost:2181(CONNECTED) 2] create -e /SecondZnode “Ephemeral-data"
Created /SecondZnode
当会话过期或客户端断开连接时,临时节点(flag:-e)将被自动删除。
临时序号节点
[zk: localhost:2181(CONNECTED) 2] create -e -s /FirstZnode “second-data"
Created /FirstZnode0000000024
创建子节点
创建子节点类似于创建新的znode。唯一的区别是,子znode的路径也将具有父路径。
[zk: localhost:2181(CONNECTED) 16] create /FirstZnode/Child1 “firstchildren"
created /FirstZnode/Child1
[zk: localhost:2181(CONNECTED) 17] create /FirstZnode/Child2 “secondchildren"
created /FirstZnode/Child2
Container节点
语法:create -c /path,当容器中没有任何子节点,该容器节点会被zk定期删除(60s)
查询节点
获取节点详细数据(data + stat)
语法:get /path
[zk: localhost:2181(CONNECTED) 1] get /FirstZnode
“Myfirstzookeeper-app"
cZxid = 0x7f
ctime = Tue Sep 29 16:15:47 IST 2015
mZxid = 0x7f
mtime = Tue Sep 29 16:15:47 IST 2015
pZxid = 0x7f
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 22
numChildren = 0
获取节点元数据(stat)
语法:stat /path
[zk: localhost:2181(CONNECTED) 1] stat /FirstZnode
cZxid = 0x7f
ctime = Tue Sep 29 16:15:47 IST 2015
mZxid = 0x7f
mtime = Tue Sep 29 17:14:24 IST 2015
pZxid = 0x7f
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 23
numChildren = 0
列出子项
语法:ls /path
[zk: localhost:2181(CONNECTED) 2] ls /MyFirstZnode
[mysecondsubnode, myfirstsubnode]
删除节点
递归删除
语法:rmr /path,移除指定的znode并递归其所有子节点。
[zk: localhost:2181(CONNECTED) 10] rmr /FirstZnode
[zk: localhost:2181(CONNECTED) 11] get /FirstZnode
Node does not exist: /FirstZnode
删除单个节点
语法:delete /path,只能删除没有子节点的Znode
乐观锁删除节点
语法:delete -v 版本号 /path,当该节点的元数据stat中的version和-v 指定的版本号一致时,才能删除成功
权限设置
-
注册当前会话的账号和密码:
addauth digest xiaowang:123456 -
创建节点并设置权限:
create /test-node abcd auth:xiaowang:123456:cdwra -
在另一个会话中必须先使用账号密码,才能拥有操作该节点的权限
watch订阅
我们可以把 Watch 理解成是注册在特定 Znode 上的触发器。当这个 Znode 发生改变,也就 是调用了 create , delete , setData 方法的时候,将会触发 Znode 上注册的对应事件, 请求 Watch 的客户端会接收到异步通知。
具体交互过程如下:
- 客户端调用 getData 方法,watch 参数是 true。服务端接到请求,返回节点数据,并 且在对应的哈希表里插入被 Watch 的 Znode 路径,以及 Watcher 列表。
- 当被 Watch 的 Znode 已删除,服务端会查找对应的哈希表中的Entry,找到该 Znode 对应的所有 Watcher,异步通知客户端,并且删除哈希表中对应的 Key-Value。
客户端使用了NIO通信模式监听服务端的调用。
使用 CLI :
create /test xxx
get -w /test 一次性监听节点
ls -w /test 监听目录,创建和删除子节点会收到通知。子节点中新增节点不会收到通知
ls -R -w /test 对于子节点中子节点的变化,但内容的变化不会收到通知
👇 只通知类型,不通知新数据
官方声明:一个Watch事件是一个一次性的触发器(也就是下次改变不会再发送通知了),当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们。
为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。
集群架构
zk集群由多个节点组成,节点的角色类型有leader、follower和observer。
follower及observer统称learner,learner需要同步leader的数据。
Zookeeper是怎么处理读写请求的: www.cnblogs.com/juniorMa/p/…
| 角色 | 备注 |
|---|---|
| leader | - 集群中有且仅有一个leader,通过leader选举过程产生。 |
- 负责所有事务的写操作(会话状态变更以及节点变更操作),保证集群事务处理的顺序性。默认设置下,也能处理读请求。 | | follower | - 处理客户端非事务性请求,转发事务请求给leader节点。
- 参与leader选举投票,参与事务操作的“过半通过”投票策略 | | observer | - 只提供读取服务,在不影响写性能的情况下提升集群读取性能。(并非上图的Client)
- 不参与任何形式的投票 |
读操作不是强一致性,只能保证 顺序一致性,即读到新版本数据,重新获取,不会回退到旧版本数据。
集群的配置信息举例:👇
zoo.cfg
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# 修改对应的zk1 zk2 zk3 zk4
dataDir=/usr/local/zookeeper/zkdata/zk1
# 修改对应的端口 2181 2182 2183 2184
clientPort=2181
# 2001为集群通信端口,3001为集群选举端口,observer表示不参与集群选举
server.1=172.16.253.54:2001:3001
server.2=172.16.253.54:2002:3002
server.3=172.16.253.54:2003:3003
server.4=172.16.253.54:2004:3004:observer
ZAB协议? 待完善
raft vs. zab www.jianshu.com/p/24307e7ca…
ZAB(Zookeeper Atomic Broadcast)协议,这个协议解决了Zookeeper集群的Leader选主和主从数据同步的问题。
Leader选主
参考:
-
ZAB协议定义了四种服务器节点状态:
- Looking :选举状态。
- Following :Follower 节点(从节点)所处的状态。
- Leading :Leader 节点(主节点)所处状态。
- Observing:观察者节点所处的状态。
-
选票的数据结构
- id:被推举的Leader的myid(集群中的每个zk节点启动前就要配置好这个全局唯一的ID)。
- zxid:被推举的Leader事务ID。
- electionEpoch:逻辑时钟,用来判断多个投票是否在同一轮选举周期中,该值在服务端是一个自增序列,每次进入新一轮的投票后,都会对该值进行加1操作。
- peerEpoch:被推举的Leader的epoch。
- state:当前服务器的状态。
-
选票PK规则:若选举轮次一致,对比两张选票的ZXID、myid(先对比ZXID(事务id越大数据量越充足)、再对比myid),选出获胜的选票
-
投票箱
- 与人类选举投票箱稍微有点不一样,ZooKeeper 集群会在每个节点的内存中维护一个投票箱。节点会将自己的选票以及其他节点的选票都放在这个投票箱中。由于选票是互相传阅的,所以最终每个节点投票箱中的选票会是一样的。
-
选票统计规则(过半机制):每轮投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息
集群上线时
节点进入选举阶段后的大体执行逻辑如下:
-
变更状态:Zookeeper集群中的非Observer节点都会将自己的状态变更为LOOKING,然后进入Leader选举过程
-
第一轮投票
- 每个服务器都会初始化自身的选票,并且在初始化阶段,每台服务器都会将自己推举为Leader
- 广播发送自己的选票给集群中的其他节点(两两互相发送)
- 自身的初始化选票 和 收到的外部选票进行PK,选出获胜的选票
- 变更投票:经过PK后,若确定了外部选票优于内部选票,那么就变更投票,即外部投票的选票信息来覆盖内部投票(投票箱中的选票更新);并更新初始化选票。若内部选票PK赢,则不更新
- 统计选票:判断是否过半,若过半则终止选主过程。否则进入下一轮投票
-
第二轮投票
- 广播发送自己的选票(经过一轮之后,更新过的选票信息)
- 选票PK -> 投票
- 统计选票:判断是否过半
-
不断的开启新一轮的投票,直至选出leader节点为止。
-
leader节点状态变更为Leading,其他非observer节点状态变更为Following
-
如果集群中已经存在Leader(此种情况一般都是某台机器启动得较晚),新机器试图去选举Leader时,会被告知当前集群的Leader信息,对于该机器而言,仅仅需要和Leader机器建立起连接,并进行状态同步即可。
崩溃恢复时
集群服务器各节点之间是有通信端口的,Learner(follower + observer)会和Leader之间建立socket连接。Leader周期性地不断向Follower发送心跳(ping命令,没有内容的socket)。
当Leader崩溃后,Follower发现socket通道已关闭,于是Follower开始进入到Looking状态,重新回到上一节中的Leader选举过程,此时集群不能对外提供服务。
主从数据同步:写操作强一致性,读操作顺序一致性
二段提交:
主收到写操作时,先本地生成事务为事务生成zxid,然后发给所有follower节点。
当follower收到事务时,先把提议事务的日志写到本地磁盘,成功后返回给leader。
leader收到过半反馈后对事务提交。再通知所有的follower提交事务,follower收到后也提交事务,提交后就可以对客户端进行分发了。
应用
分布式锁
锁的种类
读锁:大家都可以读,要想上读锁的前提:之前的锁没有写锁 写锁:只有得到写锁的才能写。要想上写锁的前提是,之前没有任何锁。
先看没有链式监听,会有羊群效应的锁实现方式:
zk实现读锁
-
创建一个临时序号节点,节点数据的前缀是read,表示是
读锁 -
获取当前zk中序号比自己小的所有节点
-
判断最小节点(根据序号排序)是否是读锁:
- 如果是读锁的话,则上锁成功
- 如果不是读锁的话,则上锁失败,为最小节点设置监听。阻塞等待,zk的watch机制 会当最小节点发生变化时通知当前节点,于是再执行第二步的流程
zk实现写锁
-
创建一个临时序号节点,节点数据的前缀是write,表示是
写锁 -
获取zk中所有的子节点
-
判断自己是否是最小的节点:
- 如果是,则上写锁成功
- 如果不是,说明前面还有锁,则上锁失败,监听最小的节点,如果最小节点有变化,则回到第二步。
羊群效应
如果用上述的监听方式,只要有节点发生变化,就会触发其他节点的监听事件,这样的话对
zk的压力非常大。链式监听可以解决这个问题。
分布式锁(互斥):本质就是写锁的逻辑
特点:
- 锁自旋,若获取锁失败,则会一直等待前一个锁释放
- 顺序解锁,可保证按加锁先后顺序进行解锁
扩展阅读:分布式锁proposal
服务注册与发现
但是通常都不会用zk去做服务发现,而是用consul去做。
配置管理
程序分布式的部署在不同的机器上,将程序的配置信息放在zk的znode下,当有配置发生改变时,也就是znode发生变化时,可以通过改变zk中某个目录节点的内容,利用watcher通知给各个客户端,从而更改配置。