学习
实践
活动
专区
工具
TVP
写文章

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:2181log4j: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 的节点,这是自带的,不用管。

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

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

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

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

伪集群模式

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

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

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

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

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

复制代码

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

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

zk1:

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

复制代码

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

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

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

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

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

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

集群模式

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

关于 ZooKeeper 是怎么实现数据同步的,可以看看 Zookeeper 的ZAB协议

设计模式

官网在这

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

ZNode

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

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

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

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 提供如下的同步保障:

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

让我们来理理看似冲突的 3⃣️和 5⃣️。第三条只能确保客户端在连接到任意一个服务器时,看到的是最新的系统镜像,但是不能保证过了一段时间之后还是最新的,这刚好对应第五条。为什么呢?因为 ZooKeeper 不是强一致性,它不能保证服务器 A(不是客户端 A)做出的更新,服务器 B 也会同时更新,这就涉及到 ZooKeeper 的原理,我这里简单说一下:

以下步骤我们均假设 ZooKeeper 集群工作正常,没有任何一台服务器宕机。

  • Client 发起一个写请求到了 ServerA,ServerA 把这个请求给发送到 Leader。
  • Leader 收到请求,包装成一个事务操作,然后广播到所有的 Follow,这是广播第一个阶段。
  • Follow 收到来自 Leader 的广播,把事务同步到磁盘,返回一个 ACK 确认响应。
  • Leader 收到 ACK,如果收到的 ACK 大于一半,则广播 Commit,此次请求结束。

P.S. 这里的广播写入到了 Follow 的一个队列里面去,也就是说 Follow 接收到的 Commit 一定在事务之后,所以不用担心 Commit 先于事务被执行。

从上面我们可以看到一些问题,比如收到的是半数以上的 ACK 而不是全部,所有可能存在某些服务器网络延迟大,而过了好久才完成事务的提交;但是,即使是那些发送了 ACK 的,他们彼此之间也可能因为执行速度等差异,导致对于事务的提交有先有后,Leader 只是广播了 Commit 而不论 Commit 的 ACK,所以就出现了在某一很短的时间内,连接到两个不同 Follow 的客户端读到的数据可能不一致

知道了这个,我们就知道 3⃣️和 5⃣️为什么能共存了。客户端连接到服务器时,会触发强制同步操作,因此这个时候客户端看到的总是最新的系统镜像,运行一段时间之后就不能保证了,想要得到此保证,可以在获取数据之前执行 Sync 操作强制等待当前 Follow 的所有事务被提交。

所以程序员不能假定每次客户端获取到的数据都是最新的,这一点需要铭记。

来看看官网的忠告:

Sometimes developers mistakenly assume one other guarantee that ZooKeeper does not in fact make. This is: * Simultaneously Consistent Cross-Client Views* : ZooKeeper does not guarantee that at every instance in time, two different clients will have identical views of ZooKeeper data. Due to factors like network delays, one client may perform an update before another client gets notified of the change. Consider the scenario of two clients, A and B. If client A sets the value of a znode /a from 0 to 1, then tells client B to read /a, client B may read the old value of 0, depending on which server it is connected to. If it is important that Client A and Client B read the same value, Client B should call the sync() method from the ZooKeeper API method before it performs its read. So, ZooKeeper by itself doesn't guarantee that changes occur synchronously across all servers, but ZooKeeper primitives can be used to construct higher level functions that provide useful client synchronization. (For more information, see the ZooKeeper Recipes.

官网这话大致意思就是:ZooKeeper 不能保证跨客户端的一致性视图,即,不能保证在每一个 Server 中,某一时刻两个客户端看到的数据是一致的。比如客户端 A 更新了节点/a 的数据,然后客户端 B 去读节点/a,那可能读到旧的数据,因为由于网络因素客户端 A 做出的更新还没被同步到客户端 B 连接的服务器。如果一定要这么做,那颗客户端 B 在读之前可以进行 Sync 操作,或者我们可以在数据上设置 Watches,这样在数据被更新就可以得到通知。

ZooKeeper 本身不保证跨 Servers 的同步操作,但是用户可以使用其他语义完成这一操作。

ZooKeeper 使用技巧

这里直接给官网链接,放出了一些 ZooKeeper 的应用场景。

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集群

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/4b54bfccfc4b54a3dbd64c3cd
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

关注

腾讯云开发者公众号
10元无门槛代金券
洞察腾讯核心技术
剖析业界实践案例
腾讯云开发者公众号二维码

扫码关注腾讯云开发者

领取腾讯云代金券