Day42_Zookeeper

一、Zookeeper概述

(一)为什么学习zk

我们为什么要学习zookeeper呢?其实从字面的意思我们就可以揣测到zookeeper就是动物园管理员的意思,如果把大数据的一些技术栈、框架比作各种动物,管理员应该就是处于协调、处理这些动物的角色,其实我们经常会发现共享的资源在并发的情况下会出现竞争,在线程间可以使用Java提供的锁机制来协调这些资源,那么在分布式的环境下,如何来协调这些资源呢?

1、分布式环境下无法保证顺序

在单机环境如果想让A先执行,B后执行,先调用A后调用B就可以了;由于网络是不可靠的,分布式环境中则不同

2、分布式环境下无法明确执行结果

单机中调用A成功和失败很明确;而在分布式环境中,即使调用A执行成功了,而在网络传输中超时了,此时无法判断A是否执行成功了,需要通过重试的方式才能判断A有没有执行成功。

,在网络延迟的情况下A可能比B执行要晚。

3、分布式环境下无法保证数据一致性

分布式环境如果很多台服务器提供相同的服务,如何保证服务的某一个改动要么同时生效,要么失败,是分布式和单机环境的最重要的区别。

(二)zk的概述

Zookeeper 是一个分布式的开源框架。 主要用来解决分布式集群中应用系统的一致性问题,即为分布式应用提供协调服务。

在学习zookeeper之前,我们可以想下微信公众号推送文章,微信服务器有两个功能。当公众号作者写文章后,点击发布,这个时候微信服务器做了两件事情,第一件事:将作者的文章保存下来,即实现了文件系统的功能,保存文件。第二件事:给每个订阅了该公众号的微信用户发送通知,而不给那些未订阅的用户推送,即实现了通知功能。

而zookeeper也有这两大功能,即Zookeeper=文件系统(可以在zk上存储数据)+通知机制。我们也可以在zookeeper上存储数据,也可以连接到zookeeper服务器事先订阅的某个数据,然后zookeeper发现数据变化后,会通知客户端。

ZooKeeper 本质上是一个含有监听、通知机制的分布式的小文件存储系统。 提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。

(三)zk的特点

1、主从架构

一个领导者(Leader),多个跟随者(Follower),【多个观察者(Observer)】

2、容错性

集群中只要有半数以上节点正常就可以保证正常对外提供服务 搭建奇数台

3、全局数据一致性

每个server都保存一份相同的数据副本,client无论链接那个server,数据都是一致的

4、顺序性

更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次进行

5、原子性

数据更新时,要么全部成功,要么全部失败

6、实时性

在一定的时间范围内,Client能读取到最新的数据

(四)数据结构

ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一个Znodezookeeper把数据存储在节点上,每个节点最多存储1M数据。

 很显然zookeeper集群自身维护了一套数据结构。这个存储结构是一个树形结构,其上的每一个节点,我们称之为"znode",每个ZNode都可以通过其路径唯一标识,如图所示

(五)zk的核心架构

1、架构图

2、角色职责介绍

(1)Leader

一个Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及Observer间的心跳。所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。

(2)Follower

一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳

Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。

(3)Observer

角色与Follower类似,但是无投票权。

角色

功能描述

领导者(Leader)

  1. Leader是集群工作的核心,是集群内部各个服务器的调度者
  2. Leader负责集群内部投票选举
  3. 处理事务性(写)请求
  4. 参与集群投票

学习者(Learner)

跟随者(Follower)

  1. Follower用于接收客户端请求,并向客户端返回结果
  2. 处理客户端非事务(读)请求
  3. 转发事务(写)请求给Leader
  4. 参与集群投票

观察者(Observer)

1. Follower用于接收客户端请求,并向客户端返回结果

2. 处理客户端非事务(读)请求

3. 转发事务(写)请求给Leader

4. 不参与集群投票

客户端(Client)

请求发起方

备注:

事务性请求:更新操作、新增操作、删除操作,因为这些操作是会影响数据的,所以要保证这些操作在整个集群内的事务性,所以这些操作就是事务性请求。

非事务性请求:像查询操作、exist操作这些不影响数据的操作,就不需要集群来保持事务性,所以这些操场就是非事务性请求。

二、ZooKeeper安装和部署

(一)安装前准备

检查ssh免密登录实现与否

[offcn@bd-offcn-01 ~]$ ssh bd-offcn-02

检查jdk的安装是否成功

[offcn@bd-offcn-01 ~]$ java -version

