分布式协调服务——zookeeper

图片

1 : zookeeper 概述:

1.1 : zookeeper简介:

zookeeper是一个分布式的, 开源的分布式应用程序协调服务,是对Google的Chubby组件的开源实现,为Hadoop和HBase的运行提供了相应的服务.他是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护(使得集群中的机器可以共享配置信息中的那些公共的部分);命名服务(是指通过指定的名字来获取资环或者服务的地址,以及提供者的信息,利用zookeeper可以很容易的创建一个全局的路径,而这个路径就可以作为一个名字,可以指向集群中的机器,提供的服务的地址,远程对象等);分布式同步(可以使得集群中各个节点具有相同的系统状态);组服务(通过zookeeper的短暂节点的特征,来监控每个应用程序的上线和下线)等。他是用java语言来编写,通过Zab协议来保证节点的一致性。zookeeper的目标就是封装好复杂容易出错的关键服务,将简单易用的接口和性能高效,功能稳定的提供给用户。

1.2:zookeeper的特点:

  1. 最终一致性:客户端无论连接到哪个server,展示给他的都是同一个视图,这就是zookeeper的重要特点之一。
  2. 可靠性:zookeeper具有简单,健壮,良好的性能,如果一条消息被一台服务器接收,那么它将会被所有的服务器所接收。
  3. 实时性:zookeeper将保证客户端将在一个时间间隔范围内,获得服务器的更新消息或者服务器的失败消息,但是由于网络延时等原因,zookeeper不能保证两个客户端能同时得到刚刚更新的数据,如果需要最新的数据,那么就需要在读数据之前调用syne( )接口。
  4. 等待无关:慢的或者失效的客户端不得干预快速的客户端的请求,这就是的每个客户端都能够有效的等待。
  5. 原子性:对zookeeper的更新要么全部成功,要么全部失败,没有中间状态。
  6. 顺序性:它包括全局有序和偏序两种,全局有序(服务端)就是如果一台服务器上消息A在消息B之前发送,则在所有server上消息A都将在消息B前被发布。偏序(客户端)是指,如果一个消息B在消息A后被同一个发送者发布,A必将排在B前面.

1.3:zookeeper的基本架构

zookeeper服务自身组成一个集群(2n+1个节点,允许哪个节点失效)。zookeeper服务具有三个角色,一个是Leader,一个是Follower,一个是Observer。

  • Leader :为客户端提供读写服务,并维护集群状态,它是由集群选举所产生的;

  • Follower :为客户端提供读写服务,并定期向 Leader 汇报自己的节点状态。同时也参与写操作“过半写成功”的策略和 Leader 的选举;

  • Observer :为客户端提供读写服务,并定期向 Leader 汇报自己的节点状态,但不参与写操作“过半写成功”的策略和 Leader 的选举,因此 Observer 可以在不影响写性能的情况下提升集群的读性能。

  • 下图就是zookeeper的架构图:
    image-20211211175151511

客户端(Client)可以选择连接到zookeeper集群中的每个服务端(server),并且每个服务端的数据完全相同,每个从节点都需要与主节点进行通信,并同步主节点上更新的数据。

对于zookeeper集群来说,zookeeper只要超过一般数量的服务端可用,那么zookeeper整体服务就是可用的。

1.4:zookeeper的工作原理:

zookeeper的核心原理就是原子广播,该原子广播就是对zookeeper集群上所有主机数据包,通过这个机制保证了各个服务端之前的数据同步,那么实现这个机制在zookeeper中有一个内部协议(Zab协议),这个协议有两种模式,一个是恢复模式,一个是广播模式。

当服务启动或者是在主节点崩溃之后,Zab协议就进入了恢复模式,当主节点再次被选举出来,且大多数服务端完成了和主节点的状态同步之后,恢复模式就结束了,状态同步保证了主节点和服务端具有相同的系统状态,一旦主节点已经和多数的从节点(也就是服务节点)进行了状态同步之后,他就可以开始广播消息即进入到了广播模式。

在广播模式下,服务端会接收客户端的请求,所有的写请求都被转发给leader,再有leader将更新广播给follower节点,当半数以上的follower完成数据写请求之后,leader才会提交这个更新,然后客户端才会收到一个更新成功的响应。

2:zookeeper安装配置:

Zookeeper集群搭建指的是ZooKeeper分布式模式安装。通常由2n+1台server组成。这是因为为了保证Leader选举(基于Paxos算法的实现)能过得到多数的支持,所以ZooKeeper集群的数量一般为奇数。

