ZooKeeper实战

概述

ZooKeeper是什么?

Zoo是动物园的意思,Keeper是管理员的意思,动物园里有各种动物,它们有的脾气暴躁,有的能爬树,有的能唱歌,还有的甚至能跳舞,所以我们需要一个管理员来管理,动物园管理员就是ZooKeeper。

我们的程序千奇百怪各种各样,当然也需要一个Boss来管理咯!

本文组织结构为跳跃式,如果你哪个名词概念不理解,可以看看后面的章节。

xdm我在这放一个官方文档链接,我感觉官方文档写的蛮清楚的,建议你先看看。如果觉得我哪里写的不好就评论告诉我,这玩意我才看了一周就结束了,肯定少了不少理论。

起步、HelloWorld

这一步比较简单,就是下载,解压缩,配置一下东西,然后跑。

官网的教程其实已经非常明了了,我就不再多演示了。

算了我还是逼逼两句吧。

单例模式

  • 在conf文件夹下创建一个zoo.cfg文件,ZK会检测conf文件夹,如果发现存在用户自定义的文件,那就使用,否则使用默认的。
# 时钟滴答数,毫秒,作为以及时间基本单位,后面的时间都是它的N倍。
tickTime=2000
# 数据目录,指出要在哪里存放数据。
dataDir=/var/lib/zookeeper
# 暴露给服务端去连接的端口,就是监听端口
clientPort=2181
  • 在bin文件夹下./zkServer.sh start进行开始。
./zkServer.sh start
  • 使用命令行连接到服务器,需要在bin文件夹下执行./zkCli.sh -server 127.0.0.1:2181
./zkCli.sh -server 127.0.0.1:2181
  • 连接成功会看到如下输出:
Connecting to localhost:2181
log4j:WARN No appenders could be found for logger (org.apache.zookeeper.ZooKeeper).
log4j:WARN Please initialize the log4j system properly.
Welcome to ZooKeeper!
JLine support is enabled
[zkshell: 0]

更多命令详情请看官网。

每个ZK在初始状态下都有一个根结点'/',然后你可以在这下面创建一个节点,比如我创建了一个test_data节点,那这个节点的路径就是/test_data,同时每个节点还能存放数据。这一点和Unix/Linux文件系统很像,你可以把节点看成文件夹,节点里的数据看成文件;如果当前节点不是叶子结点,那这个节点就包含一个或多个“文件夹”和一个“文件”,希望这能让读者理解ZK的节点概念。

现在我们通过客户端创建一个节点并写入数据:

首先查看根结点:

ZooKeeper实战_第1张图片

根结点下面有一个zookeeper的节点,这是自带的,不用管。

然后我们创建一个节点,就叫zk_test,并写入数据:

ZooKeeper实战_第2张图片

查看节点情况和节点里的数据:

ZooKeeper实战_第3张图片

我们也可以设置新的数据并读取:

ZooKeeper实战_第4张图片

至此,一个单例的ZK就结束了!其他命令详见官网。

伪集群模式

ZooKeeper实战_第5张图片

如果你和我一样没有钱或者只是想在本地调试,那我们可以考虑使用伪集群模式,在本地开多个ZK实例实现集群。

众所周知,一个网络进程可以由IP:PORT唯一确定,所以我们可以设置多个ZK实例,让它们不要监听同一个端口就行了。

  • 1⃣️首先创建多个ZK实例文件夹:

ZooKeeper实战_第6张图片

我这里创建了三个文件夹,每个文件夹下面都有一个ZK实例,这里为什么要三个呢?而且最好是奇数呢?因为ZK要求集群中只要有一半以上可用,集群就是可用的,所以是奇数,所以最少三个。

ZooKeeper实战_第7张图片

ZooKeeper实战_第8张图片

ZooKeeper实战_第9张图片

  • 2⃣️接下来记得在数据目录(就是配置文件里的那个目录)里创建一个myid的文件,里面包含一个数字,表明当前服务器的ID。
echo [服务器ID] > myid

ZooKeeper实战_第10张图片

在这里我的每一个实例的数据保存在data文件夹里,所以我cd data然后输入echo [server.id] > myid,回车即可。

  • 3⃣️然后修改每个实例的配置文件:

zk1:

