Zookeeper

logo

ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Hadoop 和 Hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

一、Zookeeper基础

1.1 使用场景

  • 分布式协调组件:通过 watch 机制可以协调好节点之间的数据一致性
  • 分布式锁:通过分布式锁可以做到强一致性
  • 无状态化实现
  • 负载均衡
  • 数据发布/订阅
  • 命名服务

1.2 部署

docker-compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
version: '3.8'
services:
zookeeper:
container_name: zk01
image: zookeeper:3.7.0
restart: always
hostname: zk01
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zk01:2888:3888;2181

zoo.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dataDir=/data
dataLogDir=/datalog
# 基本时间配置(毫秒)
tickTime=2000
# 初始化连接到leader的最大时长,单位为倍数,即初始化时间为tickTime * initLimit
initLimit=5
# follower与leader数据同步的最大时长
syncLimit=2
# 保存数据的快照数量
autopurge.snapRetainCount=3
# 自动触发清除任务时间间隔,以小时为单位,默认为0,表不清除
autopurge.purgeInterval=0
# 客户端与zk的最大并发连接数
maxClientCnxns=60
# 开启standaloneEnabled模式,即独立部署
standaloneEnabled=true
# 开启adminServer
admin.enableServer=true
# 2181是为客户端提供的端口
server.1=zk01:2888:3888;2181

1.3 基本命令

  1. 启动|关闭|查看状态
1
zkServer.sh start|stop|status
  1. 进入 zk
1
zkCli.sh
  1. 查看内部数据结构
1
ls [path]

二、内部数据结构

2.1 是如何存储数据的

Zookeeper 中的数据是保存在节点上的,即 znode,多个 znode 就构成一个树的结构。

zk数据存储结构

如图,a 和 b 就是 Zookeeper 的 znode,创建 znode 方式如下

1
2
3
4
5
create /[znode_name]
# 创建节点并创建一个数据
create /[znode_name] [data_name]
# 获取数据
get [znode_name]

2.2 znode结构

Zookeeper 中的 zonode,包含以下几个部分:

  • data:保存数据
  • acl:权限
    • c:创建权限
    • w:写权限
    • r:读权限
    • d:删除权限
    • a:admin 管理者权限
  • stat:描述当前 znode 的元数据
  • child:当前节点的子节点
1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看znode详细信息
get -s /[znode_name]
cZxid:创建节点的事务ID
ctime:创建节点时间
mZxid:修改节点的事务ID
mtime:修改节点时间
pZxid:添加和删除子节点的事务ID
cversion:当前节点的子节点版本号,初始值为-1,每对该节点的子节点进行操作,这个cversion都会自动增加
dataVersion:数据版本初识版本为0,每对该节点的数据进行操作,这个dataVersion都会自动增加
aclVersion:权限版本
ephemeralOwne:如果当前节点是临时节点,该值是当前节点的session id,如果不是临时节点则为0
dataLength:数据长度
numChildren:该节点的子节点个数

2.3 znode类型

  • 持久节点:在会话结束后仍会存在
  • 持久序号节点:根据先后顺序,会在结点之后带上一个数值,适用于分布式锁的场景(单调递增)
  • 临时节点:会话结束后会自动删除,适用于注册与服务发现的场景
  • 临时序号节点:跟持久序号节点相同,适用于分布式锁的场景
  • 容器节点:当容器节点中没有任何子节点时,该容器节点会被定期删除(60s)
  • TTL 节点:可以指定节点的到期时间

持久序号节点创建

1
create -s /[znode_name]

临时节点创建

1
create -e /[znode_name]

临时序号节点创建

1
create -e -s /[znode_name]

容器节点创建

1
create -c /[znode_name]

TTL 节点创建

1
2
# 通过系统配置开启
zookeeper.extendedTypesEnabled=true

持久与临时节点

持久节点

持久节点在创建后服务端会发送一个 session id,并一直保留着。

临时节点

临时节点在创建时后服务器也会发送一个 session id,在会话持续的过程中客户端会不断向服务端续约 session id 的时间,当客户端没有继续续约,而服务端内部的计时器到期时,就会将该 session id 所对应的 znode 全部删除。

2.4 持久化机制

Zookeeper 的数据是运行在内存中的,所以提供了两种持久化机制:

  • 事务日志:Zookeeper 将执行过的命令以日志的形式存储在 dataLogDir / dataDir 中,类似于 redis 的 AOF
  • 数据快照:在一定时间间隔内做一次数据快照,存储在快照文件中(snapshot),类似于 redis 的RDB

Zookeeper 通过这两种持久化机制,在恢复数据时先将快照文件中的数据恢复到内存中,再用日志文件中的数据做增量恢复,可以实现高效的持久化。

三、zkCli的使用

  1. 递归查询
1
ls -R /[znode_name]
  1. 删除节点
1
deleteall /[znode_name]
  1. 乐观锁删除
1
delete -v [version] /[znode_name]
  1. 给当前会话注册用户,并创建节点赋予该用户权限
1
2
addauth digest [user]:[password]
create /[znode_name] auth:[user]:[password]:[privileges]

四、分布式锁

在分布式的环境下,如果在一个节点去上了个锁,当请求被负载均衡分配到了其它节点,那么锁就无法形成互斥,所以节点之间使用 Zookeeper,做一个协调中心,将锁上传到 Zookeeper,其它节点要用到就去 Zookeeper 拿这个锁,这就是分布式锁。