java version "1.8.0_144"

Java(TM) SE Runtime Environment (build 1.8.0_144-b01)

Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

检查时钟是否同步

三台机器统一执行date命令

(二)安装配置

1、下载地址:

https://zookeeper.apache.org/

https://zookeeper.apache.org/releases.html#download

https://archive.apache.org/dist/zookeeper/

2、上传解压重命名:

cd /home/offcn/software/

tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /home/offcn/apps

cd /home/offcn/apps/

mv apache-zookeeper-3.5.7-bin zookeeper-3.5.7

3、创建zk数据存储目录:

cd /home/offcn/data/

mkdir zookeeper-3.5.7

4、创建myid

cd /home/offcn/data/zookeeper-3.5.7

echo 1 > myid

5、重命名配置文件:

cd /home/offcn/apps/zookeeper-3.5.7/conf/

mv zoo_sample.cfg zoo.cfg

6、修改核心配置文件

vim zoo.cfg

(1)指定数据存储目录

# the directory where the snapshot is stored.

# do not use /tmp for storage, /tmp here is just

# example sakes.

dataDir=/home/offcn/data/zookeeper-3.5.7/

(2)添加集群信息(节点、监听端口、选举端口)

server.1=bd-offcn-01:2888:3888

server.2=bd-offcn-02:2888:3888

server.3=bd-offcn-03:2888:3888

zk端口说明:

2181:对client端提供服务

2888:集群内机器通讯使用(Leader监听此端口)

3888:选举leader使用

(4)修改zk日志输出目录

cd /home/offcn/apps/zookeeper-3.5.7/bin

大约70行处

vim zkEnv.sh



if [ "x${ZOO_LOG_DIR}" = "x" ]

then

    # ZOO_LOG_DIR="$ZOOKEEPER_PREFIX/logs"

    ZOO_LOG_DIR="$HOME/logs/zookeeper-3.5.7/"

fi

(5)分发安装包以及修改myid

cd /home/offcn/apps/

scp -r zookeeper-3.5.7/ bd-offcn-02:$PWD

scp -r zookeeper-3.5.7/ bd-offcn-03:$PWD



cd /home/offcn/data/

scp -r zookeeper-3.5.7/ bd-offcn-02:$PWD

scp -r zookeeper-3.5.7/ bd-offcn-03:$PWD



注意:修改bd-offcn-02、bd-offcn-03的myid分别为2、3

(6)修改环境变量

sudo vim /etc/profile



#zookeeper-3.5.7

export ZOOKEEPER_HOME=/home/offcn/apps/zookeeper-3.5.7

export PATH=$PATH:$ZOOKEEPER_HOME/bin



sudo scp /etc/profile  bd-offcn-02:/etc

sudo scp /etc/profile  bd-offcn-03:/etc

三台机器全部执行

source /etc/profile



(三)服务启停

1、服务端启停

zkServer.sh start

zkServer.sh stop

zkServer.sh status

2、客户端启停

zkCli.sh

3、一键启停脚本

cd /home/offcn

mkdir bin

vim zk.sh

chmod 777 zk.sh

-e 表示将双引号中的特殊字符进行解释,\e是控制输出字符的颜色,后边紧跟着颜色

#!/bin/bash

echo -e "\e[1;35m Zookeeper $1 ....  \e[0m"

export ScriptsPath=`pwd`

for host in bd-offcn-01 bd-offcn-02 bd-offcn-03

do

    if [ $1 = status ]

    then

            ssh $host "source /etc/profile;zkServer.sh $1"

    elif [ $1 = restart ]

    then

            sh $ScriptsPath/$0 stop;sleep 3;sh $ScriptsPath/$0 start;break

    else

            ssh $host "source /etc/profile;nohup zkServer.sh $1>/dev/null 2>&1 &"

    fi

done



三、客户端命令实操

(一)shell客户端操作

命令

说明

参数

create [-s] [-e] path data [acl]

创建节点

PS:zookeeper中,节点的路径只有完整路径的概念,没有相对路径的概念,并且 不能出现同名同路径的节点

-s:指定节点特性:序列化(加编号);

-e:指定节点特性:临时节点。若两个都不指定,则表示创建的一个 非序列化的持久节点"

path:节点的 完整路径。

data:该节点绑定的数据。

acl:权限控制

ls [-s] [-w] [-R] path

列出该路径节点下的所有子节点(只能获取第一级的子节点列表)