tickTime=2000
dataDir=/usr/local/zk/zk1/data
clientPort=8391
# 初始连接时间=10 * tickTime,如果超时说明它与目标服务器不可达
initLimit=10
# 同步时间=5 * tickTime,如果超时说明同步失败,可能断网了或目标服务器宕机了
syncLimit=5
admin.serverPort=8491
# 表明怎么找到服务器1
server.1=127.0.0.1:2888:3888
# 怎么找到服务器2
server.2=127.0.0.1:2889:3889
# 怎么找到服务器3,这里注意,有两个端口,第一个端口是follow和leader,leader和follow之间通信用的,第二个端口是在leader宕机时,follow与follow之间投票选举用的。
server.3=127.0.0.1:2890:3890

ZooKeeper实战_第11张图片

zk2和zk3同理。⚠️zk2和zk3的clientPort和admin.serverPort需要改一下,这样每个实例都会监听不同的客户端端口。

  • 4⃣️最后我们就可以依次开启服务器来实现伪集群了。

ZooKeeper实战_第12张图片

ZooKeeper实战_第13张图片

ZooKeeper实战_第14张图片

我们首先看看各个服务器的数据:

还记得我们在单例模式创建了一个节点并设置了数据吗?我把那个节点作为了1号服务器,现在我们通过2号服务器查询得到如下:

ZooKeeper实战_第15张图片

可以很明显的看到,数据被同步了,设置在1号服务器的数据出现在了2号服务器,说明集群成功了!

然后读者可以尝试在三个客户端任意设置创建节点,设置数据,看其他节点的下面有没有被同步。

集群模式

这个简单的很,把伪集群的IP和PORT换成实际服务器就行了,就结束了。

设计模式

官网在这

ZK的数据模型。我们上面稍微提了一下,就是每个ZK节点都有一个根结点'/',每个根结点可以设置我们需要的子节点,子节点也可以设置子节点...,每个节点可以拥有0/N的子节点,同时可以绑定0/1个数据。如果我们把每个ZK实例类比为一个文件系统,那么每个节点就是一个文件夹,这个文件夹只能包含最多一个文件和N个文件夹。

ZooKeeper实战_第16张图片

ZNode

ZK中的每一个节点,我们称为ZNode,每一个ZNode 通过路径进行唯一标识,就像文件夹通过文件夹路径唯一标识一样。

每个ZNode包含一个状态的数据结构,用来记录数据版本号、时间戳等信息以及访问权限。数据版本号和时间戳可以验证数据的更新是否有效,因为每次对数据更新,版本号都是自动+1。每一次客户端获取这个节点的数据,同时也会得到这个数据的版本号,每次对数据更新时会把自己得到的版本号发过来,ZK看看是不是等于当前版本号,如果不是,说明被其他数据更新了。

ZNode是客户端访问的主要目标,所以需要我们好好提提。

ZooKeeper实战_第17张图片

Watches

客户端可以在一个ZNode上添加一个Watches,每次节点发生了变动,比如被删除了,子节点被删除了,数据被删除了,数据被更新了,Watches都会通知设置它的客户端,然后它就被删除了,任何一个Watches的存活周期都是一个ZNode状态变更。

数据访问

ZNode数据的读写都是原子的,每次读会读取全部数据,写会覆盖原始数据并写入全部数据。

此外,ZNode能保存的数据大小不超过1M,也就是1024KB,官方给出的建议是越小越好,不然大数据涉及到更多的IO和网络操作,会造成同步出现延迟现象。这么小的数据我们可以放配置文件,也可以放关键数据,比如Redis的键,如果你非要放大数据,可以把数据存在别的地方,然后这里放存放地点的指针。

临时节点

临时节点,顾名思义,由客户端创建,并在客户端连接关闭后自动删除。

这一特性有很多应用,比如我们可以做集群监控:每一个服务器会在启动时在其他服务器上创建一个临时节点,这样当这个服务器宕机时,其他服务器里保存的这个服务器创建的临时节点就会被删除,这样大家就知道谁down了,谁还在工作。

持久化节点

持久化节点,顾名思义,在客户端连接断开时不会被删除。

顺序持久化节点

顺序持久化节点是有序的,连接断开它不会被删除。为什么说它是顺序的呢?

当客户端申请在某个节点下创建顺序节点时,ZK会在新创建的节点名后面添加一个计数器,这个计数器是单调递增的,且格式为%010d。这样每次创建的节点就是有序的,同时命名也是唯一的,既然命名唯一,那就可以做集群中的命名服务。

顺序临时节点

有序但是临时的节点。一个很好的应用就是分布式锁。

如果我们想用顺序临时节点实现分布式锁,可以这么做:

  • 在指定路径下(比如/locks)创建一个临时顺序节点。
  • 通过getChildren()方法获取/locks下的所有节点。
  • 如果当前节点是最小的,说明得到了锁,执行;否则在当前节点前一个节点注册一个监听。
  • 如果得到了锁,执行完毕,释放锁,释放锁通过删除自己的临时节点实现,这个操作会触发监听在这个节点上的Watches,然后通知下一个节点来获取锁。

现在来回答几个问题。

一、为什么是临时顺序节点?顺序节点好理解,为了确保每次都是当前节点列表里最小的节点获得了锁,临时节点的意义在于当锁拥有者宕机时,节点会自动删除,不会触发死锁。
二、这样做有什么好处?好处之一就是避免的惊群效应,也就是一把锁的释放不会导致所有进程来竞争锁,同时实现了公平锁。

说到锁,我们再来说一下如何创建非公平、抢占式的排他锁,以及共享锁。

先说排他锁:

  • 在指定路径下,创建一个同名临时节点,因为ZK会保证多个客户端创建时只会有一个成功,所以只会有一个客户端拿到了锁(创建节点成功了)。
  • 如果节点创建失败,说明有别的客户端拿到了锁,我们可以在这个节点上注册一个监听,当节点被删除,就可以再次进行竞争。
  • 如果拿到了锁,就执行,然后释放锁(删除节点)。

再来看看共享锁:

  • 在指定路径下创建临时顺序节点,并指出节点类型(通过在节点前缀加R/W来标识是读操作还是写操作),这里顺便一提,顺序节点的自增和节点名没有任何关系,你只要创建了一个顺序节点,这个顺序节点的后缀就是自增的。
  • 当想要进行读操作,就看自己是不是最小的,如果不是,就看自己前面节点有没有写操作;如果自己是最小的/前面没有写操作,就可以进行读了。否则在前面最大的写节点上注册Watches。
  • 当想要进行读操作,就看自己是不是最小的,如果不是,就在自己前面的节点注册一个Watches。

菜鸟教程说的很明白,大家可以看看

同时也有一些封装好的基于ZK的分布式锁,大家可以直接使用。

容器节点

新特性,暂时不提

TTL节点

新特性,暂时不提

ZK时间格式

这个了解就行,我不翻译了,我直接贴:

  • Zxid Every change to the ZooKeeper state receives a stamp in the form of a zxid (ZooKeeper Transaction Id). This exposes the total ordering of all changes to ZooKeeper. Each change will have a unique zxid and if zxid1 is smaller than zxid2 then zxid1 happened before zxid2.
  • Version numbers Every change to a node will cause an increase to one of the version numbers of that node. The three version numbers are version (number of changes to the data of a znode), cversion (number of changes to the children of a znode), and aversion (number of changes to the ACL of a znode).
  • Ticks When using multi-server ZooKeeper, servers use ticks to define timing of events such as status uploads, session timeouts, connection timeouts between peers, etc. The tick time is only indirectly exposed through the minimum session timeout (2 times the tick time); if a client requests a session timeout less than the minimum session timeout, the server will tell the client that the session timeout is actually the minimum session timeout.
  • Real time ZooKeeper doesn't use real time, or clock time, at all except to put timestamps into the stat structure on znode creation and znode modification.

ZK状态结构

前面提到,状态结构用来记录节点的状态信息等,我也不翻译了,直接贴,看看状态结构保存了哪些信息:

  • czxid The zxid of the change that caused this znode to be created.
  • mzxid The zxid of the change that last modified this znode.
  • pzxid The zxid of the change that last modified children of this znode.
  • ctime The time in milliseconds from epoch when this znode was created.
  • mtime The time in milliseconds from epoch when this znode was last modified.
  • version The number of changes to the data of this znode.
  • cversion The number of changes to the children of this znode.
  • aversion The number of changes to the ACL of this znode.
  • ephemeralOwner The session id of the owner of this znode if the znode is an ephemeral node. If it is not an ephemeral node, it will be zero.
  • dataLength The length of the data field of this znode.
  • numChildren The number of children of this znode.

ZK的Watches

一个Watches就是一个事件触发器,每次它监听的节点数据变化时,它就会被触发,然后通知设置它的客户端,然后被删除。

任何对于节点的读操作都可以附带地进行一个Watches的设置操作。比如getData(),getChildren()和exists()。

