Zookeeper

1. 简介

1.1 分布式系统的问题

每个节点的信息如何同步和共享,两种方式:

  • 通过⽹络进⾏信息共享
  • 通过共享存储

1.2 Zookeeper 解决分布式系统的问题

Zookeeper 通过共享存储解决分布式系统的协调问题

image-20201206170412708

1.3 基本概念

  • 集群中的角色

    • Leader:为客户端提供读和写服务
    • Follower:提供读服务
    • Observer:提供读服务,但不参与 Leader 的选举,也不参与过半写成功策略,可以在不影响写性能的情况下提升集群的性能

    image-20201206170252161

  • 会话(Session)

    客户端启动时,会与服务端建立一个 TCP 长连接,之后用于心跳检测、发送请求、接收响应,接收来自服务端的 Watch 事件

  • 数据节点(ZNode)

    是 Zookeeper 数据模型的数据单元,存储在内存中

  • 版本

    Stat 数据结构记录了 ZNode 三个数据版本,version(当前 ZNode 版本)、cversion(当前 ZNode 子节点版本)、aversion(当前 ZNode 的ACL版本)

  • ACL(Access Control Lists):用来进行权限控制,有五种权限

    • CREATE:创建⼦节点的权限。
    • READ:获取节点数据和⼦节点列表的权限。
    • WRITE:更新节点数据的权限。
    • DELETE:删除⼦节点的权限。
    • ADMIN:设置节点ACL的权限
  • 事件监听器(Wacher)

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

2. 环境搭建

2.1 单机模式

# 下载
wget https://downloads.apache.org/zookeeper/zookeeper-3.6.2/apache-zookeeper-3.6.2-bin.tar.gz

# 解压
tar -zxvf apache-zookeeper-3.6.2-bin.tar.gz

# 进入根目录
cd apache-zookeeper-3.6.2-bin

# 创建快照文件夹和日志文件夹
mkdir data data/logs

# 拷贝样例配置文件,并修改配置文件
cp zoo_simple.cfg zoo.cfg
vim zoo.cfg

dataDir=../apache-zookeeper-3.6.2-bin/data
dataLogDir=../apache-zookeeper-3.6.2-bin/data/logs

# 启动
./bin/zkServer.sh start

# 查看状态
./bin/zkServer.sh status

2.2 伪集群模式

在一台机器下配置三个节点

# 下载
wget https://downloads.apache.org/zookeeper/zookeeper-3.6.2/apache-zookeeper-3.6.2-bin.tar.gz

# 解压
tar -zxvf apache-zookeeper-3.6.2-bin.tar.gz

# 先重命名,然后统一修改配置,最后再拷贝
mv apache-zookeeper-3.6.2 zookeeper1

# 进入根目录
cd zookeeper1

# 创建快照文件夹和日志文件夹
mkdir data data/logs

# 拷贝样例配置文件,并修改配置文件
cp zoo_simple.cfg zoo.cfg
vim zoo.cfg

clientPort=2181
dataDir=../zookeeper1/data
dataLogDir=../zookeeper1/data/logs
server.1=ip:2881:3881
server.2=ip:2882:3882
server.3=ip:2883:3883

# 创建服务器id,zookeeper1的内容为1,以此类推
vim data/myid

# 复制两份,修改clientPort、dataDir、dataLogDir、myid
cp -r zookeeper1 zookeeper2
cp -r zookeeper1 zookeeper3

# 逐一启动
./zookeeper1/bin/zkServer.sh start
./zookeeper2/bin/zkServer.sh start
./zookeeper3/bin/zkServer.sh start

3. 基本使用

3.1 系统模型

数据信息保存在一个个数据节点,这些节点叫 ZNode,组成一棵树

image-20201206223632023

3.1.1 ZNode

ZNode 的节点类型分为三大类

  • 持久性节点(Persistent)
  • 临时性节点(Ephemeral)
  • 顺序性节点(Sequential)

按照组合又有四种类型,每种类型的生命周期都不同

  • 持久节点:节点创建后一直存在服务器,直到删除才主动清除
  • 持久顺序节点:与持久节点相同,但节点名称后面有个数字后缀用来表示顺序
  • 临时节点:生命周期与客户端会话绑定在一起,会话结束自动清除,还有临时节点不能创建子节点
  • 临时顺序节点

