Zookeeper基础

Zookeeper是一个分布式协调框架,是Apache Hadoop的一个子项目,主要用来解决分布式应用中经常遇到的一些数据管理问题,如统一命名服务状态同步服务集群管理分布式应用配置项管理等。可理解为是一个用于存储少量数据基于内存的数据库,其核心为文件系统数据结构+监听通知机制

Zookeeper经典应用场景有:分布式注册中心分布式配置中心分布式锁分布式队列集群选举分布式屏障发布订阅

文件系统数据结构

每个子目录都被称为znode目录节点和文件系统类似,可自由增加、删除znode,以及在一个znode下增加、删除znode

PERSISTENT持久化目录节点:客户端与zookeeper断开连接后,该节点依旧存在,只要不手动删除该节点,他将永远存在

PERSISTENT_SEQUENTIAL持久化顺序编号目录节点:客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号,即在持久化节点的基础上带顺序;

EPHEMERAL临时目录节点:客户端与zookeeper断开连接后,该节点被删除

EPHEMERAL_SEQUENTIAL临时顺序编号目录节点:客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号;即在临时节点的基础上带顺序;

Container容器节点:3.5.3版本新增,若Container节点下面没有子节点,则Container节点在未来会被Zookeeper自动清除,定时任务默认60s检查一次

TTL节点:默认禁用,只能通过系统配置zookeeper.extendedTypesEnabled=true开启,不稳定;超过了TTL指定时间后会被自动清除;

监听机制

客户端注册监听它关心的任意节点,或者目录节点递归子目录节点,若注册的是对某个节点的监听,则当该节点被删除,或者被修改时,对应的客户端将被通知;若注册的是对某个目录的监听,则当该目录有子节点被创建,或者有子节点被删除,对应的客户端将被通知;若注册的是对某个目录的递归子节点进行监听,则当该目录下面的任意子节点有目录结构的变化即有子节点被创建被删除或者根节点有数据变化时,对应的客户端将被通知。

所有的通知都是一次性的,及无论是对节点还是对目录进行的监听,一旦触发对应监听即被移除。递归子节点,监听是对所有子节点的,每个子节点下面的事件同样只会被触发一次

Zookeeper安装

1
2
3
4
5
6
7
8
9
wget https://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.5.8/apache-zookeeper-3.5.8-bin.tar.gz
tar -zxvf apache-zookeeper-3.5.8-bin.tar.gz
cd apache-zookeeper-3.5.8-bin
# 重命名配置文件zoo_sample.cfg
cp zoo_sample.cfg zoo.cfg
# 启动Zookeeper,可通过bin/zkServer.sh来查看都支持哪些参数
bin/zkServer.sh start conf/zoo.cfg
# Zookeeper客户端连接服务端
bin/zkCli.sh -server ip:port

Zookeeper命令

可通过help查看Zookeeper所支持的所有命令

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
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] path
get [-s] [-w] path
getAcl [-s] path
history
listquota path
ls [-s] [-w] [-R] path
ls2 path [watch]
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]
rmr path
set [-s] [-v version] path data
setAcl [-s] [-v version] [-R] path acl
setquota -n|-b val path
stat [-w] path
sync path

创建Zookeeper节点

1
create [-s] [-e] [-c] [-t ttl] path [data] [acl]

中括号为可选项,没有则默认创建持久化节点-s顺序节点-e临时节点-c容器节点-t可给节点添加过期时间,默认禁用,需要通过系统参数启用-Dzookeeper.extendedTypesEnabled=true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建持久化节点并设置数据为some-data
create /test-node some-data
# 获取节点数据
get /test-node
# 更新节点数据
set /test-node some-data-change
# 创建临时节点并设置数据为data,临时节点不能创建子节点
create -e /ephemeral data
# 创建临时顺序节点,顺序节点将再seq-parent目录下顺序递增:/seq-parent/0000000000
create -s -e /seq-parent/ data
# 给顺序节点添加前缀:/seq-parent/profix-0000000001
create -s -e /seq-parent/profix-
# 创建容器节点,若未给其创建子节点,容器节点表现和持久化节点一样,若给容器节点创建了子节点,后续又把子节点清空,容器节点也会被zookeeper删除
create -c /container

