上一篇(L10)

下一篇(L12)

2021 L11

分布式锁与Zookeeper

简单版 ZK 分布式锁实现

Zookeeper 的简单分布式锁实现,通过循环创建锁文件实现,具体步骤如下:

  1. 尝试创建锁文件:通过循环创建锁文件,创建成功即表示获得锁,使用 break 退出循环。
  2. 创建失败处理:如果创建失败,使用 exists 方法判断锁文件是否存在,目的是对锁文件添加 watch
  3. 等待锁释放:成功创建锁文件的进程释放锁时,调用 delete 锁文件,其他进程会被唤醒并尝试抢夺锁。

存在的问题

该实现存在“羊群效应”问题。每次都会有大量节点去争夺一把锁,最终只有一个节点成功,这会给 Zookeeper 服务器带来很大压力。

ZK 分布式锁的正常实现

更多关于 ZK 分布式锁的正常实现,见 分布式锁

ZK 锁的应用

  • 客户端选举:Zookeeper 锁可以用于客户端选举。
  • 软锁:允许客户端如果未宕机可以正常执行一次操作;如果宕机,操作可能会被执行两次。比如在 MapReduce 的 Map 阶段,这种情况是被允许的。

状态转移与复制状态机

两种构建复制状态机的方法

  1. 使用 Raft 或 Paxos:将所有操作通过 Raft 或 Paxos 的步骤进行一致性处理。
  2. 配置中心加主从复制:例如 GFS(Google File System),这种方式更为常见。

优缺点比较

  • 使用 Raft 或 Paxos:在存储大规模数据时,快照(检查点)的传输会带来压力。
  • 配置中心加主从复制:配置中心在状态上占用较小,大量数据由主从复制解决。

链式复制(Chain Replication, CR)

特点

  • 配置中心:需要一个配置中心。
  • 读操作:仅涉及一个服务器,即读取尾节点(tail)。
  • 恢复方案简单:提供线性一致性。
  • 写操作流程:先写入头节点(head),然后按照链式顺序同步状态,每个链表节点都有对应的磁盘。等到尾节点完成写入并应用后,直接返回响应给客户端。

三种崩溃恢复方案

  1. Head 宕机
    • 配置中心直接杀掉 head 节点,将 head 后的节点提升为新的 head 节点。
    • 客户端通过配置中心刷新 head 节点的地址。
    • 可能有写操作仅存在于原 head 节点中,会丢失,这是合法操作。
  2. 中间节点宕机
    • 如同链表删除节点,宕机的节点可能未将写操作传递给下一个节点。
    • 宕机节点的上一个节点需要检查并传递缺失的操作。
  3. Tail 宕机
    • 配置中心直接杀掉 tail 节点,将 tail 前的节点变为新的 tail 节点。
    • 客户端可以刷新并通过代理节点访问新的 tail 节点,以避免访问过期的节点。

添加新节点

简单的方法是直接添加到 tail。具体步骤如下:

  • 新 tail 先从原 tail 获取数据,这可能需要数分钟到数小时。
  • 使用一张更新表记录已生效但尚未传递到新 tail 的数据。
  • 当新 tail 完成数据复制后,请求原 tail 成为真正的 tail。
  • 原 tail 响应后,添加新节点的过程完成。

注意:链式复制仅影响主备方案,不影响配置中心。

CR 与 Raft 的比较

CR 的优点

  • 客户端请求的 RPC 会发送到 head 或 tail 节点上(分拆读写操作),一定程度上减轻了压力。
  • 只需向头节点发送一个写请求 RPC 即可。
  • 读取操作仅涉及 tail 节点。
  • 故障恢复方法简单。

CR 的缺点

  • 任一节点发生故障时,链表断开(即使恢复简单,但服务仍会短暂中断),必须立刻进行故障恢复。

CR 的读请求优化

将一个资源拆分到多条链中(分片),可以提高读操作的吞吐量:

  • 多条链中的节点可以是重复的,例如有三个节点,链可以是 123、312、231。
  • 这样每个节点在链中都充当一次 head 和 tail,形成负载均衡。
  • 满足线性一致性和可扩展性(scale)。

扩展性

  • 水平扩展(scale out)
  • 垂直扩展(scale up)