跳至正文
来两杯美式
返回

ZooKeeper 原理与实践:数据模型、Watcher 与 ZAB 协议

By 来两杯美式
发布于

这是 2020 年 2 月整理的 ZooKeeper 笔记。彼时 ZooKeeper 还是分布式协调的事实标准(Dubbo、Kafka、HBase 都依赖它做注册中心/元数据/选主),ZooKeeper 3.5+ 刚引入容器节点等新特性。文中命令基于 3.5.x,与 3.4.x 基本兼容。

ZooKeeper 是什么

ZooKeeper 是一个分布式协调服务,为分布式应用提供:

一句话:让一堆机器像一台机器一样协同工作

数据模型

ZooKeeper 的数据模型可以理解为 Linux/Unix 的文件目录,是一棵

/
├── dubbo              (Dubbo 注册中心根节点)
│   └── com.example.UserService
│       ├── providers
│       └── consumers
├── config
│   └── redis          (统一配置)
└── locks
    └── order_1001_0000000001   (分布式锁临时顺序节点)

树的每个节点称之为 znode,节点上可以保存数据,也可以有子节点。

节点类型

创建节点时可指定类型,常用组合是持久 + 顺序临时 + 顺序

类型参数生命周期说明
持久节点默认一直存在,直到显式删除适合保存配置、元数据
临时节点-e客户端会话断开后自动消失适合注册中心、选主
顺序节点-s同持久/临时名字自动追加 10 位递增序号
容器节点-c最后一个子节点被删除后自动删除3.5+ 新增,适合容器语义

版本号(乐观锁)

每个节点都有各自的版本号 version每当节点数据变化,版本号都会累加

删除、修改一个节点时,如果传入的版本号不匹配会报错:

# 读取当前版本号
stat /config/redis
# 假设版本号为 3
# 带版本号更新(乐观锁),版本不匹配则更新失败
set -v 3 /config/redis "host=10.0.0.1"

这是 ZooKeeper 实现分布式锁的基础——通过 CAS(比较并交换)保证并发安全。

其他特性

典型用途

用途原理场景
主节点选举各节点竞争创建临时节点,创建成功的成为 Leader主从集群高可用,主挂了从节点通过选举选出新主
统一配置管理配置存 znode,各节点监听变化只需在一台机器上改配置,即可同步到所有服务器(如修改 Redis 统一配置)
发布与订阅发布者写数据到 znode,订阅者监听类似 MQ 机制;如 Dubbo 发布者把服务信息存于 znode,订阅者读取到数据
分布式锁临时顺序节点 + Watcher多个进程互斥访问共享资源
强一致性ZAB 协议保证集群环境中保证数据的强一致性(写)

分布式锁的一种经典实现

基于临时顺序节点

  1. 所有客户端在 /locks/order_1001 下创建临时顺序节点。
  2. 谁创建的节点序号最小,谁拿到锁。
  3. 没拿到的客户端监听比自己序号小一号的节点,该节点删除时(持锁者释放)再去检查自己是否最小。
  4. 持锁者会话断开,临时节点自动删除,锁自动释放(不会死锁)。
# 三个客户端竞争,创建出的节点序号递增
create -e -s /locks/order_1001/lock ""
# 返回 /locks/order_1001/lock0000000001(拿到锁,序号最小)
# 返回 /locks/order_1001/lock0000000002(等待 0000000001 删除)
# 返回 /locks/order_1001/lock0000000003(等待 0000000002 删除)

Session 会话

客户端与 ZooKeeper 服务端建立连接后即创建 Session

Session 是临时节点生命周期的判据,也是客户端状态(如 Watcher)的载体。

基础命令

命令作用
help查看帮助
ls path查看节点下的子节点列表
stat path查看节点状态信息
ls2 pathls + stat 的组合
get path获取节点的值
create [-s] [-e] [-c] [-t ttl] path [data]创建节点
set [-s] [-v version] path data设置节点的值
delete [-v version] path删除节点

create:临时与顺序

# 创建持久节点
create /config "hello"
# 创建临时节点(stat 时 ephemeralOwner 有值 = 持有该节点的 session id)
create -e /config/tmp "tmp"
# 创建顺序节点:节点名字 = 当前输入 path + 10 位数字,从 0000000001 开始
create -s /config/seq "seq"
# 返回 /config/seq0000000001
create -s /config/seq "seq2"
# 返回 /config/seq0000000002

注意:不管 path 是否相同,累加的数字都是唯一的、顺序增长的(全局序号由父节点维护)。

set:带版本号更新(乐观锁)

set /config/redis "host=10.0.0.1"
# 修改节点数据,版本号 +1
set -v 3 /config/redis "host=10.0.0.2"
# 带上版本号更新:版本号匹配则成功,不匹配(已被别人改过)则报错

Watcher 监听机制

当监控的某个节点发生变化,会触发 Watcher 事件,类似于 SQL 中的触发器。

Watcher 是一次性的:对一个节点设置的 Watcher,触发后立即销毁,需要再次注册才能继续监听。

客户端                      服务端
  │  ① 注册 Watcher          │
  ├────────────────────────▶ │
  │                          │ 节点发生变化
  │  ② 触发事件并销毁 Watcher │
  │◀─────────────────────────┤
  │  ③ 重新注册 Watcher      │
  ├────────────────────────▶ │

针对不同的操作,触发的 Watcher 事件也不同:

