拉勾教育学习-笔记分享の"解剖"Zookeeper

263 阅读40分钟

【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
掌握zookeeper的使用,大作业结合Netty加深对分布式架构的理解


尘埃落定之前,你我皆是黑马

一、Zookeeper前言

Part 1 - 分布式系统定义及面临的问题

ZooKeeper最为主要的使用场景 —— 作为分布式系统的分布式协同服务

虽然 分布式系统使得 工作效率高、计算能力提升, 但是 存在 【信息错漏、协作延迟、意外宕机】等致命因素
这些问题放在人类世界可以被我们灵活处理,但 机器是死的;所以我们一定要保证每一个环节的严谨。

其实只要做一件事,就可以解决上诉所有问题: 「组内信息同步」

分布式系统的协调工作,依赖于 服务进程之间的通信:

  1. 通过网络进行信息共享
    leader 将任务定时传达至 worker,当任务分配有变化时,leader 会单独告诉指定 worker(s)
  2. 通过共享存储
    leader 将任务发布在共享空间中,共享空间更新任务,相关 worker(s) 收到消息并主动从共享空间中获取任务

Part 2 - ZooKeeper如何解决分布式系统面临的问题

使用的方式 —— 共享存储其实共享存储,分布式应用也需要和存储进行网络通信

ZooKeeper (类似svn) ===> 存储了任务的分配完成情况等共享信息
从节点(Slave) 主动订阅 ===> 订阅这些共享信息
主节点(Master) 更新信息 ===> 相关订阅的从节点得到 zookeeper 的通知,取得自己最新任务分配
从节点(Slave) 完成任务 ===> 存储完成信息至 zookeeper
主节点(Master) 主动订阅 ===> 得到 zookeeper 的完工的反馈通知

注:Slave节点要想获取ZooKeeper的更新通知,需事先在关心的数据节点上设置观察点

Part 3 - ZooKeeper基本概念

设计目标
那些复杂的且容易出错的分布式⼀致性服务封装起来,构成⼀个高效可靠的原语集,并⼀些简单的接口提供给用户使用

zookeeper 是⼀个典型的分布式数据⼀致性的解决方案
分布式应用程序可以基于它实现诸如 数据订阅/发布负载均衡命名服务集群管理分布式锁分布式队列等功能

「集群角色」

一般的分布式系统中,构成⼀个集群的每⼀台机器都有自己的角色
最典型的是: Master/Slave 模式(主备模式) --> Master机器: 所有能够处理写操作的机器 --> Slave机器: 所有通过异步复制方式获取最新数据,并提供读服务的机器

Zookeeper中,它没有沿⽤传递的Master/Slave概念,而是引入了LeaderFollowerObserver 三种角色
Zookeeper集群中的所有机器通过 「Leader选举」 来选定⼀台被称为 Leader 的机器,Leader服务器为客户端提供读和写服务,除Leader外,其他机器包括 Follower 和 Observer 都能提供 读服务
区别是
Observer 不参与Leader选举过程,不参与写操作的过半写成功策略
因此Observer可以在不影响写性能的情况下提升集群的性能

「会话 session」

客户端会话

⼀个客户端连接是指 客户端和服务端之间的⼀个TCP长连接 Zookeeper对外的服务端口默认为 2181

客户端启动的时候,首先会与服务器建立⼀个TCP连接
从第⼀次连接建立开始,客户端会话的生命周期也开始了,通过这个连接
===> 客户端能够心跳检测与服务器保持有效的会话
===> 也能够向Zookeeper服务器发送请求并接受响应
===> 同时还能够通过该连接接受来自服务器的Watch事件通知

「数据节点 Znode」

在ZooKeeper中,“节点” 分为两类:

  1. 构成集群的机器 (机器节点)
  2. 数据模型中的数据单元 (数据节点 Znode)

ZooKeeper将所有数据 以Tree模型(Znode Tree) 存储在内存中,由 / 符号分割路径 视为 一个 Znode (e.g. /app/path1)

「版本」

每个Znode上都会存储数据
Zookeeper会维护Znode中的一个 名为 Stat 的数据结构

Stat 记录了:

  1. version(当前ZNode的版本)
  2. cversion(当前ZNode⼦节点的版本)
  3. aversion(当前ZNode的ACL版本)

「监听器 Watcher」 ☆☆☆

Zookeeper允许⽤户在指定节点上注册⼀些 Watcher
并且在⼀些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端
该机制是Zookeeper实现分布式协调服务的重要特性

「ACL策略」

Access Control Lists 访问控制列表 ===> 权限控制

  • CREATE --> 创建子节点的权限
  • READ --> 获取子节点数据 和 子节点列表的权限
  • WRITE --> 更新节点数据的权限
  • DELETE --> 删除子节点的权限
  • ADMIN --> 设置节点ACL的权限

CREATE 和 DELETE 是 针对子节点的权限控制

二、Zookeeper环境搭建

务必确保系统存在 jdk, 它是 Zookeeper 的基础提供

Part 1 - 搭建方式

「单机模式」

Zookeeper只运行在⼀台服务器上,适合测试环境

以Linux环境为例:

  1. 下载稳定版本的zookeeper zookeeper.apache.org/releases.ht…
  2. 上传tar至Linux系统
  3. 解压 tar -zxvf zookeeper-x.x.xx.tar.gz
  4. 进入目录并创建 data 文件夹 cd zookeeper-x.x.xx + mkdir data
  5. 修改配置文件名称 cd conf + mv zoo_sample.cfg zoo.cfg
  6. 修改zoo.cfg中的data属性 dataDir=/root/zookeeper-x.x.xx/data
  7. zookeeper服务启动 ./zkServer.sh start
  8. 关闭服务 ./zkServer.sh stop
  • 查看状态 ./zkServer.sh status

!!start后若查看status依旧是启动失败,可以去bin目录下面的zookeeper.out查看报错

「集群模式」

Zookeeper运行于⼀个集群上,适合生产环境,这个计算机集群被称为⼀个 “集合体

...按照单机模式在多台机器上安装并运行即可...

「伪集群模式」

⼀台服务器上运行多个 Zookeeper 实例, 用端口进行区分

伪集群模式为我们体验Zookeeper和做⼀些尝试性的实验提供了很⼤的便利。比如,我们在测试的时候,可以先使⽤少量数据在伪集群模式下进⾏测试。当测试可行的时候,再将数据移植到集群模式进行真实的数据实验。这样不但保证了它的可行性,同时⼤⼤提⾼了实验的效率。这种搭建方式,比较简便,成本比较低,适合测试和学习

