zookeeper原理与简介

142 阅读15分钟

简介

Zookeeper(源码)是用Java实现的一个分布式协调服务,可用于分布式锁,分布式领导选举,配置管理,服务发现等。

Zookeeper提供了一个类似于Linux文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制。

众所周知,外国人喜欢给用一个动物作为吉祥物,在IT界也不例外。比如,负责大数据工作的Hadoop是一个黄色的大象;负责数据仓库的Hive是一个虚拟蜂巢;负责数据分析的Apache Pig是一头聪明的猪;负责管理web容器的tomcat是一只公猫……那好,负责分布式协调工作的角色就叫ZooKeeper(动物园饲养员)吧。

分布式协调组件:

  • Chubby:Google自用,完全实现paxos算法,不开源
  • Zookeeper:雅虎模仿Chubby开发的,并以开源的形式捐献给了Apache;使用zab协议(paxos算法的变种)

数据模型

ZooKeeper的数据结构,跟Unix文件系统非常类似,可以看做是一颗树,每个节点叫做ZNode,结构图如下: image.png Znode的引用方式是路径引用,类似于文件系统路径:/backend/zk。这样的层级结构,让每个Znode节点都拥有唯一的路径,就像命名空间一样对不同信息作出清晰的隔离。

数据持久化

与文件系统不同的是,Znode节点都可以设置关联的数据。(linux只有文件节点可以关联数据,目录节点不可以)

zk为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得zk不能用于存放大量的数据,每个节点的存放数据上限为1M。可见,zk最适于读多写少且轻量级数据(默认设置下单个dataNode限制为1MB大小)的应用场景。

数据仅存储在内存是很不安全的,zk提供了两种持久化机制:

  • 事务日志

    • zk把执行的命令以日志形式保存在dataLogDir指定的路径中的文件中(如果没有指定 dataLogDir,则按dataDir指定的路径)。
  • 数据快照

    • 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(命令行界面)

参考: www.w3cschool.cn/zookeeper/z…

安装(略)

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 指定的版本号一致时,才能删除成功

权限设置

  1. 注册当前会话的账号和密码:addauth digest xiaowang:123456

  2. 创建节点并设置权限:create /test-node abcd auth:xiaowang:123456:cdwra

  3. 在另一个会话中必须先使用账号密码,才能拥有操作该节点的权限

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选主

参考:

www.cnblogs.com/ibigboy/p/1…

www.jasongj.com/zookeeper/f…

  1. ZAB协议定义了四种服务器节点状态:

    1. Looking :选举状态。
    2. Following :Follower 节点(从节点)所处的状态。
    3. Leading :Leader 节点(主节点)所处状态。
    4. Observing:观察者节点所处的状态。
  2. 选票的数据结构

    1. id:被推举的Leader的myid(集群中的每个zk节点启动前就要配置好这个全局唯一的ID)。
    2. zxid:被推举的Leader事务ID
    3. electionEpoch:逻辑时钟,用来判断多个投票是否在同一轮选举周期中,该值在服务端是一个自增序列,每次进入新一轮的投票后,都会对该值进行加1操作。
    4. peerEpoch:被推举的Leader的epoch。
    5. state:当前服务器的状态。

  1. 选票PK规则:若选举轮次一致,对比两张选票的ZXID、myid(先对比ZXID(事务id越大数据量越充足)、再对比myid),选出获胜的选票

  2. 投票箱

    1. 与人类选举投票箱稍微有点不一样,ZooKeeper 集群会在每个节点的内存中维护一个投票箱。节点会将自己的选票以及其他节点的选票都放在这个投票箱中。由于选票是互相传阅的,所以最终每个节点投票箱中的选票会是一样的。
  3. 选票统计规则(过半机制):每轮投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息

集群上线时

