ZooKeeper 是一个分布式协调中间件,是 Google 的 Chubby 一个开源的实现。它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
特性:
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
启动服务:
./zkServer.sh start
启动客户端:
./zkCli.sh
查看启动状态:
./zkServer.sh status
每当客户端连接到服务端会创建一个 SessionId。创建/删除 SessionId 也是事务操作,会有 zxid,会被同步到所有节点中
Session 过期,则根据该 Session 创建的临时节点 znode 都会被抛弃
可设置超时时间。未超时的情况下,客户端 api 若保存了 SessionId,重连后 Session 不会消失(还是原来的 Session)
心跳机制
Zookeeper 提供了一种树形结构级的命名空间
为了保证高吞吐和低延迟,Zookeeper 在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为 1M。
有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,以此类推
查看某个路径下有多少个节点
[zk: localhost:2181(CONNECTED) 2] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 3] ls /zookeeper
[quota]
= ls + stat
获取值
[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
[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
-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 创建临时节点
[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
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
[zk: localhost:2181(CONNECTED) 31] delete /test/sec
[zk: localhost:2181(CONNECTED) 32] ls /test
[tmp]
version: 验证版本号
若填写的版本号与当前版本号不一致则 delete 失败( 同 set)
概述: 为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
只有 stat、get、ls 才能设置 watcher
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
过程:
watchManager:
Zk 服务器端 watcher 的管理者。负责 watcher 事件的触发。
从两个维度维护 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);
}
}
概述: 针对节点可以设置相关读写权限,保障数据安全
通过 scheme:id:permissions
来构成权限列表
例: setAcl path world:anyone:d 代表为 path 下的节点设置权限为所有人都只能删除该节点
crdwa 代表的权限含义:
概述:
添加认证授权信息到 Zookeeper 库中(注册)
并使用该认证作为当前客户端的认证信息(登录),这之后进行的所有操作会以该认证为前提
密码需输入明文,但在 Zookeeper 中密码以加密形式存储
[zk: localhost:2181(CONNECTED) 2] addauth digest ccomma:ccomma
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
获取某个节点的 acl 权限信息
密码以密文形式存储
[zk: localhost:2181(CONNECTED) 8] getAcl /ccomma
'digest,'ccomma:91PXC4WimSDWZikp99kGvvjeVnY=
:crdwa
添加超级用户:
在 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
;;
安装 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
zk 有 Session,没有线程池的概念
Zookeeper API 共包含五个包:
依赖:
<dependency> <groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.14version>
dependency>
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==========");
}
}
ZooKeeper#create(String path, byte[] data, List
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);
}
}
ZooKeeper#getData(String path, boolean watch, Stat stat):
ZooKeeper#getChildren(String path, boolean 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);
}
}
Stat ZooKeeper#setData(String path, byte[] data, int version)
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));
}
}
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 ? "该节点不存在":"该节点存在");
}
}
重试策略:
// 同 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();
String path = client.create()
// 1.递归创建节点
//.creatingParentsIfNeeded()
//.withMode(createMode)
// 2.ACL
//.withACL(aclList)
.forPath(nodePath, "data".getBytes());
client.setData()
// 可带版本
//.withVersion(version)
.forPath(nodePath, "data".getBytes());
client.delete()
// 1.如果删除失败,那么在后端还是继续会删除,直到成功(强制删除)
//.guaranteed()
// 2.如果有子节点,就删除
//.deletingChildrenIfNeeded()
// 3.指定版本
//.withVersion(1)
.forPath(nodePath);
Stat stat = new Stat();
byte[] data = client.getData()
// 包含状态查询
//.storingStatIn(stat)
.forPath(nodePath);
System.out.println("节点" + nodePath + "的数据为:" + new String(data));
System.out.println("该节点的版本号为:"+ stat.getVersion());
List<String> nodeList = client.getChildren().forPath(nodePath);
public Boolean checkExists(String nodePath) {
return client.checkExists().forPath(nodePath) != null;
}
usingWatcher
概述: 监听只会触发一次
byte[] data = client.getData()
.usingWatcher(event -> System.out.println("触发watcher,节点路径为:" + event.getPath()))
.forPath(nodePath);
NodeCache
概述: 永久监听 当前路径的节点 的创建、更新、删除
start():
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):
事件类型:
// 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()));
}
});
zookeeper 系列
即所谓的配置中⼼,顾名思义就是发布者将数据发布到 ZooKeeper 的⼀个或⼀系列节点上,供订阅者进⾏数据订阅,进⽽达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
两种设计模式:
推(Push)模式:服务端主动将数据更新发送给所有订阅的客户端
拉(Pull)模式:由客户端主动发起请求来获取最新数据,通常客户端都采⽤定时进⾏轮询拉取的⽅式
ZooKeeper 采⽤的是推拉相结合的⽅式:
客户端向服务端注册⾃⼰需要关注的节点,⼀旦该节点的数据 发⽣变更,那么服务端就会向相应的客户端发送 Watcher 事件通知。
客户端接收到这个消息通知之后, 需要主动到服务端获取最新的数据。
通过调⽤ ZooKeeper 节点创建的 API 接⼝可以创建⼀个顺序节点,并且在 API 返回值中会返回这个节点的完整名字。利⽤这个特性,我们就可以借助 ZooKeeper 来⽣成全局唯⼀的 ID
我们经常会有类似于如下的需求:
问题:
如何快速、合理、动态地为每个日志收集器分配对应的⽇志生产机器。
⽇志生产机器和日志收集机器的扩容和缩容
步骤:
注册收集器: 每个收集机器启动时,都会在总节点下创建⾃⼰的节点,例如 /logs/collector/[Hostname]
节点类型为持久节点。若为临时节点,其在会话结束后会被删除,分配的日志生产节点也会消失。
所以可以通过定期维护 status 子节点来表明机器状态
任务分发: 系统根据收集器节点下⼦节点的个数,将所有⽇志生产机器分成对应的若⼲组,然后将分组后的机器列表分别写到这些收集器机器创建的⼦节点上去
状态汇报:
/logs/collector/host1/status
。动态分配: ⽇志系统始终关注 /logs/collector
这个节点下所有⼦节点的变更,⼀旦检测到有收集器停⽌汇报或是有新的收集器加⼊,就要开始进⾏任务的重新分配。
若采⽤ Watcher 机制,那么通知的消息量的⽹络开销⾮常⼤。
可采⽤⽇志系统主动轮询收集器节点的策略,这样可以节省⽹络流量,但是存在⼀定的延时。
作用: 达到只使用一台 Master 处理逻辑,同步至多台 Follower 的效果
原理: ZooKeeper 在分布式高并发下能使节点的创建保证全局唯⼀性,Master 选举可理解成多机器抢分布式锁的过程。
过程:
/master_election/2020-1111/binding
。创建成功的客户端成为 Master。其他成功创建节点的客户端,都会在节点 /master_election/2020-11-11
上注册⼀个子节点变更的 Watcher,⽤于监控当前的 Master 机器是否存活。缺点:
负载大,扩展性差。如果有上万个客户端都参与竞选,意味着同时会有上万个写请求。
由于 ZooKeeper 会把写请求转发到 Leader 来处理,再广播到 Follower,所以其写性能不高。
同时一旦 Leader 放弃领导权,ZooKeeper 需要同时通知上万个 Follower,负载较大。
概述: 加锁期间,只允许持有锁的对象对数据进⾏读取和更新操作
实现:
/exclusive_lock/lock
节点就可以被定义为⼀个锁/exclusive_lock
节点下创建临时⼦节点 /exclusive_lock/lock
,成功创建的客户端就被认为获取了锁。所有没有获取到锁的客户端就需要到 /exclusive_lock
节点上注册⼀个⼦节点变更的 Watcher 监听/exclusive_lock
节点上注册了⼦节点变更 Watcher 监听的客户端。客户端在接收到通知后,再次重新发起分布式锁获取。实现:
定义锁: 通过 ZooKeeper 上的临时数据节点来表示⼀个锁,/shared_lock/[Hostname]-请求类型-序号
的临时顺序节点
获取锁: 所有客户端都会到 /shared_lock
这个节点下⾯创建⼀个临时顺序节点,然后获取 /shared_lock
节点下所有⼦节点
exist()
方法监听前一个节点。释放锁: 客户端挂掉或者客户端完成业务删除节点。ZooKeeper 会通知监听的客户端。客户端在接收到通知后,再次重新发起分布式锁获取。
概述: 加锁期间,只允许所有持锁对象对数据进行读取操作,不允许写操作。
实现: 与公平排他锁类似
定义锁: 通过 ZooKeeper 上的临时数据节点来表示⼀个锁,/shared_lock/[Hostname]-请求类型-序号
的临时顺序节点
获取锁: 所有客户端都会到 /shared_lock
这个节点下⾯创建⼀个临时顺序节点,然后获取 /shared_lock
节点下所有⼦节点
exist()
方法监听前一个 写请求 节点。exist()
方法监听前一个节点。释放锁: 客户端挂掉或者客户端完成业务删除节点。ZooKeeper 会通知监听的客户端。客户端在接收到通知后,再次重新发起分布式锁获取。
ZooKeeper 不适合作为队列
和锁的实现相似
概述: 特指系统之间的⼀个协调条件,规定了⼀个队列的元素必须都集聚后才能统⼀进⾏安排,否则⼀直等待
应⽤场景: ⼤规模分布式并⾏计算,最终的合并计算需要基于很多并⾏计算的⼦结果来进⾏
过程:
概述:
变更状态: 事务是指能够改变 ZooKeeper 服务器状态的操作,我们也称之为事务操作或更新操作。⼀般包括数据节点创建与删除、数据节点内容更新等操作。
ZXID: 对于每⼀个事务请求(proposal 提议),ZooKeeper 都会为其分配⼀个全局唯⼀(zk 中唯一)且有序的事务 ID,用 ZXID 来表示,通常是⼀个 64 位的数字。
作用: 标识节点同步状态。
Follower 与 Observer 统称 Learner
主要工作
请求处理链
使⽤责任链来处理每个客户端的请求
预处理器(PrepRequestProcessor): 识别出当前客户端请求是否是事务请求,并对其进行预处理。
对事务请求进⾏⼀系列预处理,如创建请求事务头、事务体、会话检查、ACL 检查和版本检查等
事务处理器(ProposalRequestProcessor): Leader 事务处理流程的发起者。
事务⽇志处理器(SyncRequestProcessor): ⽤来将事务请求记录到事务⽇志⽂件中,同时
会触发 Zookeeper 进⾏数据快照。
应答处理器(AckRequestProcessor): 负责在 SyncRequestProcessor 完成事务⽇志记录后,向 Proposal 的投票收集器发送 ACK 反馈,以通知投票收集器当前服务器已经完成了对该 Proposal 的事务⽇志记录。
事务提交处理器(CommitProcessor):
应用队列处理器(ToBeAppliedRequestProcessor): 该处理器有⼀个 toBeApplied 队列,用来存储那些已经被 CommitProcessor 处理过的可被提交的 Proposal。其会将这些请求交付给 FinalRequestProcessor 处理器处理,待其处理完后,再将其从 toBeApplied 队列中移除。
最终处理器(FinalRequestProcessor): ⽤来进⾏返回请求前的操作,包括创建客户端请求的响应。针对事务请求,该处理器还会负责将事务应⽤到内存数据库中。
主要工作
请求处理链
Observer 是 ZooKeeper 自 3.3.0 版本开始引⼊的⼀个全新的服务器角色。
和 Follower 唯⼀的区别在于,Observer 不参与任何形式的投票,包括事务请求 Proposal 的投票和 Leader 选举投票。
Observer 服务器只提供非事务服务,通常⽤于在不影响集群事务处理能⼒的前提下提升集群的非事务处理能力。
概述: 用来记录 zk 服务器上某一时刻的全量内存数据内容,并将其写入到指定的磁盘文件中,可通过 dataDir 配置文件目录。
snapCount 参数: 设置两次快照之间的事务操作个数。zk 节点记录完事务日志时,若距离上次快照,事务操作次数等于 snapCount/2~snapCount 中的某个值时,会触发快照生成操作,随机值是为了避免所有节点同时生成快照,导致集群影响缓慢)。
所有事务操作都是需要记录到日志文件中的,可通过 dataLogDir 配置文件目录,文件是以写入的第一条事务 zxid 为后缀,方便后续的定位查找。
zk 会采取 “磁盘空间预分配” 的策略,来避免磁盘 Seek 频率,提升 zk 服务器对事务请求的影响能力。默认设置下,每次事务日志写入操作都会实时刷入磁盘,也可以设置成非实时(写到内存文件流,定时批量写入磁盘),但那样断电时会带来丢失数据的风险。
Zookeeper 使用⼀个单⼀的主进程来接收并处理客户端的所有事务请求
并采用 ZAB ( Zookeeper Atomic Broadcast,原子广播协议),将服务器数据的状态变更以事务 Proposal 的形式广播到所有的副本进程中
读写请求官方压测:
3888 端口通信模型:
任何两个节点都互通
官方压测 200ms 恢复
步骤:
Leader 选举过程: Leader 与过半 Follower 失去联系,Follower 服务器都会将自己的服务器状态变更为 LOOKING,并进⼊ Leader 选举过程。
数据同步: ZAB 会选举产生新的 Leader 服务器,然后有过半(防止脑裂)的机器与该 Leader 服务器完成了数据同步之后会退出恢复模式。
注意的问题:
ZAB 保证新选举出来的 Leader 服务器拥有集群中所有机器最高编号(即 ZXID 最大)事务 Id 的 Proposal 来解决上述问题
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)
接受来自各个服务器的投票
统计投票
Server 都接收到其他 Server 的变更投票后会开始统计投票,如果一台 Server 中相同的投票超过半数则该投票对应的 myid 的 Server 成为 Leader。
为什么过半机制中是大于,而不是大于等于。就是为了防止脑裂
改变服务器状态
⼀旦确定了 Leader,每个服务器就会更新⾃⼰的状态:如果是 Follower,那么就变更为 FOLLOWING,如果是 Leader,那么就变更为 LEADING。
数据同步消息方式:
消息类型 | 发送方向 | 说明 |
---|---|---|
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 同步)。
数据同步: Leader 主动向所有 Learner 发送同步数据消息,每个 Learner 有自己的发送队列。
同步结束时,Leader 会向 Learner 发送 NEWLEADER 消息,同时 Learner 会反馈一个 ACK。当 Leader 接收到来自 Learner 的 ACK 消息后,就认为当前 Learner 已经完成了数据同步,然后进入 等待是否过半阶段。
同步完成: 当 Leader 统计到收到了一半已上的 ACK 时,会向所有已经完成数据同步的 Learner 发送一个 UPTODATE
消息,用来通知 Learner 集群已经完成了数据同步,可以对外服务了。
原子广播协议特性:
有过半的 Follower 服务器完成了和 Leader 服务器的数据同步,那么就会进⼊消息⼴播模式。
扩容加入的新服务器会与 Leader 进行数据同步然后参与到消息⼴播流程中
Leader/Follower/Observer 都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。
随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。
相同点:
不同点: ZAB 协议主要⽤于构建⼀个⾼可⽤的分布式数据主备系统,Paxos 算法则⽤于构建⼀个分布式的⼀致性状态机系统
版本:3.7
github clone 下来之后用 IDEA 打开,maven clean install
服务端 debug 配置:
客户端 debug 配置
QuorumPeerMain#initializeAndRun 启动类:
是集群模式则调用 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。
QuorumCnxManager.Listener listener = qcm.listener; listener.start();
。setCurrentVote(makeLEStrategy().lookForLeader())
。进行选举是单机模式则调用 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
public interface Election {
// 寻找 Leader
Vote lookForLeader() throws InterruptedException;
// 关闭服务端之间的连接
void shutdown();
}
选举类图:
FastLeaderElection#lookForLeader:
logicalclock.incrementAndGet()
。logicalclock 为 AtomicLong 类型。接收外部投票: 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
选票 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 收到的票数是否可以结束选举
放入 voteSet 中