ZooKeeper 是什么?
Zoo 是动物园的意思,Keeper 是管理员的意思,动物园里有各种动物,它们有的脾气暴躁,有的能爬树,有的能唱歌,还有的甚至能跳舞,所以我们需要一个管理员来管理,动物园管理员就是 ZooKeeper。
我们的程序千奇百怪各种各样,当然也需要一个 Boss 来管理咯!
本文组织结构为跳跃式,如果你哪个名词概念不理解,可以看看后面的章节。
xdm 我在这放一个官方文档链接,我感觉官方文档写的蛮清楚的,建议你先看看。如果觉得我哪里写的不好就评论告诉我,这玩意我才看了一周就结束了,肯定少了不少理论。
这一步比较简单,就是下载,解压缩,配置一下东西,然后跑。
官网的教程其实已经非常明了了,我就不再多演示了。
算了我还是逼逼两句吧。
# 时钟滴答数,毫秒,作为以及时间基本单位,后面的时间都是它的N倍。tickTime=2000# 数据目录,指出要在哪里存放数据。dataDir=/var/lib/zookeeper# 暴露给服务端去连接的端口,就是监听端口clientPort=2181
复制代码
./zkServer.sh start
复制代码
./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 实例,让它们不要监听同一个端口就行了。
我这里创建了三个文件夹,每个文件夹下面都有一个 ZK 实例,这里为什么要三个呢?而且最好是奇数呢?因为 ZK 要求集群中只要有一半以上可用,集群就是可用的,所以是奇数,所以最少三个。
echo [服务器ID] > myid
复制代码
在这里我的每一个实例的数据保存在 data 文件夹里,所以我 cd data 然后输入 echo [server.id] > myid,回车即可。
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 需要改一下,这样每个实例都会监听不同的客户端端口。
我们首先看看各个服务器的数据:
还记得我们在单例模式创建了一个节点并设置了数据吗?我把那个节点作为了 1 号服务器,现在我们通过 2 号服务器查询得到如下:
可以很明显的看到,数据被同步了,设置在 1 号服务器的数据出现在了 2 号服务器,说明集群成功了!
然后读者可以尝试在三个客户端任意设置创建节点,设置数据,看其他节点的下面有没有被同步。
这个简单的很,把伪集群的 IP 和 PORT 换成实际服务器就行了,就结束了。
关于 ZooKeeper 是怎么实现数据同步的,可以看看 Zookeeper 的ZAB协议。
官网在这
ZK 的数据模型。我们上面稍微提了一下,就是每个 ZK 节点都有一个根结点'/',每个根结点可以设置我们需要的子节点,子节点也可以设置子节点...,每个节点可以拥有 0/N 的子节点,同时可以绑定 0/1 个数据。如果我们把每个 ZK 实例类比为一个文件系统,那么每个节点就是一个文件夹,这个文件夹只能包含最多一个文件和 N 个文件夹。
ZK 中的每一个节点,我们称为 ZNode,每一个 ZNode 通过路径进行唯一标识,就像文件夹通过文件夹路径唯一标识一样。
每个 ZNode 包含一个状态的数据结构,用来记录数据版本号、时间戳等信息以及访问权限。数据版本号和时间戳可以验证数据的更新是否有效,因为每次对数据更新,版本号都是自动+1。每一次客户端获取这个节点的数据,同时也会得到这个数据的版本号,每次对数据更新时会把自己得到的版本号发过来,ZK 看看是不是等于当前版本号,如果不是,说明被其他数据更新了。
ZNode 是客户端访问的主要目标,所以需要我们好好提提。
客户端可以在一个 ZNode 上添加一个 Watches,每次节点发生了变动,比如被删除了,子节点被删除了,数据被删除了,数据被更新了,Watches 都会通知设置它的客户端,然后它就被删除了,任何一个 Watches 的存活周期都是一个 ZNode 状态变更。
ZNode 数据的读写都是原子的,每次读会读取全部数据,写会覆盖原始数据并写入全部数据。
此外,ZNode 能保存的数据大小不超过 1M,也就是 1024KB,官方给出的建议是越小越好,不然大数据涉及到更多的 IO 和网络操作,会造成同步出现延迟现象。这么小的数据我们可以放配置文件,也可以放关键数据,比如 Redis 的键,如果你非要放大数据,可以把数据存在别的地方,然后这里放存放地点的指针。
临时节点,顾名思义,由客户端创建,并在客户端连接关闭后自动删除。
这一特性有很多应用,比如我们可以做集群监控:每一个服务器会在启动时在其他服务器上创建一个临时节点,这样当这个服务器宕机时,其他服务器里保存的这个服务器创建的临时节点就会被删除,这样大家就知道谁 down 了,谁还在工作。
持久化节点,顾名思义,在客户端连接断开时不会被删除。
顺序持久化节点是有序的,连接断开它不会被删除。为什么说它是顺序的呢?
当客户端申请在某个节点下创建顺序节点时,ZK 会在新创建的节点名后面添加一个计数器,这个计数器是单调递增的,且格式为 %010d。这样每次创建的节点就是有序的,同时命名也是唯一的,既然命名唯一,那就可以做集群中的命名服务。
有序但是临时的节点。一个很好的应用就是分布式锁。
如果我们想用顺序临时节点实现分布式锁,可以这么做:
现在来回答几个问题。
一、为什么是临时顺序节点?顺序节点好理解,为了确保每次都是当前节点列表里最小的节点获得了锁,临时节点的意义在于当锁拥有者宕机时,节点会自动删除,不会触发死锁。二、这样做有什么好处?好处之一就是避免的惊群效应,也就是一把锁的释放不会导致所有进程来竞争锁,同时实现了公平锁。
说到锁,我们再来说一下如何创建非公平、抢占式的排他锁,以及共享锁。
先说排他锁:
再来看看共享锁:
菜鸟教程说的很明白,大家可以看看。
同时也有一些封装好的基于 ZK 的分布式锁,大家可以直接使用。
新特性,暂时不提
新特性,暂时不提
这个了解就行,我不翻译了,我直接贴:
前面提到,状态结构用来记录节点的状态信息等,我也不翻译了,直接贴,看看状态结构保存了哪些信息:
一个 Watches 就是一个事件触发器,每次它监听的节点数据变化时,它就会被触发,然后通知设置它的客户端,然后被删除。
任何对于节点的读操作都可以附带地进行一个 Watches 的设置操作。比如 getData(),getChildren()和 exists()。
通过上面那段话,我们可以总结出三个关于 Watches 的特性:
当客户端断开重连后,之前设置的 Watches 可以继续使用;如果连接到新的服务器,那就会触发 ZK 的会话事件。
对于 Watches 的触发,只能被三种读操作触发,现在来看看具体的细节:
来看一下 ZK 支持的访问控制:
ZK 提供如下的同步保障:
让我们来理理看似冲突的 3⃣️和 5⃣️。第三条只能确保客户端在连接到任意一个服务器时,看到的是最新的系统镜像,但是不能保证过了一段时间之后还是最新的,这刚好对应第五条。为什么呢?因为 ZooKeeper 不是强一致性,它不能保证服务器 A(不是客户端 A)做出的更新,服务器 B 也会同时更新,这就涉及到 ZooKeeper 的原理,我这里简单说一下:
以下步骤我们均假设 ZooKeeper 集群工作正常,没有任何一台服务器宕机。
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 的应用场景。
这节主要告诉你怎么用 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(); }}
复制代码
然后放几个其他的例子
领取专属 10元无门槛券
私享最新 技术干货