【注意!】
⼀台机器上部署了3个server,也就是说单台机器及上运⾏多个Zookeeper实例。这种情况下,必须保证每个配置⽂档的各个端⼝号不能冲突,除clientPort不同之外,dataDir也不同。另外,还要在dataDir所对应的⽬录中创建myid⽂件来指定对应的Zookeeper服务器实例

  • clientPort 端口
    1台机器上部署多个server,那么每台机器都要不同的 clientPort
  • dataDir 和 dataLogDir
    dataDir和dataLogDir也需要区分下,将数据文件和日志文件分开存放,同时每个server的这两变量所对应的路径都是不同的
  • server.X 和 myid
    server.X 这个数字就是对应 data/myid 中的数字
    3个 server 的 myid 文件中分别写入了1,2,3,那么每个server中的zoo.cfg都配 server.1 server.2,server.3就行了。
    因为在同⼀台机器上,后⾯连着的2个端口,3个server都不要⼀样,否则端口冲突
  1. 创建集群文件夹 mkdir zkcluster
  2. 将压缩包解压到其中 tar -zxvf apache-zookeeper-x.x.xx-bin.tar.gz
  3. 改名称 mv apache-zookeeper-x.x.xx-bin zookeeper01
  4. 复制出另两个实例 cp -r zookeeper01/ zookeeper02 + cp -r zookeeper01/ zookeeper03
  5. 在他们中创建 data 和 logs 目录 mkdir data + cd data + mkdir logs
  6. 将他们的配置文件修改名称为 zoo.cfg cd conf + mv zoo_sample.cfg zoo.cfg
  7. 修改他们的 dataDir为对应/data路径,添加dataLogDir配置,并且clientPort 分别改为为 「2181」/「2182」/「2183」
  8. 他们的/data中创建 myid 文件 touch myid,并编辑他们内容为 1/2/3

  9. 配置每⼀个 zookeeper 的集群服务器IP列表
server.1=106.75.60.49:2881:3881
server.2=106.75.60.49:2882:3882
server.3=106.75.60.49:2883:3883
#server.服务器ID=服务器IP地址:服务器之间通信端⼝:服务器之间投票选举端口
  1. 依次运行三个zk实例

结果如下:

三、Zookeeper基本使用

Part 1 - ZooKeeper系统模型

「ZNode 的类型」

持久节点

节点被创建后会⼀直存在服务器,直到删除操作主动清除

持久顺序节点

有顺序的持久节点

临时节点

它的生命周期和客户端会话绑在⼀起,客户端会话结束,节点会被删除掉
临时节点不能创建子节点

临时顺序节点

有顺序的临时节点

「ZNode 的状态信息」

ZooKeeper中也有事务——“ 能够改变ZooKeeper服务器状态的操作
对于每⼀个事务请求,ZooKeeper都会为其分配⼀个全局唯⼀的事务ID,用 ZXID 来表示,通常是⼀个 64 位的数字
每⼀个 ZXID 对应⼀次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序

===> 节点信息概述(后续会详细说明如何操作)

  • cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
  • ctime 就是 Create Time,表示节点创建时间。
  • mZxid 就是 Modified ZXID,表示节点最后⼀次被修改时的事务ID。
  • mtime 就是 Modified Time,表示节点最后⼀次被修改的时间。
  • pZxid 表示该节点的⼦节点列表最后⼀次被修改时的事务 ID。只有⼦节点列表变更才会更新 pZxid,⼦节点内容变更不会更新。
  • cversion 表示⼦节点的版本号。
  • dataVersion 表示内容版本号。
  • aclVersion 标识acl版本
  • ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0
  • dataLength 表示数据⻓度。
  • numChildren 表示直系⼦节点数。

「Watcher 数据变更通知」

Zookeeper的Watcher机制主要包括客户端线程客户端WatcherManagerZookeeper服务器三部分

{{ 详细流程 }}
【客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑】

「ACL 保障数据安全」

三个方面来理解 ACL机制:

  1. 权限模式-Scheme:

用来确定权限验证过程中 使用的检验策略

* IP <br> 通过IP地址粒度来进行权限控制,同时IP模式可以⽀持按照网段方式进⾏配置
* Digest(最常用)<br> 使用`"username:password"`形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制
* World<br> 最开放的权限控制模式,这种权限控制方式几乎没有任何作用,数据节点的访问权限对所有用户开放,即所有⽤户都可以在不进行任何权限校验的情况下操作ZooKeeper上的数据
* Super<br> 超级用户,可以对任意ZooKeeper上的数据节点进行任何操作

2. 授权对象-ID:

是权限赋予的用户或⼀个指定实体,例如 IP 地址或是机器

* IP <br> 通常是⼀个IP地址或IP段:例如:192.168.10.110 或192.168.10.1/24
* Digest<br> ⾃定义,通常是username:BASE64(SHA-1(username:password))  例如:zm:sdfndsllndlksfn7c=
* World<br> 只有⼀个ID :anyone
* Super<br> 超级用户

3. 权限-Permission:

通过权限检查后可以被允许执行的操作

  • CREATE(C):数据节点的创建权限,允许授权对象在该数据节点下创建子节点。
  • DELETE(D):子节点的删除权限,允许授权对象删除该数据节点的子节点。
  • READ(R):数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等
  • WRITE(W):数据节点的更新权限,允许授权对象对该数据节点进行更新操作
  • ADMIN(A):数据节点的管理权限,允许授权对象对该数据节点进行 ACL 相关的设置操作

Part 2 - ZooKeeper命令行操作

./zkcli.sh 连接本地的zookeeper服务器
./zkCli.sh -server ip:port 连接指定的服务器

「创建节点」

create [-s][-e] path data acl
-s或-e分别指定节点特性,顺序或临时节点,若不指定,则创建持久节点;acl⽤来进行权限控制

  1. 创建顺序节点
    create -s /zk-test 123

  2. 创建临时节点
    create -e /zk-temp 123

quit退出客户端后ls /查看根目录下的节点

  1. 创建永久节点
    create /zk-permanent 123

「读取节点」

  1. ls命令 ==> 可以列出Zookeeper指定节点下的所有子节点

  2. get [-s]命令 ==> 可以获取Zookeeper指定节点的数据内容和属性信息

「更新节点」

set path data [version]

可以看到 dataVersion 从 0 变为了 1

「删除节点」

delete path [version]

若删除节点存在⼦节点,那么⽆法删除该节点,必须先删除⼦节点,再删除⽗节点

Part 3 - Zookeeper的api使用

「引子」

Zookeeper API共包含五个包