查看节点状态信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看节点状态信息
stat /test-node
cZxid = 0xf
ctime = Mon Nov 22 10:32:02 CST 2021
mZxid = 0x10
mtime = Mon Nov 22 10:32:36 CST 2021
pZxid = 0xf
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 16
numChildren = 0
# 获取数据的同时查看节点状态信息
get -s /test-node
  • cZxid:创建znode的事务ID,即Zxid值
  • mZxid:最后修改znode的事务ID
  • pZxid:最后添加或删除子节点的事务ID,子节点列表发生变化才会发生改变
  • ctime:znode创建时间
  • mtime:znode最近修改时间
  • dataVersion:znode的当前数据版本,可根据该版本号对有并发修改数据实现乐观锁的功能
  • cversion:znode子节点结果集版本,一个节点的子节点增加、删除都会影响这个版本
  • aclVersion:表示对此znode的acl版本。
  • ephemeralOwner:znode是临时znode时,表示znode所有者的sessionID。 若znode不是临时znode,则该字段设置为零。
  • dataLength:znode数据字段的长度。
  • numChildren:znode的子znode的数量。
1
2
3
4
5
# 乐观锁的实现,用set命令修改数据的时候可以把版本号带上
set -v 1 /test-node change
# 数据修改版本号会递增,再用以前的版本号去修改,将会导致修改失败
set -v 1 /test-node change
version No is not valid : /test-node

创建子节点,zookeeper是以节点组织数据的,没有相对路径这么一说

1
2
3
4
# 创建子节点,
create /test-node/test-sub-node
# 递归查看递归子节点列表
ls -R /

事件监听机制,一旦事件触发,对应的注册立刻被移除,所以事件监听是一次性的

1
2
3
4
5
6
7
8
# 注册监听的同时获取数据
get -w /test-node
# 对节点进行监听,且获取元数据信息
stat -w /test-node
# 目录的变化,会触发事件,且一旦触发,对应的监听也会被移除,后续对节点的创建没有触发监听事件
ls -w /test-node
# 递归监听子目录
ls -R -w /test-node

Zookeeper事件类型None连接建立事件NodeCreated节点创建NodeDeleted节点删除NodeDataChanged节点数据变化NodeChildrenChanged子节点列表变化DataWatchRemoved节点监听被移除ChildWatchRemoved子节点监听被移除

ACL权限控制

Zookeeper的ACL权限控制,可控制节点读写操作,保证数据安全性,Zookeeper ACL权限设置分为Scheme权限模式ID授权对象Permission权限信息3部分组成。最终组成一条如scheme:id:permission格式的ACL请求信息。

Scheme权限模式:用来设置ZooKeeper服务器进行权限验证的方式。ZooKeeper 的权限验证方式大体分为两种类型:

  • 范围验证,ZooKeeper可针对一个IP或者一段IP地址授予某种权限。如可让一个IP地址为ip:192.168.0.110的机器对服务器上的某个数据节点具有写入的权限。或者也可以通过ip:192.168.0.1/24给一段IP地址的机器赋权。
  • 口令验证,也可理解为用户名密码的方式。在ZooKeeper中这种验证方式是Digest认证,而Digest认证方式首先在客户端传送username:password这种形式的权限表示符后,ZooKeeper服务端会对密码部分使用SHA-1BASE64算法进行加密以保证安全性。
  • Super权限模式, Super可认为是一种特殊的Digest认证。具有Super权限的客户端可对ZooKeeper上的任意数据节点进行任意操作

ID授权对象:授权对象就是要把权限赋予谁,对应于4种不同的权限模式来说,若选择IP方式,使用的授权对象可以是一个IP地址或IP地址段;若使用DigestSuper方式,则对应于一个用户名。若是World模式,则是授权系统中所有用户

Permission权限信息:指可在数据节点上执行的操作种类,ZooKeeper中已经定义好的权限有5

  • 数据节点c:create创建权限,授予权限的对象可在数据节点下创建子节点
  • 数据节点w:wirte更新权限,授予权限的对象可更新该数据节点
  • 数据节点r:read读取权限,授予权限的对象可读取该节点内容以及子节点列表信息
  • 数据节点d:delete删除权限,授予权限的对象可删除该数据节点的子节点
  • 数据节点a:admin管理者权限,授予权限的对象可对该数据节点体进行ACL权限设置