-s:指定:查询该路径节点时,除了子节点列表之外,还需要返回该节点的部分属性信息;

-w:指定:查询该路径节点下的所有子节点,同时监听该节点,一旦该节点的子节点发生CUD(增删改)操作,立即推送消息给客户端;

-R:指定:返回该路径节点下的所有的子节点列表

get [-s] [-w] path

获取该路径节点的数据内容(不会返回子节点列表)

-s:指定:返回该节点的数据以及属性信息;

-w:指定:当该节点数据/属性发生改变,立即推送消息给客户端。

ls2 path [watch]

该查询方式与ls -s path类似,不过ZK不推荐使用该查询方式)列出该路径节点下的所有子节点(只能获取第一级的子节点列表)以及部分属性信息

watch:指定:查询该路径节点下的所有子节点,同时监听该节点,一旦该节点的子节点发生CUD(增删改)操作,立即推送消息给客户端

set [-s] [-v version] path data

更新节点数据

data:更新的内容;

-s:指定,当修改成功之后,同时返回该节点的属性。

-v version 表示数据版本( version值要与当前节点的 最新dataVersion 一致,否则会报错 )

delete [-v version] path

删除指定路径节点

如果要删除的节点存在子节点,那么必须先删除子节点,然后才能删除该节点

deleteall path

删除该路径下的所有节点

递归删除节点

setquota -n|-b val path

对节点添加限制

-n:表示该节点下,其子节点的最大个数(该节点本身也要算进去);

-b:表示该节点的空间大小(byte)

val:指定的值

listquota path

列出指定节点的quota

delquota [-n|-b] path

删除目标节点的限制

history

列出历史命令

1、列出根节点下所有子节点

[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]

2、创建节点

[zk: localhost:2181(CONNECTED) 2] create /shell
Created /shell
[zk: localhost:2181(CONNECTED) 3] ls /
[shell, zookeeper]

3、获取节点数据和属性

[zk: localhost:2181(CONNECTED) 9] create /shell/test01 "haha"
Created /shell/test01
[zk: localhost:2181(CONNECTED) 11] ls /shell 
[test01]
[zk: localhost:2181(CONNECTED) 10] get /shell
null

4、获取节点所有子节点以及自身属性

[zk: localhost:2181(CONNECTED) 12] ls2 /shell 
'ls2' has been deprecated. Please use 'ls [-s] path' instead.
[test01]
cZxid = 0x500000002
ctime = Sat Mar 20 13:02:57 CST 2021
mZxid = 0x500000002
mtime = Sat Mar 20 13:02:57 CST 2021
pZxid = 0x500000003
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1

5、限制节点子节点个数

[zk: localhost:2181(CONNECTED) 19] create /test02
Created /test02
[zk: localhost:2181(CONNECTED) 20] listquota /test02 
absolute path is /zookeeper/quota/test02/zookeeper_limits
quota for /test02 does not exist.  
[zk: localhost:2181(CONNECTED) 21] setquota -n 2 /test02 
[zk: localhost:2181(CONNECTED) 47] get /zookeeper/quota/test02/zookeeper_limits 
count=2,bytes=-1
[zk: localhost:2181(CONNECTED) 22] create /test02/1
Created /test02/1
[zk: localhost:2181(CONNECTED) 23] create /test02/2
Created /test02/2

查看日志发现:
WARN[CommitProcWorkThread-3:DataTree@340] - Quota exceeded: /test02 count=3 limit=2
只是警告,并无阻停

6、更新节点数据

[zk: localhost:2181(CONNECTED) 0] create /test04 "hadoop"
Created /test04
[zk: localhost:2181(CONNECTED) 1] set /test04 "zookeeper"
[zk: localhost:2181(CONNECTED) 2] get /test04
zookeeper

7、删除空节点

[zk: localhost:2181(CONNECTED) 9] create /test05 aaa
Created /test05 
[zk: localhost:2181(CONNECTED) 10] delete /test05
[zk: localhost:2181(CONNECTED) 11] ls /
[test01,test02,test03, test04, zookeeper]

8、删除节点(非空)