(1)org.apache.zookeeper (最常用,包含Zookeeper类)
(2)org.apache.zookeeper.data
(3)org.apache.zookeeper.server
(4)org.apache.zookeeper.server.quorum
(5)org.apache.zookeeper.server.upgrade

⼀旦客户端和Zookeeper服务端建立起了连接,Zookeeper系统将会给本次连接会话分配⼀个ID值,并且客户端将会周期性的向服务器端发送心跳来维持会话连接。
只要连接有效,客户端就可以使用 Zookeeper API 来做相应处理

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

「建立会话」

new Zookeeper(connectString,sesssionTimeOut,Wather)

  • connectString: 连接地址:IP:端口
  • sesssionTimeOut:会话超时时间:单位毫秒
  • Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端)
public class CreateSession implements Watcher {
    
    // CountDownLatch类可以 使⼀个线程等待,主要不让main⽅法结束
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    
    public static void main(String[] args) throws IOException, InterruptedException {
        ZooKeeper zooKeeper = new ZooKeeper("106.75.60.49:2181", 5000, new CreateSession());
        System.out.println("zookeeper当前状态:\t" + zooKeeper.getState());
        // 计数工具类:CountDownLatch:不让main方法结束,让线程处于等待阻塞
        countDownLatch.await();
        System.out.println("-------> Client has connected to zookeeper <--------");
    }
    
    /**
     * 回调方法 ==> 处理来自服务器端的 Watcher 通知
     * 收到服务端发来的 SyncConnected事件后,接触主程序在 CountDownLatch 上的等待阻塞
     * 至此,会话创建完成
     * @param watchedEvent
     */
    @Override
    public void process(WatchedEvent watchedEvent) {
        // 监听Server端是否发送了 SyncConnected事件
        if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
            System.out.println("process方法 已执行");
            countDownLatch.countDown();
        }
    }
}

ZooKeeper 客户端和服务端会话的建⽴是⼀个 异步 的过程
也就是说在程序中,构造方法会在处理完客户端初始化⼯作后⽴即返回。 会话在其声明周期处于 CONNECTING 状态
==> 当该会话真正创建完毕后ZooKeeper服务端会向会话对应的客户端发送⼀个事件通知,以告知客户端,客户端只有在获取这个通知之后,才算真正建立了会话

「创建节点」

zookeeper.create(path,data,acl,createMode)

  • path: 节点创建路径
  • data: 节点创建时保存的数据
  • acl: 节点创建的权限信息
    • ANYONE_ID_UNSAFE : 表示任何人
    • AUTH_IDS :此ID仅可用于设置ACL。它将被客户机验证的ID替换。
    • OPEN_ACL_UNSAFE :这是一个完全开放的ACL(常用)--> world:anyone
    • CREATOR_ALL_ACL :此ACL授予创建者身份验证ID的所有权限
  • createMode:创建节点的类型
    • PERSISTENT:持久节点
    • PERSISTENT_SEQUENTIAL:持久顺序节点
    • EPHEMERAL:临时节点
    • EPHEMERAL_SEQUENTIAL:临时顺序节点
public class CreateNode implements Watcher {
    
    private static ZooKeeper zookeeper;
    
    public static void main(String[] args) throws IOException, InterruptedException {
        zookeeper = new ZooKeeper("106.75.60.49:2181", 5000, new CreateNode());
        System.out.println("zookeeper当前状态:\t" + zookeeper.getState());
        // 使用线程进行无限期阻塞,从而等待节点的创建
        Thread.sleep(Integer.MAX_VALUE);
        System.out.println("-------> Client has connected to zookeeper <--------");
    }
    
