第11课:ZooKeeper

第11课:ZooKeeper 高级技术点解析

ZooKeeper 是什么?

ZooKeeper 是一个分布式协调服务,由知名互联网公司雅虎开发,是 Google Chubby 的开源实现。它是一个典型的分布式数据一致性解决方案,以分布式架构模式开发的软件系统可以基于它实现如数据发布/订阅、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁等功能。

在实际生产环境中,ZooKeeper 一般以集群方式对外提供服务,一个集群包含多个节点,每个节点都对应一台 ZooKeeper 服务器,所有节点均对外提供服务。整个集群环境对分布式应用数据一致性提供了全面的支持,具体包括如下五大特性:

  1. 严格一致性:从同一客户端发起的请求,最终将会严格地按照其发起顺序被 ZooKeeper 集群处理;

  2. 原子性:来自于分布式系统的每一个请求,集群中所有服务节点要么全部对其进行了成功处理,要么都未对其进行处理。不会出现部分节点成功处理,其余节点未成功处理的情况;

  3. 单一视图:客户端连接任何一个 ZooKeeper 服务器节点,其看到的数据总是一致的;

  4. 可靠性:当某个请求被服务器节点成功处理后,该请求所引起的服务端状态变更将会被存储下来,直到有另外一个请求又对其进行了变更;

  5. 实时性:一般情况下,由客户端发起的请求被成功处理后,其他客户端能够立即获取服务端最新数据状态。

ZooKeeper 的设计目标

ZooKeeper 能为分布式应用系统提供高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。高性能使 ZooKeeper 能够应用于对系统吞吐量要求较高的大型分布式系统,高可用使分布式系统中的单点问题得到了有效处理,而严格的顺序访问使客户端可以使用 ZooKeeper 实现一些复杂的同步功能。

总得来看,ZooKeeper 主要有以下四个设计目标:

  1. 简单的数据模型:ZooKeeper 使分布式应用程序能够通过一个共享的、树形结构的名字空间来完成协调工作。此处所说的“名字空间”是指由一系列被称为 ZNode 的数据节点组成并存储于 ZooKeeper 服务器内存中的一个数据模型,ZNode 之间的层级关系类似于文件系统的目录结构,其层级关系和下图类似:

    enter image description here

  2. 易构建集群:一个 ZooKeeper 集群通常由3-5台机器组成,如下图所示:

    enter image description here

  3. 顺序访问:ZooKeeper 会为来自于客户端的每一个更新请求分配一个全局唯一的递增编号,这个编号表示了所有请求操作的先后顺序,应用程序可以使用 ZooKeeper 的该特性实现更高层次的同步原语。

  4. 高性能:ZooKeeper 将全量数据存储在内存中,并直接服务于客户端的所有请求。曾有人用三台 3.4.x 版本的 ZooKeeper 服务器组成的集群进行性能压测,在100%读请求的场景下压测结果为 12-13W QPS。

ZooKeeper 与 ZAB 协议

ZooKeeper 实现了一种基于主备模式的系统架构,从而保持了集群中各副本之间数据的一致性。它通过一个单一的主进程(Leader 节点)来接收并处理客户端的所有事务请求,同时采用一种叫 ZooKeeper Atomic Broadcast(原子广播协议)的协议(以下简称 ZAB),将服务器数据状态变更以事务 Proposal 的形式广播到所有的 Follower 进程中。

ZooKeeper 将 ZAB 协议作为其数据一致性的核心算法。ZAB 协议是为 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。下面我们介绍下 ZAB 协议的主要内容,即消息广播和崩溃恢复过程。

1. 消息广播。

消息广播过程基于 ZAB 协议实现,类似于一个两阶段提交过程。当 Leader 服务器接收到来自于客户端的请求,便为该请求生成对应的事务 Proposal,并将该 Proposal 广播给集群中其余的服务器节点(Follower),然后再收集各个 Follower 节点的选票,根据选票结果决定是否进行事务提交。ZAB 协议消息广播流程如下图所示:

enter image description here

2. 崩溃恢复。

