Chubby:松耦合系统上的锁服务,不是又一套数据库
Chubby 2006: Lock Service for Loosely-Coupled Distributed Systems
2006 年,OSDI 上 Mike Burrows 交出的不是又一套能扛吞吐的存储,而是 Google 已经跑了一年多的粗粒度锁服务。接口看起来像一套很小的文件系统:目录、整文件读写、劝告式读写锁,再加上文件被改、句柄失效这类事件。论文把目标写得很窄。可靠性、对中等规模客户端集合的可用性、以及容易讲清的语义,排在吞吐和容量前面。一个单元(cell)通常五台副本,多数派活着才算活着;客户端靠它就谁当主、名字指向哪里达成一致,而不是把业务表塞进去。
对象是 2006 年这篇工程报告,不是 GFS 的大块,也不是 Bigtable 的稀疏表。GFS 用一把 Chubby 锁指定主节点;Bigtable 用它选举 master、让 master 发现 tablet server、让客户端找到 master,并把少量元数据放在这个大家都能找到的位置,当作分布式结构的根。两者都是 Chubby 的消费者。它也不是 etcd 的产品史,更不是 《兼职议会》 的定理页。Paxos 给出选定值不能被替换的安全声明;Chubby 是把锁、小文件和会话租约架在一个 Paxos 单元上的服务。五台怎么摆、超时填多少毫秒,不在这篇对象里。
破除误解
误解
**误解一:分布式锁就是又一套数据库,能锁就能存,能存就能当通用存储。实情** 论文把小文件写进接口,是为了让当选的主把自己的身份和参数广告出去,而不是再养一套命名服务。读写是整文件原子替换,故意不提供部分读写,以免把文件做大。生产快照里,大约九成文件不超过 1 KB,超过 10 KB 的只有 0.2%;后来他们给文件加上 256 KB 上限,正是因为有人把上传元数据整文件反复重写,占掉全单元容量的大头。KeepAlive 占 RPC 的 93%,
SetContents大约每百万次调用 680 次。这是协调点上的低流量可靠存储,不是能横向扩展的表。把它当数据库用,会在第一次按行细锁、或第一次把兆字节写进去时,把唯一的 master 打满。
误解二:Chubby 就是后来开源世界里的 ZooKeeper,读这篇等于学会那套协调组件。 ZooKeeper 是另一份对象,接口与事件语义都不在 2006 年这篇报告的范围内。Burrows 写的是 Google 内部已经在跑的服务:链进进程的客户端库、单元里的 master、以及库与 master 之间那套 KeepAlive。论文明确说自己没有新算法,目的是讲做了什么、为什么、以及预期用途和真实用途差在哪。把两个名字叠成一个词,会把劝告式锁、sequencer、以及“锁丢失必须停手”的失败语义,换成另一套教科书。本页停在 OSDI 论文里的装置与测量。
误解三:Paxos 论文已经是 Chubby,实现共识就等于有了锁服务。 Paxos 处理的是异步网络里如何选定一个值。Chubby 的副本用共识协议选 master、复制写请求;读则由仍持有 master 租约的那一台单独回答,因为租约期内不可能另有 master。他们其实另有一份不依赖 Chubby 的客户端 Paxos 库。仍然做锁服务,是因为许多系统先以原型出现,后来才补主选举;往现有结构里加“抢锁、把获取次数传给下游、拒绝过期序号”,比让旧服务器改行参加共识更容易。锁服务还把一份通用选民借给客户端:自己不必凑齐多数副本,单进程持锁也能安全推进。这是工程分工,不是把 1998 年的 Synod 换了个名字。
误解四:分布式锁应当给每一行、每一次短事务一把细锁,持有时间以秒计。 Chubby 按粗粒度来设计。典型用法是选举一个主,由它在数小时或数天内处理那份数据的访问。获取率因此只弱相关于应用的事务率;锁服务器短暂不可用,不会让所有短事务一起停住。代价是锁在客户端之间转移可能触发昂贵的恢复,所以单元故障时不希望把已持有的粗锁丢掉。细锁会把结论反过来:锁服务一抖,大量客户端就堵;事务率叠到锁服务上,扩容变成一等需求。论文只承诺粗锁。细锁若真需要,由应用按组切分、用粗锁去分配这些组——那是纸上的分工,不是要读者去搭的集群说明书。他们事后发现,开发者优化时往往先去掉多余通信,粗锁已经够用,细锁服务器一直没写。
单元、租约与客户端缓存
一个 Chubby 单元通常五台副本,放在不同机架一类位置上,为的是降低相关故障。副本用分布式共识选出 master;master 必须拿到多数票,外加这些副本在几秒的 master 租约内不另选他人的承诺。租约由多数派周期性续上。只有 master 发起对那份简单数据库的读写,其他副本经共识协议抄更新。客户端靠 DNS 里列出的副本去问谁是 master,之后把请求打到它,直到它不再应答或宣称自己不再是 master。写必须传播到单元里的多数副本才确认。读由 master 单独满足,前提是它的租约还没到期。master 挂了,其余副本在租约到期后选新的;论文举过两次选举分别是 6 秒和 4 秒,也见过高达 30 秒。这些数字是故障窗口的测量,不是一份选举超时配方。
客户端从不跟副本发明私有协议。应用链的是客户端库,RPC 全部经库走到 master。库在内存里做写穿式一致缓存:文件内容、节点元数据、以及“文件不存在”。master 记住每个客户端可能缓存了什么。要改数据时,修改被挡住,直到向可能持有缓存的客户端发出失效;客户端清空对应项,并在下一次 KeepAlive 里确认。修改继续的条件是每个客户端要么已确认,要么让缓存租约过期。失效期间该节点视为不可缓存,所以只需一轮失效,读不必停住等。协议选择失效而不是推送更新,以免一个曾经碰过文件的客户端无限期收到无关变更。他们拒绝弱缓存模型,是因为程序员更难用对。
会话是客户端与单元之间的关系,靠 KeepAlive 维持。会话有效,则句柄、锁和缓存默认仍有效。每个会话有一条伸向未来的租约,master 保证在此之前不单方面结束会话;它可以把超时往未来推,不能往回拨。默认再延长 12 秒;过载时可以提高到大约 60 秒,好少处理 KeepAlive。KeepAlive 通常被 master 堵住,直到上一段租约快到期,再带着新超时返回。事件和缓存失效也捎在这次回复里,因此不确认失效就续不上租约。客户端本地维护一份保守的租约估计,必须假定回复在路上耗了时间,且服务器时钟相对自己的前进不超过某个已知因子。本地估计到期后,会话进入危急(jeopardy):缓存被清空并禁用,默认再等 45 秒宽限期。宽限期内重新握手成功则重新打开缓存;否则视为过期,调用返回错误,而不是永远堵住。
这套装置把“锁还在”和“锁已经没了”拆成可观察的事件。库可以发出危急、安全、过期事件,让启动很重的服务先静默,而不是一抖动就整进程重启。句柄失效通常意味着通信出了问题,锁随会话一起作废。另有一个“锁已获得”(lock acquired)事件,理论上可判断主已选出;论文后来说它很少被用。选出主之后,客户端真正要的是和新主说话,于是等的是文件内容被修改——新主把自己的地址写进锁文件。冲突锁请求本想支持把锁缓存得更久,结果没人那样用。论文把这两条列为事后可以删掉的功能。劝告式是另一条硬边界:持有名为 F 的锁,既不是访问文件 F 的前提,也不阻止别人去读它。锁只与其他抢同一把锁的尝试冲突。他们拒绝强制锁,因为 Chubby 锁常常保护别的服务里的资源,强制执行要改那些服务;排障也需要在不关掉全应用的情况下读被锁文件。消息可能乱序、进程会各自崩溃,于是持锁者可以请求 sequencer:一段不透明字节,记下锁名、模式和锁世代号,传给下游去核验。对改不动协议的旧服务,还有至多一分钟的 lock-delay:持有者故障导致锁空闲后,别人要等一会儿才能抢走,挡的是日常延迟包和重启。
小文件在这里首先是名字。名字形如 /ls/foo/wombat/pouch:ls 表示锁服务,第二段是单元名,其余在单元内解释。节点可永久可短暂(ephemeral);短暂节点在没有客户端打开且目录为空时被删掉,用来当临时文件,或向别人表明某个客户端还活着。每个节点都能当劝告式读写锁。选举主的标准写法是:所有候选打开锁文件并抢锁,赢的那个用 SetContents 写下身份;其他人最好在文件修改事件之后再读。理想情况下主取出 sequencer,下游用 CheckSequencer 确认它仍是主。命名结构像文件系统,浏览工具可以接到已有文件接口上,但这不是 GFS:GFS 管大块与追加,Chubby 管少量字节上的一致观察。分布式系统 里最贵的往往是谁必须出现在热路径上;Chubby 把共识关在单元内部,客户端热路径上是缓存命中和偶尔一次 KeepAlive。
论文量到了什么,以及设计被用途改写之处
一张生产快照把负载钉死。距上次 fail-over 18 天,那次持续 14 秒。直接连 master 的活跃客户端 2.2 万,另有 3.2 万经代理。打开的文件 1.2 万,其中约 60% 与命名有关。缓存项 23 万,对应约 2.4 万个不同文件,负缓存名字 3.2 万,大约平均十个客户端缓存同一文件。排他锁约一千,共享锁为零,与“用来选主和粗划分工作”一致。文件 2.2 万,0–1 KB 占 90%。镜像的 ACL 与配置约占 27%,GFS 与 Bigtable 元数据约占 11%。RPC 大约每秒 1 千到 2 千次,KeepAlive 93%,写和抢锁极稀。他们还见过 9 万个客户端直接与一台 master 通信,因为客户端是进程,远超过机器台数。
单元“有 master 且愿意服务”就算在线。约 700 个单元日里记录了 61 次中断,不含关掉整个数据中心的维护。15 秒以内居多,52 次低于 30 秒;多数应用对半分钟以内的中断不敏感。其余九次来自网络维护、连通性、软件错误和过载。几十个单元年里丢过六次数据:四次数据库软件错误,两次操作员错误,没有一次是硬件;操作错误发生在为躲开那些软件错误而做的升级里。过载之前平均延迟是毫秒的一小截;过载典型出现在活跃会话超过 9 万,或一次启动让读请求叠成百万量级。写还要被日志再拖 5 到 10 毫秒。扩容的关键不是把读写路径再榨一点,而是少跟 master 说话。
真实用途改写了设计。尽管按锁服务来做,最热的用法是当名字服务。DNS 用 TTL 丢弃缓存;要把故障换得快,TTL 会短到把服务器打满。三千个进程彼此通信、TTL 60 秒,就要每秒 15 万次查找。Chubby 用显式失效,一次稳定的 KeepAlive 流可以在内容不变时无限期维持任意多缓存项。第一次用它做名字时,启动三千进程会砸出九百万次请求;他们把名字打成大约 100 个相关进程一批,一次查找带回来。开发者很少认真对待故障概率,会把 Chubby 当成总在线。有人建了上百台机器的系统,一换 master 就触发数十分钟的恢复,把单次故障在时间和机器数上都放大一百倍。fail-over 事件本意是提醒可能丢了别的事件、该重扫;许多人收到就崩溃,可用性不升反降。事件通知本身是成功:镜像能在一秒内把变更送到世界各地数十处。失败的是把它当成发布—订阅总线;失效而非更新、再加上沉重的一致性,只适合最琐碎的订阅。
API 上的教训同样来自论文自己的清单。Close 与 Poison 既取消长调用,也丢掉句柄在服务器上的状态,于是能抢锁的句柄不能在多线程之间共享。最初没有积极缓存“文件不存在”,也没有复用已打开的句柄;开发者会写文件不在就无限重试的循环,或反复打开再关闭来轮询。先加指数退避,最后发现让重复 Open 变便宜更省事。没有配额被他们称为事后看来的天真。Java 客户端不愿走 JNI,于是另跑协议转换服务器;不缓存的实现会把负载推回单元。这些是 2006 年文本里的证据,不是一份教人如何运营五副本或如何攻击租约的手册。
跨域连接
- 可靠性工程:故障单元是“会话租约是否仍被 master 承认”,不是“这台客户端机器是否还能 ping 通”。租约到期必须当作锁已丢失,停止以主的身份写;宽限期只覆盖选举窗口,不把不确定状态假装成仍持锁。五副本换的是相关故障下仍能凑齐多数,备份写到另一栋楼的 GFS,为的是打破“GFS 靠 Chubby 选主、Chubby 又把快照存回同一套 GFS”的环。粗粒度把短暂不可用的代价从“所有短事务停住”改成“主选举的故障窗口”,这是把失效模式写进接口。
- 信息不对称:DNS 的 TTL 是一份会过期的私有猜测,选不好就要么把服务器打满,要么让故障主的名字在客户端脑子里多活很久。Chubby 的一致缓存把“谁还拿着这份名字”收成 master 的名单,变更用失效而不是靠大家各自猜超时。sequencer 是持锁者递给下游的短证据:下游不必相信口头自称,可以核对该世代号是否仍有效。客户端本地租约必须比 master 的更保守,承认自己看见的时间是有偏差的私有知识。危急事件把“我不知道会话还在不在”从静默猜测改成可观察的知识状态。
- 概率论:开发者倾向于把服务当成必然在线,于是上百台机器会把一次十余秒的选举放大成数十分钟的群体恢复。700 个单元日里的 61 次中断是经验频率,不是独立故障的乘积公式;多数短于 30 秒,长尾来自网络与过载,不能用“五副本所以中断概率是千分之几”直接算出来。相关故障会让“五选三还活着”的组合计数立刻失效。9 万会话把 KeepAlive 叠成过载,是到达过程超过单 master 处理率,而不是锁算法本身的随机性。
- 立法机构:真实议会用法定人数防止空场立法,用会议纪要防止会后改口。Chubby 单元里的多数派像法定人数,master 租约像有限期的议长任期:任期未满,别人不能另选议长。sequencer 像一份写明届次的授权文书,下游按文书拒绝过期命令,而不是听信自称议长的人。劝告式锁更接近议事规则而不是警察:规则不自动锁上文件,靠参与者遵守。议会比喻能讲清出缺与任期,不能把这份锁服务读成一份可采购的立法程序。
- 认识论:锁还在,是世界里关于会话的事实;客户端“知道自己仍持锁”,是另一件知识。KeepAlive 既续租约,也是必须呈交的回执:不确认失效,就不能把缓存里的旧字节继续当作公共知识。宽限期里的危急状态明确允许“真尚未知”——正确动作是堵住应用观察、清空缓存,而不是猜一份仍有效的主身份。fail-over 之后新 master 发出的事件,要求客户端承认自己可能错过了中间的变更,必须重扫。工程事故常发生在把本地句柄当成已被选定的公共知识。
参考文献
- Burrows, M. The Chubby Lock Service for Loosely-Coupled Distributed Systems. USENIX OSDI, 2006.(本文对象)
- Lamport, L. The Part-Time Parliament. ACM TOCS 16(2), 1998, pp. 133–169.(单元所用共识的安全声明,非本文对象)
- Chang, F. et al. Bigtable: A Distributed Storage System for Structured Data. USENIX OSDI, 2006.(用 Chubby 选主、发现 tablet server 并存放引导位置)
- Ghemawat, S., Gobioff, H. & Leung, S.-T. The Google File System. ACM SOSP, 2003, pp. 29–43. doi:10.1145/945445.945450(用 Chubby 锁指定 GFS 主节点)
- Gray, C. G. & Cheriton, D. R. Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency. ACM SOSP, 1989, pp. 202–210.(论文自承的租约前身)
延伸阅读
- Lamport, L. Paxos Made Simple. ACM SIGACT News 32(4), 2001, pp. 18–25.(角色名的通行讲法;安全论证仍属 1998 年文本)
- Hunt, P., Konar, M., Junqueira, F. P. & Reed, B. ZooKeeper: Wait-free Coordination for Internet-scale Systems. USENIX ATC, 2010.(后来的开源协调服务,接口与本文对象不同)
- 与 兼职议会与 Paxos 对读:定理页保证选定值不能被替换;本文是锁 API、会话与小文件广告。
- 与 etcd 对读:产品史与 Raft 工程属另一页,不要把 2006 年的 Chubby 单元读成那条时间线。