可通过系统参数zookeeper.skipACL=yes进行配置,默认是no,可配置为true,,则配置过的ACL将不再进行权限检测,可通过如下命令对权限信息进行操作:

  • getAcl:获取某个节点的acl权限信息
  • setAcl:设置某个节点的acl权限信息
  • addauth:输入认证授权信息,相当于注册用户信息,注册时输入明文密码,zk将以密文的形式存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 获取某个节点的具体权限信息
getAcl /test-node
# 获取某个节点的具体权限信息同时获取节点状态信息
getAcl -s /test-node
# 创建时设置ACL权限
create /zk-node datatest digest:gj:X/NSthOB0fD/OT6iilJ55WJVado=:cdrwa
# setAcl设置ACL权限
setAcl /zk-node digest:gj:X/NSthOB0fD/OT6iilJ55WJVado=:cdrwa
# 访问前需要添加授权信息
addauth digest gj:test
# auth明文授权,使用之前需要先通过addauth注册用户信息,后续可以直接用明文授权
addauth digest u100:p100
create /node-1 node1data auth:u100:p100:cdwra
# IP授权模式,多个指定IP可以通过逗号分隔,如setAcl /node-ip ip:IP1:rw,ip:IP2:a
setAcl /node-ip ip:192.168.109.128:cdwra
create /node-ip data ip:192.168.109.128:cdwra
# Super超级管理员模式,启动时通过JVM系统参数开启
-Dzookeeper.DigestAuthenticationProvider.superDigest=super:<base64encoded(SHA1(password))>

可通过如下方式生成授权ID

1
2
3
4
5
6
// echo -n <user>:<password> | openssl dgst -binary -sha1 | openssl base64
@Test
public void generateSuperDigest() throws NoSuchAlgorithmException {
String sId = DigestAuthenticationProvider.generateDigest("gj:test");
System.out.println(sId);// gj:X/NSthOB0fD/OT6iilJ55WJVado=
}

持久化

Zookeeper数据的组织形式为一个类似文件系统的数据结构,而这些数据都是存储在内存中的,针对每一次客户端的事务操作,Zookeeper都会将其记录到事务日志中。可在zookeeper的主配置文件zoo.cfg配置内存中的数据持久化目录,也就是事务日志存储路径dataLogDir,若未配置dataLogDir,事务日志将存储到dataDir目录

zookeeper提供了格式化工具可以进行数据查看事务日志数据org.apache.zookeeper.server.LogFormatter

1
java -classpath .:slf4j-api-1.7.25.jar:zookeeper-3.5.8.jar:zookeeper-jute-3.5.8.jar org.apache.zookeeper.server.LogFormatter /usr/local/zookeeper/apache-zookeeper-3.5.8-bin/data/version-2/log.1

Zookeeper进行事务日志文件操作时会频繁进行磁盘IO操作,事务日志不断追加写操作会触发底层磁盘IO为文件开辟新的磁盘块,为了提升磁盘IO的效率,Zookeeper在创建事务日志文件时进行了文件空间预分配,即在创建文件时,就向操作系统申请一块大一点的磁盘块。该预分配的磁盘大小可以通过系统参数zookeeper.preAllocSize配置。

事务日志文件名log.<当时最大事务ID>,日志文件是顺序写入的,故该最大事务ID也将是整个事务日志文件中最小的事务ID,日志满了即进行下一次事务日志文件的创建。

数据快照用于记录Zookeeper服务器上某一时刻的全量数据,并将其写入到指定的磁盘文件中。可通过配置snapCount配置间隔事务请求个数,数据存储在dataDir指定的目录中,可通过如下方式进行查看快照数据,为避免集群中所有机器在同一时间进行快照,实际快照生成时机为事务数达到snapCount/2 + 随机数时开始快照,随机数范围为1 ~ snapCount/2

1
java -classpath .:slf4j-api-1.7.25.jar:zookeeper-3.5.8.jar:zookeeper-jute-3.5.8.jar org.apache.zookeeper.server.SnapshotFormatter /usr/local/zookeeper/apache-zookeeper-3.5.8-bin/data-dir/version-2/snapshot.0

