zookeeper整理知识点

基本概念:

概述:

Zookeeper 是一个分布式协调服务的开源框架。 主要用来解决分布式集群中应用系统的一致性问题
ZooKeeper 本质上是一个分布式的小文件存储系统。 提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。
ZooKeeper 提供给客户端监控存储在zk内部数据的功能,从而可以达到基于数据的集群管理。 
诸如: 统一命名服务(dubbo)、分布式配置管理(solr的配置集中管理)、分布式消息队列(sub/pub)、分布式锁、分布式协调等功能。

架构组成:

Leader:

Zookeeper 集群工作的核心角色
集群内部各个服务器的调度者。
事务请求(写操作) 的唯一调度和处理者,保证集群事务处理的顺序性;
    对于 create,setData, delete 等有写操作的请求,则需要统一转发给leader 处理, leader 需要决定编号、执行操作,这个过程称为一个事务。

Follower:

处理客户端非事务(读操作) 请求,
转发事务请求给 Leader;
参与集群 Leader 选举投票 2n-1台可以做集群投票。

Observer:

此外,针对访问量比较大的 zookeeper 集群, 还可新增观察者角色。
观察者角色,观察 Zookeeper 集群的最新状态变化并将这些状态同步过来,其对于非事务请求可以进行独立处理,对于事务请求,则会转发给 Leader服务器进行处理。
不会参与任何形式的投票只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。增加了集群增加并发的读请求。

特点:

1. Zookeeper:一个领导者(leader:老大),多个跟随者(follower:小弟)组成的集群。
2. Leader负责进行投票的发起和决议,更新系统状态(内部原理)
3. Follower用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票
4. 集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。
5. 全局数据一致:每个server保存一份相同的数据副本,Client无论连接到哪个server,数据都是一致的。
6. 更新请求顺序进行(内部原理)
7. 数据更新原子性,一次数据更新要么成功,要么失败。

运维安装:

安装:

tar -zxvf zookeeper-3.4.14.tar.gz -C ../servers/
mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data
mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data/logs
cd /opt/lagou/servers/zookeeper-3.4.14/conf
mv zoo_sample.cfg zoo.cfg
vim zoo.cfg
    dataDir=/opt/lagou/servers/zookeeper-3.4.14/data
    dataLogDir=/opt/lagou/servers/zookeeper-3.4.14/data/logs
    server.1=linux121:2888:3888
    server.2=linux122:2888:3888     ##server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口
    server.3=linux123:2888:3888
    autopurge.purgeInterval=1       #ZK提供了自动清理事务日志和快照文件的功能,这个参数指定了清理频率,单位是小时
    admin.serverPort=8081           # 访问http://10.0.0.15:8081/commands

添加myid配置:

cd /opt/lagou/servers/zookeeper-3.4.14/data
echo 1 > myid
其他节点对应2、3、。。。

依次启动三个zk实例:

/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh start
    启动失败查看原因:
        logs/zookeeper-root-server-10.0.0.15.out
/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh status

编写集群启动停止脚本:

ssh远程执行命令 ssh $host "source /etc/profile; /opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh $1"

数据结构与监听机制:

数据模型Znode:

概述:

在ZooKeeper中,数据信息被保存在一个个数据节点上,这些节点被称为znode。ZNode 是Zookeeper 中最小数据单位,在 ZNode 下面又可以再挂 ZNode
这样一层层下去就形成了一个层次化命名空间 ZNode 树,我们称为 ZNode Tree,它采用了类似文件系统的层级树状结构进行管理。
所有ZNode按层次化进行组织,形成这么一颗树,ZNode的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/)进行分割的路径表示
开发人员可以向这个节点写入数据,也可以在这个节点下面创建子节点。
类型
持久性节点(Persistent)
临时性节点(Ephemeral)
顺序性节点(Sequential)
在开发中在创建节点的时候通过组合可以生成以下四种节点类型:持久节点、持久顺序节点、临时节点、临时顺序节点。不同类型的节点则会有不同的生命周期

持久节点:

是Zookeeper中最常见的一种节点类型,所谓持久节点,就是指节点被创建后会一直存在服务器,直到删除操作主动清除

