ZooKeeper基础

概述

ZooKeeper是一个开源的,分布式的,为分布式系统提供协调服务的Apache项目。

分布式系统的出现主要是为了解决单机不可靠和可扩展性的问题。但其自身也存在一些问题,与单机系统相比,分布式系统没有一个全局的主控,协调或控制中心,存在内存地址不一致的问题,且其运行时出现问题的概率要高于单机系统。

分布式系统主要问题有:

  • 一致性问题
  • 容灾容错
  • 执行顺序问题
  • 事务问题

因此,基于以上问题,我们就产生了一个需求:需要一个松散耦合的分布式系统中粗粒度锁以及可靠性存储(低容量)的系统。在此背景下,ZooKeeper便应运而生。

ZooKeeper是基于Hadoop系统的,开源的,高效的,分布式同步,组服务,基于Paxo协议的分布式协调架构。(Google内部实现叫Chubby)。

什么是paxos协议?

Paxos用于解决分布式系统中一致性问题。分布式一致性算法(Consensus Algorithm)是一个分布式计算领域的基础性问题,其最基本的功能是为了在多个进程之间对某个(某些)值达成一致(强一致);简单来说就是确定一个值,一旦被写入就不可改变。paxos用来实现多节点写入来完成一件事情,例如mysql主从也是一种方案,但这种方案有个致命的缺陷,如果主库挂了会直接影响业务,导致业务不可写,从而影响整个系统的高可用性。paxos协议是只是一个协议,不是具体的一套解决方案。目的是解决多节点写入问题。

参考链接:(2条消息) 理解这两点,也就理解了paxos协议的精髓_yinn-CSDN博客_paxos协议

ZooKeeper工作机制

Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化, Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应。

ZK工作机制示意图

ZooKeeper特点

  1. Zookeeper:一个领导者(Leader),多个跟随者(Follower)组成的集群。
  2. 集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。
  3. 全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
  4. 更新请求顺序进行,来自同一个Client的更新请求按其发送顺序依次执行。
  5. 数据更新原子性,一次数据更新要么成功,要么失败。

ZooKeeper数据模型和结构

数据模型

  • 数据模型
    • 命名空间
    • 与标准文件系统很相似
    • 以 / 为间隔的路径名序列组成
    • 只有绝对路径,没有相对路径
    • 每个节点自身的信息:
      • 数据
      • 数据长度
      • 创建时间
      • 修改时间
    • 具有文件、路径的双重特点

数据结构

ZK结构图

ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一个ZNode。每一个ZNode默认能够存储1MB的数据,每个ZNode都可以通过其路径唯一标识。

ZK结构2

ZooKeeper应用场景

ZK提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等。

统一命名服务

在分布式环境下,经常需要对应用/服务进行统一命名,便于识别(比如hadoop集群的命名)。例如:IP不容易记住,而域名容易记住。

统一命名服务

统一配置管理

ZK能够实现全局配置的容错和统一。

分布式环境下,配置文件同步非常常见。

  • 一般要求一个集群中,所有节点的配置信息是一致的,比如 Kafka 集群。
  • 对配置文件修改后,希望能够快速同步到各个节点上。

配置管理可交由ZooKeeper实现。

  • 可将配置信息写入ZooKeeper上的一个Znode。
  • 各个客户端服务器监听这个Znode。
  • 一旦Znode中的数据被修改,ZooKeeper将通知各个分服务器。

统一集群管理

ZK能够实现集群管理,了解每台服务器的状态,对服务器节点的新增和删除会进行“周知”,能够进行主服务器选举。

分布式环境中,实时掌握每个节点的状态是必要的。

  • 可根据节点实时状态做出一些调整。

ZooKeeper可以实现实时监控节点状态变化

  • 可将节点信息写入ZooKeeper上的一个ZNode。
  • 监听这个ZNode可获取它的实时状态变化。

节点序列最小的是主节点,对外提供服务。

服务器动态上下线

软负载均衡

在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求。

ZooKeeper内部原理

ZooKeeper节点类型

持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除

短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除

Persistent Node(持久化目录节点)

  • 永久有效地节点,除非client显式的删除,否则一直存在

Ephemeral Nodes(临时目录节点)

  • – 临时节点,仅在创建该节点client保持连接期间有效,一旦连接丢失,zookeeper会自动删除该节点

Sequence Nodes

  • 顺序节点,client申请创建该节点时,zk会自动在节点路径末尾添加递增序号,这种类型是实现分布式锁,分布式queue等特殊功能的关键
  • 说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
  • 注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序