Zookeeper 对数据节点的操作会分配一个全局唯一的事务 ID,称为 ZXID,有64位数字,高32位代表epoch,低32位用于递增计数。通过 ZXID 可以间接识别操作的全局顺序。

为了高吞吐高可用,ZNode 存储的数据不能超过 1Mb

ZNode 的状态信息:整个 ZNode 节点内容包括节点数据内容和节点状态信息

  • 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 表示直系⼦节点数。

3.1.2 Watcher(数据变更通知)

Zookeeper使⽤Watcher机制实现分布式数据的发布/订阅功能

image-20201206231926066

具体⼯作流程:

  1. 客户端在向 Zookeeper 服务器注册的同时,会将 Watcher 对象存储在客户端的 WatcherManager 当中
  2. 当Zookeeper服务器触发 Watcher 事件后,会向客户端发送通知
  3. 客户端线程从 WatcherManager 中取出对应的 Watcher 对象来执⾏回调逻辑

3.1.3 ACL(保障数据的安全)

权限模式(Scheme)、授权对象(ID)、权限(Permission),通常使⽤"scheme : id : permission"来标识⼀个有效的ACL信息

  • 权限模式⽤来确定权限验证过程中使⽤的检验策略,有如下四种模式

    • IP:即通过IP进行权限控制,也可以用通配符指定网段
    • Digest:最常用的权限模式,使用username:password的形式进行权限配置,便于区分不同的应用来进行权限控制
    • World:对开放的权限模式,所有用户开放,只有一个权限标识 world:anyone
    • Super:超级用户
  • 授权对象指的是权限赋予的⽤户或⼀个指定实体,例如 IP 地址或是机器等。在不同的权限模式下,授权对象是不同的

    权限模式授权对象
    IP通常是⼀个IP地址或IP段
    Digest自定义,username:BASE64(SHA-1(username:password))
    World只有⼀个ID :anyone
    Super超级⽤户
  • 对数据操作的权限分为五类

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

3.2 命令行操作

# 连接本地服务器
./bin/zkCli.sh [-server host:port]

# 创建节点
create [-s] [-e] [-c] [-t ttl] path [data] [acl]
	-s 顺序节点
	-e 临时节点
	path 路径,以/开头
	data 节点内容

# 读取节点列表
ls [-s] [-w] [-R] path
	-s 状态信息
	-w 节点列表
	-R 递归查看

# 读取节点内容
get [-s] [-w] path
	-s 状态信息
	-w 节点列表

# 更新节点
set [-s] [-v version] path data
	-s 设置之后再输出状态信息

# 删除节点(若删除节点存在⼦节点,那么⽆法删除该节点,必须先删除⼦节点,再删除⽗节点)
delete [-v version] path

3.3 原生 API

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

几个重要的类:Watcher 、Zookeeper、Stat

/**
创建会话(异步)
connectString: 连接地址:IP:端口
sesssionTimeOut:会话超时时间:单位毫秒
Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端,process方法接收)
*/
ZooKeeper zooKeeper = new ZooKeeper(String connectString, int sessionTimeout, Watcher watcher);

/**
创建节点
path  	:节点创建的路径
data[]	:节点创建要保存的数据,是个byte类型的
acl   	:节点创建的权限信息(4种类型),可以用枚举类 ZooDefs.Ids 选择
    	ANYONE_ID_UNSAFE    : 表示任何人
    	AUTH_IDS    :此ID仅可用于设置ACL。它将被客户机验证的ID替换。
    	OPEN_ACL_UNSAFE    :这是一个完全开放的ACL(常用)--> world:anyone
    	CREATOR_ALL_ACL  :此ACL授予创建者身份验证ID的所有权限
createMode  :创建节点的类型(4种类型)
    	PERSISTENT:持久节点			    
    	PERSISTENT_SEQUENTIAL:持久顺序节点
    	EPHEMERAL:临时节点
    	EPHEMERAL_SEQUENTIAL:临时顺序节点
*/
public String create(String path, byte[] data, List<ACL> acl, CreateMode createMode);