Zookeeper运行需要java环境,所以需要提前安装jdk。对于安装leader+follower模式的集群,大致过程如下:

  • 配置主机名称到IP地址映射配置
  • 修改ZooKeeper配置文件
  • 远程复制分发安装文件
  • 设置myid
  • 启动ZooKeeper集群

如果要想使用Observer模式,可在对应节点的配置文件添加如下配置:

peerType=observer

其次,必须在配置文件指定哪些节点被指定为Observer,如:

server.1:node1:2181:3181:observer

这里,我们安装的是leader+follower模式

主机规划:

服务器IP主机名myid的值
192.168.88.161node11
192.168.88.162node22
192.168.88.163node33

地址规划:

软件名地址
zookeeper-3.4.6.tar.gz/export/softwate
zookeeper-3.4.6/export/servers
zookeeper数据路径/export/servers/zookeeper-3.4.6/zkdatas

2.1:第一步:下载zookeeeper的压缩包,

下载网址如下http://archive.apache.org/dist/zookeeper/

我们在这个网址下载我们使用的zk版本为3.4.6

下载完成之后,上传到我们的linux的/export/software路径下准备进行安装

2.2:第二步:解压

在node1主机上,解压zookeeper的压缩包到/export/server路径下去,然后准备进行安装

tar -zxvf zookeeper-3.4.6.tar.gz -C /export/servers/

2.3. 第三步:修改配置文件

在node1主机上,修改配置文件

cd /export/servers/zookeeper-3.4.6/conf/
cp zoo_sample.cfg  zoo.cfg
vi zoo.cfg

修改以下内容

# The number of milliseconds of each tick
# 这个时间是zookeeper服务器之间或者服务器与客户端之间维持心跳的时间机制
tickTime=2000

# The number of ticks that the initial 
# synchronization phase can take
# 配置zookeeper接收客户端初始化连接时最长能忍受多少个心跳时间间隔数
initLimit=10

# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
# leader和follower之间发送消息,请求和应答时间长度
syncLimit=5

# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.
# 数据目录,需要提前创建
dataDir=/export/servers/zookeeper-3.4.6/zkdatas

# 日志目录,也需要提前创建
dataLogDir=/export/servers/zookeeper-3.4.6/zkLogs

# the port at which the clients will connect
# 访问端口号
clientPort=2181

# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the 
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
# 在dataDir中保留快照的数量
autopurge.snapRetainCount=3

# Purge task interval in hours
# Set to "0" to disable auto purge feature
# 设置清除任务间隔
autopurge.purgeInterval=1

# zookeeper cluster properties

# 设置每个节点上的服务
server.1=node1:2888:3888
server.2=node2:2888:3888
server.3=node3:2888:3888

2.4. 第四步:添加myid配置

在node1主机的/export/server/zookeeper-3.4.6/zkdatas/这个路径下创建一个文件,文件名为myid ,文件内容为1

echo 1 > /export/servers/zookeeper-3.4.6/zkdatas/myid

2.5. 第五步:安装包分发并修改myid的值

在node1主机上,将安装包分发到其他机器

第一台机器上面执行以下两个命令

scp -r /export/servers/zookeeper-3.4.6 node2:$PWD
scp -r /export/servers/zookeeper-3.4.6 node3:$PWD

第二台机器上修改myid的值为2

echo 2 > /export/servers/zookeeper-3.4.6/zkdatas/myid

第三台机器上修改myid的值为3

echo 3 > /export/servers/zookeeper-3.4.6/zkdatas/myid

2.6. 第六步:三台机器启动zookeeper服务

三台机器分别启动zookeeper服务

这个命令三台机器都要执行

/export/servers/zookeeper-3.4.6/bin/zkServer.sh start

三台主机分别查看启动状态

/export/servers/zookeeper-3.4.6/bin/zkServer.sh  status

image-20211212115525120
在这里插入图片描述image-20211212115621726

3:zookeeper数据模型:

zookeeper维护这一个树状层次结构,树中的节点被称之为znode,znode可以用来存储数据,并且有一个与之相关联的ACL(Access Control List:访问控制列表,用于控制资源的访问权限)。zookeeper被设计用来实现协调服务(通常使用小数据文件),而不适用于大容量数据存储,因此一个znode能存储的数据被限制在1MB以内,znode的树状结构如下图所示:

在这里插入图片描述

图中的每个节点称为一个Znode。 每个Znode由3部分组成:
ZooKeeper的数据模型,在结构上和标准文件系统的非常相似,拥有一个层次的命名空间,都是采用树形层次结构,ZooKeeper树中的每个节点被称为—Znode。和文件系统的目录树一样,ZooKeeper树中的每个节点可以拥有子节点。但也有不同之处:

  1. Znode兼具文件和目录两种特点,既像文件一样维护着数据、元信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分,并可以具有子znode。用户对znode具有增、删、改、查等操作(权限允许的情况下)。

  2. Znode具有原子性操作,读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的操作。

  3. Znode存储数据大小有限制,ZooKeeper虽然可以关联一些数据,但并没有被设计为常规的数据库或者大数据存储,相反的是,它用来管理调度数据,比如分布式应用中的配置文件信息、状态信息、汇集位置等等。这些数据的共同特性就是它们都是很小的数据,通常以KB为大小单位。ZooKeeper的服务器和客户端都被设计为严格检查并限制每个Znode的数据大小至多1M,当时常规使用中应该远小于此值。

  4. Znode通过路径引用,如同Unix中的文件路径。路径必须是绝对的,因此他们必须由斜杠字符来开头。除此以外,他们必须是唯一的,也就是说每一个路径只有一个表示,因此这些路径不能改变。在ZooKeeper中,路径由Unicode字符串组成,并且有一些限制。字符串"/zookeeper"用以保存管理信息,比如关键配额信息。
    ① stat:此为状态信息, 描述该Znode的版本, 权限等信息
    ② data:与该Znode关联的数据
    ③ children:该Znode下的子节点

    znode是客户端访问的zookeeper的主要实体。

4、Zookeeper节点类型:

Znode有两种,分别为临时节点和永久节点。

节点的类型在创建时即被确定,并且不能改变。

  • 临时节点:该节点的生命周期依赖于创建它们的会话。一旦会话结束,临时节点将被自动删除,当然可以也可以手动删除。临时节点不允许拥有子节点。
  • 永久节点:该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,他们才能被删除。

Znode还有一个序列化的特性,如果创建的时候指定的话,该Znode的名字后面会自动追加一个不断增加的序列号。序列号对于此节点的父节点来说是唯一的,这样便会记录每个子节点创建的先后顺序。它的格式为“%10d”(10位数字,没有数值的数位用0补充,例如“0000000001”)。

image-20211211193910307
这样便会存在四种类型的Znode节点,分别对应:
PERSISTENT:永久节点
EPHEMERAL:临时节点
PERSISTENT_SEQUENTIAL:永久节点、序列化
EPHEMERAL_SEQUENTIAL:临时节点、序列化

5:zookeeper Watcher (监听机制)

ZooKeeper提供了分布式数据发布/订阅功能,一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使他们能够做出相应的处理。

ZooKeeper中,引入了Watcher机制来实现这种分布式的通知功能。ZooKeeper允许客户端向服务端注册一个Watcher监听,当服务端的一些事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。

触发事件种类很多,如:节点创建,节点删除,节点改变,子节点改变等。

总的来说可以概括Watcher为以下三个过程:客户端向服务端注册Watcher、服务端事件发生触发Watcher、客户端回调Watcher得到触发事件情况

1) Watch机制特点

一次性触发

事件发生触发监听,一个watcher event就会被发送到设置监听的客户端,这种效果是一次性的,后续再次发生同样的事件,不会再次触发。

事件封装

ZooKeeper使用WatchedEvent对象来封装服务端事件并传递。

WatchedEvent包含了每一个事件的三个基本属性:
通知状态(keeperState),事件类型(EventType)和节点路径(path)
event 异步发送
watcher的通知事件从服务端发送到客户端是异步的。

先注册再触发

Zookeeper中的watch机制,必须客户端先去服务端注册监听,这样事件发送才会触发监听,通知给客户端。

2) 通知状态和事件类型

同一个事件类型在不同的通知状态中代表的含义有所不同,下表列举了常见的通知状态和事件类型。

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

KeeperState (通知状态)EventType (事件类型)触发条件说明
None连接成功
SyncConnectedNodeCreatedZnode被创建此时处于连接状态
SyncConnectedNodeDeletedZnode被删除此时处于连接状态
SyncConnectedNodeDataChangedZnode数据被改变此时处于连接状态
SyncConnectedNodeChildChangedZnode的子Znode数据被改变此时处于连接状态
DisconnectedNone客户端和服务端断开连接此时客户端和服务器处于断开连接状态
ExpiredNone会话超时会收到一个SessionExpiredExceptio
AuthFailedNone权限验证失败会收到一个AuthFailedException

其中连接状态事件(type=None, path=null)不需要客户端注册,客户端只要有需要直接处理就行了。

