Zookeeper
1. Zookeeper干啥的?
- 在之前搭建Hadoop HA的集群搭建中,我们引入了该组件,可以得知其中一种作用在于监听NameNode的健康状况。那它还有其他的作用么 ?
- 它属于 分布式 应用程序 协调服务 框架,是一个开源,是Hadoop和Hbase重要的组件,这里面有两个关键点,一个是分布式? 一个是协调服务?
- 思考: 多台机器在一个网络中就是分布式么?
- 什么又是协调服务 ?
- Zookeeper所提供的服务涵盖: 主从协调, 服务器节点上下线感知,统一配置管理, 分布式共享锁, 统一名称服务等
Zookeeper 两个主要的功能:- 一, 管理(存储,读取) 用户提交的数据。
- 二, 为用户程序提供节点监听服务
2. Zookeeper运用场景
2.1 主从协调
- 一个master 节点(active ) 与 另一个maser节点(standby)之间的状态之间切换。
2.2 选举Master节点
- 当一个Master 节点宕机之后 ,从多个Slave 节点如何选举出Master保持继续工作。
2.3 分布式共享锁
- 保证各节点有序获取数据, Zookeeper有序目录的创建和删除 机制。
3. Zookeeper 安装前准备与配置
3.1 安装前Linux配置工作
- 同安装Hadoop一样,安装前需要配置Linux环境,可以参考 第5小节 Linux的基本配置。
- 关闭防火墙,
- 配置hosts (ip地址+主机名称)
- 配置免密钥
- 修改主机名称
- 修改主机ip地址为固定(查看虚拟机网络ipNat模式)
- 配置java环境变量
- 配置完成之后,上传zookeeper的解压程序到Linux上
apache-zookeeper-3.6.0-bin.tar.gz
安装程序
3.2 克隆节点
- 安装前配置好之后,开始克隆节点,需要在关闭虚拟机状态下进行操作,可以参照 第3.3 克隆节点
- 克隆 2台机器即可,然后注意的是修改,主机名称,主机ip,相互发送密钥
ssh-copy-id root@ ip地址
。 - 远程发送文件
scp myid root@hd02:/usr/zkdata/
。 - 远程发送目录
scp -r /usr/java/apache-zookeeper-3.6.0-bin root@hd02:/usr/java/
。
- 克隆 2台机器即可,然后注意的是修改,主机名称,主机ip,相互发送密钥
3.3 Zookeeper 配置
3.3.1 修改 Conf 目录
- 进入Zookeeper
/usr/java/apache-zookeeper-3.6.0-bin/conf
目录下,将配置文件 zoo_sample.cfg 复制一份修改为 zoo.cfg 。- 命令:
cp zoo_sample.cfg zoo.cfg
。
- 命令:
-
操作如下: 修改两处配置:
- dataDir 是存放zookeeper缓存文件,手动创建
/usr/zkdata
目录 (注意:是目录,别创建成文件)。 - 配置节点选举节点。server.1=节点主机名称:2888:3888
参数说明:1,选举id值、2888心跳端口、3888选举端口。
注: 在usr/zkdata/
创建一个文件 myid(注意这个是文件,别创建成目录),编辑内容为选举节点id值。
例如 :server.1 =hd01:2888:3888。 myid内容就是1。
- dataDir 是存放zookeeper缓存文件,手动创建
3.3.2 给自己机器写编号
- 在 每个克隆节点上都需要,手动创建
/usr/zkdata
目录,并且在该目录下创建文件myid文件。- 编写内容是自己的 id号。
- 编写内容是自己的 id号。
3.3 启动Zookeeper
-
在每个节点上的zookeeper的bin目录里输入启动命令,
sh zkServer.sh start 启动 sh zkServer.sh status 查看节点角色命令
4. Zookeeper的工作机制
4.1 zookeeper特性
- zookeeper是由一个Leader,和 多个follower组成的集群。
- 全局数据一致性。每个server都保存一份相同的数据副本,客户端无论从哪个server上读取,数据都是一致的。
- 分布式读取,更新,请求,转发,数据版本都是由Leader协调实施。
- 数据原子性。事物机制,即,数据要么都成功,要么失败(不存在成功一般失败一半)。
- 实时性。 在一定时间范围内,客户端读到的都是最新数据。
4.2 Zookeeper的数据结构(重点)
- 层次化目录结构,命名规范符合常规命名系统 (如图znode)。
- 每个节点在zookeeper中叫做 znode, 并且有 唯一标识路径。
- 节点znode可以包含, 数据,子节点。
- (官方默认是1M,但是最好,存储数据最好是1k字节以内这样才能保证数据实时性传输)。
4.2.1 节点类型
- Znode有 两种类型
- 短暂(ephemeral) (断开连接自己删除)
- 持久(persistent) (断开连接不删除)
- Znode 有四种形式 的目录节点。
- PERSISTENT(默认持久)
- PERSISTENT_SEQUENTIAL(持久序列/test0000000012)
- EPHEMERAL
- EPHEMERAL_SEQUENTIAL
5. Zookeeper命令行客户端的使用
5.1 客户端连接
- 通过zookeeper自带的命令来连接,在
/usr/java/apache-zookeeper-3.6.0-bin/bin
目录下;sh zkCli.sh
连接本机客户端。
5.2 Znode 创建节点 临时 顺序 永久
1.创建节点 create [-s] [-e] [-c] [-t ttl] path [data] [acl]
命令 | 节点描述 |
---|---|
create [-s] [-e] [-c] [-t ttl] path [data] [acl] | 默认是持久节点,创建一个新的znode节点,-s表示顺序节点,-e表示临时节点,acl访问控制列表 |
- 分别在zookeeper下创建节点。
- 创建临时节点
create /zookeeper/zkdata -e/zkephermeral 临时
断开就消失 - 创建持久节点
create /zookeeper/zkdata/zkpersisten
默认就是持久节点 - 创建顺序节点
create /zookeeper/zkdata/zkpersistent -s 顺序
后面会自动维护序列号
- 创建临时节点
5.3 Znode 查看节点
- 查看节点
ls [-s] [-w] [-R] path
命令 | 节点描述 |
---|---|
ls [-s] [-w] [-R] path | 只能查看一级以下的子节点,-s表示获取节点信息,-R表示递归的获取,-w 表示监听 |
- 查看zookeeper下的zkdata信息
- 查看子节点
ls /zookeeper/zkdata
- 查看子节点信息
ls /zookeeper/zkdata -s
- 递归查看所有信息
ls -R /zookeeper/zkdata
- 查看子节点
- 查看元数据信息显示内容。
5.4 Znode 获取节点
- 获取节点中的数据
get [-s] [-w] path
命令 | 节点描述 |
---|---|
get [-s] [-w] path | 获取节点数据信息,-s表示获取节点信息,包括时间戳、版本号、数据大小等 |
- 获取zookeeper下zkdata子节点信息
- 获取节点数据
get /zookeeper/zkdata
- 获取详细节点数据信息
get /zookeeper/zkdata -s
- 获取节点数据
5.5 Znode 更新节点
- 更新 节点数据,好比Mysql中的 update 操作
set [-s] path data
命令 | 节点描述 |
---|---|
set [-s] path data | 设置节点数据,-s表示显示当前节点数据 |
5.6 Znode 删除节点
- 删除节点,有两种方式
- 删除节点,如果有子节点就无法删除。
delete [-v version] path
- 递归删除子节点
deleteall path
- 删除节点,如果有子节点就无法删除。
命令 | 节点描述 |
---|---|
delete [-v version] path | 删除一级节点,只能删除无子节点的节点。 |
deleteall path | 递归删除包括子节点 |
5.7 Znode 监听
- zookeeper对外提供的主要功能之一,监听机制
- 监听 节点数据变化 (NodeDataChanged) 如:
get -w /zookeeper/zkdata
- 监听 子节点发生改变(NodeChildrenChanged) 如:
ls -w /zookeeper/zkdata
- 监听 删除节点 (NodeDeleted)
- 监听 节点数据变化 (NodeDataChanged) 如:
6. Zookeeper 客户端的Api操作
6.1 增删改查 Znode数据
-
org.apache.zookeeper.ZooKeeper;
是客户端的主要入口主类,负责建立server的会话它提供了如下的方法: -
创建一个普通的project工程,引入相关jar即可。
- 如下 增删改查 java API操作。
import org.apache.zookeeper.*;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class ZK_Test {
ZooKeeper zk =null; // 对象提取成员属性。
//1.创建连接zookeeper,拿到zookeeper的操作对象。
@Before
public void init() throws IOException, InterruptedException {
//1.1 直接创建zk的对象即可。
/*public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)//连接地址,心跳,检测
String connectString : 连接服务器的地址
int sessionTimeout : 客户端超时时间 默认2s 2000
Watcher watcher :监听事件 interface Watcher是一个接口。
*/
//开启了递减锁,有阻塞功能
final CountDownLatch cd = new CountDownLatch(1);
zk = new ZooKeeper("192.168.150.134:2181,192.168.150.135:2181,192.168.150.136:2181", 2000, new
Watcher() {
@Override
public void process(WatchedEvent watchedEvent) { //回调函数
// 连接状态是否连接
if (watchedEvent.getState()==Event.KeeperState.SyncConnected){
System.out.println("连接成功");
//连接成功之后就减1
cd.countDown();
}
//打印事件名称。
System.out.println("事件名称:"+ watchedEvent.getState());
//getData获取数据的回调函数 当watch 设置为true
// System.out.println("getDate:"+ watchedEvent.getPath());
}
});
//如果不成功就阻塞直到成功
cd.await();
}
/*
创建节点或者子节点
create(String path, byte[] data, List<ACL> acl, CreateMode createMode)
path: 创建路径
data 数据字节类型
list 权限 ids 谁能查看
createMode 创建节点类型 顺序还是持久
*/
@Test //使用单元测试
public void createNode() throws KeeperException, InterruptedException {
//拿到zk对象? 需要提取zookeeper zk为成员属性
zk.create("/zookeeper/zkdata","hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT_SEQUENTIAL);
zk.close();
}
/*
判断node是否存在
*/
@Test
public void isExist() throws KeeperException, InterruptedException {
//如果存在 ,则调用回调函数打印 zk.close()
Stat exists = zk.exists("/zookeeper/zkdata", true); //判断是否存在,
System.out.println(exists);
Thread.sleep(Long.MAX_VALUE);
zk.close();
}
/*
查询一个节点
* byte[] getData(String path, boolean watch, Stat stat)
path 路径
watch 监听 false即可. 如果使用true就需要去zkClis.sh 客户端操作set数据
stat 当前版本信息 赋值null即可。
*/
@Test
public void selectNode() throws KeeperException, InterruptedException {
byte[] data = zk.getData("/zookeeper/zkdata", false, null); //如果要使用true会调用上面监听事件
//打印字节数组
System.out.println(new String(data));
//造成阻塞监听事件 watch 为true情况下。
//Thread.sleep(Long.MAX_VALUE);
//关闭
zk.close();
}
/*
更新数据
Stat setData(String path, byte[] data, int version)
path 更新路径
data 更新数据
version 版本 -1是所有版本
*/
@Test
public void setNode() throws KeeperException, InterruptedException {
//更新即可。
// 无需返回值
zk.setData("/zookeeper/zkdata", "更新数据znode".getBytes(), -1);
System.out.println("更新成功!");
zk.close();
}
/* 查询子节点数据
List<String> getChildren(String path, boolean watch)
path 路径
watch 监听 false
*/
@Test
public void selectChrildren() throws KeeperException, InterruptedException {
List<String> children = zk.getChildren("/zookeeper/zkdata", false);
System.out.println(children); //输出数组[aa,ab,ac]
zk.close();
}
/*删除节点
delete(String path, int version)
path 路径
version 删除版本 -1就是当前版本
*/
@Test
public void deleteNdoe() throws KeeperException, InterruptedException {
zk.delete("/zookeeper/zkdata/aka",-1);
System.out.println("删除成功");
zk.close();
}
}
6.2 Watch 监听
- 当连接客户端之后会调用一次监听的回调函数。
/**
* Zookeeper监听
*/
public class ZK_Api {
private static final String coon ="192.168.150.134:2181,192.168.150.135:2181,192.168.150.136:2181";
@Test
public void watcherNode() throws IOException, KeeperException, InterruptedException {
final CountDownLatch cd = new CountDownLatch(1);
ZooKeeper zk = new ZooKeeper(coon, 2000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//1.当zk连接成功之后会调用一次该方法
System.out.println(watchedEvent.getPath());
System.out.println(watchedEvent.getType());
System.out.println(watchedEvent.getState());
System.out.println("-----------------------------------");
cd.countDown();
}
});
cd.await();
// 监听 zkdata数据是否发生改变。 如果此时在Linux客户端下修改数据,就会触发 在回调一次。
zk.getData("/zookeeper/zkdata",true, null);
//上面方法执行完就会关闭,休眠一会让它继续等待监听
Thread.sleep(Long.MAX_VALUE);
zk.close();
}
}
null
None
SyncConnected
-----------------------------------
/zookeeper/zkdata
NodeDataChanged
SyncConnected
-----------------------------------
- 循环注册监听。
/**
* Zookeeper api 循环监听
*/
public class ZK_Api {
private static final String coon ="192.168.150.134:2181,192.168.150.135:2181,192.168.150.136:2181";
ZooKeeper zk =null;
@Test
public void watcherNode() throws IOException, KeeperException, InterruptedException {
final CountDownLatch cd = new CountDownLatch(1);
zk = new ZooKeeper(coon, 2000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
try {
//只要一直连接在,循环监听。 一直在注册
zk.getData("/zookeeper/zkdata",true, null);
System.out.println(watchedEvent.getType());
System.out.println("---------------");
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
cd.countDown();
}
});
cd.await();
// 监听 zkdata数据是否发生改变。 如果此时在Linux客户端下修改数据,就会触发 在回调一次。
//zk.getData("/zookeeper/zkdata",true, null);
// 该方法只能监听一次。 执行完就释放。使用的也是 zk中的 Watcher监听,
// 如果想使用自己的回调监听 就new Watcher即可。
List<String> children = zk.getChildren("/zookeeper/zkdata", true);
System.out.println("子节点发生变化" + children);
//上面方法执行完就会关闭,休眠一会让它继续等待监听
Thread.sleep(Long.MAX_VALUE);
zk.close();
}
}
7 服务器上线下线感知
7.1 设计思路分析
- 需求,当服务器增减时,客户端能感知到。
- 设计思路:
- 需要在Linux环境中,进行打jar包测试,Client.jar 和 Server.jar 。
7.3 服务端开发
- 服务端: 注册连接,register节点,模拟业务。
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
/**
* zk服务端
*/
public class ZKServer {
/**
* 一,获取zookeeper连接
*/
private static final String conn = "192.168.150.134:2181,192.168.150.135:2181,192.168.150.136:2181";
ZooKeeper zk = null;
CountDownLatch cd = new CountDownLatch(1);
//1.获取zk连接
public void serverInit() throws IOException, InterruptedException {
//2.通过构造函数获取连接
zk = new ZooKeeper(conn, 2000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getState()== Event.KeeperState.SyncConnected){
System.out.println("连接成功");
cd.countDown();
}
}
});
cd.await();
}
private static final String pNode = "/server"; //因为每一个节点都要判断父节点是否存在。
/**
* 二,向zookeeper注册信息 register
* @param hostname 注册的机器名
*/
public void registerServer(String hostname) throws KeeperException, InterruptedException {
//1.判断所注册的父节点是否存在
Stat exists = zk.exists(pNode, false);
// 进行判断是否创建节点
if (exists==null){
//2,不存在,就需要创建父节点, 节点持久化
zk.create(pNode,null, ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
}
//2,1创建完父节点之后,在父节点下创建子节点,节点类型? 短暂序号(让zookeeper维护sequential)
String path = zk.create(pNode + "/server", hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//打印子节点路径信息
System.out.println(hostname+" 在线中... "+path);
}
/**
* 三,业务功能
*/
public void works(String hostname) throws InterruptedException {
//Todo 要处理的业务
System.out.println("Server is start...");
Thread.sleep(Long.MAX_VALUE); //假装在处理。。。
}
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
ZKServer zkServer = new ZKServer();
zkServer.serverInit();
/*
//模拟上线机器
System.out.println("请输入要上线的机器名称: hd01 hd02 hd03");
String pc = new Scanner(System.in).next();
//注册业务
zkServer.registerServer(pc);
//处理业务
zkServer.works(pc);//
*/
//注册业务
zkServer.registerServer(args[0]);
//处理业务
zkServer.works(args[0]);//传递参数在linux下
}
}
- 运行环境在Linux下进行测试,需要打jar包。
- 可以先开启一个Client.jar 作为测试,然后再开启多个Server.jar上线或者下线让客户端进行感
7.2 客户端开发
- 客户端 : 注册连接(在回调中更新列表信息), 获取子节点信息,模拟业务。
/**
客户端业务
*/
public class ZKClient {
private static final String conn ="192.168.150.134:2181,192.168.150.135:2181,192.168.150.136:2181";
ZooKeeper zk =null;
private static final String pNode = "/server";
//获取 zk连接
public void zkinit() throws Exception{
zk = new ZooKeeper(conn, 2000, new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
//更新类表重新注册监听
getNodeLists();
} catch (Exception e) {}
}
});
}
/**
* 一,获取在线pNode/下,子节点信息
*
*/
public void getNodeLists() throws KeeperException, InterruptedException {
//1.获取父类下的子节点
List<String> children = zk.getChildren(pNode, true); //设置true就会调用上面连接的回调函数。
for (String chil: children){
//1.1 获取子节点数据
byte[] data = zk.getData(pNode + "/" + chil, false, null);
System.out.println(new String(data)); //输出
System.out.println("-----------------------");
}
}
/**
* 三,模拟业务工作
* @throws Exception
*/
public void works() throws Exception{
//处理业务
System.out.println("Client is start......");
Thread.sleep(Long.MAX_VALUE);
}
//主程序
public static void main(String[] args) throws Exception {
ZKClient cilent = new ZKClient();
//创建连接
cilent.zkinit();
//获取子znode 节点信息
cilent.getNodeLists();
//启动业务服务
cilent.works();
}
}
8 选举机制(简述)
-
以一个简单的例子来说明整个选举的过程.
- 假设,有五台服务器组成的zookeeper集群,它们的 id 从1-5, 同时它们都是,最新启动的, 也就是没有历史数据, 在存放数据量这一点上,都是一样的。假设这些服务器依序启动。
-
看看会发生什么?
- 服务器1启动,此时只有它一台服务器启动了,它发出去的报没有任何响应,所以它的选举状态一直是LOOKING状态。
- 服务器2启动,它与最开始启动的服务器1进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以id值较大的服务器2胜出, 但是由于没有达到超过半数以上的服务器都同意选举它 (这个例子中的半数以上是3), 所以服务器1,2还是继续保持 LOOKING状态.
- 服务器3启动,根据前面的理论分析,服务器3成为服务器1,2,3中的老大, 而与上面不同的是,此时有三台服务器选举了它,所以它成为了这次选举的leader.
- 服务器4启动,根据前面的分析,理论上服务器4应该是服务器1,2,3,4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,所以它只能接收当小弟的命了.
- 服务器5启动,同4一样,当小弟.
-
非全新集群的选举机制(数据恢复)
- 那么,初始化的时候,是按照上述的说明进行选举的,但是当zookeeper运行了一段时间之后,有机器down掉,重新选举时,选举过程就相对复杂了。
- 需要加入数据version、leader id和逻辑时钟。
数据version:数据新的version就大,数据每次更新都会更新version。Leader id:就是我们配置的myid中的值,每个机器一个。
-
逻辑时钟: 或者叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断。
- 这个值从О开始递增,每次选举对应一个值,也就是说:如果在同一次选举中,那么这个值应该是一致的;逻辑时钟值越大,说明这一次选举leader的进程更新.选举的标准就变成:
1、逻辑时钟小的选举结果被忽略,重新投票
2、统一逻辑时钟后,数据 id大的胜出
3、数据id相同的情况下,leader id大的胜出
根据这个规则选出leader。
- 这个值从О开始递增,每次选举对应一个值,也就是说:如果在同一次选举中,那么这个值应该是一致的;逻辑时钟值越大,说明这一次选举leader的进程更新.选举的标准就变成: