ZooKeeper学习笔记
Apache ZooKeeper
ZK简介
一个分布式的,开源的分布式应用程序协调服务,是Google的分布式锁服务Chubby的一个开源实现,是Hadoop和Hbase的重要组件。ZK由Java编写,但是支持Java和C两种编程语言。
- 在Dubbo、SpringCloud中,担任
服务注册中心
的角色 - 在Hadoop、Hbase等大数据的组件中,担任
集群管理
者的角色 - 在Redis分布式开发中,担任
实现分布式锁
的角色。
ZK数据模型
ZK模型结构
模型结构为树状结构,类似Linux文件目录结构
- 每个子目录如/app1都被当作一个
znode节点
,每个znode节点被其所在的路径唯一标识
- znode节点可以有子节点目录,并且每个znode
可以存储数据
- znode中存储的
数据是有版本
的,每次对节点数据的修改都会导致版本号的增加;同时每个znode节点可以存储数据的多个版本,即一个访问路径中可以存储多份数据 - znode节点
可以被监控
,包括这个目录节点中存储数据的修改、子节点目录的变化等,一旦变化可以通知设置监控的客户端
节点分类
根据ZK模型结构中的znode节点的生命周期
和额外的节点特性
(父节点为其第一级子节点维护一份时序),可以将节点划分成四类:持久节点、持久顺序节点、临时节点、临时顺序节点。
持久节点(P)
在节点创建后,就会存盘并一直存在,直到被要求删除,值得注意的是,它不会因为创建该节点的客户端会话的失效而被删除。
持久顺序节点(PS)
在生命周期等基本特性上和持久节点相同。额外的特性是,在ZK中,每个父节点会为其第一级子节点维护一份时序,即记录每个子节点创建的先后顺序。基于这一特性,在创建子节点时,可以设置该属性,这样在创建节点的过程中,ZK会自动地为给定的节点名增加一个数字后缀(范围是1~整型的最大值)作为新的节点名,例如节点/app1/p_1这样的节点就是持久顺序节点。
临时节点(E)
与持久节点截然相反,临时节点的生命周期和客户端会话绑定,即如果客户端会话失效(会话失效!=会话连接断开),那么绑定的临时节点将自动被清除。除此之外,临时节点下不能创建子节点。
临时顺序节点(ES)
具有临时节点的特点,额外特性和持久顺序节点的额外特性相同。
ZK安装
Linux系统中安装ZK
Docker安装ZK
拉取ZK镜像
docker pull zookeeper:3.4.14
启动ZK容器
docker run --name zk -p 2181:2181 -d zookeeper:3.4.14
查看容器zk的启动日志
docker logs zk
成功绑定宿主机的2181端口。
进入zk容器(交互式方式)
docker exec -it zk bash
连接容器内的ZK服务
./bin/zkCli.sh
ZK配置文件详解
#tickTime:Client-Server通信维持心跳的时间间隔,以毫秒为单位
tickTime=2000
#initLimit:初始化集群时,集群节点同步超时时间(tickTime的数量)
initLimit=10
#syncLimit:Leader-Follower节点之间请求和应答即同步数据的超时时间(tickTime的数量)
syncLimit=5
#dataDir:数据文件目录,默认情况下,Zookeeper将写数据的日志文件也保存在这个目录里
dataDir=/tmp/zookeeper
#clientPort:zk服务的监听端口号
clientPort=2181
#maxClientCnxns:服务器能处理的最大客户端并发数,也即服务线程池的最大线程数,默认为60
maxClientCnxns=60
#维护配置:客户端在与zookeeper交互过程中会产生非常多的日志,而且zookeeper也会将内存中的数据作为snapshot保存下来,这些数据是不会被自动删除的,这样磁盘中这样的数据就会越来越多。autopurge.snapRetainCount是设置保留snapshot的个数,而autopurge.purgeInterval则代表了合并快照的时间间隔(防止快照越来越多)
#autopurge.snapRetainCount=3
#autopurge.purgeInterval=1
ZK客户端基本指令
单体命令
ls path
查看当前节点路径下的下一级子节点
ls /
ls /zookeeper
ls /zookeeper/quota
create [-s] [-e] path data acl
创建节点(四大类),默认创建持久化节点,加上-s
代表创建顺序节点,加上-e
代表创建临时节点。
#创建p节点
create /testCreate person
#创建ps节点
create -s /testCreate/person zhangsan1
create -s /testCreate/person zhangsan2
create -s /testCreate/person zhangsan3
#创建e节点
create -e /testCreate/session session1
#创建es节点
create -s -e /testCreate/session session1
查看创建的持久化节点
查看创建的临时节点
quit退出客户端,自动关闭会话,此时查看之前创建的节点的状态。
临时节点在会话失效后会自动删除,同时,临时节点下不能创建任何子节点。
stat path
查看节点的信息
stat /testCreate
get path
获取节点存储的数据及节点信息
get /testCreate
set path data
修改节点的数据并查看
set /testcreate people
get /testCreate
delete path
删除节点(该节点必须没有子节点)
ls /testCreate
delete /testCreate
delete /testCreate/person0000000002
ls /testCreate
rmr path
递归删除节点,可以删除含子节点的节点
create /testDelete person
create -s /testDelete xiaoming
create -s /testDelete xiaohong
create -s /testDelete xiaoma
rmr /testCreate
history
查看操作历史
history
quit
退出当前会话(会话失效)
quit
组合命令
ls2 path
ls path
和 stat path
的组合
ls2 /testCreate
节点监听机制(Watch)
客户端可以监听znode节点的变化,主要包括节点路径
和节点数据
两种变化。znode节点的变化会触发响应的事件,然后清除对该节点的监测,当监测一个znode节点时,zk会发送通知给监测节点。一个Watch事件是一个一次性的触发器
,当被设置了Watch的数据和节点路径发生了变化时,则服务器将会把这个改变发送给设置Watch的客户端进行通知。
节点路径监听
监听客户端设置节点路径监听
—— 该节点路径下的所有路径变化都会通知到设置该监听的客户端
ls path true
节点数据监听
监听客户端设置节点数据监听
—— 该节点路径下的所有数据变化都会通知到设置该监听的客户端
get path true
应用
微服务框架下作为服务的注册中心
Java操作ZK
依赖导入
导入zkclient依赖包
<!--引入zookeeper依赖-->
<dependencies>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
</dependencies>
导入测试时要用到的两个依赖:junit 和 lombok
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>provided</scope>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
</dependency>
测试案例
package com.youzikeji.client;
import com.youzikeji.pojo.User;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.serialize.SerializableSerializer;
import org.I0Itec.zkclient.serialize.ZkSerializer;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.Date;
import java.util.List;
public class TestZKClient {
private ZkClient zkClient;
/**
* @description: 创建节点测试
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 13:27
*/
@Test
public void testCreateNode(){
//参数1: 节点路径
//参数2: 节点数据
//参数3: 指定创建节点的类型
//1.创建P节点
zkClient.create("/testCreateNode","root", CreateMode.PERSISTENT);
zkClient.create("/testCreateNode/pNode", "p-node", CreateMode.PERSISTENT);
//2.创建PS节点
zkClient.create("/testCreateNode/PSNode", "ps-node", CreateMode.PERSISTENT_SEQUENTIAL);
//3.创建E节点
zkClient.create("/testCreateNode/ENode", "e-node", CreateMode.EPHEMERAL);
//4.创建ES节点
zkClient.create("/testCreateNode/ESNode", "es-node", CreateMode.EPHEMERAL_SEQUENTIAL);
}
/**
* @description: 删除节点测试
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 13:56
*/
@Test
public void testDeleteNode(){
/*//1.只能删除不含子节点的节点
boolean deleteStatus = zkClient.delete("/testCreateNode");
System.out.println("节点删除状态: " + deleteStatus);*/
//2.递归删除节点
boolean deleteRecursiveStatus = zkClient.deleteRecursive("/testCreateNode");
System.out.println("节点删除状态: " + deleteRecursiveStatus);
}
/**
* @description: 测试查询当前节点路径下的所有子节点
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 13:59
*/
@Test
public void testGetChidrenNodes(){
List<String> children = zkClient.getChildren("/");
for (String child : children){
System.out.println(child);
}
}
/**
* @description: 查看某个节点路径存储的数据
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 14:00
*/
@Test
public void testGetNodeData(){
//注意读数据时要保证节点创建时的数据序列化方式和获取时的序列化方式一致
Object data = zkClient.readData("/testCreateNode");
System.out.println(data);
}
/**
* @description: 获取节点的状态信息
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 14:08
*/
@Test
public void testGetNodeStatus(){
Stat stat = new Stat();
Object readData = zkClient.readData("/testCreateNode", stat);
System.out.println(readData);
System.out.println("创建版本:" + stat.getCversion());
System.out.println("创建时间:" + stat.getCtime());
}
/**
* @description: 更新节点数据
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 14:17
*/
@Test
public void modifyNodeData(){
User user = new User();
user.setId(1).setName("wandehua").setAge(23).setBirth(new Date());
zkClient.writeData("/testCreateNode", user);
User userInfo = zkClient.readData("/testCreateNode");
System.out.println("更新后的数据:" + userInfo);
System.out.println("ID:" + userInfo.getId());
System.out.println("姓名:" + userInfo.getName());
System.out.println("年龄:" + userInfo.getAge());
System.out.println("生日:" + userInfo.getBirth());
}
/**
* @description: 监听节点的数据变化
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 15:13
*/
@Test
public void testWatchDataChange() throws IOException {
zkClient.subscribeDataChanges("/testCreateNode", new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
System.out.println("当前路径: " + s);
System.out.println("当前节点变化后的数据: " + o);
}
@Override
public void handleDataDeleted(String s) throws Exception {
System.out.println("当前路径: " + s);
}
});
//阻塞客户端
System.in.read();
}
/**
* @description: 监听当前节点路径下的下一级子节点的变化
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 15:20
*/
@Test
public void testWatchNodesChange() throws IOException {
zkClient.subscribeChildChanges("/", new IZkChildListener() {
@Override
public void handleChildChange(String s, List<String> list) throws Exception {
System.out.println("父节点名称" + s);
System.out.println("发生改变的子节点名称:");
for (String name : list){
System.out.println(name);
}
}
});
//阻塞客户端
System.in.read();
}
/**
* @description: 测试之前初始化客户端对象
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 13:27
*/
@Before
public void initClient(){
//参数1: 服务器ip:port
//参数2: 会话超时时间
//参数3: 连接超时时间
//参数4: 序列化方式
zkClient = new ZkClient("127.0.0.1:2181", 60000 * 3, 60000, new SerializableSerializer());
}
/**
* @description: 测试结束,释放资源
* @params: []
* @return: void
* @author: caoyusang
* @dateTime: 2021/4/5 13:27
*/
@After
public void releaseClient() throws InterruptedException {
/*//延迟资源释放,不然临时节点的创建看不到
Thread.sleep(25000);*/
zkClient.close();
}
}
创建节点的结果
获取节点信息
更新节点
使用User对象更新节点的数据
先创建一个实体类User——要实现序列化
,这里用到了lombok提供构造器和get/set方法以及允许链式编程@Accessors(chain = true)
package com.youzikeji.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class User implements Serializable {
private Integer id;
private String name;
private Integer age;
private Date birth;
}
更新结果
监听节点数据变化事件 —— 更新节点数据
监听节点数据变化事件 —— 删除节点
ZK的集群
集群
集合同一种软件服务的多个节点同时为客户端提供服务,集群的出现就是为了解决以下几个问题:
- 单节点的并发访问压力问题
- 单节点故障问题(由于硬件老化、自然灾害等原因)
集群架构
- 两类节点:leader 和 follower,一个ZK集群中一般只有一个leader节点,follower节点又称仲裁节点,它们的配置完全一样。leader节点由
投票选举
从所有follower节点中产生。 - 搭建集群时,至少需要三台服务器,且强烈建议使用奇数个服务器,如果只有两台服务器,在其中一个节点宕机时,没办法形成多数仲裁。同时,由于存在两个单点故障,所以两台服务器甚至不如一台服务器稳定。
- 分布式
写数据不一致问题
的解决 ——zab原子广播协议
,当客户端像任意一个服务节点进行了写操作,此时服务节点并不直接更新,而是先向主节点leader询问是否可以更新,如果得到肯定的回复,那么所有follwer节点之间就会进行广播通信,同步更新写入的数据。如果写入失败,则将失败信息返回给客户端。 - 如果某一时刻leader节点宕机,那么就从所有剩下的follower节点中进行多数仲裁,选举出新的leader节点。
ZK集群的搭建
通过Java客户端操作ZK集群
在初始化客户端对象时,把服务端的ip:port换成对应集群中所有节点的ip:port即可。