3) Shell 客户端设置watcher

设置节点数据变动监听:
image-20211212141708944
通过另一个客户端更改节点数据:
image-20211212141721071
此时设置监听的节点收到通知:
在这里插入图片描述

6、ZooKeeper的shell操作

1. 客户端连接

运行 zkCli.sh –server ip 进入命令行工具。

cd /export/servers/zookeeper-3.4.6/bin
./zkCli.sh -server node1:2181  

2. shell基本操作

1) 创建节点:

格式:

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

其中,-s 表示创建一个序列化节点

​ -e 表示创建一个临时节点

​ 啥也不加表示创建了一个永久节点

​ acl:权限控制

创建永久节点:

create /aaa 123456

创建临时节点

create -e /linshijiedian 123456

创建永久序列化节点

create -s /abc 123

创建临时序列化节点

create -s -e /fff 123345
2) 读取节点

与读取相关的命令有 ls 命令和 get 命令,ls和ls2 命令可以列出 Zookeeper 指定节点下的所有子节点,只能查看指定节点下的第一级的所有子节点;get 命令可以获取 Zookeeper 指定节点的数据内容和属性信息。

格式:

ls path [watch]
get path [watch]
ls2 path [watch]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RcgLN6PI-1641199684803)(http://www.honghuboyuan.top//image-20211212141158612.png)]

dataVersion:数据版本号,每次对节点进行set操作,dataVersion的值都会增加1(即使设置的是相同的数据),可有效避免了数据更新时出现的先后顺序问题。

cversion :子节点的版本号。当znode的子节点有变化时,cversion 的值就会增加1。

cZxid :Znode创建的事务id。

mZxid :Znode被修改的事务id,即每次对znode的修改都会更新mZxid。

对于zk来说,每次的变化都会产生一个唯一的事务id,zxid(ZooKeeper Transaction Id)。通过zxid,可以确定更新操作的先后顺序。例如,如果zxid1小于zxid2,说明zxid1操作先于zxid2发生,zxid对于整个zk都是唯一的,即使操作的是不同的znode。

ctime:节点创建时的时间戳.

mtime:节点最新一次更新发生时的时间戳.

ephemeralOwner:如果该节点为临时节点, ephemeralOwner值表示与该节点绑定的session id. 如果不是, ephemeralOwner值为0.

在client和server通信之前,首先需要建立连接,该连接称为session。连接建立后,如果发生连接超时、授权失败,或者显式关闭连接,连接便处于CLOSED状态, 此时session结束。

3) 更新索引

格式:

set path data [version]

data 就是要更新的新内容,version 表示数据版本

image-20211212143238620

4) 删除节点

格式:

 delete path [version]

若删除节点存在子节点,那么无法删除该节点,必须先删除子节点,再删除父节点

rmr path: 可以递归删除节点。

5) 对节点进行限制: quota

格式1:

setquota -n|-b val path

-n:表示子节点的最大个数

-b:表示数据值的最大长度

val:子节点最大个数或数据值的最大长度

path:节点路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vhGovpl9-1641199684806)(C:\Users\Dell\AppData\Roaming\Typora\typora-user-images\image-20211212143643334.png)]

格式2: listquota path : 列出指定节点的 quota

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-deOayTfA-1641199684808)(C:\Users\Dell\AppData\Roaming\Typora\typora-user-images\image-20211212143652474.png)]
子节点个数为 2,数据长度-1 表示没限制

注意: 在实际操作的时候, 虽然设置了最大的节点数后,依然可以在整个节点下添加多个子节点, 只是会在zookeeper中的日志文件中记录一下警告信息[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UEvy8xOK-1641199684810)(C:\Users\Dell\AppData\Roaming\Typora\typora-user-images\image-20211212143708410.png)]
格式3: delquota [-n|-b] path : 删除 quota

6) 其他命令:

history: 列出命令历史
在这里插入图片描述
redo:该命令可以重新执行指定命令编号的历史命令,命令编号可以通过

7、ZooKeeper Java API操作

这里操作Zookeeper的JavaAPI使用的是一套zookeeper客户端框架 Curator ,解决了很多Zookeeper客户端非常底层的细节开发工作 。

Curator包含了几个包:

curator-framework:对zookeeper的底层api的一些封装

curator-recipes:封装了一些高级特性,如:Cache事件监听、选举、分布式锁、分布式计数器等

Maven依赖(使用curator的版本:2.12.0,对应Zookeeper的版本为:3.4.x,如果跨版本会有兼容性问题,很有可能导致节点操作失败):

