文章目录
一、开始
1. 概述
ZooKeeper 是一个分布式协调中间件,是 Google 的 Chubby 一个开源的实现。它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
特性:
- 顺序一致性: 客户端的更新将按顺序执行。因其写操作完全由单一 Leader 节点来执行
- 原子性: 操作要么成功要么失败
- 单一视图: 无论连接到哪个节点,客户端都能看到相同的视图。
- 及时性: 在特定时间范围内的数据是最新的。由最终一致性保证,同步需一定时间
- 可靠性:
- 数据不会丢失。zk 是将数据存储到内存中的,所以肯定会有持久化(日志 + 快照)
- 快速恢复 Leader。恢复模式(选主 + 数据同步)
2. 配置
conf/zoo.cfg:
# 计算时间的基本单元 ms
tickTime=2000
# 允许从节点连接并同步到主节点的初始化连接时间,以 tickTime 为单位
initLimit=10
# 主节点与从节点请求和应答(心跳)的时间长度,以 tickTime 为单位
syncLimit=5
# 快照及 Log 存储位置
dataDir=/usr/local/zookeeper/dataDir
dataLogDir=/usr/local/zookeeper/dataLogDir
# 服务端口
clientPort=2181
# host1
server.1=0.0.0.0:2888:3888
# host2
server.2=121.43.178.178:2888:3888
# host3
server.3=47.99.220.125:2888:3888
# host4,observer 角色
server.4=49.56.165.144:2888:3888:observer
集群:
-
创建 dataDir/myid,内容为 1 代表 id 为 1。其他实例同上创建不同 id 的 myid 文件
-
server.服务器 id=服务器 ip:服务器之间的通信端口:服务器之间的投票选举端口
# host1 当前机器的 host,用 0.0.0.0 表示 server.1=0.0.0.0:2888:3888 # host2 server.2=121.43.178.178:2888:3888 # host3 server.3=47.99.220.125:2888:3888
3. 启动
启动服务:
./zkServer.sh start
启动客户端:
./zkCli.sh
查看启动状态:
./zkServer.sh status
4. Session
-
每当客户端连接到服务端会创建一个 SessionId。创建/删除 SessionId 也是事务操作,会有 zxid,会被同步到所有节点中
-
Session 过期,则根据该 Session 创建的临时节点 znode 都会被抛弃
-
可设置超时时间。未超时的情况下,客户端 api 若保存了 SessionId,重连后 Session 不会消失(还是原来的 Session)
-
心跳机制
二、基本模型
1. Zookeeper 抽象模型
Zookeeper 提供了一种树形结构级的命名空间
为了保证高吞吐和低延迟,Zookeeper 在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为 1M。
2. 节点类型
- 永久无序节点: 不会因为会话结束或者超时而消失
- 永久有序节点: 不会因为会话结束或者超时而消失,且有序
- 临时无序节点: 如果会话结束或者超时就会消失
- 临时有序节点: 如果会话结束或者超时就会消失,且有序
有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,以此类推
三、命令
1. ls path [watch]
查看某个路径下有多少个节点
[zk: localhost:2181(CONNECTED) 2] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 3] ls /zookeeper
[quota]
2. ls2 path [watch]
= ls + stat
3. get path [watch]
获取值
[zk: localhost:2181(CONNECTED) 10] get /test
test
cZxid = 0x8
ctime = Mon May 27 14:12:38 CST 2019
mZxid = 0x8
mtime = Mon May 27 14:12:38 CST 2019
pZxid = 0x8
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0
4. stat path [watch]
[zk: localhost:2181(CONNECTED) 4] stat /
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
- cZxid:Create ZXID,表示节点被创建时的事务 ID。
- ctime:Create Time,表示节点创建时间。
- mZxid:Modified ZXID,表示节点最后⼀次被修改时的事务 ID。
- mtime:Modified Time,表示节点最后⼀次被修改的时间。
- pZxid:该节点的⼦节点列表最后⼀次被修改时的事务 ID。只有⼦节点列表变更才会更新 pZxid, ⼦节点内容变更不会更新。
- cversion:⼦节点的版本号。
- dataVersion:内容版本号。
- aclVersion:标识 acl 版本
- ephemeralOwner:创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0
- dataLength:数据⻓度。
- numChildren:直系⼦节点数。
5. create [-s] [-e] path data acl
-s: sequence。顺序节点,为节点自动添加
[zk: localhost:2181(CONNECTED) 15] create -s /test/sec seq
Created test/sec0000000001
[zk: localhost:2181(CONNECTED) 16] ls /test
[sec0000000001]
[zk: localhost:2181(CONNECTED) 17] create -s /test/sec seq
Created test/sec0000000002
[zk: localhost:2181(CONNECTED) 18] ls /test
[sec0000000001, sec0000000002]
-e: ephemeral。Session 创建临时节点
- 非持久化,临时节点
- 客户端与服务端之间维持心跳联系,无心跳则删除
- ephemeralOwner 为非 0x0
[zk: localhost:2181(CONNECTED) 12] create -e /test/tmp test-data
Created test/tmp
[zk: localhost:2181(CONNECTED) 13] get /test/tmp
test-data
cZxid = 0x9
ctime = Mon May 27 14:26:43 CST 2019
mZxid = 0x9
mtime = Mon May 27 14:26:43 CST 2019
pZxid = 0x9
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x16adeeddbe80003
dataLength = 9
numChildren = 0
6. set path data [version]
dataVersion 会 +1
[zk: localhost:2181(CONNECTED) 23] set /test new-test-data
cZxid = 0x8
ctime = Mon May 27 14:12:38 CST 2019
mZxid = 0xc
mtime = Mon May 27 14:53:38 CST 2019
pZxid = 0xb
cversion = 3
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 13
numChildren = 3
version: 验证版本号
若填写的版本号与当前版本号不一致则 set 失败
[zk: localhost:2181(CONNECTED) 25] set /test new-test-data-2 1
cZxid = 0x8
ctime = Mon May 27 14:12:38 CST 2019
mZxid = 0xd
mtime = Mon May 27 15:15:12 CST 2019
pZxid = 0xb
cversion = 3
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 15
numChildren = 3
[zk: localhost:2181(CONNECTED) 26] set /test new-test-data-2 1
version No is not valid : /test
[zk: localhost:2181(CONNECTED) 27] set /test new-test-data-2 3
version No is not valid : /test
7. delete path [version]
[zk: localhost:2181(CONNECTED) 31] delete /test/sec
[zk: localhost:2181(CONNECTED) 32] ls /test
[tmp]
version: 验证版本号
若填写的版本号与当前版本号不一致则 delete 失败( 同 set)
四、Watcher
概述: 为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
1. 特性
- 一次性: watcher 触发后立即销毁。
- 轻量级: watch 是轻量级的,其实就是本地 JVM 的 Callback,服务器端只是存了是否有设置了 watcher 的布尔类型
- 可见性: Watch 被触发的同时再次读取数据,客户端会得到 Watch 消息再看到更新后的数据。
- 可能会丢失: 对于一个未创建的 znode 的 exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个 Watch 事件可能会被丢失。
2. 设置 watcher
只有 stat、get、ls 才能设置 watcher
3. 事件类型
NodeCreated: 创建节点事件
[zk: localhost:2181(CONNECTED) 38] stat /ccomma watch
Note does not exist: /ccomma
[zk: localhost:2181(CONNECTED) 39] create /ccomma data
WATCHER::
WatchedEvent state:SyncConnected type:NoteCreated path:/ccomma
Created /ccomma
NodeDataChanged: 修改节点数据事件
[zk: localhost:2181(CONNECTED) 40] get /ccomma watch
data
cZxid = 0x15
ctime = Mon May 27 16:09:24 CST 2019
mZxid = 0x15
mtime = Mon May 27 16:09:24 CST 2019
pZxid = 0x15
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0
[zk: localhost:2181(CONNECTED) 41] set /ccomma 123
WATCHER::
WatchedEvent state:SyncConnected type:NoteDataChanged path:/ccomma
cZxid = 0x15
ctime = Mon May 27 16:09:24 CST 2019
mZxid = 0x16
mtime = Mon May 27 16:17:17 CST 2019
pZxid = 0x15
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0
NodeDeleted: 删除节点事件
[zk: localhost:2181(CONNECTED) 43] delete /ccomma
WATCHER::
WatchedEvent state:SyncConnected type:NoteDeleted path:/ccomma
NodeChildrenChanged: 子节点变更事件(修改子节点不会触发事件)
-
创建:
[zk: localhost:2181(CONNECTED) 47] ls /ccomma watch [] [zk: localhost:2181(CONNECTED) 48] create /ccomma/abc data WATCHER:: WatchedEvent state:SyncConnected type:NoteChildrenChanged path:/ccomma Created /ccomma/abc
-
删除:
[zk: localhost:2181(CONNECTED) 49] ls /ccomma watch [abc] [zk: localhost:2181(CONNECTED) 48] delete /ccomma/abc WATCHER:: WatchedEvent state:SyncConnected type:NoteChildrenChanged path:/ccomma
4. watcher 原理框架
过程:
- zk 客户端向 zk 服务器注册 watcher 的同时,会将 watcher 对象存储在客户端的 watchManager。
- zk 服务器触发 watcher 事件后,会向客户端发送通知,客户端线程从 watchManager 中回调 watcher 执行相应的功能。
watchManager:
Zk 服务器端 watcher 的管理者。负责 watcher 事件的触发。
从两个维度维护 watcher
- watchTable:从数据节点的粒度来维护
- watch2Paths:从 watcher 的粒度来维护
class WatchManager {
private final Map<String, Set<Watcher>> watchTable = new HashMap<String, Set<Watcher>>();
private final Map<Watcher, Set<String>> watch2Paths = new HashMap<Watcher, Set<String>>();
Set<Watcher> triggerWatch(String path, EventType type) {
return triggerWatch(path, type, null);
}
}
五、ACL(Access Control Lists)权限
1. ACL 构成
概述: 针对节点可以设置相关读写权限,保障数据安全
通过 scheme:id:permissions
来构成权限列表
- scheme: 代表采用的某种权限机制
- id: 代表允许访问的用户
- permissions: 代表允许的操作权限
例: setAcl path world:anyone:d 代表为 path 下的节点设置权限为所有人都只能删除该节点
1.1. scheme 类型
- world(world:anyone:[permissions]): 默认权限。只有一个用户 —— anyone
- auth(auth:user:password:[permissions]): 代表认证登录,需要注册用户拥有权限
- digest(digest:username:BASE64(SHA1(password)):[permissions]): 需要对密码加密才能访问
- ip(ip:192.168.1.1:[permissions]): 限制 ip 进行访问
- super: 超级管理员,拥有所有权限
1.2. permissions
crdwa 代表的权限含义:
- CREATE: 创建 子节点
- READ: 读取节点数据
- WRITE: 往节点写入数据
- DELETE: 删除 子节点,对于 delete 权限,要谨慎规划
- ADMIN: 可以使用 setAcl 命令设置权限
2. 命令
2.1. addauth scheme auth
概述:
添加认证授权信息到 Zookeeper 库中(注册)
并使用该认证作为当前客户端的认证信息(登录),这之后进行的所有操作会以该认证为前提
密码需输入明文,但在 Zookeeper 中密码以加密形式存储
[zk: localhost:2181(CONNECTED) 2] addauth digest ccomma:ccomma
2.2. setAcl path scheme🆔pwd:permissions
auth:
-
设置某个节点的 acl 权限信息
[zk: localhost:2181(CONNECTED) 6] setAcl /ccomma auth:ccomma:ccomma:crdwa cZxid = 0x18 ctime = Mon May 27 16:36:40 CST 2019 mZxid = 0x2b mtime = Mon May 30 16:10:49 CST 2019 pZxid = 0x1f cversion = 5 dataVersion = 2 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 6 numChildren = 1
-
当使用 addauth 添加认证后,setAcl 账号和密码可以省略,默认取第一个认证
[zk: localhost:2181(CONNECTED) 6] setAcl /ccomma auth::crdwa cZxid = 0x18 ctime = Mon May 27 16:36:40 CST 2019 mZxid = 0x2b mtime = Mon May 30 16:10:49 CST 2019 pZxid = 0x1f cversion = 5 dataVersion = 2 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 6 numChildren = 1
digest: 密码加密
[zk: localhost:2181(CONNECTED) 8] setAcl /ccomma digest:ccomma:91PXC4WimSDWZikp99kGvvjeVnY=:crdwa
cZxid = 0x60
ctime = Fri May 31 13:33:42 CST 2019
mZxid = 0x60
mtime = Fri May 31 13:33:42 CST 2019
pZxid = 0x60
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 1
numChildren = 0
ip:
[zk: localhost:2181(CONNECTED) 15] setAcl /ccomma/ip ip:192.168.1.7:crdwa
cZxid = 0x62
ctime = Fri May 31 13:36:51 CST 2019
mZxid = 0x62
mtime = Fri May 31 13:36:51 CST 2019
pZxid = 0x62
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 2
numChildren = 0
[zk: localhost:2181(CONNECTED) 15] getAcl /ccomma/ip
'ip,'192.168.1.7
:crdwa
2.3. getAcl path
获取某个节点的 acl 权限信息
密码以密文形式存储
[zk: localhost:2181(CONNECTED) 8] getAcl /ccomma
'digest,'ccomma:91PXC4WimSDWZikp99kGvvjeVnY=
:crdwa
3. super auth
添加超级用户:
在 nohup “ J A V A " " − D z o o k e e p e r . l o g . d i r = JAVA" "-Dzookeeper.log.dir= JAVA""−Dzookeeper.log.dir={ZOO_LOG_DIR}” “-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}” 后面加上 “-Dzookeeper.DigestAuthenticationProvider.superDigest=admin:9iPCX4WimSDWZikp99kGvvjeVnY=”
代表添加超级用户 admin:9iPCX4WimSDWZikp99kGvvjeVnY= (明文:ccomma)
case $1 in
start)
echo -n "Starting zookeeper ... "
if [ -f "$ZOOPIDFILE" ]; then
if kill -0 `cat "$ZOOPIDFILE"` > /dev/null 2>&1; then
echo $command already running as process `cat "$ZOOPIDFILE"`.
exit 0
fi
fi
nohup "$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}"
"-Dzookeeper.DigestAuthenticationProvider.superDigest=admin:9iPCX4WimSDWZikp99kGvvjeVnY=" \
-cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG" > "$_ZOO_DAEMON_OUT" 2>&1 < /dev/null &
if [ $? -eq 0 ]
then
case "$OSTYPE" in
*solaris*)
/bin/echo "${!}\\c" > "$ZOOPIDFILE"
;;
*)
/bin/echo -n $! > "$ZOOPIDFILE"
;;
esac
if [ $? -eq 0 ];
then
sleep 1
echo STARTED
else
echo FAILED TO WRITE PID
exit 1
fi
else
echo SERVER DID NOT START
exit 1
fi
;;
六、四字命令(Four Letter Words)
安装 nc: yum install nc
命令格式: echo [commond] | nc [ip] [port]
stat: 查看 zk 的状态信息
[root@izbp101vzs716yznuegsljz bin]# echo stat | nc localhost 2181
Zookeeper version: 3.4.9-1757313, built on 08/23/2016 06:50 GMTClients:
/127.0.0.1:55394 [0] (queued-0, recved=1, sent=0)
Latency min/avg/max: 0/0/0
Received: 2
Sent: 1
Connections: 1
Outatanding: 0
Zxid: 0xed
Mode: standalone
Node count: 29
conf: 查看服务器配置
[root@izbp101vzs716yznuegsljz bin]# echo conf | nc localhost 2181
clientFort=2181
dataDir=/uar/local/zookeeper-3.4.9/dataDir/version-2
dataLogDir=/usr/local/zookeeper-3.4.9/dataLoaDir/version-2
tickTime=2000
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
serverId=0
cons: 显示连接到服务器的客户端信息
[root@izbp101vzs716yznuegsljz bin]# echo cons | nc localhost 2181
/127.0.0.1:55442[1](queued-0,recved=1,sent=1,sid=0x16b1af459260001,lop=SESS,est=1559532063700,to=30000,lcxid=0x0,lzxid=0x0,lresp=1559532063718,llat=5,minlat=0,avglat=5,maxlat=5)
/127.0.0.1:55444[0](queued=0,recved=1,sent=0)
envi: 环境变量
[root@izbp101vzs716yznuegsljz bin]# echo envi | nc localhost 2181
Environment:
zookeeper.version=3.4.9-1757313, built on 08/23/2016 06:50 GMT
host,name=izbp101vzs716yznuegsliz
java.version=1.8.0_201
java.vendor=Oracle Corporation
java.home=/usr/jdk/jdk1.8.0_ 201/jre
...
mntr: 监控 zk 健康信息
[root@izbp101vzs716yznuegsljz bin]# echo mntr | nc localhost 2181
zk_version 3.4.9-1757313, built on o8/23/2016 06:50 GMT
zk_avg_latency 0
zk_max_latency 25
zk_min_latency 0
zk_packets_received 78
zk_packets_sent 77
zk_num_alive_connections
zk_outstanding_requests 0
zk_server_state standalone
zk_znode_count 29
zk_watch_count 0
zk_ephemerals_count 0
zk_approximate_data_size 553
zk_open_file_descriptor_count 27
zk_max_file_descriptor_count 65535
wchs: 显示 watch 的信息
[root@izbp101vzs716yznuegsljz bin]# echo wchs | nc localhost 2181
1 connections watching 1 paths
Total watches:1
wchc: watch 的 session 与 watch
[root@izbp101vzs716yznuegsljz bin]# echo wchc | nc localhost 2181
0x16b1af459260001
/zookeeper
wchp: watch 的 path 与 watch 信息
[root@izbp101vzs716yznuegsljz bin]# echo wchp | nc localhost 2181
/zookeeper
0x16b1af459260001
ruok(Are you OK?): 查看当前 zkServer 是否启动,返回 imok(I am OK)
[root@izbp101vzs716yznuegsljz bin]# echo ruok | nc localhost 2181
imok
dump: 列出未经处理的会话和临时节点
[root@izbp101vzs716yznuegsljz bin]# echo dump | nc localhost 2181
SessionTracker dump:
Session Sets (4):
0 expire at Mon Jun 03 11:11:24 CST 2019:
0 expire at Mon Jun 03 11:11:34 CST 2019:
0 expire at Mon Jun 03 11:11:44 CST 2019:
1 expire at Mon Jun 03 11:11:48 CST 2019:
0x16b1af459260000
ephemeral nodes dump:
Sessions with Ephemerals (1):
0x16b1af459260000:
/test/temg
七、Zookeeper API
zk 有 Session,没有线程池的概念
1. 原生 API
Zookeeper API 共包含五个包:
- org.apache.zookeeper
- org.apache.zookeeper.data
- org.apache.zookeeper.server
- org.apache.zookeeper.server.quorum
- org.apache.zookeeper.server.upgrade
依赖:
<dependency> <groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
1.1. 建立会话
ZooKeeper 客户端和服务端会话的建⽴是⼀个异步的过程。
所以如果在 new ZooKeeper
后立即结束方法会话不能建立完毕,会话的⽣命周期中处于 CONNECTING 的状态。
当会话真正创建完毕后 ZooKeeper 服务端会向会话对应的客户端发送⼀个事件通知以告知客户端。
public class CreateSession {
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException, IOException {
/*
客户端可以通过创建⼀个 zk 实例来连接 zk 服务器
- connectString:连接地址:IP:端⼝
- sesssionTimeOut:会话超时时间:单位毫秒
- Wather:监听器(当特定事件触发监听时,zk 会通过 watcher 通知到客户端)
*/
ZooKeeper zooKeeper = new ZooKeeper("10.211.55.4:2181,10.211.55.5:2181", 5000, watchedEvent -> {
if (watchedEvent.getState() == Watcher.Event.KeeperState.SyncConnected) {
countDownLatch.countDown();
}
});
System.out.println(zooKeeper.getState());
countDownLatch.await();
System.out.println("=========Client Connected to zookeeper==========");
}
}
1.2. 创建节点
ZooKeeper#create(String path, byte[] data, List<ACL> acl, CreateMode createMode):
- path:节点创建的路径
- data:节点创建要保存的数据
- acl:节点创建的权限信息(4种类型)
- ZooDefs.Ids.ANYONE_ID_UNSAFE:表示任何⼈
- ZooDefs.Ids.AUTH_IDS:此 ID 仅可⽤于设置 ACL。它将被客户机验证的 ID 替换。
- ZooDefs.Ids.OPEN_ACL_UNSAFE:这是⼀个完全开放的 ACL (常⽤)–> world:anyone
- ZooDefs.Ids.CREATOR_ALL_ACL:此 ACL 授予创建者身份验证 ID 的所有权限
- createMode:创建节点的类型(4种类型)
- CreateMode.PERSISTENT:持久节点
- CreateMode.PERSISTENT_SEQUENTIAL:持久顺序节点
- CreateMode.EPHEMERAL:临时节点
- CreateMode.EPHEMERAL_SEQUENTIAL:临时顺序节点
public class CreateNote {
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper("10.211.55.4:2181", 5000, watchedEvent -> {
if (watchedEvent.getState() == Watcher.Event.KeeperState.SyncConnected) {
countDownLatch.countDown();
}
// 调⽤创建节点⽅法
try {
createNodeSync();
} catch (Exception e) {
e.printStackTrace();
}
});
countDownLatch.await();
}
private static void createNodeSync() throws Exception {
String nodePersistent = zooKeeper.create("/lg_persistent", "持久节点内容".getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
String nodePersistentSequential = zooKeeper.create("/lg_persistent_sequential", "持久节点内容".getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
String nodeEpersistent = zooKeeper.create("/lg_ephemeral", "临时节点内容".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println("创建的持久节点是:" + nodePersistent);
System.out.println("创建的持久顺序节点是:" + nodePersistentSequential);
System.out.println("创建的临时节点是:" + nodeEpersistent);
}
}
1.3. 获取节点数据
-
ZooKeeper#getData(String path, boolean watch, Stat stat):
- path:获取数据的路径
- watch:是否开启监听。ture 代表使用创建 zk 的那个监听
- stat:节点状态信息,null 则表示获取最新版本的数据
-
ZooKeeper#getChildren(String path, boolean watch)
- path:路径
- watch:是否要启动监听,当⼦节点列表发⽣变化,会触发监听
public class GetNoteData {
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper("10.211.55.4:2181", 10000, watchedEvent -> {
if (watchedEvent.getState() == Watcher.Event.KeeperState.SyncConnected) {
try {
getNoteData();
getChildren();
} catch (Exception e) {
e.printStackTrace();
}
}
// ⼦节点列表发⽣变化时,服务器会发出 NodeChildrenChanged 通知,但不会把变化情况告诉给客户端
// 需要客户端⾃⾏获取,且通知是⼀次性的,需反复注册监听
if (watchedEvent.getType() == Watcher.Event.EventType.NodeChildrenChanged) {
// 再次获取节点数据
try {
List<String> children = zooKeeper.getChildren(watchedEvent.getPath(), true);
System.out.println(children);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
});
Thread.sleep(Integer.MAX_VALUE);
}
private static void getNoteData() throws Exception {
byte[] data = zooKeeper.getData("/lg_persistent/lg-children", true, null);
System.out.println(new String(data, StandardCharsets.UTF_8));
}
private static void getChildren() throws KeeperException, InterruptedException {
List<String> children = zooKeeper.getChildren("/lg_persistent", true);
System.out.println(children);
}
}
1.4. 修改节点数据
Stat ZooKeeper#setData(String path, byte[] data, int version)
- path:路径
- data:要修改的内容
- version:为 -1,表示对最新版本的数据进⾏修改
public class UpdateNote {
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper("10.211.55.4:2181", 5000, watchedEvent -> {
try {
updateNodeSync();
} catch (Exception e) {
e.printStackTrace();
}
});
Thread.sleep(Integer.MAX_VALUE);
}
private static void updateNodeSync() throws Exception {
byte[] data = zooKeeper.getData("/lg_persistent", false, null);
System.out.println("修改前的值:"+new String(data));
// 修改 stat:状态信息对象
// version: -1 代表最新版本
Stat stat = zooKeeper.setData("/lg_persistent", "客户端修改内容".getBytes(), -1);
byte[] data2 = zooKeeper.getData("/lg_persistent", false, null);
System.out.println("修改后的值:"+new String(data2));
}
}
1.5. 删除节点
- ZooKeeper#exists(String path, boolean watch): 判断节点是否存在
- ZooKeeper#delete(String path, int version): 删除节点
public class DeleteNote {
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper("10.211.55.4:2181", 5000, watchedEvent -> {
try {
deleteNodeSync();
} catch (Exception e) {
e.printStackTrace();
}
});
Thread.sleep(Integer.MAX_VALUE);
}
private static void deleteNodeSync() throws KeeperException, InterruptedException {
Stat exists = zooKeeper.exists("/lg_persistent/lg-children", false);
System.out.println(exists == null ? "该节点不存在":"该节点存在");
zooKeeper.delete("/lg_persistent/lg-children",-1);
Stat exists2 = zooKeeper.exists("/lg_persistent/lg-children", false);
System.out.println(exists2 == null ? "该节点不存在":"该节点存在");
}
}
2. Curator
- 【跟着实例学习 ZooKeeper】–队列
- 【跟着实例学习 ZooKeeper】–缓存
- 【跟着实例学习 ZooKeeper】–计数器
- 【跟着实例学习 ZooKeeper】–Curator 扩展库
- 【跟着实例学习 ZooKeeper】–Barrier
- 【跟着实例学习 ZooKeeper】–临时节点
- 【跟着实例学习 ZooKeeper】–Curator 框架应用
- 【跟着实例学习 ZooKeeper】– Leader 选举
- 【跟着实例学习 ZooKeeper】–分布式锁
2.1. 连接
重试策略:
- RetryNTimes: 重试 n 次
- RetryOneTime: 重试 1 次
- RetryForever: 永远重试
- RetryUntilElapsed: 重试直到超过最大重试时间
// 同 RetryNTimes
// RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 5);
// 实际调用 CuratorFrameworkFactory.builder().build();
// CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.1.110:2181", retryPolicy);
// 1. 重试 3 次,每次间隔 5s
RetryPolicy retryPolicy = new RetryNTimes(3, 5000);
// 2. 只重试 1 次
RetryPolicy retryPolicy2 = new RetryOneTime(3000);
// 3. 永远重试,每次间隔 3s
RetryPolicy retryPolicy3 = new RetryForever(3000);
// 4. 重试直到超过 2s,每次间隔 3s
RetryPolicy retryPolicy4 = new RetryUntilElapsed(2000, 3000);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.1.110:2181")
.sessionTimeoutMs(10000)
.retryPolicy(retryPolicy)
.namespace("workspace")
.build();
client.start();
2.2. 创建
String path = client.create()
// 1.递归创建节点
//.creatingParentsIfNeeded()
//.withMode(createMode)
// 2.ACL
//.withACL(aclList)
.forPath(nodePath, "data".getBytes());
2.3. 设置节点数据
client.setData()
// 可带版本
//.withVersion(version)
.forPath(nodePath, "data".getBytes());
2.4. 删除
client.delete()
// 1.如果删除失败,那么在后端还是继续会删除,直到成功(强制删除)
//.guaranteed()
// 2.如果有子节点,就删除
//.deletingChildrenIfNeeded()
// 3.指定版本
//.withVersion(1)
.forPath(nodePath);
2.5. 读取节点数据
Stat stat = new Stat();
byte[] data = client.getData()
// 包含状态查询
//.storingStatIn(stat)
.forPath(nodePath);
System.out.println("节点" + nodePath + "的数据为:" + new String(data));
System.out.println("该节点的版本号为:"+ stat.getVersion());
2.6. 获取子节点
List<String> nodeList = client.getChildren().forPath(nodePath);
2.7. 检查节点是否存在
public Boolean checkExists(String nodePath) {
return client.checkExists().forPath(nodePath) != null;
}
2.8. 监听器
usingWatcher
概述: 监听只会触发一次
byte[] data = client.getData()
.usingWatcher(event -> System.out.println("触发watcher,节点路径为:" + event.getPath()))
.forPath(nodePath);
NodeCache
概述: 永久监听 当前路径的节点 的创建、更新、删除
start():
- NodeCache#start()
- NodeCache#start(boolean buildInitial): true 表示初始化时获取 node 的值并保存
NodeCache nodeCache = new NodeCache(client, nodePath);
// true 表示初始化时获取 node 的值并保存
nodeCache.start(true);
if (nodeCache.getCurrentData() != null) {
System.out.println("节点初始化数据为:" + new String(nodeCache.getCurrentData().getData()));
}
nodeCache.getListenable().addListener(() -> {
ChildData currentData = nodeCache.getCurrentData();
System.out.println("节点 "+ currentData.getPath() + " 的数据为:" + new String(currentData.getData()));
});
PathChildrenCache
概述: 永久监听 当前路径下的子节点 的创建、更新、删除
start():
-
NodeCache#start(): 默认 buildInitial 为 false
-
NodeCache#start(boolean buildInitial): true 表示初始化时获取 node 的值并保存
-
NodeCache#start(StartMode mode):
-
- StartMode.POST_INITIALIZED_EVENT: 异步初始化,并触发 INITIALIZED 事件
- StartMode.NORMAL: 异步初始化
- StartMode.BUILD_INITIAL_CACHE: 同步初始化
事件类型:
- INITIALIZED: 初始化事件
- CHILD_ADDED: 子节点添加事件
- CHILD_REMOVED: 子节点删除事件
- CHILD_UPDATED: 子节点更新事件
// true 表示把节点内容放入 stat 里
PathChildrenCache childrenCache = new PathChildrenCache(client, nodePath, true);
childrenCache.start(StartMode.POST_INITIALIZED_EVENT);
// 同步初始化后可获取子节点数据
List<ChildData> childDataList = childrenCache.getCurrentData();
for (ChildData childData : childDataList) {
System.out.println(new String(childData.getData()));
}
// 监听子节点
childrenCache.getListenable().addListener((client, event) -> {
ChildData eventData = event.getData();
// 初始化事件
if (event.getType().equals(PathChildrenCacheEvent.Type.INITIALIZED)) {
System.out.println("子节点初始化完成");
}
// 子节点添加事件
else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_ADDED)) {
System.out.println("添加子节点 " + eventData.getPath() + ": " + new String(eventData.getData()));
}
// 子节点删除事件
else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
System.out.println("删除子节点 " + eventData.getPath());
}
// 子节点更新事件
else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
System.out.println("修改子节点 " + eventData.getPath() + ": " + new String(eventData.getData()));
}
});
八、应用场景
1. 数据发布/订阅
1.1. 概述
即所谓的配置中⼼,顾名思义就是发布者将数据发布到 ZooKeeper 的⼀个或⼀系列节点上,供订阅者进⾏数据订阅,进⽽达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
两种设计模式:
-
推(Push)模式:服务端主动将数据更新发送给所有订阅的客户端
-
拉(Pull)模式:由客户端主动发起请求来获取最新数据,通常客户端都采⽤定时进⾏轮询拉取的⽅式
-
ZooKeeper 采⽤的是推拉相结合的⽅式:
-
客户端向服务端注册⾃⼰需要关注的节点,⼀旦该节点的数据 发⽣变更,那么服务端就会向相应的客户端发送 Watcher 事件通知。
-
客户端接收到这个消息通知之后, 需要主动到服务端获取最新的数据。
-
1.2. 配置中心
- 配置获取:应⽤在启动的时候都会主动到 ZooKeeper 服务端上进⾏⼀次配置信息的获取。同时,在指定节点上注册⼀个 Watcher 监听。
- 配置变更:当配置信息发⽣变更,服务端都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的。
2. 命名服务
通过调⽤ ZooKeeper 节点创建的 API 接⼝可以创建⼀个顺序节点,并且在 API 返回值中会返回这个节点的完整名字。利⽤这个特性,我们就可以借助 ZooKeeper 来⽣成全局唯⼀的 ID
3. 集群管理
我们经常会有类似于如下的需求:
- 如何快速的统计出当前⽣产环境下⼀共有多少台机器
- 如何快速的获取到机器上下线的情况
- 如何实时监控集群中每台主机的运⾏时状态
3.1. 分布式日志收集系统
问题:
如何快速、合理、动态地为每个日志收集器分配对应的⽇志生产机器。
⽇志生产机器和日志收集机器的扩容和缩容
步骤:
-
注册收集器: 每个收集机器启动时,都会在总节点下创建⾃⼰的节点,例如
/logs/collector/[Hostname]
节点类型为持久节点。若为临时节点,其在会话结束后会被删除,分配的日志生产节点也会消失。
所以可以通过定期维护 status 子节点来表明机器状态
-
任务分发: 系统根据收集器节点下⼦节点的个数,将所有⽇志生产机器分成对应的若⼲组,然后将分组后的机器列表分别写到这些收集器机器创建的⼦节点上去
-
状态汇报:
- 每个收集器在创建完节点后,还需要在其⼦节点上创建⼀个⼦节点代表状态,例如
/logs/collector/host1/status
。 - 每个收集器需要定期向该节点写⼊⾃⼰的状态信息(⼼跳检测机制),通常写⼊⽇志收集进度信息。⽇志系统根据该节点的最后更新时间来判断对应的收集器是否存活。
- 每个收集器在创建完节点后,还需要在其⼦节点上创建⼀个⼦节点代表状态,例如
-
动态分配: ⽇志系统始终关注
/logs/collector
这个节点下所有⼦节点的变更,⼀旦检测到有收集器停⽌汇报或是有新的收集器加⼊,就要开始进⾏任务的重新分配。若采⽤ Watcher 机制,那么通知的消息量的⽹络开销⾮常⼤。
可采⽤⽇志系统主动轮询收集器节点的策略,这样可以节省⽹络流量,但是存在⼀定的延时。
4. Master 选举
作用: 达到只使用一台 Master 处理逻辑,同步至多台 Follower 的效果
原理: ZooKeeper 在分布式高并发下能使节点的创建保证全局唯⼀性,Master 选举可理解成多机器抢分布式锁的过程。
过程:
- Client 集群每天定时会通过 ZooKeeper 来实现 Master 选举
- 在 ZooKeeper 上创建⼀个⽇期节点,例如 2020-11-11。
- Client 集群每天都会定时创建⼀个临时节点,例如
/master_election/2020-1111/binding
。创建成功的客户端成为 Master。其他成功创建节点的客户端,都会在节点/master_election/2020-11-11
上注册⼀个子节点变更的 Watcher,⽤于监控当前的 Master 机器是否存活。 - ⼀旦发现当前的 Master 挂了,那么其余的客户端将会重新进行 Master 选举。
- Master 会负责进⾏⼀系列的海量数据处理,最终计算得到⼀个数据结果,并将其放置在⼀个内存/数据库中。同时,Master 还需要通知集群中其他所有的客户端从这个内存/数据库中共享计算结果。
缺点:
负载大,扩展性差。如果有上万个客户端都参与竞选,意味着同时会有上万个写请求。
由于 ZooKeeper 会把写请求转发到 Leader 来处理,再广播到 Follower,所以其写性能不高。
同时一旦 Leader 放弃领导权,ZooKeeper 需要同时通知上万个 Follower,负载较大。
5. 分布式锁
5.1. 排他锁(非公平)
概述: 加锁期间,只允许持有锁的对象对数据进⾏读取和更新操作
实现:
- 定义锁: 通过 ZooKeeper 上的临时数据节点来表示⼀个锁,例如
/exclusive_lock/lock
节点就可以被定义为⼀个锁 - 获取锁: 在
/exclusive_lock
节点下创建临时⼦节点/exclusive_lock/lock
,成功创建的客户端就被认为获取了锁。所有没有获取到锁的客户端就需要到/exclusive_lock
节点上注册⼀个⼦节点变更的 Watcher 监听 - 释放锁: 客户端挂掉或者客户端完成业务删除节点。ZooKeeper 会通知所有在
/exclusive_lock
节点上注册了⼦节点变更 Watcher 监听的客户端。客户端在接收到通知后,再次重新发起分布式锁获取。
5.2. 排他锁(公平)
实现:
-
定义锁: 通过 ZooKeeper 上的临时数据节点来表示⼀个锁,
/shared_lock/[Hostname]-请求类型-序号
的临时顺序节点 -
获取锁: 所有客户端都会到
/shared_lock
这个节点下⾯创建⼀个临时顺序节点,然后获取/shared_lock
节点下所有⼦节点- 若自己不是序号最小的子节点,那么客户端调用
exist()
方法监听前一个节点。 - 接收到 Watcher 通知后,检查自己是不是最小子节点(可能只是前面的未持锁节点宕机了)
- 若自己不是序号最小的子节点,那么客户端调用
-
释放锁: 客户端挂掉或者客户端完成业务删除节点。ZooKeeper 会通知监听的客户端。客户端在接收到通知后,再次重新发起分布式锁获取。
5.3. 共享锁
概述: 加锁期间,只允许所有持锁对象对数据进行读取操作,不允许写操作。
实现: 与公平排他锁类似
-
定义锁: 通过 ZooKeeper 上的临时数据节点来表示⼀个锁,
/shared_lock/[Hostname]-请求类型-序号
的临时顺序节点 -
获取锁: 所有客户端都会到
/shared_lock
这个节点下⾯创建⼀个临时顺序节点,然后获取/shared_lock
节点下所有⼦节点- 对于读请求: 若没有比自己序号小的子节点或所有比自己序号小的⼦节点都是读请求,那么表明自己已经成功获取到共享锁,同时开始执行读取逻辑。否则客户端调用
exist()
方法监听前一个 写请求 节点。 - 对于写请求: 若⾃⼰不是序号最小的⼦节点,那么客户端调用
exist()
方法监听前一个节点。 - 接收到 Watcher 通知后,重复步骤 1
- 对于读请求: 若没有比自己序号小的子节点或所有比自己序号小的⼦节点都是读请求,那么表明自己已经成功获取到共享锁,同时开始执行读取逻辑。否则客户端调用
-
释放锁: 客户端挂掉或者客户端完成业务删除节点。ZooKeeper 会通知监听的客户端。客户端在接收到通知后,再次重新发起分布式锁获取。
6. 分布式队列
ZooKeeper 不适合作为队列
- 节点大小不足: ZK 有 1MB 的传输限制。 实践中 ZNode 必须相对较小,而队列包含的消息非常大。
- 内存空间不足: ZK 的数据库完全放在内存中。 大量的 Queue 意味着会占用很多的内存空间。
- 启动慢: 如果有很多节点,ZK 启动时相当的慢。 而使用 queue 会导致好多 ZNode. 你需要显著增大 initLimit 和 syncLimit。
- 性能差: 包含成千上万的子节点的 ZNode 时, ZK 的性能变得不好
6.1. FIFO 先入先出队列
和锁的实现相似
- 创建持久顺序节点(由于创建的节点是持久化的,所以不必担心队列消息的丢失问题)
- 获取列表判断是否为最小顺序节点
- 最小:处理逻辑,之后删除
- 不是最小:监听并等待前一个节点
6.2. Barrier 分布式屏障(同步队列)
概述: 特指系统之间的⼀个协调条件,规定了⼀个队列的元素必须都集聚后才能统⼀进⾏安排,否则⼀直等待
应⽤场景: ⼤规模分布式并⾏计算,最终的合并计算需要基于很多并⾏计算的⼦结果来进⾏
过程:
- /queque_barrier 节点值为 10,客户端再该节点下创建子节点
- 获取 /queue_barrier 节点的数据内容:10
- 获取全部节点列表并注册对 /queque_barrier 子节点变化的监听
- 若子节点个数不足 10 个则等待直到个数等于 10
- 若子节点个数等于 10 则进行业务处理
九、集群
1. 事务 Id
概述:
-
变更状态: 事务是指能够改变 ZooKeeper 服务器状态的操作,我们也称之为事务操作或更新操作。⼀般包括数据节点创建与删除、数据节点内容更新等操作。
-
ZXID: 对于每⼀个事务请求(proposal 提议),ZooKeeper 都会为其分配⼀个全局唯⼀(zk 中唯一)且有序的事务 ID,用 ZXID 来表示,通常是⼀个 64 位的数字。
- 高 32 位是 epoch(投票轮次),用来标识 Leader 是否发生改变。从 1 开始,如果有新的 Leader 产生出来,epoch 会自增。
- 低 32 位用来递增计数。每次 epoch 变化,都将低 32 位的序号重置。
作用: 标识节点同步状态。
2. Server 工作状态
- LOOKING: 不确定 Leader 状态。该状态下的服务器认为当前集群中没有 Leader,会发起 Leader 选举。
- FOLLOWING: 跟随者状态。表明当前服务器角色是 Follower,并且它知道 Leader 是谁。
- OBSERVING: 观察者状态。表明当前服务器角色是 Observer
- LEADING: 领导者状态。表明当前服务器角色是 Leader,它会维护与 Follower 间的心跳。
3. 服务器角色
- Leader
- Follower:参与选举,参与写操作时的投票。对外提供读服务。
- Observer:与 Folower 唯一的不同在于不参与选举,也不参与集群写操作时的投票。对外提供读服务。
Follower 与 Observer 统称 Learner
3.1. Leader
主要工作
- 有且仅有一个。事务请求的唯⼀调度和处理者,保证集群事务处理的顺序性。
- 集群内部各服务器的调度者。
请求处理链
使⽤责任链来处理每个客户端的请求
-
预处理器(PrepRequestProcessor): 识别出当前客户端请求是否是事务请求,并对其进行预处理。
对事务请求进⾏⼀系列预处理,如创建请求事务头、事务体、会话检查、ACL 检查和版本检查等
-
事务处理器(ProposalRequestProcessor): Leader 事务处理流程的发起者。
- 非事务性请求:将请求转发到 CommitProcessor 处理器
- 事务性请求:将请求转发到 CommitProcessor 处理器。根据请求类型创建对应的 Proposal 提议广播给所有的 Follower。还会将 Proposal 交付给 SyncRequestProcessor 进⾏事务⽇志的记录。
-
事务⽇志处理器(SyncRequestProcessor): ⽤来将事务请求记录到事务⽇志⽂件中,同时
会触发 Zookeeper 进⾏数据快照。 -
应答处理器(AckRequestProcessor): 负责在 SyncRequestProcessor 完成事务⽇志记录后,向 Proposal 的投票收集器发送 ACK 反馈,以通知投票收集器当前服务器已经完成了对该 Proposal 的事务⽇志记录。
-
事务提交处理器(CommitProcessor):
- 对于非事务请求:该处理器会直接将其交付给下⼀级处理器处理
- 对于事务请求:其会等待集群内针对 Proposal 的投票直到该 Proposal 可被提交,利⽤ CommitProcessor,每个服务器都可以很好地控制对事务请求的顺序处理。
-
应用队列处理器(ToBeAppliedRequestProcessor): 该处理器有⼀个 toBeApplied 队列,用来存储那些已经被 CommitProcessor 处理过的可被提交的 Proposal。其会将这些请求交付给 FinalRequestProcessor 处理器处理,待其处理完后,再将其从 toBeApplied 队列中移除。
-
最终处理器(FinalRequestProcessor): ⽤来进⾏返回请求前的操作,包括创建客户端请求的响应。针对事务请求,该处理器还会负责将事务应⽤到内存数据库中。
3.2. Follower
主要工作
- 处理客户端非事务性请求(读取数据),转发事务请求给 Leader。
- 参与事务请求 Proposal 的投票。
- 参与 Leader 选举投票。
请求处理链
- FollowerRequestProcessor:当前请求若是事务请求则会将该请求转发给 Leader, Leader 在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进行处理。
- SendAckRequestProcessor:承担了事务日志记录反馈的角色,在完成事务日志记录后,会向 Leader 发送 ACK 消息以表明⾃身完成了事务日志的记录工作
3.3. Observer
Observer 是 ZooKeeper 自 3.3.0 版本开始引⼊的⼀个全新的服务器角色。
和 Follower 唯⼀的区别在于,Observer 不参与任何形式的投票,包括事务请求 Proposal 的投票和 Leader 选举投票。
Observer 服务器只提供非事务服务,通常⽤于在不影响集群事务处理能⼒的前提下提升集群的非事务处理能力。
4. 持久化
4.1. 数据快照
概述: 用来记录 zk 服务器上某一时刻的全量内存数据内容,并将其写入到指定的磁盘文件中,可通过 dataDir 配置文件目录。
snapCount 参数: 设置两次快照之间的事务操作个数。zk 节点记录完事务日志时,若距离上次快照,事务操作次数等于 snapCount/2~snapCount 中的某个值时,会触发快照生成操作,随机值是为了避免所有节点同时生成快照,导致集群影响缓慢)。
4.2. 事务日志
所有事务操作都是需要记录到日志文件中的,可通过 dataLogDir 配置文件目录,文件是以写入的第一条事务 zxid 为后缀,方便后续的定位查找。
zk 会采取 “磁盘空间预分配” 的策略,来避免磁盘 Seek 频率,提升 zk 服务器对事务请求的影响能力。默认设置下,每次事务日志写入操作都会实时刷入磁盘,也可以设置成非实时(写到内存文件流,定时批量写入磁盘),但那样断电时会带来丢失数据的风险。
十、ZAB 协议
1. 概述
Zookeeper 使用⼀个单⼀的主进程来接收并处理客户端的所有事务请求
并采用 ZAB ( Zookeeper Atomic Broadcast,原子广播协议),将服务器数据的状态变更以事务 Proposal 的形式广播到所有的副本进程中
读写请求官方压测:
3888 端口通信模型:
任何两个节点都互通
2. 恢复模式(选 Master + 数据同步)
官方压测 200ms 恢复
步骤:
-
Leader 选举过程: Leader 与过半 Follower 失去联系,Follower 服务器都会将自己的服务器状态变更为 LOOKING,并进⼊ Leader 选举过程。
-
数据同步: ZAB 会选举产生新的 Leader 服务器,然后有过半(防止脑裂)的机器与该 Leader 服务器完成了数据同步之后会退出恢复模式。
注意的问题:
- ZAB 协议需要确保那些已经在 Leader 上提交的事务最终被所有服务器都提交
- ZAB 协议需要确保丢弃那些只在 Leader 上被提出但尚未发出的事务
ZAB 保证新选举出来的 Leader 服务器拥有集群中所有机器最高编号(即 ZXID 最大)事务 Id 的 Proposal 来解决上述问题
2.1. Leader 选举
Leader 所在的机器挂了或者失去大多数的 Follower 会进入恢复模式,进行新⼀轮的 Leader 选举。
服务器运行期间的 Leader 选举和启动时期的 Leader 选举基本过程是⼀致的。
选举算法:
通过 zoo.cfg 配置文件中的 electionAlg 属性指定(0-3)
FastLeaderElection 算法(值为 3。TCP 实现,zk 3.4.0 之后只保留了该算法,废弃了 0-2)
过程:
-
每个 Server 会将自身的(myid,ZXID)发送给集群以便通知其他 Server 自己的选择。
这是投给自己的票,因为此时还不知道其他 Server 的状态。例如 Server1 的 myid 为 1,ZXID 为 0,则发送(1, 0)
-
接受来自各个服务器的投票
- 集群的每个服务器收到投票后,判断该投票的有效性,如检查是否是本轮投票、是否来⾃ LOOKING 状态的服务器。
- 针对每⼀个投票,服务器都需要将别人的投票和自己的投票进行选择,以确定是否变更投票
- 比较 ZXID:如果接收到的投票的 ZXID 比自己的大,则当前 Server 认可收到的投票,并再次将该投票发送出去。反之,若接收到的投票的 ZXID 比自己的小,则不做任何操作
- 比较 myid:如果 ZXID 相同则比较 myid。myid 较⼤的投票会被认可并再次发送出去。反之,若接收到的 myid 比自己的小,则不做任何操作
-
统计投票
Server 都接收到其他 Server 的变更投票后会开始统计投票,如果一台 Server 中相同的投票超过半数则该投票对应的 myid 的 Server 成为 Leader。
为什么过半机制中是大于,而不是大于等于。就是为了防止脑裂
-
改变服务器状态
⼀旦确定了 Leader,每个服务器就会更新⾃⼰的状态:如果是 Follower,那么就变更为 FOLLOWING,如果是 Leader,那么就变更为 LEADING。
2.2. 数据同步
数据同步消息方式:
消息类型 | 发送方向 | 说明 |
---|---|---|
DIFF | Leader -> Learner | 通知 Learner 即将与 Learner 进行增量方式的数据同步 |
TRUNC | Leader -> Learner | 触发 Learner 进行其内存数据库的回滚操作 |
SNAP | Leader -> Learner | 通知 Learner 即将与 Learner进行全量方式的数据同步 |
UPTODATE | Leader -> Learner | 通知 Learner 已完成数据同步,可对外提供服务 |
服务器初始化消息类型:
消息类型 | 发送方向 | 说明 |
---|---|---|
OBSERVERINFO | Observer -> Leader | Observer 启动时发送自身的(myid,zxid)给 Leader,用于向 Leader 表明角色并注册自己 |
FOLLOWERINFO | Follower -> Leader | Follower 启动时发送自身的(myid,zxid)给 Leader,用于向 Leader 表明角色并注册自己 |
LEADERINFO | Leader -> Learner | Leader 接收到来自 Learner 的上述两类消息后会将当前 Leader 的 epoch 发送 Learner |
ACKEPOCH | Learner -> Leader | Learner 收到 LEADERINFO 消息后会将自己最新的 zxid 和 epoch 发送给 Leader |
NEWLEAEDER | Leader -> Learner | 足够多的 Follower 连上 Leader 或完成数据同步后,Leader 会向 Learner 发送当前 Leader 最新的 zxid |
过程:
-
Leader 加载快照: 重新加载本地磁盘上的数据快照至内存,并从日志文件中取出快照之后的所有事务操作,逐条应用至内存,并添加到已提交事务缓存 commitedProposals。
这样能保证日志文件中的事务操作,必定会应用到 leader 的内存数据库中。
-
确定同步方式:
-
获取 Learner 发送的
OBSERVERINFO/FOLLOWERINFO
信息(myid,zxid),即 id 和已提交过的最大消息 zxid。 -
用 Learner 的最大 zxid 与 Leader 提交过的消息(commitedProposals)中的最小 zxid(min_zxid)和最大 zxid(max_zxid)分别作比对,确定采用哪种同步方式(DIFF 同步、TRUNC+DIFF 同步、SNAP 同步)。
- 如果该 zxid 介于 min_zxid 与 max_zxid 之间,但又不存在于 commitedProposals 中时,说明该 zxid 对应的事务需要 TRUNC 回滚
- 如果该 zxid 介于 min_zxid 与 max_zxid 之间且存在于 commitedProposals 中,则 Leader 需要将 zxid+1~max_zxid 间所有事务同步给 Learner(DIFF)
- 如果该 zxid == max_zxid,说明已经完全同步了
-
-
数据同步: Leader 主动向所有 Learner 发送同步数据消息,每个 Learner 有自己的发送队列。
同步结束时,Leader 会向 Learner 发送 NEWLEADER 消息,同时 Learner 会反馈一个 ACK。当 Leader 接收到来自 Learner 的 ACK 消息后,就认为当前 Learner 已经完成了数据同步,然后进入 等待是否过半阶段。
-
同步完成: 当 Leader 统计到收到了一半已上的 ACK 时,会向所有已经完成数据同步的 Learner 发送一个
UPTODATE
消息,用来通知 Learner 集群已经完成了数据同步,可以对外服务了。
3. 广播模式(事务操作节点同步)
原子广播协议特性:
- 原子性:要么成功,要么失败,不存在中间状态。(队列 + 2PC 实现)
- 广播:Leader 通知所有节点进行操作
有过半的 Follower 服务器完成了和 Leader 服务器的数据同步,那么就会进⼊消息⼴播模式。
扩容加入的新服务器会与 Leader 进行数据同步然后参与到消息⼴播流程中
Leader/Follower/Observer 都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。
- 转发请求: Follower 接收到写请求后会转发给 Leader 来处理
- 发送事务消息: Leader 接收到写请求后会把该写请求转换成带有各种状态的事务,并会分配给 Proposal 一个单调递增的唯一 id(zxid)。然后 Leader 会将广播的事务 Proposal 依次添加到 发送队列 中,并且根据 FIFO 策略进行消息发送。保证最终一致性
- 返回应答: 每⼀个 Follower 接收到这个事务 Proposal 之后,都会将其以事务日志的形式写入到本地磁盘中去,然后向 Leader 返回 Ack 进行投票。Observer 不参与投票
- 提交事务: Leader 接收到超过半数 Ack,会给所有的 Follower 广播⼀个 COMMIT 消息进行事务提交,给所有的 Observer 广播 INFORM 消息进行提交,同时 Leader 也会进行事务提交
随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。
4. ZAB 与 Paxos 的异同
相同点:
- 都存在⼀个类似于 Leader 进程的角色,由其负责协调多个 Follower 进程的运行
- Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将⼀个提议进行提交
- 在 ZAB 协议中,每个 Proposal 中都包含了⼀个 epoch 值,用来代表当前的 Leader 周期。在 Paxos 算法中为 Ballot
不同点: ZAB 协议主要⽤于构建⼀个⾼可⽤的分布式数据主备系统,Paxos 算法则⽤于构建⼀个分布式的⼀致性状态机系统
- Paxos 算法中,新选举产⽣的主进程会进⾏两个阶段的⼯作,第⼀阶段称为读阶段,新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第⼆阶段称为写阶段,当前主进程开始提出自己的提议。
- ZAB 协议在 Paxos 基础上添加了同步阶段。新选出的 Leader 会确保存在过半的 Follower 提交
了之前接收到的所有事务 Proposal。这⼀同步阶段的引入,能够有效地保证 Leader 在新的周期中提出事务 Proposal 之前,所有的进程都已经完成了对之前所有事务 Proposal 的提交
十一、源码
1. 搭建
版本:3.7
github clone 下来之后用 IDEA 打开,maven clean install
服务端 debug 配置:
客户端 debug 配置
2. Server 启动流程
QuorumPeerMain#initializeAndRun 启动类:
- 解析配置文件: QuorumPeerConfig#parse。zoo.cfg 配置运行时的基本参数,如 tickTime、dataDir、clientPort 等参数
- 创建并启动历史文件清理器: DatadirCleanupManager#start。对事务日志和快照数据文件进行定时清理。
- 判断是否是集群模式: QuorumPeerConfig#isDistributed
2.1. 集群模式
是集群模式则调用 QuorumPeerMain#runFromConfig
-
开启集群模式: QuorumPeerMain#runFromConfig
-
创建并配置 ServerCnxnFactory:
调用 ServerCnxnFactory#createFactory() 创建 ServerCnxnFactory。
调用 ServerCnxnFactory#configure(java.net.InetSocketAddress, int, int, boolean) 配置 ServerCnxnFactory。
-
获取 QuorumPeer 并设置相关组件:
调用 QuorumPeerMain#getQuorumPeer 获取 QuorumPeer。其父类继承了 Thread
调用 QuorumPeer#setTxnFactory 设置数据管理器
调用 QuorumPeer#setZKDatabase 设置 zkDataBase
调用 QuorumPeer#initialize 进行初始化
-
启动服务: QuorumPeer#start。
- 恢复本地数据:QuorumPeer#loadDataBase
- 启动主线程:QuorumPeer#startServerCnxnFactory
- 初始化 Leader 选举:QuorumPeer#startLeaderElection。创建选举环境,启动相关线程
- 创建选给自身的选票
- 初始化选举算法:QuorumPeer#createElectionAlgorithm(electionType)
- 开启监听:
QuorumCnxManager.Listener listener = qcm.listener; listener.start();
。 - 开启选举:FastLeaderElection#start
- 开启监听:
- 启动 QuorumPeer:Thread#start,调用其 run() 方法。一直循环判断状态
- 节点状态为 LOOKING:调用 lookForLeader() 方法。
setCurrentVote(makeLEStrategy().lookForLeader())
。进行选举 - 节点状态为 OBSERVING:设置当前节点启动模式为 Observer,调用 Observer#observeLeader 与 Leader 节点进行数据同步
- 节点状态为 FOLLOWER:设置当前节点启动模式为 Follower,调用 Follower#followLeader 与 Leader 节点进行数据同步
- 节点状态为 Leader:设置当前节点启动模式为 Leader,调用 Leader#lead 发送自己是 Leader 的通知
- 节点状态为 LOOKING:调用 lookForLeader() 方法。
-
2.2. 单机模式
是单机模式则调用 ZooKeeperServerMain#main:把启动工作委派给 ZooKeeperServerMain 类。调用 ZooKeeperServerMain#initializeAndRun
-
重新解析配置文件: ServerConfig#parse(java.lang.String)。创建服务配置对象,重新解析
-
运行服务: ZooKeeperServerMain#runFromConfig
-
创建数据管理器: new FileTxnSnapLog(config.dataLogDir, config.dataDir)
-
创建 Server 实例:
new ZooKeeperServer()
。Zookeeper 服务器首先会进行服务器实例的创建
然后对该服务器实例进行初始化,包括连接器、内存数据库、请求处理器等组件的初始化
-
创建 admin 服务: AdminServerFactory#createAdminServer。用于接收请求(创建 jetty 服务)
-
创建并配置 ServerCnxnFactory:
调用 ServerCnxnFactory#createFactory() 负责客户端与服务器的连接
调用 ServerCnxnFactory#configure(java.net.InetSocketAddress, int, int, boolean) 配置 ServerCnxnFactory
-
启动服务: ServerCnxnFactory#startup(ZooKeeperServer)
-
启动相关线程:NIOServerCnxnFactory#startxup
- new WorkerService(“NIOWorker”, numWorkerThreads, false):初始化 worker 线程池
- 开启所有 SelectorThread 线程,用于处理客户端请求
- 启动 acceptThread 线程,用于处理接收连接进行事件
- 启动 expirerThread 线程,用于处理过期连接
-
加载数据到 zkDataBase:ZooKeeperServer#startdata。ZooKeeperServer#loadData:加载磁盘上已经存储的数据
-
ZooKeeperServer#startup:
-
初始化 Session 追踪器:ZooKeeperServer#createSessionTracker
-
启动 Session 追踪器:ZooKeeperServer#startSessionTracker
-
建立请求处理链路:ZooKeeperServer#setupRequestProcessors
protected void setupRequestProcessors() { RequestProcessor finalProcessor = new FinalRequestProcessor(this); RequestProcessor syncProcessor = new SyncRequestProcessor(this, finalProcessor); ((SyncRequestProcessor) syncProcessor).start(); firstProcessor = new PrepRequestProcessor(this, syncProcessor); ((PrepRequestProcessor) firstProcessor).start(); }
-
注册 JMX:ZooKeeperServer#registerJMX
-
-
-
3. Leader 选举
public interface Election {
// 寻找 Leader
Vote lookForLeader() throws InterruptedException;
// 关闭服务端之间的连接
void shutdown();
}
选举类图:
FastLeaderElection#lookForLeader:
- 更新时钟:
logicalclock.incrementAndGet()
。logicalclock 为 AtomicLong 类型。 - 初始化选票为自身的选票(myid,zxid,epoch): FastLeaderElection#updateProposal(long leader, long zxid, long epoch)
- 发送选票: FastLeaderElection#sendNotifications。将选票信息封装成 ToSend 对象,由 workerSender(LinkedBlockingQueue)发送出去
- (循环)判断是否为 LOOKING 状态
-
接收外部投票:
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS)
。每台服务器会不断的从 recvqueue 中获取外部投票 -
处理接收到的投票(选票 PK)
-
接收到的投票的 epoch > 当前投票的 epoch 时:
n.electionEpoch > logicalclock.get()
-
更新 epoch(选举轮次)为接收到的外部投票的 epoch:
logicalclock.set(n.electionEpoch)
-
清空之前所有已经收到的投票:
recvset.clear()
。recvset(HashMap<Long, Vote>,意在收集本轮收到的选票) -
选票 PK:用 FastLeaderElection#totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) 方法判断。返回 true 则更新为接收到的选票,false 则更新为当前自身生成的选票。
此次 PK 为 接收到的外部选票 与 自身生成的选票(myid,zxid,epoch) 之间的 PK。
-
newEpoch > curEpoch:更新为接收到的选票
-
newEpoch == curEpoch && newZxid > curZxid:更新为接收到的选票
-
newEpoch == curEpoch && newZxid == curZxid && newId > curId:更新为接收到的选票
-
其余情况更新为当前自身生成的选票
依然用 FastLeaderElection#updateProposal 方法更新选票
-
-
发送更新完的选票:FastLeaderElection#sendNotifications
-
-
接收到的投票的 epoch < 当前投票的 epoch 时:忽略
-
接收到的投票的 epoch == 当前投票的 epoch 时:
-
FastLeaderElection#totalOrderPredicate 选票 PK,与第一种情况类似,返回 true 则更新为接收到的选票,false 则更新为当前持有的选票。
此次 PK 为 接收到的外部选票 与 当前持有的选票 之间的 PK。
-
发送更新完的选票:FastLeaderElection#sendNotifications
-
-
-
记录选票: 记录收到的选票到 Map 中
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch))
-
统计选票: 判断当前 Server 收到的票数是否可以结束选举
- 遍历 recvset 中的所有投票信息,将等于当前投票的
<sid, vote>
放入 voteSet 中 - 统计投票:SyncedLearnerTracker#hasAllQuorums。查看投给某个 sid(myid) 的票数是否超过一半,过半则更新服务器状态
- 遍历 recvset 中的所有投票信息,将等于当前投票的
-