[zk: localhost:2181(CONNECTED) 12] create /test05
Created /test05
[zk: localhost:2181(CONNECTED) 13] create /test05/a
Created /test05/a
[zk: localhost:2181(CONNECTED) 14] delete /test05
Node not empty: /test05
[zk: localhost:2181(CONNECTED) 15] rmr /test05
The command 'rmr' has been deprecated. Please use 'deleteall' instead.
[zk: localhost:2181(CONNECTED) 11] ls /
[test01,test02,test03, test04, zookeeper]
[zk: localhost:2181(CONNECTED) 19] create /test05
Created /test05
[zk: localhost:2181(CONNECTED) 20] create /test05/a
Created /test05/a
[zk: localhost:2181(CONNECTED) 21] deleteall /test05
[zk: localhost:2181(CONNECTED) 11] ls /
[test01,test02,test03, test04, zookeeper]

(二)javaApi

1、创建工程

 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>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.5.7</version>
    </dependency>
</dependencies>

log4j.properties
# 控制台输出配置
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %p [%c] - %m%n
# 指定日志的输出级别与输出端
log4j.rootLogger=DEBUG,Console

3、核心代码操作


package com.bigdata.myzk;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.junit.Before;
import org.junit.Test;

import java.util.List;

public class MyzkTest {
    ZooKeeper client = null;

    @Before // 获取与zookeeper通信的客户端
    public void testGetClient() throws Exception {
        client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, null);
        System.out.println(client);
    }

    @Test // 获取子节点
    public void testGetChildren() throws Exception {
        List<String> children = client.getChildren("/movie", false);
        for (String child : children) {
            System.out.println(child);
        }
    }

    @Test // 创建节点
    public void testCreateNode() throws Exception {
        String path = client.create("/story", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        System.out.println(path);
    }

    @Test // 获取数据
    public void testGetData() throws Exception {
        // 参数1:要获取数据的节点路径
        // 参数2:是否要监听
        // 参数3:stat结构体 null表示获取最新版本的数据
        byte[] data = client.getData("/story", false, null);
        String s = new String(data);
        System.out.println(s);

    }

    @Test // 修改数据
    public void testSetData() throws Exception {
        Stat stat = client.setData("/story", "world".getBytes(), -1);
        System.out.println(stat);
    }

    @Test // 判断节点是否存在
    public void testJudgeNode() throws Exception {
        Stat exists = client.exists("/story", false);
        if(exists == null){
            System.out.println("节点不存在");
        }else{
            System.out.println("节点存在");
        }
    }

    @Test
    public void testDeleteNode() throws Exception {
        client.delete("/story",-1);
    }
}







报错:
org.apache.zookeeper.KeeperException$ConnectionLossException: KeeperErrorCode = ConnectionLoss for /
1. :zookeeper所在节点或集群的防火墙未关闭。这样会导致Linux为开放zookeeper的客户端连接的端口而无法连接
2. :使用zookeeper原生的客户端API连接时,设置的sessionTimout时间太短,这个时间个人认为必须要大于zookeeper配置文件中的一个心跳的时间,如果小于一个心跳的时间,zookeeper给客户端发送心跳的时候客户端还没有收到就已经超时了,永远也不会连上;所以如果zookeeper的一个心跳时间是2000ms,那么至少客户端的sessTimout时间是3000ms
3. :网络不稳定,这种情况就是客户端和zookeeper之间网络连接不稳定的情况下也会导致这个问题;在客户端和zookeeper的服务器上互相ping一下看看是否有网络丢包率的存在
4. :如果以上三种情况都不是,那么就要检查zookeeper集群有没有在正常运行,如果是可以运行的,那么检查一下客户端连接的url是否有问题,集群的话必须保证每个地址都是对的,不然也是无法连接的

(三)Znode节点

1、特点

文件系统的核心是 Znode

如果想要选取一个 Znode , 需要使用路径的形式, 例如 /test1/test11

Znode 本身并不是文件, 也不是文件夹, Znode 因为具有一个类似于 Name 的路径, 所以可以从逻辑上实现一个树状文件系统

ZK 保证 Znode 访问的原子性, 不会出现部分 ZK 节点更新成功, 部分 ZK 节点更新失败的问题

Znode 中数据是有大小限制的, 最大只能为 1M

2、组成

Znode 是由三个部分构成

•   stat : 状态, Znode的权限信息, 版本等

•   data : 数据, 每个Znode都是可以携带数据的, 无论是否有子节点

•   children : 子节点列表

3、类型

每个 Znode 有两大特性, 可以构成四种不同类型的 Znode

持久性

持久:客户端断开时, 不会删除持有的Znode

临时 :客户端断开时, 删除所有持有的Znode, 临时Znode不允许有子Znode

顺序性

有序 :创建的Znode有先后顺序, 顺序就是在后面追加一个序列号, 序列号是由父节点管理的自增