1. 引入maven坐标

<dependencies>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.12.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.12.0</version>
        </dependency>

        <dependency>
            <groupId>com.google.collections</groupId>
            <artifactId>google-collections</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- java编译插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.2</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

java 操作zookeeper的相关操作:

package com.lixufei.zookeeper;


import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.junit.Test;

/**
 * @Author lixufei
 * @Date 2021/12/12 15:03
 * @Version 1.0
 */
public class ZookeeperTest{

    /**
     * 使用java操纵zookeeper,创建节点
     */
    @Test
    public void createZnode() throws Exception {
        // 创建java操作zookeeper的客户端对象
        String connectString = "192.168.88.161:2181,192.168.88.162:2181,192.168.88.163:2181";
        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework zookeeperClient = CuratorFrameworkFactory.newClient(connectString, retryPolicy);

        // 启动客户端
        zookeeperClient.start();

        // 执行相关的操作
        /**
         * CreateMode.PERSISTENT:永久节点
         * CreateMode.PERSISTENT_SEQUENTIAL:永久序列化节点
         * CreateMode.EPHEMERAL:临时节点
         * CreateMode.EPHEMERAL_SEQUENTIAL:临时序列化节点
         */
        zookeeperClient.create().withMode(CreateMode.PERSISTENT).forPath("/test04","123456".getBytes());

        // 释放资源
        zookeeperClient.close();
    }

    /**
     * 使用java操纵zookeeper,修改节点
     */
    @Test
    public void updataZnode() throws Exception {
        // 创建java操作zookeeper的客户端对象
        String connectString = "192.168.88.161:2181,192.168.88.162:2181,192.168.88.163:2181";
        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework zookeeperClient = CuratorFrameworkFactory.newClient(connectString, retryPolicy);

        // 启动客户端
        zookeeperClient.start();

        // 执行相关的操作
        zookeeperClient.setData().forPath("/test03","lixufei123456".getBytes());

        // 释放资源
        zookeeperClient.close();
    }

    /**
     * 使用java操纵zookeeper,删除节点
     */
    @Test
    public void deleteZnode() throws Exception {
        // 创建java操作zookeeper的客户端对象
        String connectString = "192.168.88.161:2181,192.168.88.162:2181,192.168.88.163:2181";
        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework zookeeperClient = CuratorFrameworkFactory.newClient(connectString, retryPolicy);
        // 启动客户端
        zookeeperClient.start();

        // 执行相关的操作
        zookeeperClient.delete().forPath("/test04");

        // 释放资源
        zookeeperClient.close();
    }

    /**
     * 使用java操纵zookeeper,查询节点
     */
    @Test
    public void selectZnode() throws Exception {
        // 创建java操作zookeeper的客户端对象
        String connectString = "192.168.88.161:2181,192.168.88.162:2181,192.168.88.163:2181";
        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework zookeeperClient = CuratorFrameworkFactory.newClient(connectString, retryPolicy);
        // 启动客户端
        zookeeperClient.start();

        // 执行相关的操作
        byte[] bytes = zookeeperClient.getData().forPath("/test03");
        System.out.println(new String(bytes));
        // 释放资源
        zookeeperClient.close();
    }
}

8、ZooKeeper选举机制

zookeeper默认的算法是FastLeaderElection,采用投票数大于半数则胜出的逻辑。

1. 概念

服务器ID

比如有三台服务器,编号分别是1,2,3。

编号越大在选择算法中的权重越大。

选举状态

LOOKING,竞选状态。

FOLLOWING,随从状态,同步leader状态,参与投票。

OBSERVING,观察状态,同步leader状态,不参与投票。

LEADING,领导者状态。

数据ID

服务器中存放的最新数据version。

值越大说明数据越新,在选举算法中数据越新权重越大。

逻辑时钟

也叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断。

2. 全新集群选举

假设目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下:

服务器1启动自己,给投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。

服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。

服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。

服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为小弟。

服务器5启动,后面的逻辑同服务器4成为小弟。

3. 非全新集群选举

对于运行正常的zookeeper集群,中途有机器down掉,需要重新选举时,选举过程就需要加入数据ID、服务器ID和逻辑时钟。

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

服务器ID:就是我们配置的myid中的值,每个机器一个。

逻辑时钟:这个值从0开始递增,每次选举对应一个值。 如果在同一次选举中,这个值是一致的。

这样选举的标准就变成:

​ 1、逻辑时钟小的选举结果被忽略,重新投票;

​ 2、统一逻辑时钟后,数据id大的胜出;

