官网:https://zookeeper.apache.org/
Zookeeper 是一个开源的分布式的,为分布式框架提供协调服务的 Apache 项目
Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应**。Zookeeper=文件系统+通知机制**
ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称做一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据,每个 ZNode 都可以通过其路径唯一标识
提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等
在分布式环境下,经常需要对应用/服务进行统一命名,便于识别。例如:IP不容易记住,而域名容易记住
分布式环境下,配置文件同步非常常见。一般要求一个集群中,所有节点的配置信息是一致的,比如 Kafka 集群。对配置文件修改后,希望能够快速同步到各个节点上。配置管理可交由ZooKeeper实现。可将配置信息写入ZooKeeper上的一个Znode;各个客户端服务器监听这个Znode;一旦Znode中的数据被修改,ZooKeeper将通知各个客户端服务器。
分布式环境中,实时掌握每个节点的状态是必要的。可根据节点实时状态做出一些调整。ZooKeeper可以实现实时监控节点状态变化可将节点信息写入ZooKeeper上的一个ZNode。监听这个ZNode可获取它的实时状态变化。
客户端能实时洞察到服务器上下线的变化
在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求
# 首先自行安装jdk,这里我就继续在之前的hadoop集群上继续操作了
wget https://archive.apache.org/dist/zookeeper/zookeeper-3.5.7/apache-zookeeper-3.5.7-bin.tar.gz
tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/
cd /opt/module/
mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7/
# ============配置修改=============
# 将/opt/module/zookeeper-3.5.7/conf 这个路径下的 zoo_sample.cfg 修改为 zoo.cfg
mv zoo_sample.cfg zoo.cfg
# 打开 zoo.cfg 文件,修改 dataDir 路径
vim zoo.cfg
# 修改如下内容
dataDir=/opt/module/zookeeper-3.5.7/zkData
# 在/opt/module/zookeeper-3.5.7/这个目录上创建 zkData 文件夹
mkdir zkData
# ============操作 Zookeeper============
# 启动 Zookeeper
bin/zkServer.sh start
# 查看进程是否启动
jps
# 查看状态
bin/zkServer.sh status
# 启动客户端
bin/zkCli.sh
quit
# 停止 Zookeeper
bin/zkServer.sh stop
vim zoo.cfg
# 通信心跳时间,Zookeeper服务器与客户端心跳时间,单位毫秒
tickTime=2000
# LF初始通信时限,Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量)
# 指定了Zookeeper集合中的Follower节点(从节点)在连接到Leader节点(主节点)时能够等待的时间量。在这个时间范围内,如果从节点不能连接到Leader节点,则从节点将放弃连接尝试
initLimit=10
# LF同步通信时限
# Leader和Follower之间通信时间如果超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除Follwer
syncLimit=5
# 保存Zookeeper中的数据
# 注意:默认的tmp目录,容易被Linux系统定期删除,所以一般不用默认的tmp目录。
dataDir=/opt/module/zookeeper-3.5.7/zkData
# 客户端连接端口,通常不做修改
clientPort=2181
# 在 hadoop102、hadoop103 和 hadoop104 三个节点上都部署 Zookeeper(这是之前hadoop集群用的)
# 在 hadoop102 解压 Zookeeper 安装包到/opt/module/目录下
tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/
cd /opt/module/
mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7/
# ============配置修改=============
# 在/opt/module/zookeeper-3.5.7/这个目录上创建 zkData 文件夹
mkdir zkData
# 在/opt/module/zookeeper-3.5.7/zkData 目录下创建一个 myid 的文件
# 在文件中添加与 server 对应的编号(注意:上下不要有空行,左右不要有空格)
# 这里编写一个2,注意:添加 myid 文件,一定要在 Linux 里面创建,在 notepad++里面很可能乱码
vi myid
# 拷贝配置好的 zookeeper 到其他机器上,分发脚本可以想见之前hadooop3.x学习笔记文章
xsync zookeeper-3.5.7
# 并分别在 hadoop103、hadoop104 上修改 myid 文件中内容为 3、4
# ============配置zoo.cfg文件============
# 重命名/opt/module/zookeeper-3.5.7/conf 这个目录下的 zoo_sample.cfg 为 zoo.cfg
mv zoo_sample.cfg zoo.cfg
vim zoo.cfg
# 修改数据存储路径配置
dataDir=/opt/module/zookeeper-3.5.7/zkData
# 增加如下配置
#######################cluster##########################
server.2=hadoop102:2888:3888
server.3=hadoop103:2888:3888
server.4=hadoop104:2888:3888
# 配置参数解读
# server.A=B:C:D
# A 是一个数字,表示这个是第几号服务器;集群模式下配置一个文件myid,这个文件在 dataDir 目录下,这个文件里面有一个数据就是 A 的值,
# Zookeeper 启动时读取此文件,拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是哪个 server。
# B 是这个服务器的地址;
# C 是这个服务器Follower 与集群中的 Leader 服务器交换信息的端口;
# D 是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的Leader,而这个端口就是用来执行选举时服务器相互通信的端口。
# 同步zoo.cfg 配置文件
xsync zoo.cfg
# ====================集群操作====================
# 分别三台机器启动Zookeeper
bin/zkServer.sh start
# 查看状态
bin/zkServer.sh status
在 hadoop102 的/home/atguigu/bin 目录下创建脚本: vim zk.sh
#!/bin/bash
case $1 in
"start"){
for i in hadoop102 hadoop103 hadoop104
do
echo ---------- zookeeper $i 启动 ------------
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
done
};;
"stop"){
for i in hadoop102 hadoop103 hadoop104
do
echo ---------- zookeeper $i 停止 ------------
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop"
done
};;
"status"){
for i in hadoop102 hadoop103 hadoop104
do
echo ---------- zookeeper $i 状态 ------------
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status"
done
};;
esac
# 增加脚本执行权限
chmod u+x zk.sh
zk.sh start
zk.sh stop
# 启动客户端
bin/zkCli.sh -server hadoop102:2181
# 显示所有操作命令
help
命令基本语法 | 功能描述 |
---|---|
help | 显示所有操作命令 |
ls path | 使用 ls 命令来查看当前 znode 的子节点 [可监听] -w 监听子节点变化 -s 附加次级信息 |
create | 普通创建 -s 含有序列 -e 临时(重启或者超时消失) |
get path | 获得节点的值 [可监听] -w 监听节点内容变化 -s 附加次级信息 |
set | 设置节点的具体值 |
stat | 查看节点状态 |
delete | 删除节点 |
deleteall | 递归删除节点 |
# 查看当前znode中所包含的内容
ls /
# 查看当前节点详细数据
ls -s /
# 分别创建2个普通节点(永久节点 + 不带序号)
# 注意:创建节点时,要赋值
create /sanguo "diaochan"
create /sanguo/shuguo "liubei"
# 获得节点的值
get -s /sanguo
get -s /sanguo/shuguo
# 创建带序号的节点(永久节点 + 带序号)
# 先创建一个普通的根节点/sanguo/weiguo
create /sanguo/weiguo "caocao"
# 创建带序号的节点
create -s /sanguo/weiguo/zhangliao "zhangliao"
# 重复编号会增加
create -s /sanguo/weiguo/zhangliao "zhangliao"
# 创建短暂节点(短暂节点 + 不带序号 or 带序号)
# 创建短暂的不带序号的节点
create -e /sanguo/wuguo "zhouyu"
# 创建短暂的带序号的节点
create -e -s /sanguo/wuguo "zhouyu"
# 在当前客户端是能查看到的
ls /sanguo
# 退出当前客户端然后再重启客户端
quit
bin/zkCli.sh
# 再次查看根目录下短暂节点已经删除
ls /sanguo
# 修改节点数据值
set /sanguo/weiguo "simayi"
# 删除节点
delete /sanguo/jin
# 递归删除节点
deleteall /sanguo/shuguo
# 查看节点状态
stat /sanguo
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序
# 节点的值变化监听
# 在 hadoop104 主机上注册监听/sanguo 节点数据变化
get -w /sanguo
# 在 hadoop103 主机上修改/sanguo 节点的数据
# 观察 hadoop104 主机收到数据变化的监听
set /sanguo "xisi"
# 注意:在hadoop103再多次修改/sanguo的值,hadoop104上不会再收到监听。因为注册一次,只能监听一次。想再次监听,需要再次注册
# 节点的子节点变化监听(路径变化)
# 在 hadoop104 主机上注册监听/sanguo 节点的子节点变化
ls -w /sanguo
# 在 hadoop103 主机/sanguo 节点上创建子节点
# create /sanguo/jin "simayi"
create /sanguo/jin "simayi"
前提:保证 hadoop102、hadoop103、hadoop104 服务器上 Zookeeper 集群服务端启动
创建zookeeper工程,引入对应依赖
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>RELEASEversion>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.8.2version>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.5.7version>
dependency>
需要在项目的 src/main/resources 目录下,新建一个文件,命名为"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
创建包名com.atguigu.zk,创建类名称zkClient
public class zkClient {
// 注意:逗号左右不能有空格
private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
private int sessionTimeout = 2000;
private ZooKeeper zkClient;
//创建 ZooKeeper 客户端
@Before
public void init() throws IOException {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// System.out.println("-------------------------------");
// List children = null;
// try {
// children = zkClient.getChildren("/", true);
//
// for (String child : children) {
// System.out.println(child);
// }
//
// System.out.println("-------------------------------");
// } catch (KeeperException e) {
// e.printStackTrace();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
});
}
//创建子节点
@Test
public void create() throws KeeperException, InterruptedException {
// 参数 1:要创建的节点的路径; 参数 2:节点数据 ; 参数 3:节点权限 ;参数 4:节点的类型
String nodeCreated = zkClient.create("/atguigu", "ss.avi".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//获取子节点并监听节点变化
@Test
public void getChildren() throws KeeperException, InterruptedException {
List<String> children = zkClient.getChildren("/", true);
for (String child : children) {
System.out.println(child);
}
// 延时
Thread.sleep(Long.MAX_VALUE);
}
@Test
public void exist() throws KeeperException, InterruptedException {
// 判断 Znode 是否存在
Stat stat = zkClient.exists("/atguigu", false);
System.out.println(stat==null? "not exist " : "exist");
}
}
写流程之写入请求直接发送给Leader节点(写完半数即可返回成功)
写流程之写入请求发送给follower节点
某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知到主节点服务器的上下线。
首先在集群上创建/serves节点 create /servers "servers"
在 Idea 中创建包名:com.atguigu.zkcase1,创建服务端DistributeServer
public class DistributeServer {
private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
private int sessionTimeout = 2000;
private ZooKeeper zk;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
DistributeServer server = new DistributeServer();
// 1 获取zk连接
server.getConnect();
// 2 注册服务器到zk集群
server.regist(args[0]);
// 3 启动业务逻辑(睡觉)
server.business();
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
private void regist(String hostname) throws KeeperException, InterruptedException {
String create = zk.create("/servers/"+hostname, hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(hostname +" is online") ;
}
private void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
}
});
}
}
创建客户端DistributeClient
public class DistributeClient {
private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
private int sessionTimeout = 2000;
private ZooKeeper zk;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
DistributeClient client = new DistributeClient();
// 1 获取zk连接
client.getConnect();
// 2 监听/servers下面子节点的增加和删除
client.getServerList();
// 3 业务逻辑(睡觉)
client.business();
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
private void getServerList() throws KeeperException, InterruptedException {
List<String> children = zk.getChildren("/servers", true);
ArrayList<String> servers = new ArrayList<>();
for (String child : children) {
byte[] data = zk.getData("/servers/" + child, false, null);
servers.add(new String(data));
}
// 打印
System.out.println(servers);
}
private void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
try {
getServerList();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
# 首先启动客户端
# 然后可以进行命令行的测试
create -e -s /servers/hadoop102 "hadoop102"
# 最后启动服务端,这里需要进行设置,否则同一时刻只能启动一个
# 点击 Edit Configurations,在弹出的窗口中(Program arguments)输入想启动的主机,例如,hadoop102
# 然后进行启动注册
分布式锁的概念可以参考:几种分布式锁详解
public class DistributedLock {
private final String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
private final int sessionTimeout = 2000;
private final ZooKeeper zk;
private CountDownLatch connectLatch = new CountDownLatch(1);
private CountDownLatch waitLatch = new CountDownLatch(1);
private String waitPath;
private String currentMode;
public DistributedLock() throws IOException, InterruptedException, KeeperException {
// 获取连接
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// connectLatch 如果连接上zk 可以释放
if (watchedEvent.getState() == Event.KeeperState.SyncConnected){
connectLatch.countDown();
}
// waitLatch 需要释放
if (watchedEvent.getType()== Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)){
waitLatch.countDown();
}
}
});
// 等待zk正常连接后,往下走程序
connectLatch.await();
// 判断根节点/locks是否存在
Stat stat = zk.exists("/locks", false);
if (stat == null) {
// 创建一下根节点
zk.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
// 对zk加锁
public void zklock() {
// 创建对应的临时带序号节点
try {
currentMode = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// wait一小会, 让结果更清晰一些
Thread.sleep(10);
// 判断创建的节点是否是最小的序号节点,如果是获取到锁;如果不是,监听他序号前一个节点
List<String> children = zk.getChildren("/locks", false);
// 如果children 只有一个值,那就直接获取锁; 如果有多个节点,需要判断,谁最小
if (children.size() == 1) {
return;
} else {
Collections.sort(children);
// 获取节点名称 seq-00000000
String thisNode = currentMode.substring("/locks/".length());
// 通过seq-00000000获取该节点在children集合的位置
int index = children.indexOf(thisNode);
// 判断
if (index == -1) {
System.out.println("数据异常");
} else if (index == 0) {
// 就一个节点,可以获取锁了
return;
} else {
// 需要监听 他前一个节点变化
waitPath = "/locks/" + children.get(index - 1);
zk.getData(waitPath,true,new Stat());
// 等待监听
waitLatch.await();
return;
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 解锁
public void unZkLock() {
// 删除节点
try {
zk.delete(this.currentMode,-1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
测试代码
public class DistributedLockTest {
public static void main(String[] args) throws InterruptedException, IOException, KeeperException {
final DistributedLock lock1 = new DistributedLock();
final DistributedLock lock2 = new DistributedLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock1.zklock();
System.out.println("线程1 启动,获取到锁");
Thread.sleep(5 * 1000);
lock1.unZkLock();
System.out.println("线程1 释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock2.zklock();
System.out.println("线程2 启动,获取到锁");
Thread.sleep(5 * 1000);
lock2.unZkLock();
System.out.println("线程2 释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
https://curator.apache.org/index.html
原生的 Java API 开发存在的问题会话,连接是异步的,需要自己去处理。比如使用 CountDownLatch;Watch 需要重复注册,不然就不能生效;开发的复杂性还是比较高的;不支持多节点删除和创建。需要自己去递归
Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题,首先添加依赖
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>4.3.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>4.3.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-clientartifactId>
<version>4.3.0version>
dependency>
代码实现
public class CuratorLockTest {
public static void main(String[] args) {
// 创建分布式锁1
InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");
// 创建分布式锁2
InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");
new Thread(new Runnable() {
@Override
public void run() {
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(new Runnable() {
@Override
public void run() {
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();
}
private static CuratorFramework getCuratorFramework() {
ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder().connectString("hadoop102:2181,hadoop103:2181,hadoop104:2181")
.connectionTimeoutMs(2000)
.sessionTimeoutMs(2000)
.retryPolicy(policy).build();
// 启动客户端
client.start();
System.out.println("zookeeper 启动成功");
return client;
}
}
参考:Centos7安装zookeeper和Web UI
半数机制,超过半数的投票通过,即通过
第一次启动选举规则:
第二次启动选举规则:
安装奇数台。服务器台数多:好处,提高可靠性;坏处:提高通信延时。生产经验:
ls、get、create、delete