2021 L11
分布式锁与Zookeeper
简单版 ZK 分布式锁实现
Zookeeper 的简单分布式锁实现,通过循环创建锁文件实现,具体步骤如下:
- 尝试创建锁文件:通过循环创建锁文件,创建成功即表示获得锁,使用
break
退出循环。 - 创建失败处理:如果创建失败,使用
exists
方法判断锁文件是否存在,目的是对锁文件添加watch
。 - 等待锁释放:成功创建锁文件的进程释放锁时,调用
delete
锁文件,其他进程会被唤醒并尝试抢夺锁。
存在的问题
该实现存在“羊群效应”问题。每次都会有大量节点去争夺一把锁,最终只有一个节点成功,这会给 Zookeeper 服务器带来很大压力。
ZK 分布式锁的正常实现
更多关于 ZK 分布式锁的正常实现,见 分布式锁。
ZK 锁的应用
- 客户端选举:Zookeeper 锁可以用于客户端选举。
- 软锁:允许客户端如果未宕机可以正常执行一次操作;如果宕机,操作可能会被执行两次。比如在 MapReduce 的 Map 阶段,这种情况是被允许的。
状态转移与复制状态机
两种构建复制状态机的方法
- 使用 Raft 或 Paxos:将所有操作通过 Raft 或 Paxos 的步骤进行一致性处理。
- 配置中心加主从复制:例如 GFS(Google File System),这种方式更为常见。
优缺点比较
- 使用 Raft 或 Paxos:在存储大规模数据时,快照(检查点)的传输会带来压力。
- 配置中心加主从复制:配置中心在状态上占用较小,大量数据由主从复制解决。
链式复制(Chain Replication, CR)
特点
- 配置中心:需要一个配置中心。
- 读操作:仅涉及一个服务器,即读取尾节点(tail)。
- 恢复方案简单:提供线性一致性。
- 写操作流程:先写入头节点(head),然后按照链式顺序同步状态,每个链表节点都有对应的磁盘。等到尾节点完成写入并应用后,直接返回响应给客户端。
三种崩溃恢复方案
- Head 宕机:
- 配置中心直接杀掉 head 节点,将 head 后的节点提升为新的 head 节点。
- 客户端通过配置中心刷新 head 节点的地址。
- 可能有写操作仅存在于原 head 节点中,会丢失,这是合法操作。
- 中间节点宕机:
- 如同链表删除节点,宕机的节点可能未将写操作传递给下一个节点。
- 宕机节点的上一个节点需要检查并传递缺失的操作。
- 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)。