当 Leader 服务器出现崩溃,或者由于网络原因导致 Leader 服务器与超过半数以上 Follower 服务器节点失去联系时,集群就会进入崩溃恢复模式。在 ZAB 协议中,为了保证 ZooKeeper 集群能够对外继续提供服务,当前所有 ZooKeeper Follower 节点必须重新根据 ZAB 选举算法快速选举出 Leader 节点,并保证选举出的 Leader 节点作为当前集群唯一能够接收请求的进程,同时该 Leader 节点也能将接收到的请求转换为 Proposal 顺利广播到所有 Follower 节点。

搭建 ZooKeeper 集群

实际生产环境中使用 ZooKeeper ,为了实现高可用、高性能,我们一般会搭建 ZooKeeper 集群。

ZooKeeper 集群中接收请求的节点是 Leader 节点,为了保证能够根据 ZAB 算法(投票数在机器数一半以上)选出 Leader,就一定不能出现两台机器得票相同的僵局,所以一般要求 ZooKeeper 集群的 Server 数量一定是奇数,也就是 2n+1 台(n>0),一般为3个或者3个以上节点。

下面我将为大家演示如何搭建 ZooKeeper 集群,步骤如下。

1. 打开三台虚拟机(本实例中打开了 IP 分别为 192.168.1.120、192.168.1.121、192.168.1.6 的三台虚拟机),然后利用 Xshell 连接虚拟机,并在三台虚拟机中分别执行如下命令建立目录 /usr/local/software/(注,没有特别说明,下面的命令同样需要在三台虚拟机上分别执行):

mkdir -p /usr/local/software/

2. 利用 FTP 将 zookeeper-3.4.5.tar.gz 上传到目录 usr/local/software/ 下,同时在该目录下执行如下命令解压:

tar -zxvf zookeeper-3.4.5.tar.gz -C /usr/local/

3. 进入目录 /usr/local/,执行如下命令将解压后的文件夹 zookeeper-3.4.5 修改为 ZooKeeper:

mv zookeeper-3.4.5 zookeeper

4. 执行如下命令修改环境变量:

vim /etc/profile

修改内容如下图所示:

enter image description here

注意:修改完之后需要执行命令:source /etc/profile,只有这样环境变量的设置才会生效。

5. 建立文件夹 /usr/local/zookeeper/data/ 用于存储数据,然后进入目录 /usr/local/zookeeper/conf 修改 ZooKeeper 配置文件:

mkdir -p /usr/local/zookeeper/data/ 
cd /usr/local/zookeeper/conf
mv zoo_sample.cfg zoo.cfg
vim zoo.cfg

修改后,配置文件的内容如下:

enter image description here

6. 进入目录 /usr/local/zookeeper/data/,执行 touch myid 命令创建文件 myid,再通过命令 vim myid 打开该文件,并将上图中对应 server. 后面的数字添加入文件中,之后保存关闭。

该步执行完后,192.168.1.120 中 myid 文件内容为0,192.168.1.120 中 myid 文件内容为1,192.168.1.6 中 myid 文件内容为2。

7. 执行命令启动 ZooKeeper:

zkServer.sh start

注意:安装 ZooKeeper 的服务器上需要安装 JDK,否则 ZooKeeper 无法启动。

8. 执行命令观察集群状态:

zkServer.sh status

服务器 192.168.1.120 上 ZooKeeper 的状态如下图所示:

enter image description here

服务器 192.168.1.121 上 ZooKeeper 的状态如下图所示:

enter image description here

服务器 192.168.1.6 上 ZooKeeper 的状态如下图所示:

enter image description here

从上面的图中可以看出,当前 ZooKeeper 集群中有一个 Leader节点,两个 Follower 节点,说明 ZooKeeper 安装成功。

Spring Boot 整合 ZooKeeper

接下来,我们开始实现 Spring Boot 对 ZooKeeper 的整合。主要包括以下七个步骤。

第一步,新建项目 ZooKeeper,并在 pom 文件中添加如下依赖:

<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.3.3</version>
</dependency>

第二步,新建 ZkConnectionWatcher 类,并实现 ZooKeeper Watcher 接口,代码如下:

/**
 * watcher实现类定义
 */
public class ZkConnectionWatcher implements Watcher {
    private CountDownLatch countDownLatch;


    public ZkConnectionWatcher(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        //客户端收到服务端连接成功通知时,解除客户端阻塞
        if(Event.KeeperState.SyncConnected == watchedEvent.getState()){
            countDownLatch.countDown();
        }
    }
}