快照事务日志文件名snapshot.<当时最大事务ID>,日志满了即进行下一次事务日志文件的创建,快照数据主要是为了快速恢复事务日志文件是每次事务请求都会进行追加的操作快照是达到某种设定条件下的内存全量数据。通常快照数据是反应当时内存数据的状态。事务日志是更全面的数据,所以恢复数据时,可先恢复快照数据再通过增量恢复事务日志中的数据即可。

Zookeeper集群

Zookeeper集群模式有LeaderFollowerObserver三种类型的角色:

  • Leader: 处理所有的事务写请求可处理读请求集群中只能有一个Leader
  • Follower只能处理读请求,同时作为Leader候选节点,即若Leader宕机,Follower节点要参与到新的Leader选举中,有可能成为新的Leader节点
  • Observer只能处理读请求不能参与选举

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
# 重命名zoo_sample.cfg文件
cp conf/zoo_sample.cfg conf/zoo-1.cfg
# 修改配置文件zoo-1.cfg,原配置文件里有的,修改成下面的值,没有的则加上
dataDir=/usr/local/data/zookeeper-1
clientPort=2181
server.1=127.0.0.1:2001:3001:participant # participant可不用写,默认就是participant
server.2=127.0.0.1:2002:3002:participant
server.3=127.0.0.1:2003:3003:participant
server.4=127.0.0.1:2004:3004:observer

# 拷贝zoo1.cfg文件,修改dataDir和clientPort
cp conf/zoo1.cfg conf/zoo2.cfg
cp conf/zoo1.cfg conf/zoo3.cfg
cp conf/zoo1.cfg conf/zoo4.cfg
vim conf/zoo2.cfg
dataDir=/usr/local/data/zookeeper-2
clientPort=2182
vim conf/zoo3.cfg
dataDir=/usr/local/data/zookeeper-3
clientPort=2183
vim conf/zoo4.cfg
dataDir=/usr/local/data/zookeeper-4
clientPort=2184
# 创建四个文件夹在每个目录中创建文件myid文件,写入当前实例的server id,即1,2,3,4
cd /usr/local/data/zookeeper-1
vim myid
1
cd /usr/local/data/zookeeper-2
vim myid
2
cd /usr/local/data/zookeeper-3
vim myid
3
cd /usr/local/data/zookeeper-4
vim myid
4
# 客户端连接集群
bin/zkCli.sh -server ip1:port1,ip2:port2,ip3:port3
  • tickTime:用于配置Zookeeper中最小时间单位的长度,很多运行时的时间间隔都是使用tickTime的倍数来表示的。
  • initLimit:该参数用于配置Leader服务器等待Follower启动,并完成数据同步的时间。Follower服务器在启动过程中,会与Leader建立连接并完成数据的同步,从而确定自己对外提供服务的起始状态。Leader服务器允许Follower在initLimit时间内完成该工作。
  • syncLimit:Leader与Follower心跳检测最大延时间
  • dataDir:Zookeeper保存数据目录,默认Zookeeper将写数据的日志文件也保存在这个目录里。
  • clientPort:该端口是客户端连接Zookeeper服务器的端口,Zookeeper会监听该端口,接受客户端的访问请求。
  • server.A=B:C:D:E:其中A是一个数字,表示第几号服务器;B是这个服务器的ip地址;C表示该服务器与集群中的Leader服务器交换信息的端口;D表示集群中的Leader服务器挂了重新进行选举的端口就是用来执行选举时服务器相互通信的端口。若是伪集群的配置方式,由于B都是一样,故不同的Zookeeper实例通信端口号不能一样,所以要给它们分配不同的端口号。若需要通过添加不参与集群选举以及事务请求的过半机制的 Observer节点,可在E的位置,添加observer标识。

分布式锁实现

Zookeeper非公平锁实现

Zookeeper非公平锁实现在并发比较高的情况下,性能会下降的比较厉害,因为所有连接都在对同一个节点进行监听,当服务器检测到删除事件时,要通知所有的连接,所有的连接同时收到事件,再次并发竞争,这就是羊群效应

Zookeeper公平锁实现

公平锁可以有效的解决羊群效应,借助于临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力,这种实现方式所有加锁请求都进行排队加锁,请求进来直接在/lock节点下创建一个临时顺序节点,然后判断自己是否是lock节点下最小的节点,若是则获得锁,若不是则监听前一个节点。

Zookeeper共享锁实现