持久顺序节点:

就是有顺序的持久节点,节点特性和持久节点是一样的,只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候,会在节点名后面加上一个数字后缀,来表示其顺序。

临时节点:

就是会被自动清理掉的节点,它的生命周期和客户端会话绑在一起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建子节点。

临时顺序节点:

就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后面加上数字后缀。

事务ID:

首先,先了解,事务是对物理和抽象的应用状态上的操作集合。往往在现在的概念中,狭义上的事务通常指的是数据库事务,一般包含了一系列对数据库有序的读写操作
这些数据库事务具有所谓的ACID特性,即原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
而在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新等操作。

对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用 ZXID 来表示,通常是一个 64 位的数字。每一个 ZXID 对应一次更新操作。

从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序zk中的事务指的是对zk服务器状态改变的操作(create,update data,更新字节点);
zk对这些事务操作都会编号,这个编号是自增长的被称为ZXID。

ZNode的状态信息:

整个 ZNode 节点内容包括两部分:节点数据内容和节点状态信息。
cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
ctime 就是 Create Time,表示节点创建时间。
mZxid 就是 Modified ZXID,表示节点最后一次被修改时的事务ID。
mtime 就是 Modified Time,表示节点最后一次被修改的时间。
pZxid 表示该节点的子节点列表最后一次被修改时的事务 ID。只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新。
cversion 表示子节点的版本号。
dataVersion 表示内容版本号。
aclVersion 标识acl版本
ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0
dataLength 表示数据长度。
numChildren 表示直系子节点数。

Watcher 机制:

Zookeeper使用Watcher机制实现分布式数据的发布/订阅功能
一个典型的发布/订阅模型系统定义了一种 一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。
在 ZooKeeper 中,引入了 Watcher 机制来实现这种分布式的通知功能。
ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么Zk就会向指定客户端发送一个事件通知来实现分布式的通知功能。

Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、Zookeeper服务器三部分。
具体工作流程为:
    客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中
    当Zookeeper服务器触发Watcher事件后,会向客户端发送通知
    客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑

基本使用:

命令行操作:

./zkcli.sh 连接本地的zookeeper服务器
./zkCli.sh -server ip:port(2181) 连接指定的服务器
help
stat path [watch]
set path data [version]        可以更新指定节点的数据内容
ls path [watch]                可以列出Zookeeper指定节点下的所有子节点,但只能查看指定节点下的第一级的所有子节点
delquota [-n|-b] path
ls2 path [watch]
setAcl path acl
setquota -n|-b val path
history
redo cmdno
printwatches on|off
delete path [version]
sync path
listquota path
rmr path
get path [watch]                    get命令可以获取Zookeeper指定节点的数据内容和属性信息。
create [-s] [-e] path data acl        创建节点,其中,-s或-e分别指定节点特性,顺序或临时节点,若不指定,则创建持久节点
addauth scheme auth
quit
getAcl path
close
connect host:port

API客户端:

创建客户端:

ZkClient zkClient = new ZkClient("127.0.0.1:2181");
创建路径
zkClient.createPersistent("/lg-zkClient/lg-c1",true);
zkClient.createEphemeral("/lg-client1", "123");

读取数据:

zkClient.setZkSerializer(new MyZkSerializer());      // 必须设置,否则报错
final Object o = zkClient.readData("/lg-client1");

写入数据:

