一文学会 ZooKeeper
一文学会 ZooKeeper
ZooKeeper 的由来
ZooKeeper 是雅虎为了解决内部诸多服务间例如 Master 选举、资源访问控制等与分布式服务间协调相关的问题,基于谷歌的 Chubby 闭源分布式锁服务的设计思想开发的。后来雅虎将 ZooKeeper 捐赠给了 Apache,Apache 将其更名为 Apache ZooKeeper,被广泛应用于各种分布式系统中,阿里巴巴的分布式服务 Dubbo 就是使用 ZooKeeper 来实现分布式协调。
ZooKeeper 官网 http://zookeeper.apache.org/
ZooKeeper 的安装和运行
本文介绍的 Zookeeper 是以 3.5.5 这个稳定版本为基础,最新的版本可以通过官网 https://apache.org/dist/zookeeper/stable/ 来获取,Zookeeper 的安装非常简单,下面将从单机模式和集群模式两个方面介绍 Zookeeper 的安装和配置。
单机模式
获取到 Zookeeper 的压缩包并解压到某个目录如:/opt/apache-zookeeper-3.5.5 下,Zookeeper 的启动脚本在 bin 目录下,Linux 下的启动脚本是 zkServer.sh,Windows 下的启动脚本是 zkServer.cmd。在执行启动脚本之前,还有几个基本的配置项需要配置一下。Zookeeper 的配置文件在 conf 目录下,这个目录下有 zoo_sample.cfg 和 log4j.properties,需要将 zoo_sample.cfg 复制改名为 zoo.cfg,因为 ZooKeeper 在启动时会找这个文件作为默认配置文件。这个配置文件中各个配置项的意义。
# 这个时间是作为 ZooKeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳。 tickTime=2000 # ZooKeeper 保存数据的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里。 # 以下为 Linux 下的路径,Windows 下请填写 Windows 下的路径 dataDir=/var/zookeeper/data/zk1 # 客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。 clientPort=2181
然后使用 `./bin/zkServer.sh start` 启动服务。
集群模式
Zookeeper 支持多实例组成集群来提供服务,这也是实际中主要的应用方式。也可以通过在一台物理机上运行多个 ZooKeeper 实例,也就是所谓的伪集群方式来学习集群模式。Zookeeper 的集群模式仅仅增加几个配置项即可。集群模式除了上面的三个配置项还要增加下面几个配置项:
# 配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 10 个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 5*2000=10 秒 initLimit=5 # 标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 2*2000=4 秒 syncLimit=2 # 格式 server.${序号}=${IP地址}:${端口号1}:${端口号2} # ${序号} 是一个数字,表示这个是第几号服务器 # ${IP地址} 是这个服务器的 ip 地址 # ${端口号1} 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口 # ${端口号2} 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 ${IP地址} 都是一样的,所以不同的 ZooKeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。 # 以下为在本地启动伪集群的配置,实际应用请根据服务器的地址填写 server.1=0.0.0.0:12887:13887 server.2=0.0.0.0:12888:13888 server.3=0.0.0.0:12889:13889 # 数据保存的路径 # 伪集群下请分别保存到不同的路径下 dataDir=/var/zookeeper/data/zk1 # 客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。 # 伪集群下请设置不同的端口号 clientPort=2181
可以把配置文件放入不同的文件夹,使用 `./bin/zkServer.sh --config <conf 文件夹路径> start` 的方式启动服务。除了修改 zoo.cfg 配置文件,集群模式下还要配置一个文件 myid,这个文件放置在各个实例的 dataDir 目录下,该文件里面就有一个数据就是服务器序号的值,例如上面的配置文件就是记入 1 或 2 或 3。Zookeeper 启动时会读取这个文件,用里面的序号与 zoo.cfg 里面的配置信息比较从而判断到底是自己是哪个 server。
ZooKeeper 的数据模型
Zookeeper 在内存中维护一个具有树形层次关系的数据结构,它非常类似于一个标准的文件系统,如下图所示:
ZooKeeper 这种数据结构有如下这些特点:
1、每个子目录项如 Service 都被称作为 ZNode,这个 ZNode 是被它所在的路径唯一标识,如 Service1.Addr 这个 ZNode 的标识为 /Service/Service1.Addr。
2、每个 ZNode 都可以设置值,最大不能超过 1M
3、ZNode 分为四种类型:持久型(PERSISTENT)、暂时型(EPHEMERAL)、序列型(SEQUENCE)
- 持久型(PERSISTENT)缺省类型,创建后除非明确的删除它,否则会一直保存在服务器上。
- 临时型(EPHEMERAL)创建它的客户端在断开连接后,服务器就会删除该类型的结点,该类型的结点不能包含子结点。
- 序列型(SEQUENCE)可以使用相同的名称重复创建,服务器会自动给它的名字添加 %10d 的数字后缀,序号从0开始。
此外,还有两种附加设置
- 容器型(CONTAINER)该种类型的结点的子结点全部被删除后,服务器会择机删除该结点。
- 时限型(TTL)可以为持久型(PERSISTENT)和序列型(SEQUENCE)的结点设置一个超时时间,如果过了超时时间,该结点不包含子结点且没有发生修改,服务器会择机删除该结点
4、ZNode 是有版本的,每个 ZNode 中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据
5、结点名不得包含字符 '\u0000'、'\u0001' 至 '\u001F'、 '\u007F'、'\u009F'、'\ud800' 至 '\uF8FF', '\uFFF0' 至 'uFFFF'
6、结点名中可以包含字符 '.', '..',但不得单独使用
7、字符串 'zookeeper' 系统保留,不得使用
ZooKeeper 命令行客户端
ZooKeeper 包中提供了一个客户端命令行工具 `zkCli` 让我们可以在终端中使用 ZooKeeper。Linux 下的脚本是 zkCli.sh,Windows 下的脚本是 zkCli.cmd。启动客户端程序:
bin/zkCli.sh -server 127.0.0.1:2181
如果省略 -server 选项,该工具会默认使用 127.0.0.1:2181 来连接服务器。
连接后,我们可以
- 输入 ls / 查看根目录的内容
- 输入 create /Service "服务器信息" 创建一个名为 /Service 的持久型结点,并填充值为字符串值 "服务器信息"
- 输入 create -s /Message/msg "10点举行敏捷站会" 创建一个名为 /Message/msg0000000000 的序列型结点,该条命令可以重复输入,依次创建结点 /Message/msg0000000000、/Message/msg0000000001、/Message/msg0000000002、...
- 输入 delete /Service/Service1.Addr 来删除该结点
ZooKeeper 上数据的访问
ZooKeeper 支持六种对 ZNode 的操作
- Create: 创建一个 ZNode
- Set: 更改一个 ZNode 的值
- Get: 读取一个 ZNode 的值
- Delete: 删除一个 ZNode
- Acl: 更改一个 ZNode 的访问权限
- Watch: 在一个 ZNode 上施加一个监听器,当以上五种操作任意之一执行时可以获得一个通知。一个监听器只能监听到一次事件,客户端在监听到一次事件后如需继续监听,需再执行 Watch 操作。
ZooKeeper 提供的一致性保证
ZooKeeper 作为一种分布式服务协调器,它按照下面的原则来提供数据一致性保证。
1、顺序一致性
来自任意特定客户端的更新都会按其发送顺序被提交。也就是说,如果一个客户端将Znode z的值更新为a,在之后的操作中,它又将z的值更新为b,则没有客户端能够在看到z的值是b之后再看到值a(如果没有其他对z的更新)。
2、原子性
每个更新要么成功,要么失败。这意味着如果一个更新失败,则不会有客户端会看到这个更新的结果。
3、单一系统映像
一个客户端无论连接到哪一台服务器,它看到的都是同样的系统视图。这意味着,如果一个客户端在同一个会话中连接到一台新的服务器,它所看到的系统状态不会比在之前服务器上所看到的更老。当一台服务器出现故障,导致它的一个客户端需要尝试连接集合体中其他的服务器时,所有滞后于故障服务器的服务器都不会接受该连接请求,除非这些服务器赶上故障服务器。
4、持久性
一个更新一旦成功,其结果就会持久存在并且不会被撤销。这表明更新不会受到服务器故障的影响。
ZooKeeper 开发
ZooKeeper 在实际中的主要应用
1、作为服务注册中心,实现分布式系统中各个服务的注册和发现功能。服务器使用长连接在 ZooKeeper 上创建一个临时型(EPHEMERAL)的结点,并填入自己的 IP 地址和端口号。客户端可以在 ZooKeeper 上查到该服务器的信息。由于结点是临时型结点,所以如果发生服务器故障掉线,那么对应的结点也将会被 ZooKeeper 删除,客户端通过监听该结点就可以获知该服务器的在线情况。
2、作为服务配置中心,为分布式系统中各个服务提供统一的配置服务。集群控制者可以为每个应用服务器创建一个持久型结点 /Configuration/App1.Config,在其中存储它的配置信息,应用服务器可以通过监听自己所对应的结点来获取配置变更通知。
3、服务器集群 Master 选举。多实例服务使用长连接在 ZooKeeper 上各自创建一个临时型(EPHEMERAL)+ 序列型(SEQUENCE)的结点,约定序列号最小的结点为集群 Master,如果 Master 服务掉线,下一个序列号最小的结点所对应的服务成为新的 Master。
4、作为锁和协调器。为分布式系统中的各个服务提供数据一致性和运行进度控制(可以想像成红绿灯)
ZooKeeper 可以实现分布式锁。一种常见的思路是创建临时型(EPHEMERAL)+ 序列型(SEQUENCE)结点,约定谁的结点的序号最小获得锁;已经获得锁的服务如果掉线,因为结点的临时性,它所创建的结点就会被 ZooKeeper 删除,序号次小的结点所对应的服务就认为获得锁。
Java 的样例程序
连接 ZooKeeper
假设使用 Maven 来构建程序,以下的说明稍加修改即可应用于 Gradle 或者 Sbt 构建的应用。
在 pom.xml 的 <dependencies /> 区中加入 ZooKeeper 依赖
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.5.5</version> </dependency>
连接服务器代码
import java.io.IOException; import java.util.concurrent.CountDownLatch; ... public class AppZk { public static void connect() { try { final CountDownLatch latch = new CountDownLatch(1); ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 1000, event -> { switch (event.getState()) { case SyncConnected: System.out.println("连接成功"); latch.countDown(); break; case Expired: System.out.println("连接过期"); break; case Closed: System.out.println("连接关闭"); break; case Disconnected: System.out.println("连接断开"); break; default: System.out.println("其它错误"); break; } }); latch.await(); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } }
读取某个 ZNode 的值
byte[] bytes = zooKeeper.getData("/Service/Service1.Addr", false, null); String serviceInfo = new String(bytes, StandardCharsets.UTF_8.name()); System.out.println(serviceInfo);
分布式锁例子
import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.ZooKeeper; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import static java.lang.System.out; public class DistributedLock implements AutoCloseable { private ZooKeeper zk; private CountDownLatch latch = new CountDownLatch(1); private static final String LOCK_BASE_PATH = "/zk_dist_lock"; private String lockPath; public DistributedLock(String host) throws Exception { ZooKeeper zooKeeper = new ZooKeeper(host, 1000, event -> { switch (event.getState()) { case SyncConnected: this.latch.countDown(); break; default: out.println("其它错误"); break; } }); this.latch.await(); this.zk = zooKeeper; } @Override public void close() throws Exception { if (null == this.zk) { return; } this.zk.close(); this.zk = null; } public void lock() throws Exception { if (null == this.zk) throw new NullPointerException("未初始化的对象"); this.lockPath = this.zk.create(LOCK_BASE_PATH + "/" + "lock", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); final Object sync = new Object(); synchronized(sync) { while (true) { List<String> nodes = zk.getChildren(LOCK_BASE_PATH, event -> { synchronized (sync) { sync.notifyAll(); } }); Collections.sort(nodes); // ZooKeeper node names can be sorted lexographically if (this.lockPath.endsWith(nodes.get(0))) { out.println("锁定成功"); break; } else { sync.wait(); } } } } public void unlock() throws IOException { try { zk.delete(this.lockPath, -1); this.lockPath = null; out.println("解锁成功"); } catch (KeeperException |InterruptedException e) { throw new IOException (e); } } }

浙公网安备 33010602011771号