​ 3、数据id相同的情况下,服务器id大的胜出;

根据这个规则选出leader。

9、ZAB协议

1. ZAB协议与数据一致性

ZAB 协议是 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。通过该协议,Zookeepe 基于主从模式的系统架构来保持集群中各个副本之间数据的一致性。具体如下:

Zookeeper 使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用原子广播协议将数据状态的变更以事务 Proposal 的形式广播到所有的副本进程上去。如下图:

具体流程如下:

所有的事务请求必须由唯一的 Leader 服务来处理,Leader 服务将事务请求转换为事务 Proposal,并将该 Proposal 分发给集群中所有的 Follower 服务。如果有半数的 Follower 服务进行了正确的反馈,那么 Leader 就会再次向所有的 Follower 发出 Commit 消息,要求将前一个 Proposal 进行提交。

2. ZAB协议的内容

ZAB 协议包括两种基本的模式,分别是崩溃恢复和消息广播:

1. 崩溃恢复

当整个服务框架在启动过程中,或者当 Leader 服务器出现异常时,ZAB 协议就会进入恢复模式,通过过半选举机制产生新的 Leader,之后其他机器将从新的 Leader 上同步状态,当有过半机器完成状态同步后,就退出恢复模式,进入消息广播模式。

2. 消息广播

ZAB 协议的消息广播过程使用的是原子广播协议。在整个消息的广播过程中,Leader 服务器会每个事物请求生成对应的 Proposal,并为其分配一个全局唯一的递增的事务 ID(ZXID),之后再对其进行广播。具体过程如下:

Leader 服务会为每一个 Follower 服务器分配一个单独的队列,然后将事务 Proposal 依次放入队列中,并根据 FIFO(先进先出) 的策略进行消息发送。Follower 服务在接收到 Proposal 后,会将其以事务日志的形式写入本地磁盘中,并在写入成功后反馈给 Leader 一个 Ack 响应。当 Leader 接收到超过半数 Follower 的 Ack 响应后,就会广播一个 Commit 消息给所有的 Follower 以通知其进行事务提交,之后 Leader 自身也会完成对事务的提交。而每一个 Follower 则在接收到 Commit 消息后,完成事务的提交。

10、Zookeeper的典型应用场景

10.1数据的发布/订阅

数据的发布/订阅系统,通常也用作配置中心。在分布式系统中,你可能有成千上万个服务节点,如果想要对所有服务的某项配置进行更改,由于数据节点过多,你不可逐台进行修改,而应该在设计时采用统一的配置中心。之后发布者只需要将新的配置发送到配置中心,所有服务节点即可自动下载并进行更新,从而实现配置的集中管理和动态更新。

Zookeeper 通过 Watcher 机制可以实现数据的发布和订阅。分布式系统的所有的服务节点可以对某个 ZNode 注册监听,之后只需要将新的配置写入该 ZNode,所有服务节点都会收到该事件。

10.2 命名服务

在分布式系统中,通常需要一个全局唯一的名字,如生成全局唯一的订单号等,Zookeeper 可以通过顺序节点的特性来生成全局唯一 ID,从而可以对分布式系统提供命名服务。

10.3 Master选举

分布式系统一个重要的模式就是主从模式 (Master/Salves),Zookeeper 可以用于该模式下的 Matser 选举。可以让所有服务节点去竞争性地创建同一个 ZNode,由于 Zookeeper 不能有路径相同的 ZNode,必然只有一个服务节点能够创建成功,这样该服务节点就可以成为 Master 节点。

10.4 分布式锁

可以通过 Zookeeper 的临时节点和 Watcher 机制来实现分布式锁,这里以排它锁为例进行说明:

分布式系统的所有服务节点可以竞争性地去创建同一个临时 ZNode,由于 Zookeeper 不能有路径相同的 ZNode,必然只有一个服务节点能够创建成功,此时可以认为该节点获得了锁。其他没有获得锁的服务节点通过在该 ZNode 上注册监听,从而当锁释放时再去竞争获得锁。锁的释放情况有以下两种:

  • 当正常执行完业务逻辑后,客户端主动将临时 ZNode 删除,此时锁被释放;
  • 当获得锁的客户端发生宕机时,临时 ZNode 会被自动删除,此时认为锁已经释放。

当锁被释放后,其他服务节点则再次去竞争性地进行创建,但每次都只有一个服务节点能够获取到锁,这就是排他锁。

10.5 集群管理

