一、zookeeper简介
ZooKeeper 是 Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。
ZooKeeper 的架构通过冗余服务实现高可用性。
Zookeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
zookeeper的文件系统:
它的文件系统用来保存少量,服务器相关的配置文件信息。
zookkeeper 提供的名称空间非常类似于标准文件系统,key-value 的形式存储。名称 key 由斜线 / 分割的一系列路径元素,zookeeper 名称空间中的每个节点都是由一个路径标识。
解释说明:
1.zookeeper的存储结构是一个树形结构
2.zookeeper的树形结构是由znode组成的
znode特点:
1.znode是由name-value组成
例:create /a aaa 语法:create name value
2.znode既可以看作是一个文件夹(目录)也可以看作是一个文件(文件名)
2.1当看作是一个文件时
znode的name就是文件名,value就是文件中的内容
2.2当看作是一个目录时
每个znode下面还可以存在子znode
例:create /a/b bbb
注意name必须是以根目录开始即:/...
二.zookeeper用法
ZK就是一个管理多个服务(集群分布式环境下)的通知机制 Watcher+
文件系统`
ZNode 文件系统:保存少量,服务器相关的配置文件信息。
Watcher 监听通知机制:注册监听服务器的上下线。
特点:
1. zk集群中的数据内容会自动同步,所以每个节点上的数据完全一致。
2. zk作为集群管理者,不存在单点故障等问题。
3. zk的主机是动态选举出来的,不是规定死的。
当集群启动的时候,会选举出来一个leader。如果主机挂了,就会重新选举新的leader
4.半数机制
正常运行的集群必须在集群的半数以上。即例如有三台机器,半数是1.5,半数以上就要求最少有2台可以正常工作
如有四台或5台,半数是2和2.5.则要求都是最少有3台可以正常工作
zookeeper解决的问题:
1.HDFS系统中NameNode中存在的单点故障问题
2.Yarn中ReduceManager单点故障问题
在解决单点故障过程中实现原理
以HDFS中的NameNode为例。
引入:
1. 在HDFS系统中,只能存在一个NameNode节点。那么如果这个NameNode进程挂掉了用户在发过来请求就不能在进行后续操作了,这个 现象称为NameNode的单点故障问题
解决方案:
准备一个NameNode备用机器,在主NameNode挂机的时候自动转换到备用机器运行。
流程 :
1.NameNode在启动时候向zookeeper中注册一个临时节点
2. NamoNode备用机启动时也向zookeeper中去注册临时节点,但是在注册的时候如果发现已经有人注册过了,就不再注册,自己老老实实当个备胎,然后监听着该节点什么时候挂掉
3. 如果主机挂机了,或者其他原因一段时间不再向zookeeper发送心跳,那么zookeeper就认为他挂了,并删除它当时注册的临时节点信息
4. 这时候zookeeper就会向正在监听着的备用机发送一个信息说主机挂了
5. 备用机器NameNode就向zookeeper中去注册临时节点,成为新的主机
三、Zookeeper Leader 选举原理
zookeeper 的 leader 选举存在两个阶段,一个是服务器启动时 leader 选举,另一个是运行过程中 leader 服务器宕机。
在分析选举原理前,先介绍几个重要的参数。
1.服务器 ID(myid):编号越大在选举算法中权重越大
2.事务 ID(zxid):值越大说明数据越新,权重越大
3.逻辑时钟(epoch-logicalclock):同一轮投票过程中的逻辑时钟值是相同的,每投完一次值会增加
选举状态:
1.LOOKING: 竞选状态
2.FOLLOWING: 随从状态,同步 leader 状态,参与投票
3.OBSERVING: 观察状态,同步 leader 状态,不参与投票
4.LEADING: 领导者状态
1、服务器启动时的 leader 选举
每个节点启动的时候都会进入 LOOKING 观望状态,接下来就开始进行选举主流程。这里选取三台机器组成的集群为例。第一台服务器 server1启动时,无法进行 leader 选举,当第二台服务器 server2 启动时,两台机器可以相互通信,进入 leader 选举过程。
选举流程:
(1)每台 server 发出一个投票,由于是初始情况,server1 和 server2 都将自己作为 leader 服务器进行投票,每次投票包含所推举的服务器myid、zxid、epoch,使用(myid,zxid)表示,此时 server1 投票为(1,0),server2 投票为(2,0),然后将各自投票发送给集群中其他机器。
(2)接收来自各个服务器的投票。集群中的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自 LOOKING 状态的服务器。
(3)分别处理投票。针对每一次投票,服务器都需要将其他服务器的投票和自己的投票进行对比,对比规则如下:
a. 优先比较 epoch
b. 检查 zxid,zxid 比较大的服务器优先作为 leader
c. 如果 zxid 相同,那么就比较 myid,myid 较大的服务器作为 leader 服务器
(4)统计投票。每次投票后,服务器统计投票信息,判断是都有过半机器接收到相同的投票信息。server1、server2 都统计出集群中有两台机器接受了(2,0)的投票信息,此时已经选出了 server2 为 leader 节点。
(5)改变服务器状态。一旦确定了 leader,每个服务器响应更新自己的状态,如果是 follower,那么就变更为 FOLLOWING,如果是 Leader,变更为 LEADING。此时 server3继续启动,直接加入变更自己为 FOLLOWING。
2、运行过程中的 leader 选举
当集群中 leader 服务器出现宕机或者不可用情况时,整个集群无法对外提供服务,进入新一轮的 leader 选举。
流程:
(1)变更状态。leader 挂后,其他非 Oberver服务器将自身服务器状态变更为 LOOKING。
(2)每个 server 发出一个投票。在运行期间,每个服务器上 zxid 可能不同。
(3)处理投票。规则同启动过程。
(4)统计投票。与启动过程相同。
(5)改变服务器状态。与启动过程相同。
四、Zookeeper 分布式锁实现原理
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
排他锁
排他锁(Exclusive Locks),又被称为写锁或独占锁,如果事务T1对数据对象O1加上排他锁,那么整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能进行读或写。
定义锁:
/exclusive_lock/lock
实现:
利用 zookeeper 的同级节点的唯一性特性,在需要获取排他锁时,
所有的客户端试图通过调用 create() 接口,
在 /exclusive_lock 节点下创建临时子节点 /exclusive_lock/lock,
最终只有一个客户端能创建成功,那么此客户端就获得了分布式锁。
同时,所有没有获取到锁的客户端可以在 /exclusive_lock 节点上注册一个子节点变更的 watcher 监听事件,以便重新争取获得锁。
共享锁
共享锁(Shared Locks),又称读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都释放。
定义锁:
/shared_lock/[hostname]-请求类型W/R-序号
实现:
-
客户端调用 create 方法创建类似定义锁方式的临时顺序节点
-
客户端调用 getChildren 接口来获取所有已创建的子节点列表。
-
判断是否获得锁:
如果得到锁
对于读请求:如果所有比自己小的子节点都是读请求或者自己后面没有节点了,表明已经成功获取共享锁,同时开始执行自己要进行的操作。
对于写请求,如果自己不是序号最小的子节点,那么就进入等待。一直到所有的读操作执行完,写操作前面的都用完了轮到自己的 时候,自己再执行
如果没有获取到共享锁,读请求向比自己序号小的最后一个写请求节点注册 watcher 监听,写请求向比自己序号小的最后一个节点注册watcher 监听。
实际开发过程中,可以 curator 工具包封装的API帮助我们实现分布式锁。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>x.x.x</version>
</dependency>
五、集群环境下部署zookeeper
1.准备三台服务器 (hadoop11/hadoop12/hadoop13)
0. 设置ip
1. 安装jdk
2. 配置java环境变量
3. 关闭防火墙
4. 设置hostname
5. 设置hosts(3台彼此之间集群互通)
2.安装
上传下载包到Linux服务器上
1. 解压: tar zxvf zookeeper-3.4.6.tar.gz -C /opt/installs/
2. 修改文件名:mv zookeeper-3.4.6/ zookeeper3.4.6
3. 编辑环境变量配置文件:vim /etc/profile
# ------------------下面是添加的内容------
export PATH=$PATH:/opt/installs/zookeeper3.4.6/bin/
4. 重新加载profile配置:source /etc/profile
3.标识自己的身份
1. zookeeper目录下新建一个data目录
mkdir data
2. 在data下,新建一个myid文件。
cd data
3. 里面内容填写当前zk节点的编号
echo 11 > myid
4.初始化配置文件
1. 拷贝zoo.cfg文件:cp zoo_sample.cfg zoo.cfg
2. 配置zoo.cfg
配置以下内容:
#Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。
tickTime=2000 #心跳时间周期(单位: 毫秒)
#集群中的Follower跟随者服务器与Leader领导者服务器之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器连接到Leader的时限。
initLimit=10 #启动zk时候的时间延迟最大值(10倍心跳)
#Leader发送心跳包给集群中所有Follower,若Follower在syncLimit时间内没有响应,那么Leader就认为该follower已经挂掉了,单位:tickTime
syncLimit=5 # zk的主机和从机之间的通信访问的最大延迟(5倍心跳)
#设置zookeeper的数据存储位置(这个要改)
dataDir=/opt/installs/zookeeper3.4.6/data/
clientPort=2181 # zk的客户端访问zk的端口号
# server.myid=zookeeper的ip:2888:3888
server.11=hadoop11:2888:3888
server.12=hadoop12:2888:3888
server.13=hadoop13:2888:3888
# 2888(内部数据通信的端口)
#3888(选举投票使用的端口)
5.同步配置文件到另两台机器上
1. 同步zookeeper的软件及其内部配置文件信息
scp -r zookeeper3.4.6 root@hadoop12:/opt/installs/
scp -r zookeeper3.4.6 root@hadoop13:/opt/installs/
2. 同步zookeeper的环境变量文件/etc/profile
scp -r /etc/profile root@hadoop12:/etc
scp -r /etc/profile root@hadoop13:/etc
3. 重新加载其他节点上的zk的环境变量
source /etc/profile
4. 修改其他节点上的myid的zk编号。
在12上的zookeeper安装目录的data 目录下:echo 12 > myid
在13上的zookeeper安装目录的data 目录下:echo 13 > myid
6.zookeeper服务器管理命令
1. 启动:zkServer.sh start
查看进程: jps
2610 QuorumPeerMain # zk节点的进程。
3500 Jps
2. 查看状态: zkServer.sh status
3. 停止: zkServer.sh stop
4. 客户端
zkCli.sh 登录本机的zk
zkCli.sh -server ip:2181 登录指定ip的zk主机
7.查看日志
如果zookeeper启动异常,查看日志文件:zookeeper.out
默认位置: 启动zkServer的命令所在的目录,
可以通过bin/zkEnv.sh修改 ZOO_LOG_DIR = "/opt/installs/zookeeper3.4.6/logs"
六、zookeeper客户端命令
1.操作客户端命令
客户端使用基本命令
1. 进入客户端
zkCli.sh
2. 查看帮助命令
[zk: localhost:2181(CONNECTED) 1] help
3. 退出客户端
[zk: localhost:2181(CONNECTED) 1] quit
2. znode管理命令
3.节点类型
节点类型
zookeeper可以将节点设置不同的类型
1. 持久化节点
节点只要不删除,会一致存在。
2. 顺序编号节点
每次对相同节点,重复创建,会自动对znode名称进行编号
3. 临时节点
客户端断开,则节点消失。
顺序节点的应用场景:分布式锁
线程 synchronized : 两个线程同时访问一个资源(临界资源),出现线程不安全问题
分布式项目( 说白了可以理解为两个项目 ) 需要同时使用一个资源,传统的synchronized无法使用
使用zookeeper的顺序节点可以解决分布式项目的锁问题
4.Watcher监听器命令
1. 监听节点值得修改(set)和删除(delete)变化
2. 监听某个节点及其子节点的增加、删除。
七、使用Java代码操作zookeeper
1、首先导入依赖和日志文件
<!--zk的客户端( curator )-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.7.1</version>
</dependency>
<!--junit测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
2、编写代码测试
public class TestCurator {
//定义一个全局变量,每个类中到要用到
CuratorFramework curator;
@Before
public void test1(){
// 1. 创建一个连接(自动重连)
RetryNTimes retry = new RetryNTimes(10,1000);// 重连10次,每次间隔1000秒
// 2. 创建一个客户端对象。
curator = CuratorFrameworkFactory.newClient("hadoop11:2181,hadoop12:2181,hadoop13:2181", retry);
// 3. 启动客户端
curator.start();
}
@Test
public void test2()throws Exception{
//create /a1 测试创建一个节点
String s = curator.create().withMode(CreateMode.PERSISTENT).forPath("/a1", "测试".getBytes());
System.out.println(s);
curator.close();//记得关闭连接
}
@Test
public void test3()throws Exception{
//get /a1 测试查看节点的值
byte[] bytes = curator.getData().forPath("/a1");
System.out.println(new String(bytes));
curator.close();
}
@Test
public void test4()throws Exception{
//set /a1 测试修改节点的值
curator.setData().forPath("/a1","测试2".getBytes());
curator.close();
}
@Test
public void test5()throws Exception{
//stat /a1
Stat stat = curator.checkExists().forPath("/a1");
System.out.println(stat);
curator.close();
}
@Test
public void test6()throws Exception{
//delete /a1
//.deletingChildrenIfNeeded() rmr /a1
curator.delete().deletingChildrenIfNeeded().forPath("/a1");
curator.close();
}
@Test
public void test7()throws Exception{
//ls /a1 查看某个节点下的所有节点信息,并监听下节点的变化(添加删除子节点)
List<String> strings = curator.getChildren().forPath("/a1");
for (String node : strings) {
//System.out.println(node);
// get /a1/b1
byte[] bytes = curator.getData().forPath("/a1/" + node);
System.out.println("/a1/" + node + " " + new String(bytes));
}
curator.close();
}
@Test
public void test8()throws Exception{
//get /a1 watch
// 1 创建节点监听客户端
final NodeCache nodeCache = new NodeCache(curator, "/a1");
// 2 启动客户端监听器
nodeCache.start();
// 3 为客户端监听器,绑定监听事件函数
nodeCache.getListenable().addListener(new NodeCacheListener() {
/**
* 一旦节点值变化,调用函数
* @throws Exception
*/
public void nodeChanged() throws Exception {
byte[] data = nodeCache.getCurrentData().getData();
System.out.println(new String(data));
}
});
// 5 程序停止,客户端消失,监听也就失效
Thread.sleep(Long.MAX_VALUE);
}
@Test
public void test9()throws Exception{
//ls /a1 watch
// 1 创建子节点监听器客户端
PathChildrenCache childCache = new PathChildrenCache(curator, "/a1", true);
// 2 启动监听器
childCache.start();
// 3 为监听器绑定注册事件函数
childCache.getListenable().addListener(new PathChildrenCacheListener() {
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event)
throws Exception {
System.out.println(event.getType());
switch(event.getType()){
case CHILD_ADDED:
System.out.println("节点添加");
System.out.println(event.getData().getPath()+":"+new String(event.getData().getData()));
break;
case CHILD_UPDATED:
System.out.println("节点更新");
System.out.println(event.getData().getPath()+":"+new String(event.getData().getData()));
break;
case CHILD_REMOVED:
System.out.println("节点删除");
System.out.println(event.getData().getPath()+":"+new String(event.getData().getData()));
break;
default:
System.out.println("其他");
}
}
});
// 5 客户端程序永不停止。
Thread.sleep(Long.MAX_VALUE);
}
}