// 查看节点数据(可以传入stat,能同时获取状态信息)
public byte[] getData(String path, boolean watch, Stat stat);

// 查看子节点列表(watch为true时可以监听新增、删除节点事件)
public List<String> getChildren(String path, boolean watch);

// 查看节点是否存在(Stat为null时不存在)
public Stat exists(String path, boolean watch);

// 更新节点(version为-1表示对最新版本的数据进行修改)
public Stat setData(String path, byte[] data, int version);

// 删除节点
public void delete(String path, int version);

3.3 第三方客户端

3.3.1 ZkClient

封装了原生的 API,并且实现了 Session 超时重连、Watcher 反复注册等功能

<dependency>
	<groupId>com.101tec</groupId>
	<artifactId>zkclient</artifactId>
	<version>0.2</version>
</dependency>
// 创建会话(与原生不同,这是同步的,而且可以用,分隔地址连接多个zk)
ZkClient zkClient = new ZkClient(String zkServers);

// 创建节点(createParents设置为true时可以递归创建节点)
public void createPersistent(String path, boolean createParents);

// 查看节点数据
public <T extends Object> T readData(String path);

// 查看子节点
public List<String> getChildren(String path);
// 注册子节点列表变更监听事件
public List<String> subscribeChildChanges(String path, IZkChildListener listener);

// 查看节点是否存在
public boolean exists(final String path);

// 更新节点(expectedVersion为-1表示对最新版本的数据进行修改)
public Stat writeData(final String path, Object datat, final int expectedVersion);

// 删除节点
public boolean delete(final String path);
// 递归删除节点(先删除子节点再删除父节点)
public boolean deleteRecursive(String path);

3.3.2 Curator

Curator 是 Netflix 开源的客户端,也解决了Session 超时重连、Watcher 反复注册和NodeExistsException异常等,同时还支持 Fluent 编程风格

<dependency>
	<groupId>org.apache.curator</groupId>
	<artifactId>curator-framework</artifactId>
	<version>2.12.0</version>
</dependency>
// 创建会话
// 不使用fluent编程风格
CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(1000, 3));
curatorFramework.start();
// 使用 fluent 编程风格
CuratorFramework client = CuratorFrameworkFactory.builder()
    .connectString("127.0.0.1:2181")
    .sessionTimeoutMs(50000)
    .connectionTimeoutMs(30000)
    .retryPolicy(new ExponentialBackoffRetry(1000, 3)) //重试策略
    .namespace("base") //独立的命名空间,相当于path前缀/base
    .build();
client.start();

// 创建节点
client.create().forPath(path);
client.create().forPath(path,"我是内容".getBytes());
// creatingParentsIfNeeded会递归创建父节点,可以避免NoNodeException
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);

// 查看节点
client.getData().forPath(path);
// 包含状态查询
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath(path);

// 更新节点
client.setData().forPath(path,"新内容".getBytes());
// 指定版本更新节点,-1代表最新版本,如果指定的版本不存在会抛出BadVersionException
client.setData().withVersion(-1).forPath(path);

// 删除节点
client.delete().forPath(path);
// 递归删除节点
client.delete().deletingChildrenIfNeeded().forPath(path);
// 指定版本删除,-1代表最新版本,如果指定的版本不存在会抛出BadVersionException
client.delete().withVersion(-1).forPath(path);
// 强制保证删除⼀个节点
client.delete().guaranteed().forPath(path);

重试策略

  • ExponentialBackoffRetry(基于backoff的重连策略)
  • RetryNTimes(重连N次策略)
  • RetryForever(永远重试策略)

4. 应用场景

4.1 数据发布/订阅

  • 数据发布/订阅(Publish/Subscribe)系统,是发布者将数据发布到 ZooKeeper 的⼀个或⼀系列节点上,实现配置信息的集中式管理和数据的动态更新,如配置中心。

  • 数据发布/订阅有两种设计模式

    • 推(push):服务端主动将数据更新发送给所有订阅的客户端
    • 拉(pull):客户端主动发起请求来获取最新数据(定时轮询)
  • ZooKeeper 采⽤的是推拉相结合的⽅式

    客户端启动时向服务端注册自己需要关注的节点,如果节点的数据发生变更,服务端会向相应的客户端推送 Wather 事件通知,客户端收到通知后再主动到服务到拉取数据。