Zookeeper 还能解决大多数分布式系统中的问题:

  • 如可以通过创建临时节点来建立心跳检测机制。如果分布式系统的某个服务节点宕机了,则其持有的会话会超时,此时该临时节点会被删除,相应的监听事件就会被触发。
  • 分布式系统的每个服务节点还可以将自己的节点状态写入临时节点,从而完成状态报告或节点工作进度汇报。
  • 通过数据的订阅和发布功能,Zookeeper 还能对分布式系统进行模块的解耦和任务的调度。
  • 通过监听机制,还能对分布式系统的服务节点进行动态上下线,从而实现服务的动态扩容。

11:Zookeeper ACL

一:前言:

为了避免存储在 Zookeeper 上的数据被其他程序或者人为误修改,Zookeeper 提供了 ACL(Access Control Lists) 进行权限控制。只有拥有对应权限的用户才可以对节点进行增删改查等操作。下文分别介绍使用原生的 Shell 命令和 Apache Curator 客户端进行权限设置。

二、使用Shell进行权限管理

2.1 设置与查看权限

想要给某个节点设置权限 (ACL),有以下两个可选的命令:

 # 1.给已有节点赋予权限
 setAcl path acl
 
 # 2.在创建节点时候指定权限
 create [-s] [-e] path data acl

查看指定节点的权限命令如下:

getAcl path
2.2 权限组成

Zookeeper 的权限由[scheme : id :permissions]三部分组成,其中 Schemes 和 Permissions 内置的可选项分别如下:

Permissions 可选项

  • CREATE:允许创建子节点;
  • READ:允许从节点获取数据并列出其子节点;
  • WRITE:允许为节点设置数据;
  • DELETE:允许删除子节点;
  • ADMIN:允许为节点设置权限。

Schemes 可选项

  • world:默认模式,所有客户端都拥有指定的权限。world 下只有一个 id 选项,就是 anyone,通常组合写法为 world:anyone:[permissons]
  • auth:只有经过认证的用户才拥有指定的权限。通常组合写法为 auth:user:password:[permissons],使用这种模式时,你需要先进行登录,之后采用 auth 模式设置权限时,userpassword 都将使用登录的用户名和密码;
  • digest:只有经过认证的用户才拥有指定的权限。通常组合写法为 auth:user:BASE64(SHA1(password)):[permissons],这种形式下的密码必须通过 SHA1 和 BASE64 进行双重加密;
  • ip:限制只有特定 IP 的客户端才拥有指定的权限。通常组成写法为 ip:182.168.0.168:[permissions]
  • super:代表超级管理员,拥有所有的权限,需要修改 Zookeeper 启动脚本进行配置。
2.3 添加认证信息

可以使用如下所示的命令为当前 Session 添加用户认证信息,等价于登录操作。

# 格式
addauth scheme auth 

#示例:添加用户名为heibai,密码为root的用户认证信息
addauth digest heibai:root 
2.4 权限设置示例
1. world模式

world 是一种默认的模式,即创建时如果不指定权限,则默认的权限就是 world。

[zk: localhost:2181(CONNECTED) 32] create /hadoop 123
Created /hadoop
[zk: localhost:2181(CONNECTED) 33] getAcl /hadoop
'world,'anyone    #默认的权限
: cdrwa
[zk: localhost:2181(CONNECTED) 34] setAcl /hadoop world:anyone:cwda   # 修改节点,不允许所有客户端读
....
[zk: localhost:2181(CONNECTED) 35] get /hadoop
Authentication is not valid : /hadoop     # 权限不足

2. auth模式
[zk: localhost:2181(CONNECTED) 36] addauth digest heibai:heibai  # 登录
[zk: localhost:2181(CONNECTED) 37] setAcl /hadoop auth::cdrwa    # 设置权限
[zk: localhost:2181(CONNECTED) 38] getAcl /hadoop                # 获取权限
'digest,'heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=   #用户名和密码 (密码经过加密处理),注意返回的权限类型是 digest
: cdrwa

#用户名和密码都是使用登录的用户名和密码,即使你在创建权限时候进行指定也是无效的
[zk: localhost:2181(CONNECTED) 39] setAcl /hadoop auth:root:root:cdrwa    #指定用户名和密码为 root
[zk: localhost:2181(CONNECTED) 40] getAcl /hadoop
'digest,'heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=  #无效,使用的用户名和密码依然还是 heibai
: cdrwa

3. digest模式
[zk:44] create /spark "spark" digest:heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=:cdrwa  #指定用户名和加密后的密码
[zk:45] getAcl /spark  #获取权限
'digest,'heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=   # 返回的权限类型是 digest
: cdrwa

