Zookeeper学习笔记
一 基础概念
1.1 描述
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:服务注册与发现,配置维护、域名服务、分布式同步、组服务等。
ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
ZooKeeper包含一个简单的原语集,提供Java和C的接口。
ZooKeeper代码版本中,提供了分布式独享锁、选举、队列的接口,代码在$zookeeper_home\src\recipes。其中分布锁和队列有Java和C两个版本,选举只有Java版本。
Zookeeper是一个基于观察者模式设计的分布式服务管理框架。
1.2 数据结构
Zookeeper数据模型的结果和Unix类似,整体上可以看做是一棵树,每个节点称作ZNode,每个ZNode默认可以存储1MB的数据,每个ZNode都可以通过其路径唯一标识。
1.3 应用场景
- 统一命名服务:在分布式环境下,对服务进行统一命名,方便识别
- 统一配置管理:统一管理配置文件
- 统一集群管理:可根据节点实时状态对集群做出调整
- 服务节点动态上下线:客户端可以实时观察服务器上下线的变化
- 软负载均衡
1.4 选举机制
1.4.1 第一次
- 半数:某一个节点获取到的票数超过总节点数的半数,则为Leader;
- myid最大:当没有选出Leader节点之前,所有票都投给当前myid(服务器ID)最大的节点;
1.4.2 非第一次
- EPOCH(节点的Leader任期次数)最大的为Leader节点;
- EPOCH相同,zxid(事务ID)大的为leader节点;
- zxid相同,myid(服务器ID)最大的为Leader节点。
1.5 CAP
- Consistency(一致性):分布式环境中,数据在多个副本之间保持一致。
- Available(可用性):分布式系统提供的服务一直处于可以状态,所有请求都能在有限的时间内得到回应。
- Partition Tolerance(分区容错性):分布式系统遇到任何网络分区故障时,都能保证提供满足一致性和可用性的服务。
框架 | C | A | P |
---|---|---|---|
Zookeeper | ✔ | ✘ | ✔ |
eureka | ✘ | ✔ | ✔ |
Zookeeper不保证可用性。在极端环境下,Zookeeper会丢弃一些请求,需要重新发起请求。并且在选举Leader时是不可用的。
二 下载安装与集群搭建
2.1 下载安装
-
安装jdk;
-
从官网下载Zookeeper,下载带有bin的压缩包,上传服务器;
-
解压,改名(可选)
tar -zxvf /root/apache-zookeeper-3.7.0-bin.tar.gz -C /usr/local/zookeeper/ mv apache-zookeeper-3.7.0-bin/ zookeeper-3.7.0
-
配置环境变量
export ZOOKEEPER_HOME=/usr/local/zookeeper/zookeeper-3.7.0 export PATH=$PATH:$ZOOKEEPER_HOME/bin
2.2 集群搭建
-
单节点修改配置
cd /usr/local/zookeeper/zookeeper-3.7.0 mkdir data # 配置服务器编号 cd data/ # 创建myid文件,写入该节点编号(数字1、2、3等,每个节点唯一) vim myid cd /usr/local/zookeeper/zookeeper-3.7.0/conf/ cp zoo_sample.cfg zoo.cfg vim zoo.cfg # 修改dataDir dataDir=/usr/local/zookeeper/zookeeper-3.7.0/data # 在zoo.cfg末尾添加集群配置。 # server后面的数字是在data/myid中配置的集群中节点的编号 # IP为各个节点对应的IP地址 # 2888是Leader和Follower的通信端口 # 3888是Leader挂掉后选举用的端口 server.2=192.168.10.102:2888:3888 server.3=192.168.10.103:2888:3888 server.4=192.168.10.104:2888:3888
-
将环境变量配置文件和Zookeeper分发(拷贝)到其他节点,
-
集群配置
在集群各节点,修改/usr/local/zookeeper/zookeeper-3.7.0//data/myid 中的节点编号。
刷新环境变量。
2.3 集群脚本
#!/bin/bash
if [ $# -ne 1 ]
then
echo "请输入一个参数:start(开启集群)、stop(关闭集群)、status(查看状态)"
exit;
fi
for host in 192.168.10.102 192.168.10.103 192.168.10.104
do
echo "===== $1:$host ====="
ssh $host "zkServer.sh $1"
done
# 在/root/bin目录下创建脚本
vim myZKServer
# 给脚本myzookeeper添加执行权限
chmod a+x myZKServer
# 启动Zookeeper集群
myZKServer start
# 查看Zookeeper集群状态
myZKServer status
# 关闭Zookeeper集群
myZKServer stop
2.4 配置文件解读
# Zookeeper客户端与服务端的心跳时间,单位毫秒
tickTime=2000
# Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量)
initLimit=10
# Leader和Follower之间通信时间超过 syncLimit * tickTime,Leader认为Follower挂掉,从服务器列表中剔除Follower
syncLimit=5
# Zookeeper数据的保存目录
dataDir=/tmp/zookeeper
# 客户端连接端口
clientPort=2181
三 客户端操作
3.1 shell操作命令
# 连接本地服务器
zkCli.sh
# 连接指定服务器
zkCli.sh -server 192.168.10.102:2181
# 帮助命令,查看所有客户端命令
help
# 查看根节点下的节点
ls /
# 查看根节点下的节点以及根节点的状态信息
ls -s /
# 开启监听节点/aaa的子节点是否有变化(注:开启一次监听,只能监听一次变化)
ls -w /aaa
# 查看节点/aaa的状态信息
stat /aaa
# 创建不带序号的持久节点/aaa
create /aaa
# 创建不带序号的持久节点/aaa并赋值内容bbb
create /aaa "bbb
# 创建带序号的持久节点/aaa
create -s /aaa
# 创建不带序号的临时节点/aaa
create -e /aaa
# 获取节点/aaa的值
get /aaa
# 获取节点/aaa的值以及节点/aaa的状态信息
get -s /aaa
# 开启监听节点/aaa的数据是否有变化(注:开启一次监听,只能监听一次变化)
get -w /aaa
# 给节点/aaa赋值ccc
set /aaa "ccc"
# 删除单个节点/aaa
delete /aaa
# 删除节点/aaa以及下面的所有子节点
deleteall /aaa
# 端口客户端和服务器的连接
quit
3.2 节点属性
- cZxid:创建节点的事务ID。每次修改Zookeeper状态都会产生一个Zookeeper事务ID,事务ID是Zookeeper中所有修改总的次序,每次修改都有唯一的zxid。如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
- ctime:当前节点的创建时间
- mZxid:节点最后更新的事务ID
- mtime:节点最后更新的时间
- pZxid:节点最后更新的子节点的事务ID
- cversion:当前节点的子节点的变化号,即子节点的修改次数
- dataVersion:节点数据变化号
- aclVersion:节点访问控制列表的变化号
- ephemeralOwner:如果是临时节点,这个是节点拥有者的session ID。如果不是临时节点则是0
- dataLength:节点的数据长度
- numChildren:节点的子节点数量
3.3 节点类型
节点类型分为两类四种,分别是持久的、临时的、有序号的、无序号的。
- 持久的:客户端和服务器断开连接后,创建的节点不删除。
- 临时的:客户端和服务器端口连接后,创建的节点自己删除。
- 有序号的:创建节点时,自动在节点名字后面拼接一串递增的数字
- 无序号的:创建节点时,不会在节点名字后面拼接一串递增的数字
3.4 API操作
3.4.1 maven
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<dependencies>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
</dependencies>
</project>
3.4.2 日志
log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
3.4.3 api
package com.guoli.zk;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* Zookeeper客户端
*
* @author guoli
* @data 2022-01-31 16:42
*/
public class ZkClientTest {
/**
* Zookeeper服务器地址
*/
private static final String CONNECT_STRING = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";
/**
* 连接Zookeeper服务器的超时时间
*/
private static final int SESSION_TIMEOUT = 2000;
/**
* Zookeeper客户端
*/
private ZooKeeper zkClient;
@Before
public void init() throws IOException {
zkClient = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, watchedEvent -> {
System.out.println("-------------------------");
try {
// 监听跟目录下的节点变化
zkClient.getChildren("/", true).forEach(System.out::println);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
System.out.println("-------------------------");
});
}
@Test
public void ls() throws InterruptedException, KeeperException {
zkClient.getChildren("/", false).forEach(System.out::println);
}
@Test
public void create() throws InterruptedException, KeeperException {
// 节点路径
String path = "/aaa";
// 节点数据
byte[] data = "aaa".getBytes(StandardCharsets.UTF_8);
/*
节点权限
OPEN_ACL_UNSAFE:对所有人开放
*/
List<ACL> aclList = ZooDefs.Ids.OPEN_ACL_UNSAFE;
/*
节点类型
PERSISTENT:持久无序号的
PERSISTENT_SEQUENTIAL:持久有序号的
EPHEMERAL:临时无序号的
EPHEMERAL_SEQUENTIAL:临时有序号的
*/
CreateMode createMode = CreateMode.PERSISTENT;
System.out.println(path.equals(zkClient.create(path, data, aclList, createMode)));
}
/**
* 监控子节点的数据变化
* 使用创建Zookeeper客户端时候的监听器
*/
@Test
public void getW() throws InterruptedException {
Thread.sleep(Integer.MAX_VALUE);
}
@Test
public void exit() throws InterruptedException, KeeperException {
Stat stat = zkClient.exists("/aa", false);
if (stat == null) {
System.out.println("不存在");
} else {
System.out.println(stat);
}
}
@Test
public void delete() throws InterruptedException, KeeperException {
zkClient.delete("/aa", 0);
}
@After
public void close() throws InterruptedException {
zkClient.close();
}
}
四 服务器动态上下线监听
4.1 服务器上线
package com.guoli.zk.monitorServerOnlineAndOffline;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* 服务器上线
*
* @author guoli
* @data 2022-01-31 18:56
*/
public class ServerOnline {
/**
* Zookeeper服务器地址
*/
private static final String CONNECT_STRING = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";
/**
* 连接Zookeeper服务器的超时时间
*/
private static final int SESSION_TIMEOUT = 2000;
/**
* Zookeeper客户端
*/
private static ZooKeeper zkClient;
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
getConnect();
register(args[0]);
Thread.sleep(Integer.MAX_VALUE);
}
/**
* 获取服务器客户端
*
* @throws IOException IO异常
*/
public static void getConnect() throws IOException {
zkClient = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, watchedEvent -> {
});
}
/**
* 服务器上线注册
*
* @param hostname 上线的服务器主机名
* @throws InterruptedException 中断你异常
* @throws KeeperException 保持异常
*/
public static void register(String hostname) throws InterruptedException, KeeperException {
Stat stat = zkClient.exists("/servers", false);
if (null == stat) {
zkClient.create("/servers", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
zkClient.create("/servers/" + hostname, hostname.getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
}
}
4.2 监听服务器上下线
package com.guoli.zk.monitorServerOnlineAndOffline;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 监听服务上下线
*
* @author guoli
* @data 2022-01-31 19:15
*/
public class MonitorServerOnlineAndOffline {
/**
* Zookeeper服务器地址
*/
private static final String CONNECT_STRING = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";
/**
* 连接Zookeeper服务器的超时时间
*/
private static final int SESSION_TIMEOUT = 2000;
/**
* Zookeeper客户端
*/
private static ZooKeeper zkClient;
/**
* 所有已上线服务的主机名
*/
private static final List<String> SERVERS = new ArrayList<>();
public static void main(String[] args) throws IOException, InterruptedException {
getConnect();
Thread.sleep(Integer.MAX_VALUE);
}
public static void getConnect() throws IOException {
zkClient = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, watchedEvent -> {
SERVERS.clear();
try {
Stat stat = zkClient.exists("/servers", false);
if (null == stat) {
zkClient.create("/servers", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
zkClient.getChildren("/servers", true).forEach(s -> {
try {
SERVERS.add(new String(zkClient.getData("/servers/" + s, false, null)));
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
});
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
System.out.println(SERVERS);
});
}
}
五 分布式锁
每个客户端在服务器指定节点 /locks 下创建一个临时的、带序号的节点,然后监听节点 /locks ,判断自己是否是序号最小的节点,是则得到锁。
5.1 自定义
package com.guoli.zk;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* 分布式锁
*
* @author guoli
* @data 2022-01-31 20:09
*/
public class DistributedLock {
private final ZooKeeper zk;
private final String rootNode = "locks";
/**
* 当前 client 等待的子节点
*/
private String waitPath;
/**
* ZooKeeper 连接
*/
private final CountDownLatch connectLatch = new CountDownLatch(1);
/**
* ZooKeeper 节点等待
*/
private final CountDownLatch waitLatch = new CountDownLatch(1);
/**
* 当前 client 创建的子节点
*/
private String currentNode;
public DistributedLock() throws IOException, InterruptedException, KeeperException {
String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
zk = new ZooKeeper(connectString, 2000, event -> {
// 连接建立时, 打开 latch, 唤醒 wait 在该 latch 上的线程
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
connectLatch.countDown();
}
// 发生了 waitPath 的删除事件
if (event.getType() == Watcher.Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
waitLatch.countDown();
}
});
// 等待连接建立
connectLatch.await();
//获取根节点状态
Stat stat = zk.exists("/" + rootNode, false);
//如果根节点不存在,则创建根节点,根节点类型为永久节点
if (stat == null) {
System.out.println("根节点不存在");
zk.create("/" + rootNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
public void zkLock() {
try {
//在根节点下创建临时顺序节点,返回值为创建的节点路径
String subNode = "seq-";
currentNode = zk.create("/" + rootNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// wait 一小会, 让结果更清晰一些
Thread.sleep(10);
// 注意, 没有必要监听"/locks"的子节点的变化情况
List<String> childrenNodes = zk.getChildren("/" + rootNode, false);
// 列表中只有一个子节点, 那肯定就是 currentNode , 说明
if (childrenNodes.size() > 1) {
//对根节点下的所有临时顺序节点进行从小到大排序
Collections.sort(childrenNodes);
//当前节点名称
String thisNode = currentNode.substring(("/" + rootNode + "/").length());
//获取当前节点的位置
int index = childrenNodes.indexOf(thisNode);
if (index == -1) {
System.out.println("数据异常");
} else if (index > 0) {
// 获得排名比 currentNode 前 1 位的节点
this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);
// 在 waitPath 上注册监听器, 当 waitPath 被删除时,
zk.getData(waitPath, true, new Stat());
//进入等待锁状态
waitLatch.await();
}
}
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
public void zkUnlock() {
try {
zk.delete(this.currentNode, -1);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
DistributedLock lock1 = new DistributedLock();
DistributedLock lock2 = new DistributedLock();
new Thread(() -> {
try {
lock1.zkLock();
System.out.println("lock1 获取到锁");
Thread.sleep(2000);
lock1.zkUnlock();
System.out.println("lock1 释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
lock2.zkLock();
System.out.println("lock2 获取到锁");
Thread.sleep(2000);
lock2.zkUnlock();
System.out.println("lock2 释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
5.2 curator框架
5.2.1 maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<curator.version>4.3.0</curator.version>
</properties>
<dependencies>
<!-- curator框架,分布式锁框架 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-client</artifactId>
<version>${curator.version}</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>${curator.version}</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>${curator.version}</version>
</dependency>
</dependencies>
</project>
5.2.2 使用
package com.guoli.zk;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
/**
* curator框架测试
*
* @author guoli
* @data 2022-02-01 14:14
*/
public class CuratorLockTest {
public static void main(String[] args) {
InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");
InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");
new Thread(() -> {
try {
lock1.acquire();
System.out.println("线程 1 获取锁");
// 测试锁重入
lock1.acquire();
System.out.println("线程 1 再次获取锁");
Thread.sleep(5 * 1000);
lock1.release();
System.out.println("线程 1 释放锁");
lock1.release();
System.out.println("线程 1 再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
lock2.acquire();
System.out.println("线程 2 获取锁");
// 测试锁重入
lock2.acquire();
System.out.println("线程 2 再次获取锁");
Thread.sleep(5 * 1000);
lock2.release();
System.out.println("线程 2 释放锁");
lock2.release();
System.out.println("线程 2 再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
/**
* 分布式锁初始化
*
* @return CuratorFramework
*/
public static CuratorFramework getCuratorFramework() {
//通过工厂创建 Curator
// zookeeper server 列表
String connectString = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";
// connection 超时时间
int connectionTimeout = 2000;
// session 超时时间
int sessionTimeout = 2000;
//重试策略,初试时间 3 秒,重试 3 次
RetryPolicy policy = new ExponentialBackoffRetry(3000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(connectString)
.connectionTimeoutMs(connectionTimeout)
.sessionTimeoutMs(sessionTimeout)
.retryPolicy(policy).build();
//开启连接
client.start();
System.out.println("zookeeper 初始化完成...");
return client;
}
}