4.2 命名服务

  • 在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等——这些我们都可以统称它们为名字(Name),其中较为常⻅的就是⼀些分布式服务框架(如RPC、RMI)中的服务地址列表,通过使⽤命名服务,客户端应⽤能够根据指定名字来获取资源的实体、服务地址和提供者的信息等。

  • 利用顺序节点实现分布式全局唯⼀ ID

    客户端可以通过 create() 方法创建一个顺序节点,返回一个完整的节点名,如 20201209 -> create() -> 202012090000000000(自增序号为10位数)

image-20201209223104325

4.3 集群管理

  • 集群管理包括集群监控(侧重于状态收集)与集群控制两⼤块

  • 场景:在线机器数、机器上下线、机器运行状态

  • 通过 Zookeeper 的两大特性实现集群监控

    • Watcher 机制:数据节点的内容或者子节点列表发生变更,会向订阅的客户端发送通知
    • 临时节点:客户端与服务端之间的会话失效时,临时节点会被自动删除
  • 分布式⽇志收集系统

    image-20201209225229363

    • 解决的问题

      • 1.变化的⽇志源机器
      • 2.变化的收集器机器
    • 实现步骤

      • 1.注册收集器机器

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

      • 2.任务分发

        当所有收集器机器都创建了自己对应的节点后,系统根据收集器子节点个数,将所有日志源机器分组,并将分组的机器列表分别写到子节点中,之后每个收集器机器都从自己对应的收集器节点上获取日志源机器列表,进而开始日志收集工作;

      • 3.状态汇报

        每个收集器机器创建完自己的host节点后,还要再创建一个子节点 status,定期写入自己的状态信息,日志系统根据该状态节点的最后更新时间来判断对应的收集器是否存活;

      • 4.动态分配

        日志系统始终关注 /log/collector 的子节点列表,如果收集器停止汇报状态或者有新的收集器加入,那么需要重新分配任务,有全局动态分配和局部动态分配两种方式。

    • 注意事项

      • 节点类型是持久的,如果临时的,当机器下线时记录在节点上的日志源机器列表也会被清除,无法转移
      • 如果用 Watcher 机制会导致通知的网络流量很大,日志系统节点监听采用主动轮询收集器节点的方式,可以节省流量,但存在延时

4.4 Master 选举

利用 Zookeeper 的强一致性,保证在分布式高并发情况下创建的节点一定能保证全局唯一性,即不会重复创建存在的数据节点,成功创建节点的客户端就成为 Master,其他客户端则注册监听。

4.5 分布式锁

4.5.1 排它锁(Exclusive Locks)

  • 简称 X 锁,又称写锁、独占锁,加锁期间只允许一个事务进行读写操作

  • 用临时节点实现排它锁

    • 1.定义锁

      用一个数据节点表示锁,如 /exclusive_lock/lock

    • 2.获取锁

      所有客户端尝试创建 /exclusive_lock/lock,只有一个客户端能创建成功获得锁,其他客户端注册 /exclusive_lock 的子节点变更事件

    • 3.释放锁

      获取锁的客户端宕机或者完成业务操作,与服务端断开连接,/lock 节点被删除,服务端发送子节点变更事件,客户端重新尝试获取锁

    image-20201209233034141

4.5.2 共享锁(Shared Locks)

  • 简称 S 锁,又称读锁,加锁期间其他事务也可见

  • 用临时顺序节点实现共享锁

    • 1.定义锁

      用一个数据节点表示锁,如 /shared_lock/[hostname]-请求类型-序号

      image-20201209233744111

    • 2.获取锁

      所有客户端都会在 /shared_lock 创建一个临时顺序节点,需要判断读写顺序

      • (1)获取 /share_lock 的子节点列表,并对该节点的子节点变更注册监听

      • (2)确定自己的节点在列表的顺序

      • (3)如果自己是读请求

        • 若没有序号比自己的小的节点或者比自己小的节点都是读请求,那么获取共享锁成功执行读逻辑;

        • 若比自己小的节点存在写请求,则需要等待

          如果自己是写请求

        • 若自己不是序号最小的节点,则需要等待

      • (4)接收到 Watcher 通知后,重复步骤1

    • 3.释放锁

      释放锁的流程与独占锁相同