无序 : 创建的Znode没有先后顺序

zookeeper把数据存储到节点上
节点的类型:两大类型(四小类)
永久性(持久性)节点:客户端连接到zookeeper集群后,创建的是永久性节点的话,那么客户端在退出zookeeper集群后,创建的节点还在
分为两类:
永久普通的:create /movie 111
永久带序号的:create -s /music 222 serial 
短暂性(临时性)节点:客户端连接到zookeeper集群后,创建的是短暂性节点的话,那么客户端在退出zookeeper集群后,创建的节点就没了
短暂普通的:create -e /art 333   ephemeral
短暂带序号的;create -e  -s /sci 444

列出某个节点的子节点
ls /
通过ephemeralOwner的值可以判断节点的类型,如果值为0x0,则该节点是永久的,否则是临时的
ls -s /zookeeper

查看节点数据
get /moive
修改节点数据
set /movie 222 
删除节点
delete只能删除没有子节点的节点
delete /music0000000001
rmr 也能删除带有节点的节点
rmr /movie
查看状态
stat /music0000000002



监听的类型:
子节点的增减
ls /movie watch
ls -w /movie
数据监听
get /movie watch
节点是否存在监听

4、属性

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子节点数量

四、zk的监听/通知机制

通知类似于数据库中的触发器, 对某个Znode设置 Watcher , 当Znode发生变化的时候,WatchManager 会调用对应的 Watcher

当Znode发生删除, 修改, 创建, 子节点修改的时候, 对应的 Watcher 会得到通知

(一)Watcher 的特点

Zk的回调函数分两种,一种是连接/断开连接相关的系统回调,一种是基于节点事件触发的回调,系统回调不需要注册,内部实现,在创建、断开连接时进行触发。而节点事件触发回调需要自己注册监听,在节点的创建、删除、数据的修改事件进行触发,

一次性触发:一个 Watcher 只会被触发一次, 如果需要继续监听, 则需要再次添加Watcher

事件封装: Watcher 得到的事件是被封装过的, 包括三个内容 keeperState,eventType, path

(二)实现监听

1、shell客户端模拟监听

(1)监听节点值变化

bd-offcn-01创建普通节点
[zk: localhost:2181(CONNECTED) 26] create /app1 "app1"

监听节点值变化
[zk: localhost:2181(CONNECTED) 27] get /app1 watch
在bd-offcn-02服务器上面开启zk连接客户端,更改app1节点的值
[root@bd-offcn-02 ~]#zkCli.sh
进入shell客户端更新app1节点的值
[zk: localhost:2181(CONNECTED) 0] set /app1 "hello"
查看bd-offcn-01节点的反应

(2)子节点变化监听

bd-offcn-01服务器监听子节点变化

[zk: localhost:2181(CONNECTED) 30] ls /app1 watch

bd-offcn-02服务器给/app3节点增加或减少子节点



[zk: localhost:2181(CONNECTED) 3] create /app1/hello "addnode"

Created /app1/hello



bd-offcn-01服务器查看响应状态

2、javaApi模拟监听

(1)系统回调


    @Before
    public void testGetClient() throws Exception {
        ZooKeeper client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, new Watcher() {
            // 回调方法,用于zookeeper集群调用
            // 系统默认回调:建立与集群的连接,关闭与集群的连接,会调用此方法
            public void process(WatchedEvent event) {
                System.out.println(event.getPath() + "-" + event.getState() + "-" + event.getType());
            }
        });
        System.out.println("获取到了客户端");

        client.close();
    }


(2)自定义回调


   @Test
    public void testEventWatch() throws Exception{
        ZooKeeper zookeeper = new ZooKeeper(
                "bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181",
                5000,
                new Watcher() {
                    //回调函数
                    @Override // 此处是默认的回调函数,什么时候会被回调呢?
                    // 1 刚刚获取客户端的时候
                    // 2 关闭客户端的时候
                    // 3 自己注册监听,如果没有提供监听,即没有重写process方法,也会调用默认的procees
                    public void process(WatchedEvent event) {
                        System.out.println("事件监听");
                    }
                }
        );

        //注册监听 监听子节点的增减  如果此处自定义了Watcher,那么在回调的时候,就会回调此处重写的process
        //  如果在此处只给了一个true,在回调的时候,就会回调获取客户端时候重写的那个process
        List<String> list = zookeeper.getChildren("/",new Watcher() {
            //回调函数
            @Override
            public void process(WatchedEvent event) {
                System.out.println("hahaha");
            }
        } );

         Thread.sleep(Long.MAX_VALUE);
//        zookeeper.close();
    }