    /**
     * 回调方法 ==> 处理来自服务器端的 Watcher 通知
     * @param watchedEvent
     */
    @Override
    public void process(WatchedEvent watchedEvent) {
        // 监听Server端是否发送了 SyncConnected事件
        if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
            System.out.println("process方法 已执行");
            try {
                createNodeSync();
            } catch (KeeperException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 创建节点具体实现
     * @throws KeeperException
     * @throws InterruptedException
     */
    private static void createNodeSync() throws KeeperException, InterruptedException {
        // 持久节点
        String node_persistent = zookeeper.create("/archie-persistent", "持久节点内容XXXXXX".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        System.out.println("持久节点已创建");
        // 临时节点
        String node_ephemeral = zookeeper.create("/archie-temporary", "临时节点内容YYYYYY".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        System.out.println("临时节点已创建");
        // 持久顺序节点
        String node_persistent_sequential = zookeeper.create("/archie-persistent-sequential", "持久顺序节点内容ZZZZZZ".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
        System.out.println("持久顺序节点已创建");
    }
}

「获取节点数据」

zk.getData(path, watch, stat);

  • path: 获取数据的路径
  • watch: 是否开启监听
  • stat: 节点状态信息 (null 表示 获取最新版本数据)

zooKeeper.getChildren(path, watch);

  • path: 节点路径
  • watch: 是否要启动监听,当子节点列表发生变化,会触发监听
public class GetNodeData implements Watcher {
    
    private static ZooKeeper zookeeper;
    
    public static void main(String[] args) throws IOException, InterruptedException {
        zookeeper = new ZooKeeper("106.75.60.49:2181", 5000, new GetNodeData());
        System.out.println("zookeeper当前状态:\t" + zookeeper.getState());
        // 使用线程进行无限期阻塞,从而等待节点的获取
        Thread.sleep(Integer.MAX_VALUE);
    }
    
    @Override
    public void process(WatchedEvent watchedEvent) {
        // 监听Server端是否发送了 NodeChildrenChanged 事件
        if (watchedEvent.getType() == Event.EventType.NodeChildrenChanged) {
            System.out.println("节点发生了改变!");
    
            List<String> children = null;
            try {
                children = zookeeper.getChildren("/archie-persistent", true);
            } catch (KeeperException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(children);
        }
    
        // 监听Server端是否发送了 SyncConnected事件
        if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
            System.out.println("process方法 已执行");
            try {
                getNodeData();
                getChildren();
            } catch (KeeperException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 获取某个节点信息
     * @throws KeeperException
     * @throws InterruptedException
     */
    private static void getNodeData() throws KeeperException, InterruptedException {
        byte[] data = zookeeper.getData("/archie-persistent", false, null);
        System.out.println("节点数据 ==>\t" + new String(data));
    }
    
    /**
     * 获取某个节点下的所有 子节点列表
     * @throws KeeperException
     * @throws InterruptedException
     */
    private static void getChildren() throws KeeperException, InterruptedException {
        List<String> children = zookeeper.getChildren("/archie-persistent", true);
        for (String child : children) {
            System.out.println("子节点 ==>\t" + child);
        }
    }
}

ls

「修改节点数据」

zooKeeper.setData(path, data,version)

  • path: 路径
  • data: 要修改的内容 byte[]
  • version: 为-1,表示对最新版本的数据进行修改
public class UpdateNodeData implements Watcher {
    
    private static ZooKeeper zooKeeper;
    
    public static void main(String[] args) throws InterruptedException, IOException {
        zooKeeper = new ZooKeeper("106.75.60.49:2181", 5000, new UpdateNodeData());
        System.out.println("zookeeper当前状态:\t" + zooKeeper.getState());
        // 使用线程进行无限期阻塞,从而等待节点的获取
        Thread.sleep(Integer.MAX_VALUE);
    }
    
    @Override
    public void process(WatchedEvent watchedEvent) {
        // 监听Server端是否发送了 SyncConnected事件
        if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
            System.out.println("process方法 已执行");
            try {
                updateNodeSync();
            } catch (KeeperException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 更新数据节点
     */
    private static void updateNodeSync() throws KeeperException, InterruptedException {
        byte[] currentData = zooKeeper.getData("/archie-persistent", false, null);
        String currentStr = new String(currentData);
        System.out.println("修改前数据:\t" + currentStr);
        
        Stat stat = zooKeeper.setData("/archie-persistent", (currentStr + "(已修改)").getBytes(), -1);
    
        byte[] modifiedData = zooKeeper.getData("/archie-persistent", false, null);
        System.out.println("修改后数据:\t" + new String(modifiedData));
    }
}

「删除节点」

zooKeeper.exists(path,watch)

zooKeeper.delete(path,version)

public class DeleteNode implements Watcher {
    
    private static ZooKeeper zooKeeper;
    
    public static void main(String[] args) throws IOException, InterruptedException {
        zooKeeper = new ZooKeeper("106.75.60.49:2181", 5000, new DeleteNode());
        System.out.println("zookeeper当前状态:\t" + zooKeeper.getState());
        // 使用线程进行无限期阻塞,从而等待节点的获取
        Thread.sleep(Integer.MAX_VALUE);
    }
    
    @Override
    public void process(WatchedEvent watchedEvent) {
        if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
            System.out.println("process方法 已执行");
            try {
                deleteNodeSync();
            } catch (KeeperException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 删除节点
     */
    private static void deleteNodeSync() throws KeeperException, InterruptedException {
        Stat stat = zooKeeper.exists("/archie-persistent", false);
        System.out.println(stat == null ? "节点不存在" : "节点存在");
    
        if(stat != null){
            zooKeeper.delete("/archie-persistent/ephemeral-node",-1);
        }
    
        Stat stat2 = zooKeeper.exists("/archie-persistent/ephemeral-node", false);
        System.out.println(stat2 == null ? "该节点不存在":"该节点存在");
    }
}

Part 3 - Zookeeper开源客户端

「ZkClient」

Github上⼀个开源的zookeeper客户端,在Zookeeper原生API接口之上进行了包装,是⼀个更易用的Zookeeper客户端,同时,zkClient在内部还实现了诸如Session超时重连、Watcher反复注册等功能

<dependency>
 <groupId>com.101tec</groupId>
 <artifactId>zkclient</artifactId>
 <version>0.2</version>
</dependency>
  1. 创建会话
public class CreateSessionZKClient {
    public static void main(String[] args) {
        ZkClient zkClient = new ZkClient("106.75.60.49:2181");
        System.out.println("会话被创建了");
    }
}
  1. 创建节点
public class CreateNodeZKClient {
    
    public static void main(String[] args) {
        ZkClient zkClient = new ZkClient("106.75.60.49:2181");
        System.out.println("会话被创建了..");

        zkClient.createPersistent("/archie-zkclient/p1",true);
        System.out.println("节点递归创建完成");
    }

}
  1. 删除节点
public class DeleteNodeZKClient {
    public static void main(String[] args) {
        ZkClient zkClient = new ZkClient("106.75.60.49:2181");
        System.out.println("会话被创建了..");
        // 递归删除节点
        String path = "/lg-zkclient/c1";
        zkClient.createPersistent(path+"/c11");
        zkClient.deleteRecursive(path);
        System.out.println("递归删除成功");
    }
}
  1. 获取子节点 + 获取数据
public class GetNodeZKClient {
    
    public static void main(String[] args) throws InterruptedException {
        ZkClient zkClient = new ZkClient("106.75.60.49:2181");
        System.out.println("会话被创建了..");
        
        // 获取子节点列表
        List<String> children = zkClient.getChildren("/archie-zkclient");
        System.out.println(children);
        
        /*
            客户端可以对一个不存在的节点进行子节点变更的监听
            只要该节点的子节点列表发生变化,或者该节点本身被创建或者删除,都会触发监听
         */
        zkClient.subscribeChildChanges("/archie-zkclient", new IZkChildListener() {
            public void handleChildChange(String parentPath, List<String> list) throws Exception {
                System.out.println(parentPath + "的子节点列表发生了变化,变化后的子节点列表为" + list);
            }
        });
    
        //测试
        zkClient.createPersistent("/archie-zkclient/p2");
        Thread.sleep(1000);
    }
    
}

「Curator客户端」

Netflix公司开源的Zookeeper客户端框架
和ZKClient⼀样,Curator解决了很多Zookeeper客户端⾮常底层的细节开发工作,包括连接重连,反复注册Watcher和NodeExistsException异常等,是最流行的Zookeeper客户端之⼀。
从编码风格上来讲,它提供了 基于Fluent的编程风格支持

 <dependency>
 <groupId>org.apache.curator</groupId>
 <artifactId>curator-framework</artifactId>
 <version>2.12.0</version>
 </dependency>
  1. 创建会话

① CuratorFramework的静态方法创建客户端

public static CuratorFramework newClient(String connectString, RetryPolicy retryPolicy)
public static CuratorFramework newClient(String connectString, int sessionTimeoutMs, int connectionTimeoutMs, RetryPolicy retryPolicy)

RetryPolicy 提供重试策略的接口,可以让用户实现自定义的重试策略,默认提供了以下实现
ExponentialBackoffRetry(基于backoff的重连策略)
RetryNTimes(重连N次策略)
RetryForever(永远重试策略)

new ExponentialBackoffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs)
baseSleepTimeMs: 初始的sleep时间,⽤于计算之后的每次重试的sleep时间 ==> 计算公式:当前sleep时间=baseSleepTimeMs*Math.max(1,random.nextInt(1<<(retryCount+1)))
maxRetries: 最⼤重试次数
maxSleepMs: 最⼤sleep时间,如果上述的当前sleep计算出来⽐这个⼤,那么sleep⽤这个时间,默认的最⼤时间是Integer.MAX_VALUE毫秒

② CuratorFramework的start()方法启动会话

client.start()

public class CreateSession {

    // 创建会话
    public static void main(String[] args) {
    
        //不使用fluent编程风格
        RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("106.75.60.49:2181", exponentialBackoffRetry);
        curatorFramework.start();
        System.out.println( "会话被建立了");

        // 使用fluent编程风格
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("106.75.60.49:2181")
                .sessionTimeoutMs(50000) // 会话超时时间(默认60s)
                .connectionTimeoutMs(30000) // 连接超时时间(默认15s)
                .retryPolicy(exponentialBackoffRetry)
                .namespace("base")  // 独立的命名空间 /base
                .build();
        client.start();
        System.out.println("会话2创建了");
    }

}
  1. 创建节点
  • 创建⼀个初始内容为空的节点: client.create().forPath(path);
  • 创建⼀个包含内容的节点: client.create().forPath(path,"我是内容".getBytes());
  • 递归创建⽗节点,并选择节点类型:client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
    其中 creatingParentsIfNeeded() 可以自动递归创建所有需要的父节点
public class CreateNodeCurator {

    // 创建会话
    public static void main(String[] args) throws Exception {

        //不使用fluent编程风格
        RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);

        // 使用fluent编程风格
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("106.75.60.49:2181")
                .sessionTimeoutMs(50000) // 会话超时时间(默认60s)
                .connectionTimeoutMs(30000) // 连接超时时间(默认15s)
                .retryPolicy(exponentialBackoffRetry)
                .namespace("base")  // 独立的命名空间 /base
                .build();
        client.start();
        System.out.println("会话2创建了");

        // 创建节点
        String path = "/archie-curator/p1";
        String s = client.create().creatingParentsIfNeeded()
                .withMode(CreateMode.PERSISTENT).forPath(path, "init".getBytes());

        System.out.println("节点递归创建成功,该节点路径" + s);
    }
}
  1. 删除节点
  • 删除⼀个子节点: client.delete().forPath(path);
  • 删除节点并递归删除其子节点: client.delete().deletingChildrenIfNeeded().forPath(path);
  • 指定版本进行删除: client.delete().withVersion(1).forPath(path);
  • 强制保证删除⼀个节点: client.delete().guaranteed().forPath(path);
public class DeleteNoteCurator {

    // 创建会话
    public static void main(String[] args) throws Exception {

        //不使用fluent编程风格
        RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("106.75.60.49:2181", exponentialBackoffRetry);
        curatorFramework.start();
        System.out.println( "会话被建立了");

        // 使用fluent编程风格
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("106.75.60.49:2181")
                .sessionTimeoutMs(50000)
                .connectionTimeoutMs(30000)
                .retryPolicy(exponentialBackoffRetry)
                .namespace("base")  // 独立的命名空间 /base
                .build();
        client.start();
        System.out.println("会话2创建了");

        // 删除节点
        String path = "/archie-curator";
        client.delete().deletingChildrenIfNeeded().withVersion(-1).forPath(path);

        System.out.println("删除成功,删除的节点" + path);
    }
}

  1. 获取子节点 + 获取数据
  • 普通查询: client.getData().forPath(path);
  • 包含状态查询: client.getData().storingStatIn(new Stat()).forPath(path);
public class GetNoteCurator {

    // 创建会话
    public static void main(String[] args) throws Exception {

        //不使用fluent编程风格
        RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
        
        // 使用fluent编程风格
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("106.75.60.49:2181")
                .sessionTimeoutMs(50000)
                .connectionTimeoutMs(30000)
                .retryPolicy(exponentialBackoffRetry)
                .namespace("base")  // 独立的命名空间 /base
                .build();
        client.start();
        System.out.println("会话2创建了");

        // 创建节点
        String path = "/archie-curator/p1";
        String s = client.create().creatingParentsIfNeeded()
                .withMode(CreateMode.PERSISTENT).forPath(path, "init".getBytes());

        System.out.println("节点递归创建成功,该节点路径" + s);

        // 获取节点的数据内容及状态信息
        // 数据内容
        byte[] bytes = client.getData().forPath(path);
        System.out.println("获取到的节点数据内容:" + new String(bytes));

        // 状态信息
        Stat stat = new Stat();
        client.getData().storingStatIn(stat).forPath(path);

        System.out.println("获取到的节点状态信息:" + stat );
    }
}

四、Zookeeper应用场景

Part 1 - 数据发布/订阅

发布/订阅系统⼀般有两种设计模式,分别是推(Push)模式和拉(Pull)模式

通常情况下,应用在启动的时候都会主动到ZooKeeper服务端上进行⼀次配置信息的获取,同时,在指定节点上注册⼀个Watcher监听,这样⼀ 来,但凡配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的的

在进行配置管理之前,⾸先我们需要将初始化配置信息存储到Zookeeper上去,⼀般情况下,我们可以在Zookeeper上选取⼀个数据节点用于配置信息的存储
例如:/app1/database_config

Part 2 - 命名服务

UUID 长度过长、含义不明,所以产生了一套根据节点名称定位的 全局唯一ID策略

  1. 所有客户端都会根据自己的任务类型,在指定类型的任务下面通过调用create()接口来创建⼀个顺序节点,例如创建“job-”节点。
  2. 节点创建完毕后,create()接口会返回⼀个完整的节点名,例如“job-0000000003”。
  3. 客户端拿到这个返回值后,拼接上 type 类型,例如“type2-job-0000000003”,这就可以作为⼀个全局唯⼀的ID了。

Part 3 - 集群管理

中小规模中

通过在集群中的每台机器上部署⼀个 Agent,由这个 Agent 负责主动向指定的⼀个监控中心系统(监控中心系统负责将所有数据进⾏集中处理,形成⼀系列报表,并负责实时报警,以下简称“监控中心”)汇报自己所在机器的状态。

大规模下

利用Zookeeper的两大特性实现 集群机器存货监控。
e.g. 监控系统在/clusterServers节点上注册⼀个Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建⼀个临时节点:/clusterServers/XXXXX,这样,监控系统就能够实时监测机器的变动情况

Zookeeper的两大特性

  1. 客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其⼦节点列表发⽣变更时,Zookeeper服务器就会向订阅的客户端发送变更通知
  2. 对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被⾃动删除

「分布式日志收集系统」

核心工作: 收集分布在不同机器上的系统日志

需要解决的问题

  1. 变化的日志源机器
    在生产环境中,伴随着机器的变动,每个应用的机器几乎每天都是在变化的 --> 也就是说每个组别中的日志源机器通常是在不断变化

  2. 变化的收集器机器
    日志收集系统自身也会有机器的变更或扩容,于是会出现新的收集器机器加入 OR 老的收集器机器退出的情况

日志源机器 ==> 需要收集的日志机器(会被分为不同组别)
收集器机器 ==> 在每个组别中手机日志的后台机器

使⽤Zookeeper的场景步骤

① 注册收集器机器

在ZooKeeper上创建⼀个节点作为收集器的根节点,例如/logs/collector;
每个收集器机器在启动的时候,都会在收集器节点下创建自己的节点

② 任务分发

待所有收集器机器都创建好自己对应的节点后,系统根据收集器节点下子节点的个数,将所有日志源机器分成对应的若干组,然后将分组后的机器列表分别写到这些收集器机器创建的子节点(例如/logs/collector/host1)上去。
这样⼀来,每个收集器机器都能够从自己对应的收集器节点上获取日志源机器列表,进而开始进行日志收集工作

③ 状态汇报

完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能。
因此,针对这个问题,我们需要有⼀个收集器的状态汇报机制: 每个收集器机器在创建完自己的专属节点后,还需要在对应的⼦节点上创建⼀个状态子节点,例如/logs/collector/host1/status,每个收集器机器都需要定期向该节点写入自己的状态信息
==> 可以把这种策略看作是⼀种 心跳检测机制

④ 动态分配

如果收集器机器挂掉或是扩容了,就需要动态地进行收集任务的分配。
在运行过程中,日志系统始终关注着 /logs/collector 这个节点下所有⼦节点的变更,⼀旦检测到有收集器机器停止汇报或是有新的收集 器机器加入,就要开始进行任务的重新分配。无论是针对收集器机器停止汇报还是新机器加入的情况,日志系统都需要将之前分配给该收集器的所有任务进行转移。
可以采取以下两种方式解决:

  • 全局动态分配
    在出现收集器机器挂掉或是新机器加入的时候,日志系统需要根据新的收集器机器列表,立即对所有的日志源机器重新进行⼀次分组,然后将其分配给剩下的收集器机器
    存在问题==>⼀个或部分收集器机器的变更,就会导致全局动态任务的分配,影响⾯⽐较⼤,因此风险也就比较大

  • 局部动态分配
    在小范围内进行任务的动态分配。在这种策略中,每个收集器机器在汇报自己日志收集状态的同时,也会把自己的负 载汇报上去。
    如果⼀个收集器机器挂了,那么日志系统就会把之前分配给这个机器的任务重新分配到那些负载较低的机器上去。同样,如果有新的收集器机器加入,会从那些负载高的机器上转移部分任务给这个新加入的机器

Part 4 - Master选举

(待跟进)

Part 5 - 分布式锁

控制分布式系统之间同步访问共享资源的⼀种方式

(待跟进)

Part 6 - 分布式队列

(待跟进)

五、Zookeeper深入进阶

Part 1 - ZAB协议

「概念」

zookeeper并没有完全采用paxos算法,而是使用了⼀种称为 Zookeeper Atomic Broadcast(ZAB,Zookeeper原子消息广播协议)的协议作为其数据⼀致性的核心算法。

ZAB 是一种专门为Zookeeper设计的 支持奔溃恢复的 原子广播协议。

基于该协议,Zookeeper实现了⼀种 主备模式的系统架构 来保持集群中各副本之间的数据的⼀致性,表现形式就是:
「 使用⼀个单⼀的主进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的 状态变更 以事务Proposal的形式 广播到所有的副本进程中 」

「ZAB核心」

定义了对于那些 会改变Zookeeper服务器数据状态的 事务请求 的处理方式

所有事务请求必须由⼀个全局唯⼀的服务器 (Leader服务器) 来协调处理
它负责 将⼀个客户端事务请求 转化成⼀个 事务Proposal(提议)
并将该 Proposal 分发给 集群中所有的 Follower服务器
超过半数的Follower进行正确的反馈后,Leader向所有Follower分发Commit消息,要求他们提交事务

「ZAB协议介绍」

奔溃恢复模式

当整个服务框架启动过程中,或者是Leader服务器出现网络中断、崩溃退出或重启等异常情况时,ZAB协议就会进入崩溃恢复模式,同时选举产生新的Leader服务器。
当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式。

  • 需要⼀个高效且可靠的Leader选举算法:

    • 快速选举出 Leader
    • 让 Leader 自身知道被当选了
    • 让其他机器快速知道谁是新 Leader
  • 选举算法两大核心规则:

    1. 议需要确保那些已经在Leader服务器上提交的事务 最终被所有服务器都提交
    2. 需要确保丢弃 那些只在Leader服务器上被提出的事务

【选举算法总结】

确保提交已经被 Leader 提交的 事务Proposal,同时丢弃已经被跳过的 事务Proposal
确保方式:让Leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最⾼编号(即ZXID最⼤)的事务Proposal,就可以保证新选举出来的Leader⼀定具有所有已经提交的提案.
P.S. 让具有最⾼编号事务Proposal 的机器来成为 Leader,就可以省去 Leader 服务器检查Proposal的提交和丢弃⼯作的这⼀步操作


消息广播模式

当集群中过半Follower完成了 同Leader的状态同步(数据同步)==> 整个服务框架便可以进入 消息广播模式;
当有其他遵循ZAB的服务器欲加入时,会自觉地进入 数据恢复模式:找到Leader并进行数据同步,并一起参与到消息广播中。

整个消息广播协议 基于具有 FIFO特性的TCP协议 进行网络通信(消息收发的顺序性得以保证)

在广播事务Proposal之前,Leader服务器会首先为这个 事务Proposal 分配⼀个 全局单调递增的唯⼀ID ——> 事务IDZXID
由于ZAB协议需要保证每个消息严格的因果关系,因此必须将每个事务Proposal按照其ZXID的先后顺序来进行排序和处理

【具体过程总结】

  1. Leader服务器会为每⼀个Follower服务器都各⾃分配⼀个单独的队列
  2. 将需要⼴播的事务 Proposal 依次放⼊这些队列
  3. 根据 FIFO策略 进行消息发送
  4. Follower服务器接收到 这个事务Proposal后,将其以事务日志的形式写入到本地磁盘中
  5. 成功写⼊后反馈给Leader服务器⼀个Ack响应
  6. Leader服务器接收到超过半数Follower的Ack响应 --> 广播⼀个Commit消息给所有的Follower服务器以通知其进行事务提交
  7. 每⼀个Follower服务器在接收到Commit消息后,完成对事务的提交

「运行时状态分析」

在ZAB协议的设计中,每个进程都有可能处于如下三种状态之⼀

  1. LOOKING: Leader选举阶段
  2. FOLLOWING: Follower服务器和Leader服务器保持同步状态
  3. LEADING: Leader服务器作为主进程领导状态

一个Follower跟随一个Leader,通过心跳检测感知彼此。
在指定时间内Leader⽆法从过半的Follower进程那⾥接收到⼼跳检测,或者TCP连接断开,那么Leader会放弃当前周期的领导,并转换到LOOKING状态,其他的Follower也会选择放弃这个Leader,同时转换到LOOKING状态,之后会进⾏新⼀轮的Leader选举

「ZAB与Paxos的联系和区别」

  • 共同点

    • 都存在⼀个类似于Leader进程的角色,由其负责协调多个Follower进程的运行
    • Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将⼀个提议进行提交
    • 在ZAB协议中,每个Proposal中都包含了⼀个epoch值,用来代表当前的Leader周期;
      在Paxos算法中,同样存在这样的⼀个标识,名字为Ballot
  • 不同点

    • Paxos算法中,新选举产⽣的主进程会进行两个阶段的工作,第⼀阶段称为读阶段,新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第⼆阶段称为写阶段,当前主进程开始提出自己的提议
    • ZAB协议在Paxos基础上添加了同步阶段,此时,新的Leader会确保 存在过半的Follower已经提交了之前的Leader周期中的所有事务Proposal。这⼀同步阶段的引入,能够有效地保证Leader在新的周期中提出事务Proposal之前,所有的进程都已经完成了对之前所有事务Proposal的提交

总的来说,ZAB协议和Paxos算法的本质区别在于,两者的设计目标不太⼀样:
==> ZAB协议主要用于构建⼀个⾼可⽤的分布式数据主备系统
==> Paxos算法则⽤于构建⼀个分布式的⼀致性状态机系统

Part 2 - 服务器角色

「Leader」

职责

  1. 事务请求的唯⼀调度和处理者,保证集群事务处理的顺序性
  2. 集群内部各服务器的调度者

请求处理链

通过7个请求处理链组成Leader服务器的请求处理链

那些会改变服务器状态的请求称为事务请求(创建节点、更新数据、删除节点、创建会话等)

  • PrepRequestProcessor(请求预处理器)
    识别出当前客户端请求是否是事务请求,并进行预处理 (创建请求事务头、事务体、会话检查、ACL检查和版本检查等)
  • ProposalRequestProcessor(事务投票处理器)
    • 对于非事务请求,转发到 CommitProcessor;
    • 对于事务请求,根据请求类型创建对应的Proposal提议,由所有Follower进行投票,同时转发到 SyncRequestProcessor 进行事务日志的记录
  • SyncRequestProcessor(事务⽇志记录处理器)
    将事务请求记录到事务日志文件中,同时会触发Zookeeper进⾏数据快照
  • AckRequestProcessor(ACK请求处理器)
    向Proposal的投票收集器发送ACK反馈,以通知投票收集器当前服务器 已经完成了对该Proposal的事务日志记录
  • CommitProcessor(事务提交处理器)
    • 对于非事务请求,直接流向下一级;
    • 对于事务请求,会等待集群内 针对Proposal的投票结果 直到该Proposal可被提交
  • ToBeCommitProcessor(预提交处理器)
    内含一个 toBeApplied 队列,用于存储那些已经被CommitProcessor处理过的可被提交的Proposal
    (在 FinalRequestProcessor处理完毕,toBeApplied 队列会被移除)
  • FinalRequestProcessor(最终请求处理器)
    在返回客户端响应前,进行一些操作(创建响应等)。同时还会将事务应用到内存数据库中.

「Follower」

职责

  1. 处理客户端⾮事务性请求(读取数据),转发事务请求给Leader服务器
  2. 参与事务请求Proposal的投票
  3. 参与Leader选举投票

请求处理链

Follower 服务器的第⼀个处理器换成了 FollowerRequestProcessor处理器
同时由于不需要处理事务请求的投票,因此也没有了ProposalRequestProcessor处理器

  • FollowerRequestProcessor(Follower请求预处理器)
    识别当前请求是否是事务请求,若是,那么Follower就会将该请求转发给Leader服务器
  • SendAckRequestProcessor(请求预处理器)
    完成事务⽇志记录后,会向Leader服务器发送ACK消息以表明自身完成了事务日志的记录工作

「Observer」

v3.3.0+ 开始引入的⼀个全新的服务器角色
只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力

职责

  1. 对于非事务请求,进行独立的处理
  2. 对于事务请求,转发给Leader服务器进行处理
  3. 区别于Follower,不参与任何形式的投票,包括事务请求Proposal的投票和Leader选举投票

请求处理链

初始化阶段会将SyncRequestProcessor处理器也组装上去,但是在实际运行过程中,Leader服务器不会将事务请求的投票发送给Observer服务器。

Part 3 - 服务器启动

「服务端整体架构图」

图片来源:拉勾教育Java高薪训练营

启动步骤

  1. 配置文件的解析
  2. 初始化数据管理器
  3. 初始化网络I/O管理器
  4. 数据恢复
  5. 对外服务

「单机版服务器启动」

预处理阶段:

  1. ⽆论单机或集群,在zkServer.cmd和zkServer.sh中都配置了QuorumPeerMain作为启动入口类
  2. zoo.cfg 配置运⾏时的基本参数: tickTime、dataDir、clientPort等
  3. 创建⽂件清理器DatadirCleanupManager ==> 对事务日志和快照数据⽂件进行定时清理
  4. 判定为单机模式 ==> 委托ZooKeeperServerMain进行启动。
  5. 再次进行配置⽂件zoo.cfg的解析
  6. 创建服务器实例 ZooKeeperServer ==> 服务器实例的创建、初始化 + 连接器、内存数据库、请求处理器等组件的初始化

初始化阶段:

  1. 创建服务器统计器ServerStats —— Zookeeper服务器运行时的统计器
  2. 创建数据管理器FileTxnSnapLog —— Zookeeper上层服务器和底层数据存储之间的对接层
    (根据zoo.cfg⽂件中解析出的快照数据目录dataDir和事务日志目录dataLogDir来创建)
  3. 设置 服务器tickTime会话超时时间限制
  4. 创建ServerCnxnFactory —— 通过配置系统属性zookeper.serverCnxnFactory来指定
    --> 使⽤Zookeeper自己实现的NIO
    --> 使用Netty框架作为Zookeeper服务端网络连接⼯⼚
  5. 初始化ServerCnxnFactory —— Zookeeper会初始化Thread作为ServerCnxnFactory的主线程,然后再初始化NIO服务器
  6. 启动ServerCnxnFactory主线程 —— 进⼊Thread的run方法,此时服务端还不能处理客户端请求
  7. 恢复本地数据 —— 启动时,需要从本地快照数据⽂件和事务日志⽂件进行数据恢复
  8. 创建并启动SessionTracker会话管理器
  9. 初始化Zookeeper的请求处理链
  10. 注册JMX服务 —— 将服务器运行时的⼀些信息以JMX的⽅式暴露给外部
  11. 注册Zookeeper服务器实例 —— 将Zookeeper服务器实例注册给ServerCnxnFactory,之后Zookeeper就可以对外提供服务

「集群服务器启动」

预处理 阶段:

  1. 由QuorumPeerMain作为启动类
  2. 解析配置⽂件zoo.cfg
  3. 创建并启动历史⽂件清理器DatadirCleanupFactory
  4. 判定为集群模式 ==> 按需选择配置的集群启动。

初始化 阶段:

  1. 创建+初始化ServerCnxnFactory
  2. 创建Zookeeper数据管理器FileTxnSnapLog
  3. 创建 QuorumPeer实例 —— Quorum 是集群模式下特有的对象,是Zookeeper服务器实例(ZooKeeperServer)的托管者,QuorumPeer代表了集群中的⼀台机器,在运⾏期间,QuorumPeer会不断检测当前服务器实例的运⾏状态,同时根据情况发起Leader选举
  4. 创建 内存数据库ZKDatabase —— 负责管理 ZooKeeper的所有会话记录 以及 DataTree和事务日志 的存储。
  5. 初始化QuorumPeer —— 将核⼼组件如FileTxnSnapLog、ServerCnxnFactory、ZKDatabase注册到QuorumPeer中,同时配置QuorumPeer的参数,如服务器列表地址、Leader选举算法和会话超时时间限制等
  6. 恢复本地数据
  7. 启动ServerCnxnFactory主线程

Leader选举 阶段:

  1. 初始化Leader选举

    集群模式特有,Zookeeper⾸先会根据 自身的服务器ID(SID)、最新的ZXID(lastLoggedZxid)和当前的服务器epoch(currentEpoch)来生成⼀个初始化投票
    初始化过程中,每个服务器都会给自己投票。然后,根据zoo.cfg的配置,创建相应Leader选举算法实现,
    Zookeeper提供了三种默认算法(LeaderElectionAuthFastLeaderElectionFastLeaderElection)---> 可通过zoo.cfg中的electionAlg属性来指定,但现只⽀持FastLeaderElection选举算法。
    在初始化阶段,Zookeeper会创建Leader选举所需的⽹络I/O层QuorumCnxManager,同时启动对Leader选举端⼝的监听,等待集群中其他服务器创建连接

  2. 注册JMX服务
  3. 检测当前服务器状态

    运期间,QuorumPeer会不断检测当前服务器状态
    在正常情况下,Zookeeper服务器的状态在LOOKING、LEADING、FOLLOWING/OBSERVING之间进⾏切换。在启动阶段,QuorumPeer的初始状态是LOOKING,因此开始进⾏Leader选举。

  4. Leader选举

    ZooKeeper的Leader选举过程,简单地讲,就是⼀个集群中所有的机器相互之间进⾏⼀系列投票,选举产⽣最合适的机器成为Leader,同时其余机器成为Follower或是Observer的集群机器⻆⾊初始化过程。关于Leader选举算法,简⽽⾔之,就是集群中哪个机器处理的数据越新(通常我们根据每个服务器处理过的最⼤ZXID来⽐较确定其数据是否更新),其越有可能成为Leader。当然,如果集群中的所有机器处理的ZXID⼀致的话,那么SID最⼤的服务器成为Leader,其余机器称为Follower和Observer

Leader与Follower启动期交互 阶段:

  1. 创建Leader服务器和Follower服务器 ==> 完成Leader选举后,每个服务器会根据自己服务器的角色创建相应的服务器实例,并进⼊各⾃⻆⾊的主流程。
  2. Leader服务器启动Follower接收器LearnerCnxAcceptor ==> 运行期间,Leader服务器需要和所有其余的服务器(统称为Learner)保持连接以确集群的机器存活情况,LearnerCnxAcceptor负责接收所有⾮Leader服务器的连接请求。
  3. Learner服务器开始和Leader建立连接 ==> 所有Learner会找到Leader服务器,并与其建⽴连接。
  4. Leader服务器创建LearnerHandler ==> Leader接收到来自其他机器连接创建请求后,会创建⼀个LearnerHandler实例,每个LearnerHandler实例都对应⼀个Leader与Learner服务器之间的连接,其负责Leader和Learner服务器之间几乎所有的消息通信和数据同步。
  5. 向Leader注册 ==> Learner完成和Leader的连接后,会向Leader进行注册,即将Learner服务器的基本信息(LearnerInfo),包括SID和ZXID,发送给Leader服务器。
  6. Leader解析Learner信息,计算新的epoch ==> Leader接收到Learner服务器基本信息后,会解析出该Learner的SID和ZXID,然后根据ZXID解析出对应的epoch_of_learner,并和当前Leader服务器的epoch_of_leader进行比较,如果该Learner的epoch_of_learner更大,则更新Leader的epoch_of_leader = epoch_of_learner + 1。然后LearnHandler进行等待,直到过半Learner已经向Leader进行了注册,同时更新了epoch_of_leader后,Leader就可以确定当前集群的epoch了。
  7. 发送Leader状态 ==> 计算出新的epoch后,Leader会将该信息以⼀个LEADERINFO消息的形式发送给Learner,并等待Learner的响应。
  8. Learner发送ACK消息 ==> Learner接收到LEADERINFO后,会解析出epoch和ZXID,然后向Leader反馈⼀个ACKEPOCH响应。
  9. 数据同步 ==> Leader收到Learner的ACKEPOCH后,即可进行数据同步。
  10. 启动Leader和Learner服务器 ==> 当有过半Learner已经完成了数据同步,那么Leader和Learner服务器实例就可以启动了

Leader与Follower启动 阶段:

  1. 创建启动会话管理器
  2. 初始化Zookeeper请求处理链,集群模式的每个处理器也会在启动阶段串联请求处理链
  3. 注册JMX服务

六、Zookeeper源码分析

(待跟进)