4.5.3 羊群效应

  • 问题:Zookeeper 发送子节点更变 Wathcer 通知给所有机器,除了给个别机器产生影响外,对其他机器没有任何作用,大量的Watcher通知和子节点列表获取两个操作会重复运行,影响服务器性能。

  • 共享锁核心逻辑:判断自己是否是所有子节点中序号最小的

  • 改进:客户端只关注比自己序号小的节点,而不需要关注子列表变更情况

    image-20201209235947548

4.6 分布式队列

4.6.1 FIFO

  • 设计思路:所有客户端都在 /queue_fifo 节点下面创建临时顺序节点,然后按顺序执行

    image-20201210001321466

  • 执行顺序

    • 1.调用 getChildern 获取 queue_fifo 节点的子节点列表
    • 2.确定自己的节点序号在列表中的顺序
    • 3.如果自己的序号不是最小的,需要等待,同时向比自己序号小的最后一个节点注册监听
    • 4.接收到 Watcher 通知后,重复步骤1

    image-20201210001332670

4.6.2 Barrier 分布式屏障

  • 设计思路:开始时,/queue_barrier 节点内容有一个数字 n,然后所有客户端都在 /queue_barrier 节点下面创建临时节点,直到子节点数达到 n,才会打开 barrier,所有子节点并行执行

    image-20201210001935974

  • 执行顺序

    • 1.调用 getData 获取 /queue_barrier 节点的数据内容 n
    • 2.调用 getChildren 获取 /queue_barrier 节点的子节点列表,同时对子节点变更注册监听
    • 3.统计子节点个数
    • 4.如果子节点个数不足 n 个,继续等待
    • 5.接收到 Watcher 通知后,重复步骤2

    image-20201210002259068

5. 深入

5.1 ZAB 协议

5.1.1 概念

  • ZAB 是一种支持崩溃恢复的原子广播协议,Zookeeper 将其作为数据一致性算法
  • Zookeeper 基于 ZAB 实现了一种主备模式的架构来保持集群中各副本的数据一致性,表现形式为使用一个单一主进程来接收并处理客户端的所有事务请求,并采用 ZAB 的原子广播协议,将服务器数据的状态变更以事务 Proposal 的形式广播到所有副本进程中

5.1.2 ZAB 核心流程

image-20201211002833688

  • Leader 负责将客户端事务请求转化成事务 Proposal(提议),并将该Proposal 分发给集群中所有的 Follower,之后 Leader 等待 Follower 的反馈
  • 一旦超过半数的 Follower 进行了正确的反馈后,那么 Leader 就会再次向
    所有的Follower服务器分发 Commit 消息,要求其将前一个 Proposal 进行提交

5.1.3 ZAB 协议的两种模式

  • 崩溃恢复模式
    • 进入时机:集群启动时、Leader 宕机或者与过半 Follower 出现网络中断
    • 退出时机:产生新的 Leader 后,过半的 Follower 与 Leader 完成状态同步
  • 消息广播模式
    • 进入时机:过半的 Follower 与 Leader 完成状态同步,如果有新的机器加入,新机器会进入数据恢复模式,与 Leader 同步数据,并加入到消息广播流程
    • 退出时机:Leader 宕机或者与过半 Follower 出现网络中断
    • 详细介绍:
      • ZAB 协议移除了二阶段提交中的中断逻辑,只需要过半的 Follower 反馈了就可以提交事务 Proposal
      • Leader 会为每个事务 Proposal 分配一个全局单调递增唯一 ID,称之为事务 ID,即 ZXID
      • Leader 会为每一个 Follower 分配一个单独的队列,然后将事务 Proposal 依次放入到队列,根据 FIFO 策略发送出去,Follower 收到消息后先以事务日志的形式写到本地磁盘,然后反馈 ACK

5.1.4 ZAB 的基本特性

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

5.1.5 数据同步