Zookeeper 锁的分类:

  • 读锁:大家都可以读,前提是之前没有写锁。(读锁比喻成约会,大家都有机会和女神约会,约会前提是女神没结婚)
  • 写锁:只有写锁才能写,前提是不能有任何锁。(写锁比喻成结婚,结婚后只有老公能和女神约会,结婚前提是女神和其他人的关系断干净了)

4.1 上读锁

  • 创建一个临时序号节点,节点数据是 read,表示为读锁
  • 获取当前 Zookeeper 中序号比自己小的所有节点
  • 判断最小节点是否为读锁:
    • 如果是读锁:则上锁失败,因为如果最小节点是读锁,那么后面就不可能有写锁,接着为最小节点设置监听,Zookeeper 的 watch 机制会在最小节点发生变化时通知当前节点,再进行后面的步骤,被称为阻塞等待
    • 如果不是读锁:则上锁成功

4.2 上写锁

  • 创建一个临时序号节点,节点数据是 write,表示为写锁
  • 获取 Zookeeper 中的所有节点
  • 判断自己是否为最小节点:
    • 如果是:上锁成功
    • 如果不是:说明前面还有锁,所以上锁失败,接着监听最小节点,如果最小节点发生变化,则重新进行第二步

羊群效应

假设有一百个请求都是要去写锁,那么就会有一百个请求去监听最小节点,那么 Zookeeper 的压力就会非常大,解决方法是将这一百个请求按请求顺序排列,后一个请求去监听前一个请求即可,实现链式监听。

4.3 watch机制

Zookeeper 的 watch 可以看作是一个触发器,当监控的 znode 发生改变,就会触发 znode 上注册的对应事件,请求 watch 的客户端就会接收到异步通知。

zkCli.sh 中使用 watch

1
2
3
4
5
6
7
create /test
# 一次性监听,监听节点内容
get -w /test
# 监听目录,但所监听节点下创建和删除子节点不会触发监听
ls -w /test
# 与上面相对,都会触发监听
ls -R -w /test

五、集群部署

Zookeeper 的集群角色有三个:

  • Leader:处理集群所有事务的请求,集群只有一个 Leader
  • Follower:只处理读请求,参与 Leader 选举
  • Observer:只处理读请求,提升集群的性能,但不能参与 Leader 选举

docker-compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
version: '3.8'
services:
zk01:
container_name: zk01
image: zookeeper:3.7.0
restart: always
hostname: zk01
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
# 2888:用于集群内zk之间的通信
# 3888:用于选举投票
# 2181:客户端使用
# 要创建observer则在2181端口后加:observer
ZOO_SERVERS: server.1=zk01:2888:3888;2181 server.2=zk02:2888:3888;2181 server.3=zk03:2888:3888;2181

zk02:
container_name: zk02
image: zookeeper:3.7.0
restart: always
hostname: zk02
ports:
- 2182:2181
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zk01:2888:3888;2181 server.2=zk02:2888:3888;2181 server.3=zk03:2888:3888;2181

zk03:
container_name: zk03
image: zookeeper:3.7.0
restart: always
hostname: zk03
ports:
- 2183:2181
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zk01:2888:3888;2181 server.2=zk02:2888:3888;2181 server.3=zk03:2888:3888;2181

通过命令查看节点角色

1
2
zkServer.sh status
Mode: leader

连接集群

1
zkCli.sh -server zk01:2181,zk02:2181,zk03:2181

5.1 ZAB协议

ZAB(Zookeeper Atomic Broadcast)即 Zookeeper 原子广播协议,通过这个协议解决了集群数据一致性和崩溃恢复的问题。

ZAB 协议中节点的四种状态

  • Looking:选举状态
  • Following
  • Leading
  • Observing

初始化集群时 leader 的选举

  • 当集群中两台节点启动时,就会开始 leader 的选举,选票的格式为 (myid,zXid)
  • 第一轮投票时,每个节点会生成自己的选票,即自己的 (myid,zXid),然后将选票给到对方,这时候每个节点就会有两张选票,即自己的和对方节点的
  • 接着就会比较两张选票的 zXid,如果都相同就对比 myid,将大的一票投到投票箱中
  • 第二轮投票时,每个节点会将上一轮投出去的选票给到其它节点,然后再对比 (myid,zXid),将大的一票投出去,就能够选出 leader
  • 后来新启动的节点会发现已经有 leader了,就不用做选举的过程了
  • 可以看出初始化集群时,leader 的选举主要看 myid

崩溃恢复时的 leader 选举

在 leader 确定了之后,leader 会周期性地向 follower 发送心跳包,当 follower 没有收到 leader 发送过来的心跳包,就会进入选举过程,这时候集群不能对外提供服务。

  • 当 leader 挂了之后,follower 的状态会变成 looking
  • 接着就进行选举投票,过程和初始化集群时一样

5.2 主从同步原理

主从同步原理

5.3 NIO和BIO

NIO

用于被客户端连接的 2181 端口,使用的就是 NIO 的连接模式;客户端开启 watch 时,使用的也是 NIO。

BIO

集群在进行选举时,多个节点之间的通信端口,使用的是 BIO 的连接模式。