zkClient.setZkSerializer(new MyZkSerializer());
zkClient.writeData("/lg-client1", "new data");
删除路径
zkClient.deleteRecursive(path);
zkClient.delete("/lg-client/c1");
监听节点变化
zkClient.subscribeChildChanges("/lg-client", new IZkChildListener() {
    public void handleChildChange(String path, List<String> childs) throws Exception {
        System.out.println(path + " childs changes ,current childs " + childs);
    }
});
客户端可以对一个不存在的节点进行子节点变更的监听。
一旦客户端对一个节点注册了子节点列表变更监听之后,那么当该节点的子节点列表发生变更时,服务端都会通知客户端,并将最新的子节点列表发送给客户端
该节点本身的创建或删除也会通知到客户端。
获取数据(节点是否存在、更新、删除)
zkClient.setZkSerializer(new ZkStrSerializer());
zkClient.subscribeDataChanges("/lg-client1", new IZkDataListener() {
    public void handleDataChange(String path, Object data) throws Exception {
        System.out.println(path + " data is changed ,new data " + data);
    }
    public void handleDataDeleted(String path) throws Exception {
        System.out.println(path + " is deleted!!");
    }
});
//设置自定义的序列化类型,否则会报错!!
public class ZkStrSerializer implements ZkSerializer {
    public byte[] serialize(Object o) throws ZkMarshallingError {
        return String.valueOf(o).getBytes();        //序列化,数据--》byte[]
    }
    public Object deserialize(byte[] bytes) throws ZkMarshallingError {
        return new String(bytes);       //反序列化,byte[]--->数据
    }
}

内部原理:

Leader选举:

选举机制:

半数机制:集群中半数以上机器存活,集群可用。所以Zookeeper适合安装奇数台服务器。
Zookeeper虽然在配置文件中并没有指定Master和Slave。但是,Zookeeper工作时,是有一个节点为Leader,其它为Follower,Leader是通过内部的选举机制产生的。

集群首次启动:

会根据myid最大值(一般为3)来确定第一次的leader
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一样称为follower。

集群非首次启动:

每个节点在选举时都会参考自身节点的zxid值(事务ID);优先选择zxid值大的节点称为Leader!!

ZAB一致性协议:

分布式数据一致性问题:

解决:CAP理论,zk是CP模型,master宕调后集群在切换过程不可用,为了数据一致性。

ZAB协议:

概述:
ZK就是分布式一致性问题的工业解决方案,paxos是其底层理论算法(晦涩难懂著名),其中zab,raft和众多开源算法是对paxos的工业级实现。
ZK没有完全采用paxos算法,而是使用了一种称为Zookeeper Atomic Broadcast(ZAB,Zookeeper原子消息广播协议)的协议作为其数据一致性的核心算法。
ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复和原子广播协议。
主备模式保证一致性:
所有客户端写入数据都是写入Leader中,然后,由 Leader 复制到Follower中。
ZAB会将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上,ZAB协议能够保证了事务操作的一个全局的变更序号(ZXID)。
广播消息:
ZAB 协议的消息广播过程类似于 二阶段提交过程。对于客户端发送的写请求,全部由 Leader 接收,Leader 将请求封装成一个事务 Proposal(提议),将其发送给所有 Follwer 
如果收到超过半数反馈ACK,则执行 Commit 操作(先提交自己,再发送 Commit 给所有 Follwer)。
不能正常反馈Follower恢复正常后会进入数据同步阶段最终与Leader保持一致!!
细节
    Leader接收到Client请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一 ID,称为事务ID(ZXID),ZAB 协议要求保证事务的顺序
    因此必须将每一个事务按照 ZXID进行先后排序然后处理。
    ZK集群为了保证任何事务操作能够有序的顺序执行,只能是 Leader 服务器接受写请求,即使是Follower 服务器接受到客户端的请求,也会转发到 Leader 服务器进行处理。
注意:
    zk提供的应该是最终一致性的标准。zk所有节点接收写请求之后可以在一定时间内保证所有节点都能看到该条数据!!
Leader崩溃问题:
Leader宕机后,被选举的新Leader需要解决的问题
    1. ZAB 协议确保那些已经在 Leader 提交的事务最终会被所有服务器提交。
    2. ZAB 协议确保丢弃那些只在 Leader 提出/复制,但没有提交的事务。
基于上面的目的,ZAB协议设计了一个选举算法:能够确保已经被Leader提交的事务被集群接受,丢弃还没有提交的事务。
这个选举算法的关键点:保证选举出的新Leader拥有集群中所有节点最大编号(ZXID)的事务!!

Zookeeper应用实践:

概述:

Zookeeper的两大特性:

1.客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。

2.对在Zookeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么临时节点也会被自动删除

利用其两大特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册一个Watcher监听,那么但凡进行动态添加机器的操作
就会在/clusterServers节点下创建一个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。

