简介:Zookeeper是一个底层的分布式协调服务工具!它是hadoop生态体系中很多分布式系统(HDFS、YARN(HA高可用)、HBASE(ZK)、KAFKA.........)的基础组件
基本的功能:
为客户提供写数据功能 存储关键状态数据 数据量小
为客户提供读取据功能
为用户提供数据变化时的监控功能 1) 节点的子节点个数 2) 节点的数据变化
作用:
HDFS YARN的mast是自己制定的(配置HA) namenode
分布式系统中的主节点选举! 比如hbase中的老大的产生 Hmaster(HA)
分布式系统中的主从节点感知!
分布式系统中的配置文件同步!
系统服务器的动态上下线感知!!!
分布式系统中的分布式锁的实现!分布式中的同一个对象
分布式系统中的名称服务!
分布式系统中的负载均衡!
........
Zookeeper的功能其实很简单:就是提供协调服务!
协调服务具体来说有三方面:
1. 帮使用者存储一些状态信息
2. 帮使用者读取一些信息
3. 帮使用者监视一些信息的变化,并将变化作为事件通知给使用者
特点:
Zookeeper的高度可靠性
是一个分布式的系统,多个节点 并且节点中记录的数据是完全一致(一致性) , 当某个zk的节点宕机之后不会影响工作 .
Zookeeper的节点不存在单点故障!Zookeeper的主节点是可以动态选举出来的!
Zookeeper的选举机制(奇数台)
Zookeeper的主节点叫做 leader (1个)
从节点叫做 follower(多个)
Leader选举过程(以3个节点的集群为例):
注:每个节点的配置文件中都有一个自己的独一无二的id
zookeeper的进程在不同的工作模式下,有不同的通信端口(比如选举时,通过端口3888通信;作为leader或者follower接收客户端请求时通过端口2181;leader和follower之间通信用2888)
!!!注意在zk集群安装的时候 会人为的为每台机器分配一个唯一的id
(1) 集群初次启动时的选举流程
A、第一台机器(id=1)启动,发现没有leader,进入投票模式,投自己,并收到自己投这一票,得1票,不能当选leader(当leader的条件是,集群机器数量过半的票数)
B、第2台机器(id=2)启动,发现没有leader,进入投票模式,投自己(因为自己的id>1 收到的另一台机器的票的id)
C、第1台机器收到2的票,发现集群中有一个比自己id大的机器上线了,重新投票,投id=2
D、第2台收到的得票数为2票,过半数,自己当选,切换模式:Leader模式
E、第1台就发现有Leader存在了,自己切换模式:Follower
F、第3台启动,发现有Leader,自动进入Follower状态
如果每个节点是同时启动的zk 同时选举自己 ,同时广播 , 同时获取别人的广播,3号机器会当选
(2) 集群在运行过程中的选举流程
a. 在某个时间点上,id=2机器挂了(leader),别的机器发现没有leader了,全体进入投票模式
b. 先投自己,票中会携带(自己的id,自己的数据的版本号)
c. 大家都投数据版本最新的节点做leader,如果有多个节点数据版本一样,则从中选id最大的那个作为投票目标!
从上述投票机制可以看出:
Zookeeper集群的节点数最好配置为奇数!
Zookeeper集群的节点规模一般在3~5台就够!
Zookeeper的数据存储模型:
znode类似于Linux的目录结构(TREE) , 维护了节点的层级关系 ,
真正的数据存储是以key:value的形式存储的 , 存储的是字节数据!
zookeeper中对数据的存储采用key-value的形式
然后,它的key有特别的格式——路径的形式!( /service/dn1 doit01 /service/dn2 doit02 ) (key 是路径,value是路径对应的值)
之所以采取这种形式,是因为zookeeper中的数据节点(znode)之间可以存在父子关系;
zookeeper的数据节点分为以下类型:
永久的znode:客户一旦创建这个znode,它就会被zookeeper一直保存,除非人为删除;create 文件夹名
短暂的znode:客户创建完这个znode后,如果客户断开与zookeeper的连接,则该数据马上会被zookeeper删除;cerste e 文件夹名 e 代表临时
带序号的znode:客户创建一个key,zookeeper会为客户的key自动拼接一个递增的序号! cerste s 文件夹名 s 代表有序,如果有重名出现会给每一个都加上序号
(客户创建的是/aaa/x ,那么zookeeper真实生成的key为: /aaa/x0000000000001
如果客户继续创建 /aaa/y,那么zookeeper真实生成的key为:/aaa/y000000000002)
另外: 永久的和短暂的,都可以跟“带序号的”特性进行组合!组合下来之后共有4种:
永久不带序号
永久且带序号的
短暂不带序号
短暂且带序号的
安装:
1.上传压缩包到/opt/apps/ rz zookeeper-3.4.6.tar.gz 回车
2.解压到当前目录下;tar -zxvf zookeeper-3.4.6.tar.gz 得到zookeeper-3.4.6
3.在zookeeper-3.4.6目录下新建一个文件夹用来存数据
mkdir zkData
4.修改 /opt/apps/zookeeper-3.4.6/conf 下zoo_sample.cfg的文件名 为 zoo.cfg
mv zoo_sample.cfg zoo.cfg
5.编辑更名的zoo.cfg内容:
①dataDir=/opt/apps/zookeeper-3.4.6/zkData
②在最后一行后添加:server.1=linux01:2888:3888
server.2=linux02:2888:3888
server.3=linux03:2888:3888
6.在/opt/appdata/zkdata/ 下新建一个文件 myid 用来存本节点的id
7.在myid 中写入id
echo 1 > /opt/apps/zookeeper-3.4.6/zkData/myid
8.将配置好的zookeeper-3.4.6分发到 linux02 和linux03
scp zookeeper-3.4.6 linux02:$PWD
scp zookeeper-3.4.6 linux03:$PWD (PWD代表复制到当前目录下,所以该操作要在/opt/apps/zookeeper-3.4.6 进行)
9.修改linux02 和linux03中myid文件中的id 分别为 2 3
10. zookeeper没有自带一个批启脚本,只能手动在每一台节点上一个一个地启动每台机器都执行
bin/zkServer.sh start zk服务启动
bin/zkServer.sh status zk 查看服务状态
bin/zkServer.sh stop zk停止服务
11.写脚本在/opt/apps/zookeeper-3.4.6 下,通过脚本实现一键启停集群中节点
①下建一个文件 :touch zk.sh
②在zk.sh中写入脚本内容:
#!/bin/bash
arg=$1
for name in linux01 linux02 linux03
do
ssh $name "source /etc/profile;/opt/apps/zookeeper-3.4.6/bin/zkServer.sh $arg ;exit"
done
③ 一键启停操作
sh zk.sh start 启动
sh zk.sh start 停止
linux上客户端操作:
/opt/apps/zookeeper-3.4.6/bin/zkCli.sh 直接进入本地客户端
/opt/apps/zookeeper-3.4.6/bin/zkCli.sh -server linux02:2181 连接到指定的服务节点
客户端中常用的操作命令:
ls path 查看路径下内容
create -s path创建带序号的节点
create -e path创建临时的节点,关闭在启动后自动消失
get path 查看路径下节点内容
rmr path 删除路径下节点
quit 或 close 退出客户端
Zookeeper的选举机制: 以linux01 linux03 为集群的ZK)
初次启动时:
linux01 会向域网发送组播寻找leader(端口2888)发现集群中没有leader,它就进入选举状态(3888端口),并向局域网组播投票(投自己);
linux02的服务器启动了,它向局域网寻找leader,发现没有,进入投票状态(3888端口),收到服务器1所投的票;然后发现1<自己2,投票(投2)
并向局域网组播选了自己;此时,linux01也会收到2的投票,发现2>自己,重新投(投2);此时,服务器2会收到两票;然后通过配置文件获知集群总共有3台机器,
从而知道自己已经得多数票(当选);服务器2就切换到leader状态(2888);
linux03服务器启动,它向局域网寻找leader,发现服务器2是leader,自己主动进入follower状态;
leader宕机时:
当leader宕机后,集群中没有了leader,全体立即选举机制,依旧是linux01 寻找leader,发现没有后投自己一票,并向局域网组播自己的投票情况。
通过比较配置文件发现自己一票并没有过半,不能当选。
linux03进入选举状态,它也首相在集群中寻找leader,发现没有,进入投票状态投自己并组播。此时linux01也发现了Linux03 投的自己,发现1<3所以linux01 改投linux03;
此时linux03 有两票,通过配置文件比较发现票数过半,自己当选,更改装填为leader。
ZK各个节点的数据如何保证一致 :
Zookeeper采用ZAB(Zookeeper Atomic Broadcast)协议来保证分布式数据一致性:
ZAB协议的核心是定义了对事务请求的处理方式,整个过程可以概括如下:
1.所有的事务请求都交由集群的Leader服务器来处理,Leader服务器会将一个事务请求转换成一个Proposal(提议),并为其生成一个全局递增的唯一ID,这个ID就是事务ID,
即ZXID,Leader服务器对Proposal是按其ZXID的先后顺序来进行排序和处理的。
2.之后Leader服务器会将Proposal放入每个Follower对应的队列中(Leader会为每个Follower分配一个单独的队列),并以FIFO的方式发送给Follower服务器。
3.Follower服务器接收到事务Proposal后,首先以事务日志的方式写入本地磁盘,并且在成功后返回Leader服务器一个ACK响应。
4.Leader服务器只要收到过半Follower的ACK响应,就会广播一个Commit消息给Follower以通知其进行Proposal的提交,同时Leader自身也会完成Proposal的提交。
事件监听机制:Zookeeper中可以通过Watcher来实现事件监听机制。客户端可以向服务端注册Watcher用以监听某些事件,一旦该事件发生,服务端即会向客户端发送一个通知。
事件监听特征
1.当监听器监听的事件被触发,服务端会发送通知给客户端,但通知信息中不包括事件的具体内容。以监听ZNode结点数据变化为例,
当Znode的数据被改变,客户端会收到事件类型为NodeDataChanged的通知,但该Znode的数据改变成了什么客户端无法从通知中获取,
需要客户端在收到通知后手动去获取。
2.Watcher是一次性的。一旦被触发将会失效。如果需要反复进行监听就需要反复进行注册。这么设计是为了减轻服务端的压力,
但是对开发者而言却是相当不友好,不过不用着急,可以通过一些Zookeeper的开源客户端轻松实现对某一事件的永久监听。
ZK保证服务故障的容错
Zookeeper通过事务日志和数据快照来避免因为服务器故障导致的数据丢失。
事务日志是指服务器在更新内存数据前先将事务操作以日志的方式写入磁盘,Leader和Follower服务器都会记录事务日志。
数据快照是指周期性通过深度遍历的方式将内存中的树形结构数据转入外存快照中。但要注意这种快照是"模糊"的,因为可能在做快照时内存数据发生了变化。
但是因为Zookeeper本身对事务操作进行了幂等性保证,故在将快照加载进内存后会通过执行事务日志的方式来讲数据恢复到最新状态。
Zookeeper自己的命令行:
shell连接到ZK的任意一台节点上 例如连接到linux01 :bin/zkCli.sh -server linux01:2181
1.创建节点:create [-s] [-e] path data acl
-s 指定节点特性:顺序性 (如有重名的都会加上序号) 永久有序
-e 指定节点类型:写-e 就是临时节点,下次启动后节点不存在了,什么都不写默认为永久节点 临时无序
-s -e 临时有序
无 永久无序
acl 进行权限控制
例如在跟节点下添加一个子节点aa,内容为123(注意不写内容不能成功创建新节点) create -s /aa 123
2.查看节点:get
例如查跟节点下的aa 节点的内容 get /aa 显示的结果就是123
3.展示所有的节点: ls /
4.更新节点 :例如跟新/aa的内容为234 set /aa 234
5. 删除节点:例如删除 跟下的aa节点 delete /user
6. 递归删除: rmr /aaa
Java API:通过程序获取zk的客户端对象,在Java端实现
添加节点
删除节点
查看子节点
查看节点数据
判断节点是否存在
public class Test1 {
static ZooKeeper zk = null;
static {
try {//创建连接
zk = new ZooKeeper("linux01:2181,linux02:2181,linux03:2181", 2000, null);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取指定节点的数据
* @param args
* @throws Exception
*/
public static void getNodeData() throws KeeperException, InterruptedException {
byte[] data = zk.getData("/a", null, null);
System.out.println(new String(data));
}
/**
* 获取指定路径下的所有的子节点 获取值
* @throws KeeperException
* @throws InterruptedException
*/
public static void getNodesDatas() throws KeeperException, InterruptedException {
// 遍历/ 节点 获取所有的子节点
List<String> list = zk.getChildren("/", null);
//有子节点
if(list!=null && list.size()>0) {
// 获取所有子节点的值
for (String name : list) {
byte[] data = zk.getData("/"+name, null, null);
System.out.println(name+":"+new String(data));
}
}
}
public static void createNode() throws Exception {
/**
* 参数一 节点名称 注意 /开头
* 参数二 节点的数据值
* 参数三 权限
* 参数四 节点的类型 永久节点 临时节点 有序节点 无序节点
* PERSISTENT 默认的永久节点
* PERSISTENT_SEQUENTIAL 永久有序
* EPHEMERAL 临时节点 当客户端连接断开以后 节点会自动删除
* EPHEMERAL_SEQUENTIAL 临时有序的
*/
// 如果创建节点成功 返回当前节点名字
String name = zk.create("/aa",
"hello".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
System.out.println(name);
}
/**
* 更新节点数据
* @throws KeeperException
* @throws InterruptedException
*/
public static void setData() throws KeeperException, InterruptedException {
// 更新数据 如果返回的stat!=null 说明成功
Stat stat = zk.setData("/aa", "hi".getBytes(), -1) ;
}
/**
* 删除空节点
* @throws Exception
*/
public static void deleteEmptyNode() throws Exception {
// 删除节点 删除没有子节点的节点(相当于叶子节点)
zk.delete("/a", -1);
}
/**
* 删除任意节点
* 1 遍历当前要删除的节点
* 2 递归删除
* @throws Exception
*
*/
public static void rmr(String path) throws Exception {
// 判断是否有子节点 /aa/b/c
List<String> list = zk.getChildren(path, null);
if(list!=null && list.size()>0) { //有子节点
for (String child : list) {
//递归
rmr(path+"/"+child);
}
}
//每次遍历到最后一个节点时 删除当前节点
zk.delete(path, -1);
}
public static void main(String[] args) throws Exception {
//getZkConnection();//获取连接
//zk.getNodeData();//获取节点数据
//getNodesDatas() ;//获取所有节点和数据
//zk.create();//创建节点
//zk.setData();//更新节点数据
//zk.deleteEmptyNode();//上传没子节点的节点
zk.rmr();//递归删除所有节点
}
}
Zookeeper的监控
/**
public class GetWatcher {
public static void main(String[] args) throws Exception {
ZooKeeper zk = new ZooKeeper("linux01:2181", 2000, new Watcher() {
@Override
public void process(WatchedEvent event) {
// TODO Auto-generated method stub
System.out.println("连接成功.....");
}
}) ;
zk.close();
}
}
单次监控子节点变化:
public class ChangeChildrenWatcher {
public static void main(String[] args) throws Exception {
// 获取连接对象
ZooKeeper zk = new ZooKeeper("linux01:2181", 2000, null);
// 监控只监控一次
zk.getChildren("/", new Watcher() {
// 当监控的目录 / 的子节点的个数发生变化的时候会被执行
@Override
public void process(WatchedEvent event) {
//输出节点变化的类型,和节点变化的路径 System.out.println(event.getType()+":"+event.getPath());
System.out.println("/ 目录下的子节点发生变化了.....");
}
});
// 阻塞
Thread.sleep(Integer.MAX_VALUE);
zk.close();
}
}
一直监控
/**
*/
public class ChangeChildrenWatcher2 {
public static void main(String[] args) throws Exception {
// 获取连接对象
ZooKeeper zk = new ZooKeeper("linux01:2181", 2000, null);
// 监控只监控一次
List<String> list = zk.getChildren("/", new Watcher() {
// 当监控的目录 / 的子节点的个数发生变化的时候会被执行
@Override
public void process(WatchedEvent event) {
// 节点变化的时候
System.out.println(event.getType() + ":" + event.getPath());
// 再次注册监控
try {
// 注意this 代表的是上面的哪个匿名内部类 process()方法
List<String> children = zk.getChildren("/", this);
for (String string : children) {
System.out.println(string);
}
} catch (KeeperException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
for (String string : list) {
System.out.println(string);
}
// 阻塞
Thread.sleep(Integer.MAX_VALUE);
zk.close();
}
}
监控节点数据发生变化
public class DataChangerWatcher {
public static void main(String[] args) throws Exception {
ZooKeeper zk = new ZooKeeper("linux01:2181", 2000, null);
byte[] data =zk.getData("/aa", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println(event.getType()+"---"+event.getPath());
try {
// 再次注册
byte[] data1 = zk.getData("/aa", this, null);
// 打印新值
System.out.println(new String(data1));
} catch (KeeperException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, null) ;
System.out.println(new String(data));
Thread.sleep(Integer.MAX_VALUE);
zk.close();
}
}
综合案例
动态感知服务器上下线
服务器端
/**
*/
public class Servers {
static ZooKeeper zk = null ;
// 1 获取连接
public static void getConnection(String hostname) throws IOException {
zk = new ZooKeeper("linux01:2181", 2000, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println(hostname+"上线了.......");
}
});
}
// 2 注册节点 (添加节点)
public static void register(String hostname) throws KeeperException, InterruptedException {
// 注册节点
zk.create("/servers/server", hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
}
// 3 对外服务
public static void service(String hostname) throws InterruptedException {
System.out.println(hostname+"正在服务.......");
Thread.sleep(Integer.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
getConnection(args[0]);
register(args[0]) ;
service(args[0]);
}
}
客户端
/**
*/
public class Clients {
static ZooKeeper zk = null;
// 1 获取连接
public static void getConnection() throws IOException {
zk = new ZooKeeper("linux01:2181", 2000, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("发现一个客户.......");
}
});
}
// 2 获取服务列表
public static void getServersList() throws Exception, InterruptedException {
List<String> children = zk.getChildren("/servers", new Watcher() {
// 当servers下面的节点发生变化的时候执行一次
@Override
public void process(WatchedEvent event) {
// 重新遍历servers下面的节点
try {
List<String> children2 = zk.getChildren("/servers", this);
//链表用于存/servers下的子节点
List<String> hosts = new ArrayList<>() ;
for (String string : children2) {
byte[] data = zk.getData("/servers/"+string, null, null);
//
hosts.add(new String(data));
}
System.out.println(hosts);
} catch (KeeperException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
List<String> list = new ArrayList<String>();
for (String string : children) {
byte[] data = zk.getData("/servers/" + string, null, null);
list.add(new String(data));
}
System.out.println(list);
}
// 3 处理业务
public static void service() throws InterruptedException {
System.out.println("服务...........");
Thread.sleep(Integer.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
getConnection();
getServersList();
service();
}
}