Leader 服务器会为每一个 Follower 服务器准备一个队列,将那些没有被 Follower 服务器同步的事务以 Proposal 消息的形式逐个发送给 Follower 服务器,并且在每一个 Proposal 消息后面紧接着一个再发送一个 Commit 消息,当 Leader 服务器上所有未同步的事务 Proposal 都同步到 Follower 服务器时,Leader 服务器才会将 Follower 加入到真正可用的 Follower 列表。

5.1.6 运行时状态

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

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

所有进程初始状态都是LOOKING状态;

一个 Follower 只能和一个 Leader 保持同步,Leader 进程和所有的 Follower 进程之间都通过心跳检测机制来感知彼此的情况。

5.1.7 ZAB 与 Paxos 的联系和区别

联系

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

区别

  • Paxos 算法,新选举的主进程会进行两个阶段的工作,第一阶段是读阶段,新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交,第二阶段是写阶段,当前主进程开始提出自己的提议。
  • ZAB 在 Paxos 的基础上添加了同步阶段,新的 Leader 会确保过半 Follower 已经提交了之前的 Leader 周期所有事务 Proposal。
  • 两者的本质区别在于设计目标不同,ZAB 协议用于构建高可用的分布式数据主备系统,而 Paxos 算法用于构建分布式的一致性状态机系统。

5.2 服务器角色

5.2.1 Leader

Leader 服务器是 Zookeeper 集群工作的核心,主要有两个作用:

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

使用责任链处理客户端的请求是 Zookeeper 的特色

image-20201211220838651

  • PreRequestProcessor

    请求预处理器,识别请求是否为事务请求,如果是事务请求,会有一系列处理,如创建事务头、事务体、会话检查、ACL 检查和版本检查等

  • ProposalRequestProcessor

    事务投票处理器会将非事务请求直接转发到 CommitProcessor,而对于事务请求,还会根据请求类型创建对应的 Proposal,发送给所有 Follower 服务器进行投票,同时还需要 SyncRequestProcessor 记录事务日志

  • SyncRequestProcessor

    事务日志记录处理器,记录事务请求记录到事务日志文件中,同时还会触发 Zookeeper 进行数据快照

  • AckRequestProcessor

    通知投票收集器当前服务器已经完成对改 Proposal 的事务日志记录

  • CommitProcessor

    事务提交处理器,非事务请求会被直接交给下一级处理器,而对于事务请求,会等待 Follower 投票过半才提交

  • ToBeCommitProcessor

    该处理器有一个 toBeApplied 队列,用来储存那些已经被 CommitProcessor 处理过的可被提交的 Proposal,最后交给 FinalRequestProcessor 处理,处理完之后,再将其从 toBeApllied 队列中移除

  • FinalRequestProcessor

    用来进行客户端请求返回之前的操作,包括创建客户端请求的响应,针对事务请求,该处理器还会负责将事务应用到内存数据库中。

5.2.2 Follower

Follower 服务器是 Zookeeper 集群的追随者,主要有三个作用:

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

Follower 不需要处理事务请求,也不需要处理事务请求的投票

image-20201211223145936

5.2.3 Observer

Observer 是 Zookeeper 自 3.3.0 引入的新角色,与 Follower 的异同:

  • 处理客户端非事务性请求(读取数据),转发事务请求给 Leader 服务器
  • 不参与事务请求 Proposal 的投票
  • 不参与 Leader 选举投票

简单来说,Observer 只提供非事务服务,通常用于不影响集群事务处理能力的前提下提升集群的非事务处理能力

image-20201211225910323

5.3 服务器启动

整体架构图

image-20201211232133819

Zookeeper 服务器的启动,可以分为五个步骤:

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

5.3.1 单机版服务器启动

单机版的启动可以分为与启动和初始化两个阶段

image-20201211232349348

阶段一:预启动

  1. 无论是单机还是集群,统一由 QuorumPeerMain 作为启动类,zkServer.cmd 和 zkServer.sh 都配置了 QuorumPeerMain 作为启动入口
  2. 解析配置文件 zoo.cnf
  3. 创建并启动历史文件清理器 DatadirCleanupManager,对事务日志和快照数据文件进行定时清理
  4. 判断当前启动模式,如果是单机则委托给 ZookeeperServerMain 启动
  5. 创建服务器实例 ZookeeperServer,包括连接器、内存数据库、请求处理器的初始化

