Zookeeper
Zookeeper概念
- Zookeeper是Apache Hadoop项目下的一个子项目,是一个树形目录服务。
- Zookeeper(翻译:动物管理员),用来管Hadoop(大象)、Hive(蜜蜂)、Pig(小猪)的管理员。简称zk。
- Zookeeper是一个分布式的、开源的分布式应用程序的协调服务。
- 主要功能包括:
- 配置管理
- 分布式锁
- 集群管理
- 真正干活的还是各个服务,zookeeper只是起到一个管理协调这些服务的作用。
Zookeeper安装
1、环境准备
Zookeeper服务器是用Java创建的,它运行在JVM上。需要安装JDK7或更高版本。
zookeeper官网:https://zookeeper.apache.org/
2、解压配置
本次下载的是官网推荐版本
我们建议您下载以下站点:
https://dlcdn.apache.org/zookeeper/zookeeper-3.7.1/apache-zookeeper-3.7.1-bin.tar.gz
下载完成后,上传到服务器。
解压到指定目录,这里以/opt/zookeeper/为例
tar -zxvf apache-zookeeper-3.7.1-bin.tar.gz
创建目录,用于存储zookeeper数据 zkdata/
mkdir zkdata
进入apache-zookeeper-3.7.1-bin/conf/目录下面,拷贝zoo_sample.cfg到zoo.cfg
cp zoo_sample.cfg zoo.cfg
# 因为配置文件改成zoo.cfg才能生效。
默认端口:2181
配置zoo.cfg文件中的dataDir
# 存储zookeeper数据的位置
dataDir=/opt/zookeeper/zkdata
3、启动zookeeper
启动zookeeper服务
cd /opt/zookeeper/apache-zookeeper-3.7.1-bin/bin/
#启动
./zkServer.sh start
# 提示STARTED 表示启动成功
查看zookeeper状态
./zkServer.sh status
# standalone代表zk没有搭建集群,现在是单节点。
# It is probably not running 表示没有启动。
[root@localhost bin]# ./zkServer.sh version
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/apache-zookeeper-3.7.1-bin/bin/../conf/zoo.cfg
Apache ZooKeeper, version 3.7.1 2022-05-07 06:45 UTC
[root@localhost bin]# ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/apache-zookeeper-3.7.1-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[root@localhost bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/apache-zookeeper-3.7.1-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: standalone
[root@localhost bin]# ./zkServer.sh restart
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/apache-zookeeper-3.7.1-bin/bin/../conf/zoo.cfg
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/apache-zookeeper-3.7.1-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/apache-zookeeper-3.7.1-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[root@localhost bin]# ./zkServer.sh stop
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/apache-zookeeper-3.7.1-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED
[root@localhost bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/apache-zookeeper-3.7.1-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Error contacting service. It is probably not running.
[root@localhost bin]#
Zookeeper命令操作
1、Zookeeper数据模型
- Zookeeper是一个树形目录服务,其数据模型和Unix的文件系统目录树很类似,拥有一个层次化结构。
- 这里面的每一个节点都被称为:ZNode,每个节点上都会保存自己的数据和节点信息。
- 节点可以拥有子节点,同时也允许少量(1M)数据存储在该节点之下。
- 节点可以分为四大类:
- PERSISTENT持久化节点
- EPHEMERAL临时节点:-e
- PERSISTENT_SEQUENTIAL持久化顺序节点:-s
- EPHEMERAL_SEQUENTIAL临时顺序节点:-es
2、Zookeeper服务端常用命令
启动:./zkServer.sh start
查看状态:./zkServer.sh status
停止:./zkServer.sh stop
重启:./zkServer.sh restart
3、Zookeeper客户端常用命令
启动客户端:./zkCle.sh -server ip:port
如:./zkCle.sh -server localhost:2181
退出客户端:quit
查看指定路径下的子节点:ls /zookeeper
新建节点:create /app2
给节点设置数据:set /app2 haha
新建节点,同时设置数据:create /app1 hehe
获取节点数据:get /app1
删除单个节点:delete /app2
删除包含子节点的节点:deleteall /app1
创建临时节点:create -e /app1 当前会话关闭后,该节点会自动删除
创建顺序节点:create -s /app1 持久化顺序节点app10000000003, app10000000004, app10000000005
创建临时顺序节点:create -es /app3
查看节点详细信息:ls -s /
旧版本中可以用ls2 /
Zookeeper JavaAPI操作
1、Curator介绍
- Curator是Apache Zookeeper的Java客户端库。
- 常见的Zookeeper Java API:
- 原生Java API
- ZkClient 对原生的简单封装
- Curator
- Curator项目的目标是简化Zookeeper客户端的使用。
- 3.5版本以上的zookeeper,要使用4.0的curator
2、Curator API常用操作
- 建立连接
- 添加节点
- 删除节点
- 修改节点
- 查询节点
- Watch事件监听
- 分布式锁实现
package com.zxl.curator;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.retry.ExponentialBackoffRetry;
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.util.List;
public class CuratorTest {
private CuratorFramework client;
// 需要关闭防火墙
@Before
public void testClient() {
// // 重试策略
// RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
//
// /**
// *
// * @param connectString 连接字符串。zk server 地址和端口"192.168.135.128:2181,192.168.135.128:2181”集群用逗号隔开
// * @param sessionTimeoutMs 会话超时时间 单位ms
// * @param connectionTimeoutMs connection timeout 连接超时时间 单位ms
// * @param retryPolicy retry policy to use 重试策略
// * @return client
// */
// // 第一种方式
// client = CuratorFrameworkFactory.newClient("192.168.135.128:2181",
// 60*1000,15*1000,retryPolicy);
// // 开启连接
// client.start();
// 重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
// 第二种方式
client = CuratorFrameworkFactory.builder().connectString("192.168.135.128:2181")
.sessionTimeoutMs(60*1000)
.connectionTimeoutMs(15*1000)
.retryPolicy(retryPolicy)
.namespace("zxl")
.build();
// namespace 设置名称空间后,后面的所有操作,默认都是以/zxl为根目录,并且会自动创建这个目录。
// 开启连接
client.start();
}
// ==================================create==================================
// 创建持久化单节点,并设置数据
@Test
public void testCreate() throws Exception {
// 默认类型:持久化
String path = client.create().forPath("/app2", "haah".getBytes());
System.out.println(path);
}
// 创建临时节点
@Test
public void testCreateMode() throws Exception {
// withMode设置类型,CreateMode.EPHEMERAL临时节点
String path = client.create().withMode(CreateMode.EPHEMERAL).forPath("/app3", "haah".getBytes());
System.out.println(path);
}
// 创建多级节点
@Test
public void testCreate4() throws Exception {
// .creatingParentsIfNeeded()创建多级节点,如果父节点不存在,则创建父节点
String path = client.create().creatingParentsIfNeeded().forPath("/app1/p1", "haah".getBytes());
System.out.println(path);
}
// ==================================get==================================
// 查询指定目录的子节点
@Test
public void testGet() throws Exception {
List<String> strings = client.getChildren().forPath("/");
System.out.println(strings);
}
// 查询指定节点的数据
@Test
public void testGetData() throws Exception {
byte[] bytes = client.getData().forPath("/app2");
System.out.println(new String(bytes));
}
// 查询指定节点的状态
@Test
public void testGetStatus() throws Exception {
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath("/app2");
System.out.println(stat);
}
// ==================================set==================================
// 设置值,如果forPath只有一个地址,则默认设置该节点数据为IP
@Test
public void testSet() throws Exception {
Stat stat = client.setData().forPath("/app2");
System.out.println(stat);
// 57,62,1660185902972,1660186251961,1,0,0,0,13,0,57
}
// 修改节点的值
@Test
public void testSetDate() throws Exception {
Stat stat = client.setData().forPath("/app2","bbb".getBytes());
System.out.println(stat);
}
// 根据版本修改,保证原子性操作
@Test
public void testSetByVersion() throws Exception {
// 创建stat,记录节点状态信息
Stat stat = new Stat();
// 将查出来的节点信息封装到stat中
client.getData().storingStatIn(stat).forPath("/app2");
// 获取当前节点的版本,目的是上其他客户端或其他线程不干扰该操作。
int version = stat.getVersion();
client.setData().withVersion(version).forPath("/app2","haha".getBytes());
}
// ==================================delete==================================
// 删除节点
@Test
public void testDelete() throws Exception {
client.delete().forPath("/abc");
}
// 删除有子节点的节点
@Test
public void testDelete2() throws Exception {
client.delete().deletingChildrenIfNeeded().forPath("/app4");
}
// 必须成功的删除,如果有网络抖动导致删除失败,则会重试
@Test
public void testDelete3() throws Exception {
client.delete().guaranteed().forPath("/app4");
}
// 删除后回调
@Test
public void testDelete4() throws Exception {
client.delete().guaranteed().inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
// 回调函数的具体操作
System.out.println("我被删除了");
System.out.println(curatorEvent);
// 根据resultCode=的值,判断删除是否成功
}
}).forPath("/app1");
}
// 关闭客户端
@After
public void testClose() {
if (client != null)
client.close();
}
}
Curator API常用操作-Watch事件监听
-
Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端上去,该机制是Zookeeper实现分布式协调服务的重要特性。
-
Zookeeper中引入了Watcher机制来实现了发布/订阅功能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。
-
Curator引入了Cache来实现对Zookeeper服务端事件的监听。
-
Zookeeper提供了三种Watcher:
- NodeCache:只是监听某一个特定的节点
- PathChildrenCache:监控一个ZNode的子节点,但是不包括子节点的子节点
- TreeCache:可以监控整个树上的所有节点,包括子节点的子节点,类似于PathChildrenCache和NodeCache的组合。
NodeCache
// 创建NodeCache对象
final NodeCache nodeCache = new NodeCache(client,"/app1");
// 注册监听
nodeCache.getListenable().addListener(new NodeCacheListener(){
@Override
public void nodeChanged() throws Exception {
System.out.println("节点变化了~");
// 获取修改节点后的数据
byte[] data = nodeCache.getCurrentData().getData();
System.out.println(new String(data));
}
});
// 开启监听 ,如果设置为true,则开启监听时,加载缓冲数据
nodeCache.start(true);
PathChildrenCache
@Test
public void testWatcher() throws Exception {
// 创建PathChildrenCache对象
PathChildrenCache pcc = new PathChildrenCache(client,"/app1",true);
// 注册监听
pcc.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
System.out.println("节点变化了");
// 获取修改节点后的数据
List<ChildData> currentData = pcc.getCurrentData();
for (ChildData cd : currentData) {
System.out.println(new String(cd.getData()));
}
System.out.println(event);
// 监听子节点的数据变更,并且拿到变更后的数据。
PathChildrenCacheEvent.Type type = event.getType();
if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
byte[] data = event.getData().getData();
System.out.println(new String(data));
}
}
});
// lambda表达式
/*
pcc.getListenable().addListener((client1, event) -> {
System.out.println("节点变化了");
// 获取修改节点后的数据
List<ChildData> currentData = pcc.getCurrentData();
for (ChildData cd : currentData) {
System.out.println(new String(cd.getData()));
}
});
*/
// 开启监听 ,如果设置为true,则开启监听时,加载缓冲数据
pcc.start();
// 为了让客户端不会关闭
while (true){
}
}
分布式锁
- 单机应用开发设计并发同步的时候,我们采用synchronized或者lock的方式来解决多线程间的代码同步问题。这时多线程的运行都是在同一个JVM之下,没有任何问题。
- 但是当我们的应用是分布式集群工作的情况下,属于多JVM下的工作环境,跨JVM之间已经无法通过多线程的锁解决同步问题。
- 那么就需要一种更加高级的锁机制,来处理这种跨机器的进程之间的数据同步问题–这就是分布式锁。
- 分布式锁实现
- 基于缓存
- Redis
- 性能高,不可靠,因为不一致性
- Memcache
- Redis
- zookeeper
- Curator
- 数据库层面
- 悲观锁、乐观锁
- 性能低,不推荐
- 基于缓存
Zookeeper分布式锁的原理
- 核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除节点。
- 客户端获取锁时,在lock(名称随意)节点下创建临时顺序节点。
- 为什么是临时节点呢?
- 因为如果是持久化节点,当client宕机后,该节点一直存在,未被删除,则无法释放锁。
- 如果是临时节点,尽管client宕机,该临时节点会在会话结束后自动删除,释放锁。
- 为什么是临时节点呢?
- 然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除。
- 如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点(如果有1、2、3、4、5个节点,4要找3,3要找2,5要找4)同时对其注册事件监听器,监听删除事件。
- 如果发现比自己小的那个节点被删除,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
- 为什么要重复进行,因为是临时节点,如果自己监听的子节点客户端宕机,那对应的子节点也会被删除,所以要重新比较。
- 客户端获取锁时,在lock(名称随意)节点下创建临时顺序节点。
Curator实现分布式锁API
在Curator中有五种锁方案:
- InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
- 非可重入:如果客户端获取共享资源之后,因为某些原因退出了,则后面再想获取这个共享资源,需要先释放锁,和其他客户端竞争该共享资源。
- 可重入:如果之前已经获取过该资源且锁未释放,则无需竞争,直接继续访问该资源。
- InterProcessMutex:分布式可重入排它锁
- InterProcessReadWriteLock:分布式读写锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器
- InterProcessSemaphoreV2:共享信号量
// 初始化锁
InterProcessMutex lock = new InterProcessMutex(client,"/lock");
// 获取锁
lock.acquire(3,TimeUnit.SECONDS);
// 释放锁
lock.release();
Zookeeper集群介绍
Leader选举:
- Serverid:服务器ID
- 比如有三台服务器,编号分别是1、2、3。
- 编号越大在选举算法中的权重越大。
- Zxid:数据ID
- 服务器中存放的最大数据ID,值越大说明数据越新,在选举算法中数据越新权重越大。
- 数据的ID越大,说明数据更新的越频繁,所以所占权重就越大。
- 在Leader选举的过程中,如果某台Zookeeper获得了超过半数的选票,则此Zookeeper就可以成为Leader了。
- Leader选举,如果有5台服务器,且是按照服务器ID顺序重启,
- 第一台服务器启动,选自己;(票数未过半,故不是Leader)
- 第二台启动,自己和第一台都选第二台(票数未过半,故不是Leader)
- 第三台启动,自己和前面两台都选第三台(此时票数已过半,当选为Leader)
- 第四台启动,发现已经有Leader,则自己为follower
- 第五台启动,发现已有Leader,则自己为follower
配置集群
- 需要在每个Zookeeper的data(存储zookeeper数据的目录)下创建一个myid文件,内容是id值。这个文件就是记录每个服务的id。
- 在每一个zookeeper的zoo.cfg配置文件中,配置客户端访问端口(clientPort)和集群服务器IP列表。
# 在每个配置文件中配置如下列表
server.1=192.168.135.127:2881:3881
server.2=192.168.135.128:2881:3881
server.3=192.168.135.129:2881:3881
# 解释:server.服务器id=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口
# 客户端和服务端连接默认端口是:2181
# 服务器和服务器通信(数据同步)默认端口是:2881
# 服务器之间投票选举默认端口是:3881
集群异常情况
3台zookeeper服务器组成集群,服务id分别为1、2、3;依次启动机器。2号为Leader,1和3为从服务器
- 当3号机器挂掉,集群可正常访问;
- 当1号机器也挂掉,虽然2号机器没有关掉,但是集群已经不可用,所以2号机器也访问不到(状态为停止运行),不再是leader;
- 得出结论,3个节点的集群,2个从服务器挂掉,主服务器也无法运行。因为可运行的机器没有超过集群总数的一半。
- 此时重启1号服务器,集群正常运行,2号恢复Leader状态。
- 重启3号服务器,关掉2号服务器,则3号为Leader。
Zookeeper集群角色
在Zookeeper集群服务中有三个角色:
- Leader领导者:
- 处理事务请求
- 集群内部各服务器的调度者
- Follower跟随者:
- 处理客户端非事务请求,转发事务请求给Leader服务器
- 参与Leader选举投票
- Observer观察者:
- 处理客户端非事务请求,转发事务请求给Leader服务器
- 不参与投票
总结:因为Follower的读取压力比较大,所以Observer帮助处理非事务的请求,但是不参与投票。
当跟随者和观察者收到事务请求,会将请求转发到Leader处理,Leader处理完成后,将数据同步到跟随者和观察者。