通过上面那段话,我们可以总结出三个关于Watches的特性:

  • 一次性触发。Watches事件一旦被触发,Watches就会被删除,除非客户端再次设置,否则同一个节点的再次更改也无法让客户端得到通知。
  • 对客户端的通知。如果一个客户端没有设置Watches,那么无论ZK的节点怎么变,客户端都不知道;此外,即使设置了Watches,也不能保证永远看到最新的数据;怎么理解呢?比如客户端在某个节点设置了一个Watches,然后数据更改,Watches被发送会设置它的客户端,但是在发送途中又有其他客户端修改了数据,此时的节点又发生了变化,而因为Watches还在发送,所以数据的第二次更新就丢失了。原因在于Watches的发送是异步的,ZK不会等待Watches被成功发送才进行下一步操作。其实也很好理解,毕竟让整个集群等待网卡的用户是不可能的。
  • 作用于不同类型的Watches。Watches可以分为两种,一种是对节点数据变更的监视,另一种是对子节点的监视,虽然客户端可以通过读节点操作设置Watches,但是设置的Watches却不一定是同类型的。比如说,setData()会触发这个节点上的DataWatches,而create()则会触发父节点的ChildWatches和DataWatches,delete()操作则会触发DataWatches和父节点的ChildWatchs以及子节点的ChildWatches。

当客户端断开重连后,之前设置的Watches可以继续使用;如果连接到新的服务器,那就会触发ZK的会话事件。

对于Watches的触发,只能被三种读操作触发,现在来看看具体的细节:

  • Created Event: 由exists()触发。
  • Deleted Event: 由exists(),getData(),和getChildren()触发。
  • Changed Event: 由exists()和getData()触发。
  • Child Event: 由getChildren()触发。

ZK的访问控制

来看一下ZK支持的访问控制:

  • CREATE: you can create a child node
  • READ: you can get data from a node and list its children.
  • WRITE: you can set data for a node
  • DELETE: you can delete a child node
  • ADMIN: you can set permissions

ZK的一致性保障

ZK提供如下的同步保障:

  • 顺序同步。同一个客户端发起的多个更新操作被执行的顺序和它们被发送的顺序一致。
  • 原子性。更新操作只有成功或者失败两个结果。
  • 单一镜像。无论客户端连接到了哪个实例,它所看到的都是一致的,整个ZK集群对外暴露的就是一个实例镜像。
  • 可靠性。一旦更新成功,数据就会持久化,直到下一次更新发生。
  • 时效性。ZK可以保证客户端在某一时间范围内看到的系统是最新的,在这个时间范围内,系统的更新也可以被客户端得知。

ZooKeeperJavaClient

这节主要告诉你怎么用Java访问ZK服务器并进行操作。

当使用客户端连接时,可以指定多个IP:PORT,客户端会随便选一个,然后连接,如果失败,就尝试另一个直至成功。如果中途断开,也会尝试重新连接。

这里仅给出最简单的用法:

public class Main {
    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:8392", 2000, event -> System.out.println(atomicInteger.incrementAndGet() + ": " + event.toString()));
        if (zooKeeper.exists("/zk_test1", true) == null) {
            // System.out.println("节点: /zk_test1不存在,准备创建");
            zooKeeper.create("/zk_test1", "test_data".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        if (zooKeeper.exists("/zk_test1/sub1", true) == null) {
            // System.out.println("节点: /zk_test1/sub1不存在,准备创建");
            zooKeeper.create("/zk_test1/sub1", "sub_test_data".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        if (zooKeeper.exists("/zk_test1/sub1", true) != null) {
            Stat stat = new Stat();
            zooKeeper.getData("/zk_test1/sub1", true, stat);
            // System.out.println("data ver: " + stat.getVersion());
            // set数据时,zookeeper会自动把数据版本+1
            zooKeeper.setData("/zk_test1/sub1", "sub_test_data0".getBytes(), stat.getVersion());
            zooKeeper.getData("/zk_test1/sub1", true, stat);
            // System.out.println("data ver: " + stat.getVersion());
        }
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
        zooKeeper.delete("/zk_test1/sub1", -1);
        zooKeeper.delete("/zk_test1", -1);
        zooKeeper.close();
    }
}

然后放几个其他的例子

ZooKeeper基本操作

参考

ZooKeeper官方文档

如何构建ZooKeeper集群

你可能感兴趣的:(javazookeeper)