6.824 分布式系统 ZooKeeper
2023-10-28 12:15:00 # Papers

一致性辨析

  1. 顺序一致性:
    各进程之间的操作合并成一个操作序列, 每个进程自己的操作序列顺序和大操作序列中的相对顺序相同(不一定相邻), 这样不一定满足全局时钟顺序
  2. 线性一致性(强一致性):
    • 任意时刻, 节点中的数据都是一样的
    • 集群内服务器数据都同步完成之后才能对外提供服务
  3. 弱一致性:
    • 任意时刻, 节点中的数据不一定一样
  4. 最终一致性:
    • 弱一致性的特殊形式
    • 在没有新的写入时, 不同节点上的同一份数据趋向于相同

ZooKeeper 实现的一致性是 写入操作的线性一致性和 读操作的顺序一致性

服务

  1. 客户端: 使用 ZooKeeper 服务的用户
  2. 服务器: 提供服务的进程
  3. znode: Zookeeper 数据的内存数据节点
  4. 数据树: 以层次命名空间组织起来的 znode
  5. 会话: 客户端连接到 ZooKeeper 时建立一个会话, 通过会话句柄提交请求

服务概述

客户端可以创建两种 znode

  1. 普通: 通过创建和删除来操作普通节点
  2. 临时: 客户端创建这类节点, 显式删除或者会话结束时自动删除

创建新的 znode 节点时, 可以指定 sequential 标识, 这样该节点名字后面会附加序号

生成的序号不会比父节点之前创建的所有子节点小

Zookeeper 实现了 watch, 客户端无需轮询, 而是由服务器在状态变化时通知客户端

数据模型

每个 znode 默认 1M 大小, 通常用来存储某种元数据

image

会话

有类似心跳的机制让客户端和服务器之间确认对方是否正常工作

会话持续期间, 客户端可以持续观察到状态变化

客户端 API

所有的方法都有同步和异步两个版本, 当没有并发情况的时候使用同步

Zookeeper 使用绝对路径访问 znode, 这样可以简化 API 和服务器需要维护的状态

每次更新都是对特定版本的更新, 如果版本号与期望的版本号不一致, 那么更新就会失败

原语例子

配置管理

配置信息保存在节点 Zc 中 (watch), 配置被更新的时候, 客户端会读取到更新信息

信息汇合

客户端创建一个 Zr 节点, 并把绝对路径传给主进程和工作进程

主进程把 IP 地址和端口号写入 Zr
工作进程监视 Zr 节点, 更新的时候才能和主进程通信

如果 Zr 节点是临时节点, 那么主进程和工作进程可以监视 Zr 的存活情况(客户端是否退出)从而决定是否退出进程

群组关系

设置一个 Zg 节点表示组, 这个组的成员进程启动时, 在 Zg 下面创建一个临时节点, 进程启动

进程故障或者终止的时候, 相关的 znode 自动删除

简单锁

客户端尝试创建一个 znode 创建成功就表示持有锁, 删除表示释放锁

创建失败就 watch 这个节点, 被删除了再尝试创建

这样会有群体效应问题, 很多客户端竞争一个锁

其次这样只实现了互斥锁

无群体效应的简单锁

定义一个 znode 名称为 l

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// lock
n = create(l + "/lock-", EPHEMERAL|SEQUENTIAL)

while (true)
{
C = getChildren(l, false)
if n 是 l 子节点中编号最小的
{
持有锁; return;
}

p = 比 n 编号小的 最大的节点
if exists(p, true) 等待
}
1
2
// unlock
delete(n)

优点:

  1. 移除一个锁只会释放一个客户端
  2. 没有轮询或超时
  3. 方便观察和调试

读写锁

1
2
3
4
5
6
7
8
9
10
11
12
13
// write lock
n = create(l + "/write-", EPHEMERAL|SEQUENTIAL)

while (true)
{
C = getChildren(l, false)
if n 是 l 子节点中编号最小的
{
持有锁; return;
}
p = 比 n 编号小的 最大的节点
if exists(p, true) 等待
}
1
2
3
4
5
6
7
8
9
10
11
12
// read lock
n = create(l + "/read-", EPHEMERAL|SEQUENTIAL)
C = getChildren(l, false)
while (true)
{
if 如果没有 write 节点的编号比 n 小
{
持有锁; return;
}
p = 比 n 编号小的 最大的 write 节点
if exists(p, true) 等待
}

ZooKeeper 实现

对于读请求, 服务器读取本地数据库并返回

对于写请求, 使用 zab 一致性协议实现同步

写请求被发送给 Leader

ZooKeeper 在将修改应用到数据库的时候会写入磁盘, 方便故障之后通过本地磁盘类似快照的方式恢复

请求处理器

请求处理器在收到写入请求的时候, 根据请求内容计算出新的状态, 版本号和时间戳, 等待应用到数据库中

原子广播

使用 Zab 一致性协议, 使用多数认同来达成一致性

领导节点广播之前要收到前一个领导的广播

复制数据库

模糊快照: 不需要锁定状态而生成快照

因为写入的状态变更消息是幂等的

如果服务器从快照恢复, Zab 会重新发送状态变更, 最终保证与宕机前 的状态一致

C/S 交互

  1. 串行写: 所有更新操作都是串行的, 数据更新时, server 会通知 观测它的所有 client, 注意这些事件会随会话的销毁而消亡
  2. 本地读: 可以选择在本地读, 但是可能会读取旧数据, 可以选择使用 sync 得到最新数据, 但是效率降低
  3. 一致性视图: 维护一个 zxid, client 在故障重启时连接到一个新的 server, 如果该 serverzxid 小于 clientzxid, 那么就换一个 server 连接
  4. 会话过期: 心跳未响应的时候过期