Persistent Sequence Nodes

  • 客户端与Zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号

Ephemeral Sequence Nodes

  • 客户端与Zookeeper 断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

ZooKeeper的Stat结构体解析

  1. czxid-创建节点的事务 zxid

    • 每次修改ZooKeeper 状态都会收到一个zxid 形式的时间戳,也就是ZooKeeper 事务ID。事务 ID 是 ZooKeeper 中所有修改总的次序。每个修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么zxid1 在 zxid2 之前发生。
  2. ctime - znode: 被创建的毫秒数(从1970 年开始)

  3. mzxid - znode: 最后更新的事务 zxid
  4. mtime - znode: 最后修改的毫秒数(从1970 年开始)
  5. pZxid-znode: 最后更新的子节点 zxid
  6. cversion - znode :子节点变化号,znode 子节点修改次数
  7. dataversion - znode: 数据变化号
  8. aclVersion - znode: 访问控制列表的变化号
  9. ephemeralOwner- 如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是0
  10. dataLength- znode: 的数据长度
  11. numChildren - znode: 子节点数量

ZooKeeper监听器原理**

监听原理

  1. 首先要有一个main()线程。
  2. 在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
  3. 通过connect线程将注册的监听事件发送给Zookeeper。
  4. 在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
  5. Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
  6. listener线程内部调用了process()方法。

常见的监听

  1. 监听节点数据的变化
    • get path [watch]
  2. 监听子节点增减的变化
    • ls path [watch]

监控机制(watch)

  • 在数据节点上设置
  • 客户端被动收到通知
  • 各种读请求,如getData()getChildren()exists()

三个关键点

  • 一次性监控,触发后需要重新设置
  • 保证先收到事件再收到数据修改的信息
  • 传递性
    • 如create节点,会触发父节点的监控点监控变化
    • 如delete会节点,会触发节点数据监控点,同时也会触发父节点的监控点

风险

  • 客户端有可能看不到所有数据的变化
  • 多个事件的监控,有可能只会触发一次
    • 一个客户端设置了关于某个数据点exists和getData的监控,则当该数据被删除的时候,只会触发“文件被删除”的通知。
  • 客户端网络中断的过程的无法收到监控的窗口时间,要由模块进行容错设计

ZooKeeper选举机制**

  1. 半数机制:集群中半数以上机器存活,集群可用。所以 Zookeeper 适合安装奇数台服务器。
  2. Zookeeper 虽然在配置文件中并没有指定 Master 和Slave。但是,Zookeeper 工作时,是有一个节点为Leader,其他则为Follower,Leader 是通过内部的选举机制临时产生的。
  3. 以一个简单的例子来说明整个选举的过程。

假设有五台服务器组成的Zookeeper 集群,它们的id 从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么,如图所示。

ZK选举机制

1.服务器1 启动,发起一次选举。服务器 1 投自己一票。此时服务器 1 票数一票,不够半数以上(3 票),选举无法完成,服务器 1 状态保持为LOOKING;

2.服务器2 启动,再发起一次选举。服务器1 和2 分别投自己一票并交换选票信息:此时服务器 1 发现服务器 2 的 ID 比自己目前投票推举的(服务器 1)大,更改选票为推举服务器2。此时服务器 1 票数0 票,服务器2 票数2 票,没有半数以上结果,选举无法完成,服务器1,2 状态保持 LOOKING ;

3.服务器 3 启动,发起一次选举。此时服务器 1 和 2 都会更改选票为服务器 3。此次投票结果:服务器1 为0 票,服务器2 为0 票,服务器 3 为3 票。此时服务器 3 的票数已经超过半数,服务器3 当选Leader。服务器1,2 更改状态为FOLLOWING,服务器 3 更改状态为LEADING;

  1. 服务器 4 启动,发起一次选举。此时服务器 1,2,3 已经不是 LOOKING 状态,不会更改选票信息。交换选票信息结果:服务器3 为3 票,服务器4 为 1 票。此时服务器4服从多数,更改选票信息为服务器 3,并更改状态为 FOLLOWING;

5.服务器5 启动,同4一样当Follower。

ZooKeeper写数据流程

ZK写数据流程

ZooKeeper数据访问

ZK每个节点上的“访问控制链”(ACL, Access Control List)保存了各客户端对于该节点的访问权限。

用一个三元组来定义客户端的访问权限:(scheme:expression, perms)

  • (ip:19.22.0.0/16, READ)表示IP地址以19.22开头的主机有该数据节点的读权限。