阶段二:初始化

  1. 创建服务器统计器 ServerStats
  2. 创建数据管理器 FileTxnSnapLog,提供了操作数据文件的接口
  3. 设置服务器 tickTime 和会话超时时间限制
  4. 创建 ServerCnxnFactory,通过配置 zookeeper.serverCnxnFactory 来指定网络连接的实现是 NIO 还是 Netty
  5. 初始化 ServerCnxnFactory,将 Thread 做为主线程
  6. 启动 ServerCnxnFactory 主线程,进入 Thread 的 run 方法
  7. 恢复本地数据,根据事务日志和快照数据文件恢复数据
  8. 创建并启动会话管理器 SessionTracker
  9. 初始化请求处理链,将多个处理器串联成一个请求处理器连
  10. 注册 JMX 服务,暴露给外部
  11. 注册 Zookeeper 服务器示例,注册给 ServerCnxnFactory,之后可以对外服务

5.3.2 集群版服务器启动

集群版与单机版类似,但涉及 Leader 选举

image-20201211234244627

阶段一:预启动

阶段二:初始化

阶段三:Leader 选举

阶段四:Leader 与 Follower 启动器交互

阶段五:Leader 与 Follower 启动

5.4 Leader 选举

5.4.1 服务器启动时 Leader 选举

  1. 发起投票,每个服务器将自己作为 Leader 的投票发给其他服务器,使用 (myid, ZXID) 表示
  2. 接收来自各个服务器的投票,检查有效性,如是否为本轮投票、是否来自 LOOKING 状态的服务器
  3. 处理投票,优先检查 ZXID,如果 ZXID 相同,那么将较大的 myid 作为 Leader 服务器(想想3台机器,启动第二台时就确定了 Leader 的场景)
  4. 统计投票,判断是否有过半 (n/2 + 1) 的机器接收相同的投票信息
  5. 改变服务器状态,LOCKING 变为 LEADING,其余的变为 FOLLOWING

5.4.2 服务器运行时 Leader 选举

Leader 宕机或者与过半的服务器无法通信

  1. 改变状态,除了 Observer,所有服务器状态变为 LOCKING
  2. 发起投票
  3. 接收来自各个服务器的投票
  4. 处理投票
  5. 统计投票
  6. 改变服务器状态

6. 源码分析

源码地址

6.1 单机启动

  1. 从 QuorumPeerMain 的 main 方法开始,作为入口
  2. 调用 initializeAndRun 方法
  3. 解析配置文件 zoo.cfg
  4. 通过参数长度和投票者数量来判断是否为集群模式
  5. 如果不是集群模式,委托给 ZookeeperServerMain 执行,调用其 main 方法

6.2 Leader 选举

核心接口为 Election ,包含了一个重要的方法 lookForLeader,默认实现类是 FastLeaderElection

选举流程(FastLeaderElection 选举算法核心步骤)

  1. 自增选举轮次,属性 logicalclock 每轮开始时都会自增
  2. 初始化选票,一个 Vote 数据结构,推举自己为 Leader
  3. 发送初始化选票,选票放到 sendqueue 队列中,由 WorkSender 发送
  4. 接收外部选票,由 WorkReceiver 从 recvqueue 取出选票
  5. 判断选举轮次,有三种情况,如果外部投票的选举轮次与内部投票一致,则进入选票 PK 步骤
  6. 选票 PK 逻辑:选举轮次 》ZXID 》 SID
  7. 变更投票,选票 PK 后,如果外部投票由于内部,那么外部的投票信息就要覆盖内部
  8. 选票归档,不管是否变更了投票,都会将外部投票放入选票集合
  9. 统计投票,如果超过半数认可内部投票,则终止投票
  10. 更新服务器状态,如果自己是 Leader 则更新为 LEADING

image-20201214221414865

6.3 集群启动

如果判断出是集群模式,那么会先调用runFromConfig 方法,接着调用QuorumPeer 的 start 方法

7. 拓展

  • 临时节点的删除有延迟性,原因是 Zookeeper 为了防止网络抖动等原因而导致网络短暂不可达提供的一种容错机制
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火车站卖橘子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值