摘要: Google Chubby 锁服务
Chubby 锁服务
1. 定位
We describe our experiences with the Chubby lock ser- vice, which is intended to provide coarse-grained lock- ing as well as reliable (though low-volume) storage for a loosely-coupled distributed system
我们描述了Chubby锁服务的经验,该服务旨在为松耦合的分布式系统 提供粗粒度的锁 以及 可靠的(尽管容量较小)存储
这里粗粒度的锁指的是应用加锁的时间比较长,例如 leader election chubby 服务本质上不算新的研究成果,而是一种工程上的努力实现
1.1 lock vs distributed library
Google 内部实际上也提供了一个独立于 Chubby 的一致性库(基于paxos),但考虑到锁服务有如下一些优点:
- 开发者(用户侧) 在应用刚起步的时候并不会像我们想象的考虑高可用,代码也没有为使用一致性协议而专门优化。(PS: 好的架构是演化来的)
- 可以在锁服务存储少量数据。
- 开发者(用户侧) 更熟悉锁的接口、临界区,而不用去考虑底层的一些细节。
- 锁服务可以减少对接的业务节点数量(使用一致性库需要使用 quorum)
2. 系统架构
一个 chubby 单元由副本集合组成(通常是5个、放在不同的机柜),这些副本通过一次 paxos 选举出一个 master,同时要保证在 master 的租约有效期内不会再选举另一个 master,租约会周期性的更新。
master 负责读写,slave 只是复制 master。客户端通过请求 DNS 获取各副本列表,然后像这些副本发送 master 定位请求,非 master 副本返回 master标识,一旦定位到 master 则将请求全部切到 master (直到 master 失去响应或者 因为不再是 master 拒绝请求)。
对于 副本异常 且无法自动恢复的场景,一个替换系统会从空闲的机器池中选择一台新的机器并运行 chubby 服务,然后更新 DNS 表,将出错的改为新的。并且从文件系统的备份中选择一份最近的数据和接收最新的更新。 master 节点会定期检测 DNS 表,在发现该变化时会更新它所在的单元的副本列表
3. 文件系统设计
Chubby提供了一个类似于UNIX但是相对简单的文件系统接口。
/ls/foo/wombat/pouch
- ls 代表了 lock service
- foo 代表了 foo 这个chubby单元,通过 DNS 解析为一组副本。特殊单元 local 表示了离客户端最近的那个单元
- /wombat/pouch 由落到的 foo 单元自行处理
这样的设计可以降低使用的门槛,文件和目录统称为Node,和client同生命周期的临时节点可以用来实现节点存活监控。 Node 除了记录数据外还包括了一些元信息
3.1 元信息
- ACL
- /r/acl/node1 – user1
- /w/acl/node1 – user1
- 四个严格递增的64位数字
- 实例编号 (大于该节点之前的任何实例编号)
- 内容版本号 (内容发生改变时,随之增加)
- 锁版本号 (锁发生状态变化时,随之增加)
- ACL版本号 (权限被修改时,随之增加)
4. 锁接口设计
- 每一个 Node 都可以作为一个读写锁,要么是一个 client 独占读锁,要么是多个 client 共享写锁。
- 提供友好的API:
- open,close
- Acquire,TryAcquire,Release
- GetContentsAndStat,SetContents,SetACL
- Delete
- GetSequencer,SetSequencer,CheckSequencer
4.1 实现
- 发起 Acquire request, master 生成锁描述 Sequencer 和 lock 版本号,在对应节点的元信息中记录这个lock版本号 ,每次被Acquire 时加一
- Acquire request 成功后,在对应的 Handler 也记录这个 锁描述 Sequencer
- 后续的操作通过对比元信息的 锁描述 Sequencer 和 lock 版本号 判断是否有效
- 发起Release request,则释放并修改对应元信息
4.2 事件
- 文件内容改变
- 子节点的增删改
- master 故障恢复
- 锁的获取
- 锁冲突
这里的事件都是实际动作发生后才传递的。
5. Cache & Session & Fail-overs
5.1 cache
Chubby 的 Cache 主要用来缓存文件数据、元信息。当文件发生改变时,修改的请求会被阻塞,直到 master 将失效通知发给所有的客户端。
5.2 session
Chubby 的 Session 是 Chubby Cell 和 Chubby 客户端的一种关系,通过周期性的 KeepAlive 握手维护。
每个 Session 都通过一个租约时间来保证 master 不单方面结束会话。
master 在下面的三个时机会选择续期租约:
- 会话刚创建时
- master 发生故障恢复时
- 在租约快到期响应客户端的 KeepAlive RPC 调用时 (master 在收到 client 的 KeepAlive RPC 时,会阻塞到本地,直到租约快到期,master 会延长租约时间(默认是12s)并返回阻塞的 KeepAlive RPC 给 client)
- client 的租约过期会进入 jeopardy 状态,client 会进入一个宽限期(grace period,默认45s),以期待与 master 重新进行 KeepAlive 交互,如果还是无法交互则返回失败。
- 这样的设计可以保证通常只有一个 KeepAlive 调用阻塞在master端
- 客户端也维护了一个本地租约时间,它的过期时间要比 master 久(1. KeepAlive 的传输时间 2. master 时钟快于 client)
- 宽限期可以保障在API调用时候不会无限期的阻塞
5.3 Fail-overs
我们重点关注在宽限期(grace period)内,新的 master 被选举出来后的恢复操作,如下步骤所示:
- 它会选择一个新的epoch number (master 会拒绝使用老的 epoch 编号的客户端)
- 恢复 Session 和 锁的内存数据结构,租约会被重置一个很大的值
- master 开始接受 client 的 KeepAlive RPC,第一个因为 epoch 是老的会被拒绝,第二个会带着第一个返回的最新 epoch 发起请求,master 立即返回并通知 client 设置本地的 租约时间(lease C3),接着 master 开始阻塞第三个 KeepAlive RPC,恢复正常!
6. Chubby 的使用
以论文中选主场景为例:
- 潜在参与选主的节点,调用
open()
同一个节点,然后用返回的 Handler 调用Aquire()
- 只有一个成功拿到锁,成为 Primary 节点,其余的是 Secondary 节点
- Primary 节点调用
SetContents()
写入自己的标识,client 和其他 Secondary 通过GetContentsAndStat()
获取该信息,并注册监听内容变化的 Event,以便发现 Primary 的改变 - Primary 节点 通过
GetSequencer()
获取一个sequencer,发给它所通信的依赖锁的服务,该服务通过CheckSequencer()
来验证它是否仍然是 Primary
7. 总结
可以发现,Chubby 是有很多的优化和稳定努力,例如通过增加租约时间来减少 rpc造成的负载、增加缓存提高读的性能、对于资源的全面监控可以更好的做降级、单元的不同副本不同机柜、每隔几个小时会把存储数据快照写入另一栋楼的 GFS 文件系统,避免了 GFS 和 Chubby 的循环依赖。