节点进入选举阶段后的大体执行逻辑如下:

  1. 变更状态:Zookeeper集群中的非Observer节点都会将自己的状态变更为LOOKING,然后进入Leader选举过程

  2. 第一轮投票

    1. 每个服务器都会初始化自身的选票,并且在初始化阶段,每台服务器都会将自己推举为Leader
    2. 广播发送自己的选票给集群中的其他节点(两两互相发送)
    3. 自身的初始化选票 和 收到的外部选票进行PK,选出获胜的选票
    4. 变更投票:经过PK后,若确定了外部选票优于内部选票,那么就变更投票,即外部投票的选票信息来覆盖内部投票(投票箱中的选票更新);并更新初始化选票。若内部选票PK赢,则不更新
    5. 统计选票:判断是否过半,若过半则终止选主过程。否则进入下一轮投票
  3. 第二轮投票

    1. 广播发送自己的选票(经过一轮之后,更新过的选票信息)
    2. 选票PK -> 投票
    3. 统计选票:判断是否过半
  4. 不断的开启新一轮的投票,直至选出leader节点为止。

  5. leader节点状态变更为Leading,其他非observer节点状态变更为Following

  6. 如果集群中已经存在Leader(此种情况一般都是某台机器启动得较晚),新机器试图去选举Leader时,会被告知当前集群的Leader信息,对于该机器而言,仅仅需要和Leader机器建立起连接,并进行状态同步即可。

崩溃恢复时

集群服务器各节点之间是有通信端口的,Learner(follower + observer)会和Leader之间建立socket连接。Leader周期性地不断向Follower发送心跳(ping命令,没有内容的socket)。

当Leader崩溃后,Follower发现socket通道已关闭,于是Follower开始进入到Looking状态,重新回到上一节中的Leader选举过程,此时集群不能对外提供服务

主从数据同步:写操作强一致性,读操作顺序一致性

www.jianshu.com/p/fb11cfbfa…

www.cnblogs.com/juniorMa/p/…

二段提交:

主收到写操作时,先本地生成事务为事务生成zxid,然后发给所有follower节点。

当follower收到事务时,先把提议事务的日志写到本地磁盘,成功后返回给leader。

leader收到过半反馈后对事务提交。再通知所有的follower提交事务,follower收到后也提交事务,提交后就可以对客户端进行分发了。

应用

分布式锁

详细文章:juejin.cn/post/715901…

锁的种类

读锁:大家都可以读,要想上读锁的前提:之前的锁没有写锁 写锁:只有得到写锁的才能写。要想上写锁的前提是,之前没有任何锁。

先看没有链式监听,会有羊群效应的锁实现方式:

zk实现读锁

  1. 创建一个临时序号节点,节点数据的前缀是read,表示是读锁

  2. 获取当前zk中序号比自己小的所有节点

  3. 判断最小节点(根据序号排序)是否是读锁:

    1. 如果是读锁的话,则上锁成功
    2. 如果不是读锁的话,则上锁失败,为最小节点设置监听。阻塞等待,zk的watch机制 会当最小节点发生变化时通知当前节点,于是再执行第二步的流程

zk实现写锁

  1. 创建一个临时序号节点,节点数据的前缀是write,表示是 写锁

  2. 获取zk中所有的子节点

  3. 判断自己是否是最小的节点:

    1. 如果是,则上写锁成功
    2. 如果不是,说明前面还有锁,则上锁失败,监听最小的节点,如果最小节点有变化,则回到第二步。

羊群效应

如果用上述的监听方式,只要有节点发生变化,就会触发其他节点的监听事件,这样的话对

zk的压力非常大。链式监听可以解决这个问题。

分布式锁(互斥):本质就是写锁的逻辑

特点:

  1. 锁自旋,若获取锁失败,则会一直等待前一个锁释放
  2. 顺序解锁,可保证按加锁先后顺序进行解锁

github.com/go-zookeepe…

扩展阅读:分布式锁proposal

服务注册与发现

但是通常都不会用zk去做服务发现,而是用consul去做。

阿里巴巴为什么不用 ZooKeeper 做服务发现?

配置管理

程序分布式的部署在不同的机器上,将程序的配置信息放在zk的znode下,当有配置发生改变时,也就是znode发生变化时,可以通过改变zk中某个目录节点的内容,利用watcher通知给各个客户端,从而更改配置。

参考

分布式架构之Zookeeper

29道Zookeeper面试题超详细(附答案)

Zookeeper 基础篇

tech.bytedance.net/videos/set/…