zookeeper本身提供了ACL机制,表示为 scheme:id:permissions,第一个字段表示采用哪一种机制,第二个id表示用户,permissions表示相关权限(如只读,读写,管理等)

ZooKeeper一致性保证

  • 序列一致性:
    • 客户端发送的更新将按序在Zookeeper进行更新
  • 原子一致性:
    • 更新只能成功或者失败,没有中间状态
  • 单系统镜像:
    • 无论连接哪台Zookeeper服务器,客户端看到的服务器数据一致
  • 可靠性:
    • 任何一个更新成功后都会持续生效,直到另一个更新将它覆盖。可靠性有两个关键点:
      • 第一,当客户端的更新得到成功的返回值时,可以保证更新已经生效,但在某些异常情况下(超时, 连接失败),客户端无法知道更新是否成功;
      • 第二,当更新成功后,不会回滚到以前的状态,即使是在服务器失效重启之后;
  • 实时性:
    • Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,
    • Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口;

ZooKeeper实践

ZK客户端命令行操作

zk命令行语法

临时节点与永久节点操作

1.ZooKeeper启动客户端

1
2
3
4
5
6
7
(base) [root@main ~]# zkCli.sh 
Connecting to localhost:2181
WATCHER::

WatchedEvent state:SyncConnected type:None path:null

[zk: localhost:2181(CONNECTING) 0]

2.显示所有操作命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[zk: localhost:2181(CONNECTING) 0] help
ZooKeeper -server host:port cmd args
stat path [watch]
set path data [version]
ls path [watch]
delquota [-n|-b] path
ls2 path [watch]
setAcl path acl
setquota -n|-b val path
history
redo cmdno
printwatches on|off
delete path [version]
sync path
listquota path
rmr path
get path [watch]
create [-s] [-e] path data acl
addauth scheme auth
quit
getAcl path
close
connect host:port
[zk: localhost:2181(CONNECTED) 1]

3.查看当前znode所包含的内容,“[]”里是节点名称。

1
2
[zk: localhost:2181(CONNECTED) 1] ls /                                 
[cluster, controller_epoch, brokers, zookeeper, admin, isr_change_notification, consumers, config, hbase]

4.查看当前节点详细数据

1
2
3
4
5
6
7
8
9
10
11
12
13
[zk: localhost:2181(CONNECTED) 2] ls2 /
[cluster, controller_epoch, brokers, zookeeper, admin, isr_change_notification, consumers, config, hbase]
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x100000011b
cversion = 57
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 9

5.分别创建 2 个普通节点

1
2
3
4
5
[zk: localhost:2181(CONNECTED) 4] create /subject "3subjects" 
Created /subject
[zk: localhost:2181(CONNECTED) 5] create /subject/Chinese "150score"
Created /subject/Chinese
[zk: localhost:2181(CONNECTED) 6]

6.获得节点的值

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
[zk: localhost:2181(CONNECTED) 6] get /subject
"3subjects"
cZxid = 0x100000011d
ctime = Mon Apr 26 23:23:37 CST 2021
mZxid = 0x100000011d
mtime = Mon Apr 26 23:23:37 CST 2021
pZxid = 0x100000011e
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 1
[zk: localhost:2181(CONNECTED) 7] get /subject/Chinese
"150score"
cZxid = 0x100000011e
ctime = Mon Apr 26 23:24:27 CST 2021
mZxid = 0x100000011e
mtime = Mon Apr 26 23:24:27 CST 2021
pZxid = 0x100000011e
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 10
numChildren = 0
[zk: localhost:2181(CONNECTED) 8]

7.创建临时节点

1
2
3
4
[zk: localhost:2181(CONNECTED) 8] create -e /subject/lnode "moment"
Created /subject/lnode
[zk: localhost:2181(CONNECTED) 10] ls /subject
[Chinese, lnode]

临时节点在当前客户端和其他节点是可以看到的,现在退出客户端重新登陆查看。

1
2
3
4
[zk: localhost:2181(CONNECTED) 13] quit 
Quitting...
2021-04-26 23:29:46,403 [myid:] - INFO [main:ZooKeeper@684] - Session: 0x790ebceeee0000 closed
2021-04-26 23:29:46,403 [myid:] - INFO [main-EventThread:ClientCnxn$EventThread@509] - EventThread shut down

可以看到,退出创建临时节点的客户端后,临时节点就没了。

创建带序号的节点 (顺序节点)

1.创建带序号的节点