事件触发条件
NodeCreated节点被创建
NodeDeleted节点被删除
NodeDataChanged节点数据被修改(set
NodeChildrenChanged子节点列表变化(创建/删除子节点)

ls 与 stat 监控范围的差异

ZAB 协议:保证一致性

ZAB(ZooKeeper Atomic Broadcast)协议的消息广播过程使用的是原子广播协议,类似二阶段提交(2PC)过程。

读写分离

写请求的完整流程

客户端 ──写请求──▶ Leader
                    │  ① 封装成事务 Proposal,分配全局唯一递增的 ZXID
                    │  ② 放入与每个 Follower 之间的 FIFO 队列

              ┌─────┴─────┐
              ▼           ▼
          Follower1   Follower2  Follower3 ...
            │ Ack       │ Ack      │ Ack
              └─────┬─────┘
                    │  ③ 超过半数成功响应,执行 commit
                    │     (先提交自己,再发送 commit 给所有 Follower)

                事务生效

关键点:

  1. ZXID:全局递增的唯一事务 ID,ZAB 必须将每一个事务按 ZXID 先后排序后处理。
  2. FIFO 队列:Leader 与每个 Follower 之间维护独立的 FIFO 消息队列,异步解耦——同步方式会引起阻塞,性能下降很多。
  3. 过半机制:超过半数 Follower 成功响应即 commit(先提交自己,再广播 commit 给所有 Follower)。

CAP 取舍

由于”过半 Follower 确认提交”的设计:

所以 ZooKeeper 属于 CP 系统(强一致优先,牺牲部分可用性与读一致性)。

崩溃恢复与消息广播

ZAB 协议有两种模式:

崩溃情况 1:Leader 提交后、commit 广播前挂掉

一个事务在 Leader 上提交,得到半数 Follower 的 Ack 反馈,但在把 commit 消息发给所有 Follower 之前 Leader 挂掉了:

崩溃情况 2:Leader 提出事务后、尚未广播就挂掉

Leader 提出一个事务后就崩溃,导致其他服务器没收到这个事务:

ZXID(64 位)
┌───────────────────────────────┬──────────────────────────────┐
│  高 32 位:epoch(Leader 周期)  │  低 32 位:计数器(单调递增)   │
└───────────────────────────────┴──────────────────────────────┘

当一个包含上一个 Leader 周期中未被提交的事务 Proposal 的服务器加入集群:

  1. 自动以 Follower 角色加入。
  2. Leader 发现它有上一个 Leader 周期的事务 Proposal 时,会要求它执行回退操作
  3. 回退到一个确实已经被集群中过半机器提交的最新事务 Proposal(丢弃未提交的孤儿事务)。

安装与配置

  1. 下载安装包(如 apache-zookeeper-3.5.10-bin.tar.gz),解压。
  2. 复制 conf/zoo_sample.cfgconf/zoo.cfg
  3. 按需修改 zoo.cfg

zoo.cfg 参数说明

参数作用
tickTime基本时间单元(ms),其他时间配置 = 数值 × tickTime
initLimit集群:允许从节点连接并同步到主节点的初始连接时间(倍数 × tickTime)
syncLimit集群:主节点与从节点之间发送消息、请求和应答的时间长度(心跳机制),超时从节点被抛弃
dataDirZooKeeper 的数据存放位置
dataLogDirZooKeeper 的日志存放位置(建议与 dataDir 分开磁盘)
clientPort客户端连接端口(默认 2181)
# conf/zoo.cfg 单机示例
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/usr/local/zookeeper/data
dataLogDir=/usr/local/zookeeper/logs
clientPort=2181

集群搭建

使用 conf/zoo.cfg 配置文件:

  1. 修改 dataDir,为每个节点设置好目录。
  2. 设置 dataLogDir,配置日志。
  3. 在配置的 dataDir 里创建文件 myid,里面的数值要与下面配置中 server. 后面的序号一致。
  4. 添加集群节点配置(2888 为集群通信端口,3888 为 Leader 宕机后的选举端口)。

三节点集群示例

节点 1(192.168.1.10):

# conf/zoo.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/usr/local/zookeeper/data
dataLogDir=/usr/local/zookeeper/logs
clientPort=2181

server.0=192.168.1.10:2888:3888
server.1=192.168.1.11:2888:3888
server.2=192.168.1.12:2888:3888
# dataDir 下创建 myid,内容与 server. 后的序号一致
echo "0" > /usr/local/zookeeper/data/myid

节点 2(myid1)、节点 3(myid2)同理,zoo.cfg 内容相同。

验证集群

# 三台机器都启动后
bin/zkServer.sh start
bin/zkServer.sh status
# 输出 Mode: leader / follower,集群中恰好一个 leader,其余为 follower

要点myid 必须与 server.N 的 N 一致;集群最少 3 台(过半机制要求 2n+1),2 台无法形成过半共识。

小结


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
查看系列全部文章
  1. 01.CAP 与 BASE:分布式系统的理论基石
  2. 02.拜占庭将军问题:两忠一叛、口信与签名消息之解
  3. 03.ZooKeeper 原理与实践:数据模型、Watcher 与 ZAB 协议
  4. 04.分布式事务五种方案:消息驱动 / XA / TCC / 本地消息表 / Saga
  5. 05.微服务限流与容错:雪崩效应、熔断器与三种限流算法

上一篇
DDD 复杂业务系统设计(四):实体与值对象,把“学员”和“地址”放进代码的两种姿势
下一篇
设计模式之原型模式,及深浅拷贝