第三步,连接 ZooKeeper 集群,代码如下:

ZooKeeper zooKeeper = new ZooKeeper(connectionStr, timeoutTime,new ZkConnectionWatcher(countDownLatch));

//客户端、服务端连接未成功时,阻塞程序往下执行
countDownLatch.await();

注意: connectionStr = "192.168.1.120:2181,192.168.1.121:2181,192.168.1.6:2181" 表示 ZooKeeper 服务端所在服务器的 IP:Port,多台服务器之间以逗号分隔;timeoutTime=5000 表示客户端心跳最大有效时间间隔;ZkConnectionWatcher 实例用于监听客户端、服务端是否连接成功,一旦连接成功,便会解除客户端阻塞。

第四步,在 ZooKeeper 上新建一个 /zkService 节点,并获取该节点的数据信息:

String servicePath = zooKeeper.create("/zkService",        "zkService".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL);

//获取当前节点数据
String data = new String(zooKeeper.getData(servicePath,true,new Stat()));

System.out.println("zk新建节点数据信息:" + data);

下面为大家解释下 zooKeeper.create() 方法中每个参数的含义:

  1. "/zkService":表示创建数据节点的路径;
  2. "zkService".getBytes():表示节点创建后的初始内容;
  3. Ids.OPEN_ACL_UNSAFE:表示不对节点权限进行控制;
  4. CreateMode.PERSISTENT_SEQUENTIAL:为持久顺序节点,其中节点类型有如下四种:
  • CreateMode.PERSISTENT:为持久型节点,会话结束后,节点仍然存在;
  • CreateMode.PERSISTENT_SEQUENTIAL:为持久顺序节点;
  • CreateMode.EPHEMERAL:为临时节点,客户端、服务器会话结束后节点不保存;
  • CreateMode.EPHEMERAL_SEQUENTIAL:为临时顺序节点。

第五步,按顺序启动 ZooKeeper 集群、应用程序、利用 Postman 调用接口:http://localhost:8080/zkClient/zookeeperApi,控制台输出如下信息说明节点创建成功:

enter image description here

第六步,新建节点 /countService,并在该节点下分别添加两个子节点 /service1/service2,实现代码如下:

 //创建节点“/countService”
String countServicePath = zooKeeper.create("/countService", "countService".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);

//创建子节点“/countService/service1”
zooKeeper.create(countServicePath + "/service1","service1".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);

//创建子节点“/countService/service2”
zooKeeper.create(countServicePath + "/service2","service2".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);

List<String> childrenList = zooKeeper.getChildren(countServicePath,false);

System.out.println("节点/countService的孩子节点为:" + childrenList);

enter image description here

这里注意两点:

  1. 临时节点下面不能创建子节点;
  2. 当在某个节点上创建子节点时,子节点路径必须是全路径,如 /countService/service2,而不能是 /service2

第七步,删除某个节点,代码如下:

//删除节点“/countService/service1”
zooKeeper.delete("/countService/service1",-1);

List<String> childrenList = zooKeeper.getChildren("/countService",false);

System.out.println("节点/countService的孩子节点为:" + childrenList);

执行结果如下:

enter image description here

Spring Boot 整合 ZooKeeper 的过程就介绍到这里。如需进一步了解 ZooKeeper 相关 API,可参考 ZooKeeper 官网上的相关文档。

ZooKeeper Watcher 机制

除了上面的基本功能之外,ZooKeeper 还有一个比较重要的功能,即分布式数据的发布/订阅功能。

一个典型的发布/订阅模型系统定义了一对多的订阅关系,能够为多个订阅者提供同时监听一个主题的功能,当主题发生变化时,能够及时通知监听该主题的所有订阅者。

ZooKeeper 利用 Watcher 机制实现分布式通知功能,ZooKeeper 为其客户端提供了向服务端申请注册 watcher 监听的功能。当服务端发生某些事件时,会触发客户端注册的对应 Watcher。

下面是客户端注册 Watcher、服务端触发事件通知客户端的流程图:

enter image description here

上图只是 Watcher 机制的交互流程图。

由于时间关系,ZooKeeper 相关内容就介绍到这里,如果各位想更深入了解 ZooKeeper 相关知识,可以在朋友圈、微信和我交流。

课程源码下载地址:

GitHub

上一篇
下一篇
目录