1
2
3
4
5
6
7
[zk: localhost:2181(CONNECTED) 7] create -s /subject/Chinese/Junior "100score"
[zk: localhost:2181(CONNECTED) 7] create -s /subject/Chinese/Junior "100score"
Created /subject/Chinese/Junior0000000000
[zk: localhost:2181(CONNECTED) 8] create -s /subject/Chinese/Senior "150score"
Created /subject/Chinese/Senior0000000001
[zk: localhost:2181(CONNECTED) 9] create -s /subject/Chinese/University "score"
Created /subject/Chinese/University0000000002

如果原来没有序号节点,序号从0 开始依次递增。如果原节点下已有2 个节点,则再排序时从2 开始,以此类推。

结合之前在Subject创建了一个永久节点,一个临时节点,所以创建顺序节点English时,是从2开始的。

1
2
3
4
[zk: localhost:2181(CONNECTED) 3] create -s /subject/English "120score"
Created /subject/English0000000002
[zk: localhost:2181(CONNECTED) 4] ls /subject
[Chinese, English0000000002]

2.修改节点数据值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[zk: localhost:2181(CONNECTED) 10] get /subject
"3subjects"
cZxid = 0x100000011d

[zk: localhost:2181(CONNECTED) 11] set /subject "6subjects"
cZxid = 0x100000011d
ctime = Mon Apr 26 23:23:37 CST 2021
mZxid = 0x1000000129
mtime = Mon Apr 26 23:45:20 CST 2021
pZxid = 0x1000000124
cversion = 4
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 2

[zk: localhost:2181(CONNECTED) 12] get /subject
"6subjects"
cZxid = 0x100000011d

节点值变化监听

1.在slaves2节点上注册监听/subject节点数据变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# slaves2
[zk: localhost:2181(CONNECTED) 3] get /subject watch
"6subjects"
cZxid = 0x100000011d
ctime = Mon Apr 26 23:23:37 CST 2021
mZxid = 0x1000000129
mtime = Mon Apr 26 23:45:20 CST 2021
pZxid = 0x1000000124
cversion = 4
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 2

2.在main节点上对/subject节点进行修改

1
2
3
4
5
6
7
8
9
10
11
12
[zk: localhost:2181(CONNECTED) 13] set /subject "9subjects"
cZxid = 0x100000011d
ctime = Mon Apr 26 23:23:37 CST 2021
mZxid = 0x100000012a
mtime = Mon Apr 26 23:49:47 CST 2021
pZxid = 0x1000000124
cversion = 4
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 2

修改被slaves2节点上的监听器监听到

image-20210426235025836

节点的子节点变化监听(路径变化)

在slaves2 节点上注册监听/subject 节点的子节点变化。

1
2
[zk: localhost:2181(CONNECTED) 4] ls /subject watch
[Chinese, English0000000002]

在main主机的/subject 节点上创建子节点

1
2
[zk: localhost:2181(CONNECTED) 14] create -s /subject/math "120scores"
Created /subject/math0000000003

观察slaves2收到子节点上的变化监听

删除节点

1
2
3
4
5
[zk: localhost:2181(CONNECTED) 15] ls /subject
[Chinese, English0000000002, math0000000003]
[zk: localhost:2181(CONNECTED) 17] delete /subject/math0000000003
[zk: localhost:2181(CONNECTED) 18] ls /subject
[Chinese, English0000000002]

递归删除节点

1
2
3
4
5
6
7
[zk: localhost:2181(CONNECTED) 19] ls /subject
[Chinese, English0000000002]
[zk: localhost:2181(CONNECTED) 20] ls /subject/Chinese
[Junior0000000000, Senior0000000001, University0000000002]
[zk: localhost:2181(CONNECTED) 21] rmr /subject/Chinese
[zk: localhost:2181(CONNECTED) 22] ls /subject
[English0000000002]

查看节点状态

1
2
3
4
5
6
7
8
9
10
11
12
13
[zk: localhost:2181(CONNECTED) 24] stat /subject/English0000000002
cZxid = 0x1000000124
ctime = Mon Apr 26 23:35:21 CST 2021
mZxid = 0x1000000124
mtime = Mon Apr 26 23:35:21 CST 2021
pZxid = 0x1000000124
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 10
numChildren = 0
[zk: localhost:2181(CONNECTED) 25]

补充

zk共享锁

共享锁理解为排队形式。

server007

[server006, server007, server008]

此时server006正在使用且其ID小于server007,server007继续等待。

[server007, server008]

此时查看,发现server007最小和自己匹配,server007开始提供服务。