到这里你可以发现使用 auth 模式设置的权限和使用 digest 模式设置的权限,在最终结果上,得到的权限模式都是 digest。某种程度上,你可以把 auth 模式理解成是 digest 模式的一种简便实现。因为在 digest 模式下,每次设置都需要书写用户名和加密后的密码,这是比较繁琐的,采用 auth 模式就可以避免这种麻烦。

4. ip模式

限定只有特定的 ip 才能访问。

[zk: localhost:2181(CONNECTED) 46] create  /hive "hive" ip:192.168.0.108:cdrwa  
[zk: localhost:2181(CONNECTED) 47] get /hive
Authentication is not valid : /hive  # 当前主机已经不能访问

这里可以看到当前主机已经不能访问,想要能够再次访问,可以使用对应 IP 的客户端,或使用下面介绍的 super 模式。

5. super模式

需要修改启动脚本 zkServer.sh,并在指定位置添加超级管理员账户和密码信息:

"-Dzookeeper.DigestAuthenticationProvider.superDigest=heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=" 

在这里插入图片描述
修改完成后需要使用 zkServer.sh restart 重启服务,此时再次访问限制 IP 的节点:

[zk: localhost:2181(CONNECTED) 0] get /hive  #访问受限
Authentication is not valid : /hive
[zk: localhost:2181(CONNECTED) 1] addauth digest heibai:heibai  # 登录 (添加认证信息)
[zk: localhost:2181(CONNECTED) 2] get /hive  #成功访问
hive
cZxid = 0x158
ctime = Sat May 25 09:11:29 CST 2019
mZxid = 0x158
mtime = Sat May 25 09:11:29 CST 2019
pZxid = 0x158
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0

三、使用Java客户端进行权限管理

3.1 主要依赖

这里以 Apache Curator 为例,使用前需要导入相关依赖,完整依赖如下:

<dependencies>
    <!--Apache Curator 相关依赖-->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>4.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>4.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.13</version>
    </dependency>
    <!--单元测试相关依赖-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
3.2 权限管理API

Apache Curator 权限设置的示例如下:

public class AclOperation {

    private CuratorFramework client = null;
    private static final String zkServerPath = "192.168.0.226:2181";
    private static final String nodePath = "/hadoop/hdfs";

    @Before
    public void prepare() {
        RetryPolicy retryPolicy = new RetryNTimes(3, 5000);
        client = CuratorFrameworkFactory.builder()
                .authorization("digest", "heibai:123456".getBytes()) //等价于 addauth 命令
                .connectString(zkServerPath)
                .sessionTimeoutMs(10000).retryPolicy(retryPolicy)
                .namespace("workspace").build();
        client.start();
    }

    /**
     * 新建节点并赋予权限
     */
    @Test
    public void createNodesWithAcl() throws Exception {
        List<ACL> aclList = new ArrayList<>();
        // 对密码进行加密
        String digest1 = DigestAuthenticationProvider.generateDigest("heibai:123456");
        String digest2 = DigestAuthenticationProvider.generateDigest("ying:123456");
        Id user01 = new Id("digest", digest1);
        Id user02 = new Id("digest", digest2);
        // 指定所有权限
        aclList.add(new ACL(Perms.ALL, user01));
        // 如果想要指定权限的组合,中间需要使用 | ,这里的|代表的是位运算中的 按位或
        aclList.add(new ACL(Perms.DELETE | Perms.CREATE, user02));

        // 创建节点
        byte[] data = "abc".getBytes();
        client.create().creatingParentsIfNeeded()
                .withMode(CreateMode.PERSISTENT)
                .withACL(aclList, true)
                .forPath(nodePath, data);
    }


    /**
     * 给已有节点设置权限,注意这会删除所有原来节点上已有的权限设置
     */
    @Test
    public void SetAcl() throws Exception {
        String digest = DigestAuthenticationProvider.generateDigest("admin:admin");
        Id user = new Id("digest", digest);
        client.setACL()
                .withACL(Collections.singletonList(new ACL(Perms.READ | Perms.DELETE, user)))
                .forPath(nodePath);
    }

    /**
     * 获取权限
     */
    @Test
    public void getAcl() throws Exception {
        List<ACL> aclList = client.getACL().forPath(nodePath);
        ACL acl = aclList.get(0);
        System.out.println(acl.getId().getId() 
                           + "是否有删读权限:" + (acl.getPerms() == (Perms.READ | Perms.DELETE)));
    }

    @After
    public void destroy() {
        if (client != null) {
            client.close();
        }
    }
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