(3)默认回调

/* ************************************************************************
 * 功能描述:默认回调
 *          对节点注册监听时,没有自定义回调函数时间执行
 */
@Test
public void testDefault() throws Exception{
    ZooKeeper zookeeper = new ZooKeeper(
            "bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181",
            5000,
            new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    System.out.println("默认系统回调");
                }
            }
    );

    //注册监听  客户端执行 set /shell/test01 "hello"
    zookeeper.getData("/shell/test01",true,null);

    Thread.sleep(5000);
    zookeeper.close();
}

(4)永久监听

/* ************************************************************************
 * 功能描述:永久监听
 *          在系统默认监听时继续 注册监听
 */
ZooKeeper client=null;
@Test
public void testForEver() throws Exception{
    client = new ZooKeeper(
            "bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181",
            5000,
            new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    System.out.println("默认系统回调");
                    try {
                        client.getData("/shell/test01",true,null);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
    );

    Thread.sleep(Long.MAX_VALUE);
    client.close();
}
package com.bigdata.myzk;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.junit.Before;
import org.junit.Test;

public class MzkWithWatcher {

    ZooKeeper client = null;

    @Before
    public void testGetClient() throws Exception {
        client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, new Watcher() {
            // 回调方法,用于zookeeper集群调用
            // 系统默认回调:建立与集群的连接,关闭与集群的连接,会调用此方法
            public void process(WatchedEvent event) {
                System.out.println(event.getPath() + "-" + event.getState() + "-" + event.getType());
                try {
                    // client.getChildren("/movie", true);
                    client.getData("/movie", true, null);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("获取到了客户端");

    }

    @Test
    public void testGustomCallback() throws Exception {

//        List<String> children = client.getChildren("/movie", true);
//        for (String child : children) {
//            System.out.println(child);
//        }
        byte[] data = client.getData("/movie", true, null);
        System.out.println(new String(data));

        // client.close();

        Thread.sleep(Long.MAX_VALUE);
    }

五、zk的可视化操作

(一)ZooInspector的使用

解压文件,运行jar包,测试连接

 (二)ZKUI的使用

1、解压zip文件、编译文件

cmd控制台切换到项目文件夹根部录下,使用mvn clean install,执行前需要安装java环境,maven环境,

执行成功后会生成一个jar文件,这个jar包在项目根目录文件夹的target文件夹里。如图

 2、修改复制config.cfg

将config.cfg复制到上一步生成的jar文件所在目录,然后修改配置文件中的zookeeper地址。

这个配置文件在项目文件夹根目录下,修改后将它复制到target就可以了。

3、执行jar

windows环境下,cmd控制台切到target文件夹下,然后执行

4、访问登录

六、监听服务器动态上下线案例

1.需求

某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知到主节点服务器的上下线。

2.需求分析

1)提供时间查询服务,客户端系统调用该时间查询服务。

2)动态上线,下线该时间查询服务的节点,让客户端实时感知服务器列表变化,查询时候访问最新的机器节点。

3)具体实现:

先在集群上创建/servers节点

[zk: localhost:2181(CONNECTED) 10] create /servers "servers"

Created /servers

时间服务器:


package com.bigdata.anli;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;

public class TimeServer {
    ZooKeeper client = null;

    public static void main(String[] args) throws Exception {
        TimeServer timeServer = new TimeServer();
        // 1 获取zookeeper的客户端
        timeServer.getClient();
        // 2 把时间服务器的地址和端口号写入到zookeeper集群  /servers/server
        timeServer.registServerInfo(args[0],Integer.parseInt(args[1]));
        // 3 提供授时服务
        new TimeService(Integer.parseInt(args[1])).start();

    }

    public void getClient() throws Exception {
        client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, null);
    }
    public void registServerInfo(String hostname,int port) throws Exception {
        String path = client.create("/servers/server", (hostname + ":" + port).getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

        // /servers/server0000001  localhost:6666
        System.out.println("时间服务器上线了,主机和端口号是:"+hostname+port+",创建的节点是:"+path);
    }

}


时间服务线程:


package com.bigdata.anli;

import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

public class TimeService extends Thread{

    private int port;

    public TimeService(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        System.out.println("开始提供授时服务了");
        // 监听本地的端口
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            while(true){
                // 该accept是个阻塞的方法,只有Socket过来建立连接,该行才会继续往下执行
                Socket accept = serverSocket.accept();
                OutputStream out = accept.getOutputStream();
                out.write(new Date().toString().getBytes());
                out.flush();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

时间客户端:


package com.bigdata.anli;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class TimeClient {

    ZooKeeper client = null;
    //  localhost:6666
    //  localhost:7777
    List<String> serverList = null;

    public static void main(String[] args) throws Exception {
        TimeClient timeClient = new TimeClient();
        //  1 获取zookeeper集群的客户端
        timeClient.getClient();
        // 2 获取zookeeper上时间服务器的地址  /servers
        timeClient.getServerList();
        // 3 访问某个时间服务器,获取时间
        timeClient.getTime();

    }

    public void getClient() throws Exception {
        client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, new Watcher() {
            public void process(WatchedEvent event) {
                System.out.println("执行回调方法了");
                try {
                    // 当zookeeper回调的时候,意味着有新的时间服务器上线了,或者宕机了,
                    // 要重新获取时间服务器列表,重新注册监听
                    getServerList();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }});
    }
    public void getServerList() throws KeeperException, InterruptedException {
        //创建集合存放时间服务器的地址
        List<String> list = new ArrayList();
        List<String> children = client.getChildren("/servers", true);
        for (String child : children) {
            // child server0000001
            byte[] data = client.getData("/servers/" + child, false, null);
            String hostAndPort = new String(data);
            list.add(hostAndPort);
        }
        serverList = list;
        System.out.println("获取到时间服务器地址:"+list);
    }

    public void  getTime() throws Exception {
        Random random = new Random();
        while(true){
            int i = random.nextInt(serverList.size());
            // localhost:6666
            String hostAndPort = serverList.get(i);
            String host = hostAndPort.split(":")[0];
            String port = hostAndPort.split(":")[1];
            System.out.println("访问主机:"+host+port);
            Socket socket = new Socket(host, Integer.parseInt(port));
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("hello".getBytes());
            outputStream.flush();
            InputStream inputStream = socket.getInputStream();
            byte[] bytes = new byte[1024];
            inputStream.read(bytes);
            String time = new String(bytes);
            System.out.println(time);
            inputStream.close();
            outputStream.close();
            socket.close();

            Thread.sleep(5000);

        }

    }

}

七、zk的选举机制

Leader选举是保证分布式[数据一致性]的关键所在。当Zookeeper集群中的一台服务器出现以下两种情况之一时,需要进入Leader选举。

1、全新集群选主

  以一个简单的例子来说明整个选举的过程:假设有五台服务器组成的 zookeeper 集群,它们 的 serverid 从 1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点 上,都是一样的。假设这些服务器依序启动,来看看会发生什么

(1)服务器 1 启动,此时只有它一台服务器启动了,它发出去的报没有任何响应,所以它的 选举状态一直是 LOOKING 状态

(2)服务器 2 启动,它与最开始启动的服务器 1 进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以 id 值较大的服务器 2 胜出,但是由于没有达到超过半数以上的服务器都同意选举它(这个例子中的半数以上是 3),所以服务器 1、2 还是继续保持 LOOKING 状态

(3)服务器 3 启动,根据前面的理论分析,服务器 3 成为服务器 1,2,3 中的老大,而与上面不 同的是,此时有三台服务器(超过半数)选举了它,所以它成为了这次选举的 leader

(4)服务器 4 启动,根据前面的分析,理论上服务器 4 应该是服务器 1,2,3,4 中最大的,但是 由于前面已经有半数以上的服务器选举了服务器 3,所以它只能接收当小弟的命了

(5)服务器 5 启动,同 4 一样,当小弟

2、非全新集群选主

那么,初始化的时候,是按照上述的说明进行选举的,但是当 zookeeper 运行了一段时间之 后,有机器 down 掉,重新选举时,选举过程就相对复杂了。

需要加入数据 version、serverid 和逻辑时钟。

Data version:数据新的 version 就大,数据每次更新都会更新 version

server id:就是我们配置的 myid 中的值,每个机器一个

逻辑时钟:这个值从 0 开始递增,每次选举对应一个值,也就是说:如果在同一次选举中, 那么这个值应该是一致的;逻辑时钟值越大,说明这一次选举 leader 的进程更新,也就是 每次选举拥有一个 zxid,投票结果只取 zxid 最大的

选举的标准就变成:

  • 1.逻辑时钟小的选举结果被忽略,重新投票
  • 2.逻辑时钟相同的话,数据 version 大的胜出
  • 3.数据 version 相同的情况下,server id 大的胜出

根据这个规则选出 leader。

八、zk的应用场景

(一)统一的命名服务

在分布式调度系统中可以使用zookeeper实现统一命名服务,以获得类似于UUID的全局唯一名称。使用zookeeper创建顺序节点时,成功创建的每个节点都会返回一个编号,使用该编号以及给定的名称即可生成具有特定含义的统一名称。

(二)统一配置中心

配置文件比如数据库连接,缓存更新时间,接口调用地址,加解密密钥,sesion超时时间,等等每个项目里面用的太多,如果项目里面都统一放在一个properties文件里面,会出现的问题,就是一旦一个地方修改了,假如有10台机器或者上百台,那么就需要重新部署这10台或者上百台的服务器

(三)数据发布/订阅

具体来说,是服务器在zk上订阅自己要监听的节点,在节点上存放配置信息,然后注册一个watch监听器,当这个节点信息发生变化,zk不是直接将变更内容发布至服务器,而是告诉服务器这个节点内容有变化,由服务器感应到通知后重新拉取节点信息,实现动态更新。

(四)分布式锁

分布式锁用于控制分布式系统之间同步访问共享资源的一种方式,可以保证不同系统访问一个或一组资源时的一致性,主要分为排它锁和共享锁。

1、排它锁

排它锁又称为写锁或独占锁,若事务T1对数据对象O1加上了排它锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作,直到T1释放了排它锁。

(1)非公平锁

缺陷:

这种方式在并发问题比较严重的情况下,性能较低,主要原因是,所有的连接都在对同一个节点进行监听,当服务器检测到删除事件时,要通知所有的连接,所有的连接同时收到事件,再次并发竞争,这就是羊群效应

(2)公平锁

 公平锁的实现,通过zk提供的临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力,避免羊群效应

2、共享锁

另一种分布式锁的类型是共享锁。它在性能上要优于排他锁,这是因为在共享锁的实现中,只对数据对象的写操作加锁,而不为对象的读操作进行加锁。这样既保证了数据对象的完整性,也兼顾了多事务情况下的读取操作。可以说,共享锁是写入排他,而读取操作则没有限制。

对于共享锁,首先所有的客户端都会到某个节点,例如:/shared_lock 下创建一个临时顺序节点,如果是读请求,就会创建诸如 /shared_lock/192.168.0.1-R-0000000001 的节点,如果是写操作,则创建诸如 /shared_lock/192.168.0.1-W-0000000001 的节点。是否获取到共享锁,从以下四个步骤来判断:

1、创建完节点后,获取/shared_lock节点下的所有子节点,并对该节点注册子节点变更的watcher监听。

2、确定自己的节点序号在所有子节点中的顺序。

3、对于读请求:

如果没有比自己序号小的子节点,或是所有比自己序号小的子节点都是读取请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑。

如果比自己序号小的子节点中有写请求,那么就需要进入等待。

对于写请求:

如果自己不是序号最小的子节点,那么就需要进入等待。

4、接收到Watcher通知后,重复步骤1

(五)分布式队列

在传统的单进程编程中,我们使用队列来存储一些数据结构,用来在多线程之间共享或传递数据。分布式环境下,我们同样需要一个类似单进程队列的组件,用来实现跨进程、跨主机、跨网络的数据共享和数据传递,这就是我们的分布式队列。利用zk的短暂序列化节点特性,生产者创建节点,消费者按照顺序消费节点,即可实现。

(六)软负载均衡

当单台服务器由于性能不足无法处理众多用户的访问时,就要考虑用多台服务器来提供服务,如何保证多台服务器请求数量相差不大,实现的方式就是负载均衡。zookeeper本身是不提供负载均衡的策略,需要自己来实现,所以这里确切的说,是在负载均衡中应用到了zookeeper做集群的协调。

首先建立/servers节点,并建立监听器监听/servers子节点的状态(用于在服务器增添时及时同步当前集群中服务器列表)。

当每个服务器启动时,在/servers节点下建立子节点worker server(可以用服务器地址命名),并在对应的字节点下存入服务器的相关信息。

这样,我们在zookeeper服务器上可以获取当前集群中的服务器列表及相关信息,然后可以自定义一个负载均衡算法,在每个请求过来时从zookeeper服务器中获取当前集群服务器列表,根据负载均衡算法选出其中一个服务器来处理请求。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值