服务器动态上下线监听:

服务端:

zkClient.createEphemeralSequential("/servers/server", ip +":"+port);
创建递增的节点,存的信息为ip+port 

客户端:

zkClient.subscribeChildChanges("/servers", new IZkChildListener() {
    public void handleChildChange(String s, List<String> children) throws Exception {
        ArrayList<String> list = new ArrayList<String>();           //接收到通知,说明节点发生了变化,client需要更新infos集合中的数据
        for (String path : children) {
            final Object o = zkClient.readData("/servers/" + path); //遍历更新过后的所有节点信息
            list.add(String.valueOf(o));
        }
        infos = list;
        System.out.println("--》接收到通知,最新服务器信息为:" + infos);
    }
});
然后随机从list中取出一个用于请求即可。

对比服务注册中心Eureka:

原理:
eureka集群是peer-to-peer集群,机器地位对等。
zk是leader+follower两种角色,leader负责读
一致性保证:
eureka是ap模型
服务发现的时效性:
zk时效性更好,秒级
eureka默认配置了缓存导致分钟级别
容量:
zk大规模时,服务上下线需要瞬间推荐数据到所有的其他服务实例,瞬间大量占用zk所在节点的网络带宽
eureka需要心跳检测所有的服务,每台机器压力比较大。

分布式锁:

概述:

利用Zookeeper可以创建临时带序号节点的特性来实现一个分布式锁
开源框架curator

思路:

1.锁就是zk指定目录下序号最小的临时序列节点,多个系统的多个线程都要在此目录下创建临时的顺序节点,因为Zk会为我们保证节点的顺序性,所以可以利用节点的顺序进行锁的判断。
2.每个线程都是先创建临时顺序节点,然后获取当前目录下最小的节点(序号),判断最小节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
3.获取锁失败的线程获取当前节点上一个临时顺序节点,并对对此节点进行监听,当该节点删除的时候(上一个线程执行结束删除或者是掉线zk删除临时节点)这个线程会获取到通知,
    代表获取到了锁。

实现:

获取锁的方法:
currentNoePath = zkClient.createEphemeralSequential("/distrilock/","lock");
List<String> childs = zkClient.getChildren("/distrilock");
Collections.sort(childs);
String minNode = childs.get(0);
if (currentNoePath.equals("/distrilock/" + minNode)) {
    return true;
else {
    int i = Collections.binarySearch(childs,currentNoePath.substring("/distrilock/".length()));
    String lastNodeChild = childs.get(i - 1);
    beforNodePath = "/distrilock/" + lastNodeChild;
}
return false;
等待锁的方法:
IZkDataListener iZkDataListener = new IZkDataListener() {
    public void handleDataChange(String s, Object o) throws Exception {
    }
    public void handleDataDeleted(String s) throws Exception {
        countDownLatch.countDown(); //把值减1变为0,唤醒之前await线程
    }
};
zkClient.subscribeDataChanges(beforNodePath, iZkDataListener);//监控前一个节点
if (zkClient.exists(beforNodePath)) {        //在监听的通知没来之前,该线程应该是等待状态,先判断一次上一个节点是否还存在
    countDownLatch = new CountDownLatch(1);     //开始等待,CountDownLatch:线程同步计数器
    try {
        countDownLatch.await();     //阻塞,等待回调函数将countDownLatch值变为0
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
zkClient.unsubscribeDataChanges(beforNodePath, iZkDataListener);        //解除监听

实现的几种思路:

1.监听父节点,每次获取所有子节点,判断自己是否最小
2.第一个不监听,第二个节点监听比自己小的节点,可以避免羊群效应

特殊情况:

临时节点持有的客户端由于网络原因,被zk认为释放了。
解决: 
    持久节点,释放锁的时候删除自己,最后一个节点删除父节点。
    无法解决的情况:
        客户端异常导致没有释放锁,这个时候需要加上超时时间,但由于zk没有过期时间,需要访问判断。
posted @ 2022-02-28 10:13  心平万物顺  阅读(111)  评论(0编辑  收藏  举报