一、基本介绍
ZooKeeper 是 Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。
ZooKeeper 的架构通过冗余服务实现高可用性。
Zookeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
1.1 数据结构
zookeeper 提供的名称空间非常类似于标准文件系统,key-value 的形式存储。名称 key 由斜线 / 分割的一系列路径元素,zookeeper 名称空间中的每个节点都是由一个路径标识。
1.2 官方文档
二、安装部署
2.1 直装单节点部署
2.1.1 安装jdk
zookeeper依赖jdk环境
yum install -y java-1.8.0-openjdk-devel.x86_64
复制
2.1.2 下载
wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz
tar -zxvf zookeeper-3.4.14.tar.gz
mv zookeeper-3.4.14 /opt/
cd /opt/zookeeper-3.4.14
复制
2.1.3 配置
cp conf/zoo_sample.cfg conf/zoo.cfg
复制
默认配置
#单次会话超时时间
tickTime=2000
#初始化同步限制时间单位
initLimit=10
#请求应答限制时间单位
syncLimit=5
#数据存放目录
dataDir=/opt/zookeeper-3.4.14/data
#服务端口
clientPort=2181
#最大连接数,可以调大
maxClientCnxns=60
#数据快照数量
autopurge.snapRetainCount=3
# Purge任务频率/小时
autopurge.purgeInterval=1
复制
2.1.4 启动服务
#启动服务端
./bin/zkServer.sh start
#查看服务端状态
./bin/zkServer.sh status
#启动客户端
./bin/zkCli.sh
复制
2.2 集群三节点部署
zookeeper 的三个端口作用
- 2181 : 对 client 端提供服务
- 2888 : 集群内机器通信使用
- 3888 : 选举 leader 使用
准备三个机器,提前关掉防火墙
192.168.10.108 192.168.10.109 192.168.10.110
复制
2.2.1 编辑配置
在每一台得原有得配置文件上加上集群节点得通信地址
server.1=192.168.10.108:2888:3888 server.2=192.168.10.109:2888:3888 server.3=192.168.10.110:2888:3888
复制
在数据目录下添加myid文件
#分别在108、109、110节点上添加1、2、3
vim data/myid
复制
2.2.2 启动集群
集群启动之后数据就会自动同步到对应节点了
#启动服务端
./bin/zkServer.sh start
#查看服务端状态,不同节点可以看到leader和follower角色
./bin/zkServer.sh status
复制
2.3 Docker部署
2.3.1 单节点
拉取镜像
docker pull zookeeper:3.8.1
复制
拷贝配置文件
docker run --name zk --restart always -d zookeeper:3.8.1 docker cp /conf /opt/zk/ docker rm -f zk
复制
挂载配置和数据启动
docker run --name zk -d --restart always \ -p 2181:2181 \ -v /opt/zk/conf:/conf \ -v /opt/zk/data:/data \ -v /opt/zk/datalog:/datalog \ zookeeper:3.8.1
复制
2.3.2 启动客户端
docker run -it --rm zookeeper:3.8.1 zkCli.sh -server 192.168.10.108:2181
复制
2.3.3 docker-compose
一台机器起个三节点集群,测试使用不挂载数据。挂载方式参考单节点。
version: '3.3'
services:
zoo1:
image: zookeeper:3.8.1
restart: always
hostname: zoo1
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
zoo2:
image: zookeeper:3.8.1
restart: always
hostname: zoo2
ports:
- 2182:2181
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
zoo3:
image: zookeeper:3.8.1
restart: always
hostname: zoo3
ports:
- 2183:2181
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
复制
2.4 可视化工具
本地安装ZA工具,下载地址
三、客户端命令
#启动客户端
./bin/zkCli.sh
复制
3.1 查询操作
3.1.1 ls
ls 命令用于查看某个路径下目录列表。
ls /
ls /test
复制
3.1.2 ls2
ls2 命令用于查看某个路径下目录列表,它比 ls 命令列出更多的详细信息。
ls2 /
复制
3.1.3 get
get 命令用于获取节点数据和状态信息。
get /test
#增加watch监听,节点变更就会被通知
get /test watch
复制
3.1.4 stat
stat 命令用于查看节点状态信息。
stat /test
#监听
stat /test watch
复制
3.2 更新操作
3.2.1 create 命令
create 命令用于创建节点并赋值。
create [-s] [-e] path data acl
复制
- [-s] [-e]:-s 和 -e 都是可选的,-s 代表顺序节点, -e 代表临时节点,注意其中 -s 和 -e 可以同时使用的,并且临时节点不能再创建子节点。
- path:指定要创建节点的路径,比如 /test。
- data:要在此节点存储的数据。
- acl:访问权限相关,默认是 world,相当于全世界都能访问。
#创建临时顺序节点/test2,值0。顺序节点默认会在节点名后加上顺序值
create -s -e /test2 0
复制
3.2.2 set 命令
set 命令用于修改节点存储的数据。
set /test 123
#赋值时指定版本号,只有版本号和当前dataVersion相等才能赋值成功
set /test 456 3
复制
3.2.3 delete 命令
delete 命令用于删除某节点。
delete /test
#删除时指定版本号,同set逻辑一样需要dataVersion相等才能删除成功
delete /test 3
复制
四、ZooKeeper 数据模型
4.1 znode节点
在 zookeeper 中,可以说 zookeeper 中的所有存储的数据是由 znode 组成的,节点也称为 znode,并以 key/value 形式存储数据。整体结构类似于 linux 文件系统的模式以树形结构存储。其中根路径以 / 开头。需要绝对路径。
4.1.1 查看节点信息
#查看/根下节点
ls /
#默认存在的zookeeper节点
ls /zookeeper/quota
#查看/test路径下节点
ls /test
复制
4.1.2 节点信息属性
#获取/test节点的信息
get /test
复制
节点信息
#节点的value值
hello zk
#最早修改节点时的事务ID
cZxid = 0x3
#创建节点时的时间
ctime = Sat May 27 23:38:53 CST 2023
#最后修改节点时的事务ID
mZxid = 0x3
#最后修改节点时的时间
mtime = Sat May 27 23:38:53 CST 2023
#pZxid表示该节点的子节点列表最后一次修改的事务ID
#添加子节点或删除子节点就会影响子节点列表,但是修改子节点的数据内容则不影响该ID
#注意,只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid
pZxid = 0x3
#子节点版本号,子节点每次修改版本号加1
cversion = 0
#数据版本号,数据每次修改该版本号加1
dataVersion = 0
#权限版本号,权限每次修改该版本号加1
aclVersion = 0
#创建该临时节点的会话的sessionID。(如果该节点是持久节点,那么这个属性值为0)
ephemeralOwner = 0x0
#该节点的数据长度
dataLength = 8
#该节点拥有子节点的数量(只统计直接子节点的数量)
numChildren = 0
复制
4.1.3 节点特性
-
同一级节点 key 名称是唯一的
-
创建节点时,必须要带上全路径
-
session 关闭,临时节点清除
-
自动创建顺序节点
#执行多次创建顺序节点会自动顺序 create -s -e /test 0
复制 -
watch 机制,监听节点变化
事件监听机制类似于观察者模式,watch 流程是客户端向服务端某个节点路径上注册一个 watcher,同时客户端也会存储特定的 watcher,当节点数据或子节点发生变化时,服务端通知客户端,客户端进行回调处理。特别注意:监听事件被单次触发后,事件就失效了。
-
delete 命令只能一层一层删除
deleteall
可以递归删除
4.2 权限控制 ACL
ACL 权限可以针对节点设置相关读写等权限,保障数据安全性。
4.2.1 ACL相关命令
-
getAcl 命令:获取某个节点的 acl 权限信息。
getAcl /test
复制 -
setAcl 命令:设置某个节点的 acl 权限信息。
#没有删除权限 setAcl /test world:anyone:crwa
复制 -
addauth 命令:输入认证授权信息,注册时输入明文密码,加密形式保存。auth 用于授予权限,注意需要先创建用户。
#登录user1 addauth digest user1:123456 #赋予节点user1 setAcl /test auth:user1:123456:cdrwa
复制
4.2.2 ACL构成
zookeeper 的 acl 通过 scheme:id:permissions
来构成权限列表。
- scheme:代表采用的某种权限机制,包括 world、auth、digest、ip、super 几种。
- id:代表允许访问的用户。
- permissions:权限组合字符串,由 cdrwa 组成,其中每个字母代表支持不同权限, 创建权限 create©、删除权限 delete(d)、读权限 read®、写权限 write(w)、管理权限admin(a)。
4.3 session基本原理
在ZooKeeper中,客户端和服务端建立连接后,会话随之建立,生成一个全局唯一的会话ID(Session ID)。服务器和客户端之间维持的是一个长连接,在SESSION_TIMEOUT时间内,服务器会确定客户端是否正常连接(客户端会定时向服务器发送heart_beat,服务器重置下次SESSION_TIMEOUT时间)。因此,在正常情况下,Session一直有效,并且ZK集群所有机器上都保存这个Session信息。在出现网络或其它问题情况下(例如客户端所连接的那台ZK机器挂了,或是其它原因的网络闪断),客户端与当前连接的那台服务器之间连接断了,这个时候客户端会主动在地址列表(实例化ZK对象的时候传入构造方法的那个参数connectString)中选择新的地址进行连接。
4.3.1 session的属性
sessionID: 会话ID,用来唯一标识一个会话,每次客户端创建会话的时候,zookeeper 都会为其分配一个全局唯一的 sessionID。
Timeout:会话超时时间。客户端在构造 Zookeeper 实例时候,向服务端发送配置的超时时间,server 端会根据自己的超时时间限制最终确认会话的超时时间。
TickTime:下次会话超时时间点,默认 2000 毫秒。可在 zoo.cfg 配置文件中配置,便于 server 端对 session 会话实行分桶策略管理。
isClosing:该属性标记一个会话是否已经被关闭,当 server 端检测到会话已经超时失效,该会话标记为"已关闭",不再处理该会话的新请求。
4.3.2 Session的状态
connecting:连接中,session 一旦建立,状态就是 connecting 状态,时间很短。
connected:已连接,连接成功之后的状态。
closed:已关闭,发生在 session 过期,一般由于网络故障客户端重连失败,服务器宕机或者客户端主动断开。
4.3.3 Session建立
- client会随机选一个我们提供的地址,然后委托给
ClientCnxnSocket
去创建与zk之间的TCP链接。 - 接下来SendThread(Client的网络发送线程)构造出一个ConnectRequest请求(代表客户端与服务器创建一个会话)。同时,Zookeeper客户端还会进一步将请求包装成网络IO的Packet对象,放入请求发送队列——outgoingQueue中去。
- ClientCnxnSocket从outgoingQueue中取出Packet对象,将其序列化成ByteBuffer后,向服务器进行发送。
- 服务端的SessionTracker为该会话分配一个sessionId,并发送响应。
- Client收到响应后,此时此刻便明白自己没有初始化,因此会用
readConnectResult
方法来处理请求。 - ClientCnxnSocket会对接受到的服务端响应进行反序列化,得到ConnectResponse对象,并从中获取到Zookeeper服务端分配的会话SessionId。
- 通知SendThread,更新Client会话参数(比如重要的connectTimeout),并更新Client状态;另外,通知地址管理器HostProvider当前成功链接的服务器地址。
4.3.4 会话超时与会话激活
zookeeper 的 leader 服务器再运行期间定时进行会话超时检查,时间间隔是 ExpirationInterval,单位是毫秒,默认值是 tickTime,每隔 tickTime 进行一次会话超时检查。
ExpirationTime = CurrentTime + SessionTimeout; ExpirationTime = (ExpirationTime / ExpirationInterval + 1) * ExpirationInterval;
复制
根据不同的时间设置不同的老桶和新桶,计算最新的过期时间,并放置到新桶里,再移除掉老桶里的会话实例。
4.3.5 连接断开
连接断开(CONNECTIONLOSS),一般发生在网络的闪断或是客户端所连接的服务器挂机的时候,这种情况下,ZooKeeper客户端自己会首先感知到这个异常。还有就是Server服务器挂了,这个时候,ZK客户端首选会捕获异常。
4.4 watcher事件机制原理
4.5 数据同步流程
在 Zookeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性。
ZAB 协议分为两部分:
- 消息广播
- 崩溃恢复
4.5.1 消息广播
Zookeeper 使用单一的主进程 Leader 来接收和处理客户端所有事务请求,并采用 ZAB 协议的原子广播协议,将事务请求以 Proposal 提议广播到所有 Follower 节点,当集群中有过半的Follower 服务器进行正确的 ACK 反馈,那么Leader就会再次向所有的 Follower 服务器发送commit 消息,将此次提案进行提交。这个过程可以简称为 2pc 事务提交,整个流程可以参考下图,注意 Observer 节点只负责同步 Leader 数据,不参与 2PC 数据同步过程。
4.5.2 崩溃恢复
在正常情况消息广播情况下能运行良好,但是一旦 Leader 服务器出现崩溃,或者由于网络原理导致 Leader 服务器失去了与过半 Follower 的通信,那么就会进入崩溃恢复模式,需要选举出一个新的 Leader 服务器。在这个过程中可能会出现两种数据不一致性的隐患,需要 ZAB 协议的特性进行避免。
- Leader 服务器将消息 commit 发出后,立即崩溃
- Leader 服务器刚提出 proposal 后,立即崩溃
ZAB 协议的恢复模式使用了以下策略:
- 选举 zxid 最大的节点作为新的 leader
- 新 leader 将事务日志中尚未提交的消息进行处理
4.6 Leader选举原理
zookeeper 的 leader 选举存在两个阶段,一个是服务器启动时 leader 选举,另一个是运行过程中 leader 服务器宕机。在分析选举原理前,先介绍几个重要的参数。
- 服务器 ID(myid):编号越大在选举算法中权重越大
- 事务 ID(zxid):值越大说明数据越新,权重越大
- 逻辑时钟(epoch-logicalclock):同一轮投票过程中的逻辑时钟值是相同的,每投完一次值会增加
选举状态:
- LOOKING: 竞选状态
- FOLLOWING: 随从状态,同步 leader 状态,参与投票
- OBSERVING: 观察状态,同步 leader 状态,不参与投票
- LEADING: 领导者状态
4.6.1 服务器启动时的 leader 选举
每个节点启动的时候都 LOOKING 观望状态,接下来就开始进行选举主流程。这里选取三台机器组成的集群为例。第一台服务器 server1启动时,无法进行 leader 选举,当第二台服务器 server2 启动时,两台机器可以相互通信,进入 leader 选举过程。
- 每台 server 发出一个投票,由于是初始情况,server1 和 server2 都将自己作为 leader 服务器进行投票,每次投票包含所推举的服务器myid、zxid、epoch,使用(myid,zxid)表示,此时 server1 投票为(1,0),server2 投票为(2,0),然后将各自投票发送给集群中其他机器。
- 接收来自各个服务器的投票。集群中的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自 LOOKING 状态的服务器。
- 分别处理投票。针对每一次投票,服务器都需要将其他服务器的投票和自己的投票进行对比,对比规则如下:
- 优先比较 epoch
- 检查 zxid,zxid 比较大的服务器优先作为 leader
- 如果 zxid 相同,那么就比较 myid,myid 较大的服务器作为 leader 服务器
- 统计投票。每次投票后,服务器统计投票信息,判断是都有过半机器接收到相同的投票信息。server1、server2 都统计出集群中有两台机器接受了(2,0)的投票信息,此时已经选出了 server2 为 leader 节点。
- 改变服务器状态。一旦确定了 leader,每个服务器响应更新自己的状态,如果是 follower,那么就变更为 FOLLOWING,如果是 Leader,变更为 LEADING。此时 server3继续启动,直接加入变更自己为 FOLLOWING。
4.6.2 运行过程中的 leader 选举
当集群中 leader 服务器出现宕机或者不可用情况时,整个集群无法对外提供服务,进入新一轮的 leader 选举。
- 变更状态。leader 挂后,其他非 Oberver服务器将自身服务器状态变更为 LOOKING。
- 每个 server 发出一个投票。在运行期间,每个服务器上 zxid 可能不同。
- 处理投票。规则同启动过程。
- 统计投票。与启动过程相同。
- 改变服务器状态。与启动过程相同。
4.7 curator的锁方案
InterProcessLock
是锁的顶层接口。包含多重锁、不可重入锁、可重入锁、读写锁等。
4.7.1 InterProcessMutex
分布式可重入排它锁。InterProcessMutex通过在zookeeper的指定路径节点下创建临时序列节点来实现分布式锁,即每个线程(跨进程的线程)获取同一把锁前,都需要在同样的路径下创建一个节点,节点名字由uuid + 递增序列组成。而通过对比自身的序列数是否在所有子节点的第一位,来判断是否成功获取到了锁。当获取锁失败时,它会添加watcher来监听前一个节点的变动情况,然后进行等待状态。直到watcher的事件生效将自己唤醒,或者超时时间异常返回。
CuratorFramework zkClient = getZkClient();
String lockPath = "/lock";
InterProcessMutex lock = new InterProcessMutex(zkClient, lockPath);
lock.acquire();
lock.release();
复制
4.7.2 InterProcessSemaphoreMutex
分布式不可重入排它锁。InterProcessSemaphoreMutex是一种不可重入的互斥锁,也就意味着即使是同一个线程也无法在持有锁的情况下再次获得锁,所以需要注意,不可重入的锁很容易在一些情况导致死锁。
4.7.3 InterProcessReadWriteLock
共享可重入读写锁。读锁和读锁不互斥,只要有写锁就互斥。
CuratorFramework zkClient = getZkClient();
String lockPath = "/lock";
InterProcessReadWriteLock lock = new InterProcessReadWriteLock(zkClient, lockPath);
//获取读写锁(使用 InterProcessMutex 实现, 所以是可以重入的)
InterProcessReadWriteLock.ReadLock readLock = lock.readLock();
InterProcessReadWriteLock.WriteLock writeLock = lock.writeLock();
复制
4.7.4 InterProcessSemaphoreV2
共享信号量。
InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, lockPath, 1);
Lease lease = semaphore.acquire();
...
semaphore.returnLease(lease);
semaphore.returnAll(acquire);
复制
4.7.5 InterProcessMultiLock
多重共享锁。
InterProcessLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2));
//获取参数集合中的所有锁
lock.acquire();
//释放参数集合中的所有锁
lock.release();
复制