一、 Zookeeper概述
1、 概述
Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。
在学习zookeeper之前,我们可以想下微信公众号推送文章,微信服务器有两个功能。当公众号作者写文章后,点击发布,这个时候微信服务器做了两件事情,第一件事:将作者的文章保存下来,即实现了文件系统的功能,保存文件。第二件事:给每个订阅了该公众号的微信用户发送通知,而不给那些未订阅的用户推送,即实现了通知功能。
而zookeeper也有这两大功能,即Zookeeper=文件系统(可以在zk上存储数据)+通知机制。我们也可以在zookeeper上存储数据,也可以连接到zookeeper服务器事先订阅的某个数据,然后zookeeper发现数据变化后,会通知客户端。
2、 特点(选举机制)
1)Zookeeper:一个领导者(leader),多个跟随者(follower)组成的集群。
2)Leader负责进行投票的发起和决议,更新系统状态
3)Follower用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票
4)集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。所以一般设置奇数台服务器。
5)全局数据一致:每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的。
6)更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行。
7)数据更新原子性,一次数据更新要么成功,要么失败。
8)实时性,在一定时间范围内,client能读到最新数据。
全新集群,选举机制其实就是集群内部进行投票,只要选出了leader,后期即使加入新的服务器也不会重新选举。
3、 数据结构
ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一个Znode,zookeeper把数据存储在节点上。
很显然zookeeper集群自身维护了一套数据结构。这个存储结构是一个树形结构,其上的每一个节点,我们称之为"znode",每一个znode默认能够存储1MB的数据,每个ZNode都可以通过其路径唯一标识,如图1所示
4、 下载地址
1.官网首页:
https://zookeeper.apache.org/
2.下载截图,如图5,6,7所示
5、角色
二、 Zookeeper安装
1、 分布式安装部署
1.集群规划
在hadoop003、hadoop004和hadoop005三个节点上部署Zookeeper。
在一个节点上配置好,再拷贝分发到其他机器上。
2.解压安装
1)解压zookeeper安装包到/opt/module/目录下
[root@hadoop003 software]$ tar -zxvf zookeeper-3.4.10.tar.gz -C /opt/module/
(2)在/opt/module/zookeeper-3.4.10/这个目录下创建zkData
mkdir -p zkData
(3)重命名/opt/module/zookeeper-3.4.10/conf这个目录下的zoo_sample.cfg为zoo.cfg
cp zoo_sample.cfg zoo.cfg
3.配置zoo.cfg文件
(1)具体配置
dataDir=/opt/module/zookeeper-3.4.10/zkData
增加如下配置
#######################cluster##########################
server.1=hadoop003:2888:3888
server.2=hadoop004:2888:3888
server.3=hadoop005:2888:3888
(2)配置参数解读
server.A=B:C:D。
A是一个数字,表示这个是第几号服务器;
B是这个服务器的ip地址;
C是这个服务器与集群中的Leader服务器交换信息的端口;
D是万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选出一个新的Leader,而这个端口就是用来执行选举时服务器相互通信的端口。
集群模式下配置一个文件myid,这个文件在dataDir目录下,这个文件里面有一个数据就是A的值,Zookeeper启动时读取此文件,拿到里面的数据与zoo.cfg里面的配置信息比较从而判断到底是哪个server。
4.集群操作
(1)在/opt/module/zookeeper-3.4.10/zkData目录下创建一个myid的文件
touch myid
添加myid文件,注意一定要在linux里面创建,在notepad++里面很可能乱码
(2)编辑myid文件
vim myid
在文件中添加与server对应的编号:如1
(3)拷贝配置好的zookeeper分发到其他机器上
scp -r zookeeper-3.4.10/ hadoop004:/opt/module
scp -r zookeeper-3.4.10/ hadoop005:/opt/module
并分别修改myid文件中内容为2、3
(4)分别启动zookeeper
[root@hadoop003 zookeeper-3.4.10]# bin/zkServer.sh start
[root@hadoop004 zookeeper-3.4.10]# bin/zkServer.sh start
[root@hadoop005 zookeeper-3.4.10]# bin/zkServer.sh start
(5)查看状态
jps
[root@hadoop003 zookeeper-3.4.10]# bin/zkServer.sh status
JMX enabled by default
Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg
Mode: follower
[root@hadoop004 zookeeper-3.4.10]# bin/zkServer.sh status
JMX enabled by default
Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg
Mode: leader
[root@hadoop005 zookeeper-3.4.5]# bin/zkServer.sh status
JMX enabled by default
Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg
Mode: follower
2、 配置参数解读
解读zoo.cfg 文件中参数含义
- 1.tickTime:通信心跳数,Zookeeper服务器心跳时间,单位毫秒
Zookeeper使用的基本时间,服务器之间维持心跳的时间间隔,也就是每隔tickTime时间就会发送一个心跳,时间单位为毫秒。 - 2.initLimit:LF(leader、follower)初始通信时限
集群中的follower跟随者服务器(F)与leader领导者服务器(L)之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器连接到Leader的时限。 - 3.syncLimit:LF同步通信时限
集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,
Leader认为Follwer死掉,从服务器列表中删除Follwer。
在运行过程中,Leader负责与ZK集群中所有机器进行通信,例如通过一些心跳检测机制,来检测机器的存活状态。
如果L发出心跳包在syncLimit之后,还没有从F那收到响应,那么就认为这个F已经不在线了。 - 4.dataDir:数据文件目录+数据持久化路径
保存内存数据库快照信息的位置,如果没有其他说明,更新的事务日志也保存到数据库。 - 5.clientPort:客户端连接端口
监听客户端连接的端口
三、 Zookeeper内部原理
1、 节点类型
- 1.Znode有两种类型
短暂(ephemeral):临时的、客户端和服务器端断开连接后,创建的节点自己删除
持久(persistent):永久的,客户端和服务器端断开连接后,创建的节点不删除 - 2.Znode有四种形式的目录节点(默认是persistent )
(1)持久化目录节点(PERSISTENT)
客户端与zookeeper断开连接后,该节点依旧存在
(2)持久化顺序编号目录节点(PERSISTENT_SEQUENTIAL)
客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
(3)临时目录节点(EPHEMERAL)
客户端与zookeeper断开连接后,该节点被删除
(4)临时顺序编号目录节点(EPHEMERAL_SEQUENTIAL)
客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号,如图9所示
- 3.创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
- 4.在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
2、 stat结构体
1)czxid- create 引起这个znode创建的zxid,创建节点的事务的zxid
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。
事务ID是ZooKeeper中所有修改总的次序。每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
2)ctime - create znode被创建的毫秒数(从1970年开始)
3)mzxid - modify znode最后更新的zxid
4)mtime - modify znode最后修改的毫秒数(从1970年开始)
5)pZxid-znode最后更新的子节点zxid
6)cversion - znode子节点变化号,znode子节点修改次数
7)dataversion - znode数据变化号
8)aclVersion - znode访问控制列表的变化号
9)ephemeralOwner- 如果是临时节点,这个是znode拥有者的session id。如果不是临时节点则是0。
10)dataLength- znode的数据长度
11)numChildren - znode子节点数量
3、 监听器原理
- 1.监听原理详解,如图10所示
1)首先要有一个main()线程
2)在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connect),一个负责监听(listener)。
3)通过connect线程将注册的监听事件发送给Zookeeper。
4)在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
5)Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
6)listener线程内部调用了process()方法。 - 2.常见的监听
(1)监听节点数据的变化:
get path [watch]
(2)监听子节点增减的变化
ls path [watch]
4、 写数据流程(client向zookeeper写入数据)
ZooKeeper 的写数据流程主要分为以下几步,如图11所示:
1)比如 Client 向 ZooKeeper 的 Server1 上写数据,发送一个写请求。
2)如果Server1不是Leader,那么Server1 会把接受到的请求进一步转发给Leader,因为每个ZooKeeper的Server里面有一个是Leader。这个Leader 会将写请求广播给各个Server,比如Server1和Server2, 各个Server写成功后就会通知Leader。
3)当Leader收到大多数 Server 数据写成功了,那么就说明数据写成功了。如果这里三个节点的话,只要有两个节点数据写成功了,那么就认为数据写成功了。写成功之后,Leader会告诉Server1数据写成功了。
4)Server1会进一步通知 Client 数据写成功了,这时就认为整个写操作成功。ZooKeeper 整个写数据流程就是这样的。
四、 Zookeeper实战
1、 客户端命令行操作
1.启动客户端
[root@hadoop003 zookeeper-3.4.10]$ bin/zkCli.sh
2.显示所有操作命令
[zk: localhost:2181(CONNECTED) 1] help
3.查看当前znode中所包含的内容
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
4.查看当前节点数据并能看到更新次数等数据
[zk: localhost:2181(CONNECTED) 1] ls2 /
[zookeeper]
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
5.创建普通节点
[zk: localhost:2181(CONNECTED) 2] create /app1 "hello app1"
Created /app1
[zk: localhost:2181(CONNECTED) 4] create /app1/server101 "192.168.1.101"
Created /app1/server101
6.获得节点的值
[zk: localhost:2181(CONNECTED) 6] get /app1
hello app1
cZxid = 0x20000000a
ctime = Mon Jul 17 16:08:35 CST 2017
mZxid = 0x20000000a
mtime = Mon Jul 17 16:08:35 CST 2017
pZxid = 0x20000000b
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 10
numChildren = 1
[zk: localhost:2181(CONNECTED) 8] get /app1/server101
192.168.1.101
cZxid = 0x20000000b
ctime = Mon Jul 17 16:11:04 CST 2017
mZxid = 0x20000000b
mtime = Mon Jul 17 16:11:04 CST 2017
pZxid = 0x20000000b
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 13
numChildren = 0
7.创建短暂节点
[zk: localhost:2181(CONNECTED) 9] create -e /app-emphemeral 8888
(1)在当前客户端是能查看到的
[zk: localhost:2181(CONNECTED) 10] ls /
[app1, app-emphemeral, zookeeper]
(2)退出当前客户端然后再重启客户端
[zk: localhost:2181(CONNECTED) 12] quit
[bigdata@hadoop104 zookeeper-3.4.10]$ bin/zkCli.sh
(3)再次查看根目录下短暂节点已经删除
[zk: localhost:2181(CONNECTED) 0] ls /
[app1, zookeeper]
8.创建带序号的节点
(1)先创建一个普通的根节点app2
[zk: localhost:2181(CONNECTED) 11] create /app2 "app2"
(2)创建带序号的节点
[zk: localhost:2181(CONNECTED) 13] create -s /app2/aa 888
Created /app2/aa0000000000
[zk: localhost:2181(CONNECTED) 14] create -s /app2/bb 888
Created /app2/bb0000000001
[zk: localhost:2181(CONNECTED) 15] create -s /app2/cc 888
Created /app2/cc0000000002
如果原节点下有1个节点,则再排序时从1开始,以此类推。
[zk: localhost:2181(CONNECTED) 16] create -s /app1/aa 888
Created /app1/aa0000000001
9.修改节点数据值
[zk: localhost:2181(CONNECTED) 2] set /app1 999
10.节点的值变化监听
(1)在hadoop005主机上注册监听/app1节点数据变化
[zk: localhost:2181(CONNECTED) 26] get /app1 watch
(2)在hadoop004主机上修改/app1节点的数据
[zk: localhost:2181(CONNECTED) 5] set /app1 777
(3)观察hadoop005主机收到数据变化的监听
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/app1
11.节点的子节点变化监听(路径变化)
(1)在hadoop005主机上注册监听/app1节点的子节点变化
[zk: localhost:2181(CONNECTED) 1] ls /app1 watch
[aa0000000001, server101]
(2)在hadoop004主机/app1节点上创建子节点
[zk: localhost:2181(CONNECTED) 6] create /app1/bb 666
Created /app1/bb
(3)观察hadoop005主机收到子节点变化的监听
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/app1
12.删除节点
[zk: localhost:2181(CONNECTED) 4] delete /app1/bb
13.递归删除节点
[zk: localhost:2181(CONNECTED) 7] rmr /app2
14.查看节点状态
[zk: localhost:2181(CONNECTED) 12] stat /app1
cZxid = 0x20000000a
ctime = Mon Jul 17 16:08:35 CST 2017
mZxid = 0x200000018
mtime = Mon Jul 17 16:54:38 CST 2017
pZxid = 0x20000001c
cversion = 4
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 2
注意:监听只管用一次,要想再次监听,只能再次执行监控命令。
2、 API应用
1) Idea环境搭建
1.创建一个Maven工程
2.添加pom文件
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
</dependency>
</dependencies>
2) Java客户端操作(不带监听)
public class WithoutWatch {
ZooKeeper client=null;
@Before//创建客户端
public void getClient() throws IOException {
client = new ZooKeeper("hadoop003:2181,hadoop004:2181,hadoop005:2181", 2000, null);
}
@Test//创建节点 报NullPointerExceptions=是因为没带监听,监听设为null
//参数一:节点名
//参数二:参数值
//参数三:访问权限,所有客户端都能访问
//参数四:永久节点
public void createNode() throws KeeperException, InterruptedException {
String node = client.create("/idea", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println(node);
}
@Test//获取节点
public void getChildrenNode() throws KeeperException, InterruptedException {
List<String> children = client.getChildren("/",null,null);
for(String c:children){
System.out.println(c);
}
}
@Test//获取节点值
//参数三:获取该节点最新的数据
public void getNodeData() throws KeeperException, InterruptedException {
byte[] data = client.getData("/idea", false, null);
System.out.println(new String(data));
}
@Test//修改节点数据
//参数三:-1,表示修改最新版本号的数据
public void modifyNodeData() throws KeeperException, InterruptedException {
Stat stat = client.setData("/idea", "666".getBytes(), -1);
if(stat==null){
System.out.println("修改失败");
}else{
System.out.println("修改成功");
}
}
@Test//删除节点
public void deleteNode() throws KeeperException, InterruptedException {
client.delete("/idea",-1);
System.out.println("删除成功");
}
}
3) Java客户端操作(带监听)
public class WithWatch {
ZooKeeper client=null;
@Before//创建客户端
public void getClient() throws IOException {
client = new ZooKeeper("hadoop003:2181,hadoop004:2181,hadoop005:2181", 2000, new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println(watchedEvent.getType()+"-"+watchedEvent.getPath());
try {
client.getChildren("/",true,null);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
@Test//获取节点
public void getChildrenNode() throws KeeperException, InterruptedException {
// List<String> children = client.getChildren("/",true,null);
// for(String c:children){
// System.out.println(c);
// }
Thread.sleep(Long.MAX_VALUE);
}
}
3、 监听服务器动态上下线案例
1.需求
某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知到主节点服务器的上下线。
2.需求分析
1)提供时间查询服务,客户端系统调用该时间查询服务。
2)动态上线,下线该时间查询服务的节点,让客户端实时感知服务器列表变化,查询时候访问最新的机器节点。
3)具体实现:
先在集群上创建/servers节点
[zk: localhost:2181(CONNECTED) 10] create /servers "servers"
Created /servers
public class server {
public static void main(String[] args) throws Exception {
//开启zookeeper客户端
ZooKeeper client = new ZooKeeper("hadoop003:2181,hadoop004:2181,hadoop005:2181", 2000, null);
//向zookeeper创建/servers的子节点,//127.0.0.1 6666 创建临时有序节点,可以监控服务器上线下线
String node = client.create("/servers/server", (args[0] + ":" + args[1]).getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("服务器上线了,地址为:"+args[0]+":"+args[1]+"---"+node);
//开启授时服务,发送时间给客户端
ServerSocket serverSocket = new ServerSocket(Integer.parseInt(args[1]));//端口号
while (true){
Socket accept = serverSocket.accept();//程序卡在此处监听,有请求才往下执行
OutputStream outputStream = accept.getOutputStream();
outputStream.write(new Date().toString().getBytes());//将时间写回给客户端
System.out.println("本次服务结束");
}
}
}
public class client {
List<String> children=null;
ZooKeeper client=null;
List<String> serverList=null;
public static void main(String[] args) throws Exception {
com.bigdata.test.client c= new client();
//开启zookeeper客户端 开启监听,每当servers下的节点数发生变化,以为着服务器上线下线,需要重新获取节点列表,更新在线的服务器列表
c.getClient();
//获取/servers子节点,监听节点
c.getNodeList();
//向服务器发送请求,获取时间
c.getTime();//每隔5秒就发送请求,获取一次时间
}
public void getClient() throws Exception {
client = new ZooKeeper("hadoop003:2181,hadoop004:2181,hadoop005:2181", 2000, new Watcher() {
public void process(WatchedEvent watchedEvent) {
try {
getNodeList();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
public void getNodeList() throws KeeperException, InterruptedException {
List<String>idAndPost=new ArrayList<String>();
children = client.getChildren("/servers", true, null);
for(String node:children){
byte[] data = client.getData("/servers/" + node, false, null);
idAndPost.add(new String(data));
}
serverList=idAndPost;//存储每个节点的ip和port,相当于是服务器地址列表
}
public void getTime() throws IOException, InterruptedException {
while (true){
Random random = new Random();
int i = random.nextInt(serverList.size());
//127.0.0.1 6666
String ipPort = serverList.get(i);
String ip =ipPort.split(":")[0];
String port = ipPort.split(":")[1];
//发送请求
Socket socket = new Socket(ip,Integer.parseInt(port));
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello".getBytes());
byte[] b=new byte[1024];
InputStream inputStream = socket.getInputStream();
inputStream.read(b);
String time = new String(b);
System.out.println("访问"+ipPort+"获取到的时间是:"+time);
outputStream.close();
inputStream.close();
socket.close();
//每个5秒发送一次请求,获取一次时间
Thread.sleep(5000);
}
}
}