一文学会 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);
        }
    }
} 

 

posted @ 2019-07-01 16:19  郭洋  阅读(163)  评论(0)    收藏  举报