IT 论文精读 · PAPER 20
Mike Burrows · Google · OSDI 2006
Google 内部有个不起眼的小服务叫 Chubby,几乎所有大系统——上一批讲过的 GFS、Bigtable、MapReduce——都靠它来干一件事:在一群机器里选出「谁当头儿」,并让所有机器都公认同一个头儿。它的真本事是把一个出了名难的分布式难题,打包成一个人人都会用的小工具,让写业务的程序员不用自己去啃那套烧脑的理论。
假设你有一堆机器在合作,需要指定其中一台当「主」(负责拍板、记账)。要求是:所有机器必须公认同一个主,一个时刻绝不能出现两台都以为自己说了算。听起来简单?可这些机器会突然宕机、网线会断、消息会迟到。在这种世界里,「让大家一致同意一件事」难到成了一门专门的学问。更糟的是,几乎每个团队都要重新面对这个坑。
Chubby 的做法很妙:它提供一个像共享网盘的东西,里面能建小文件、放几行字;每个文件上还能挂一把「同一时刻只有一个人抢得到」的锁。于是选主变成一件小学生都懂的事——谁抢到那把锁,谁就是主,抢到后把自己的名字写进文件;其他机器一看文件,就知道现在谁是主。想选主、想存点关键小纸条、想让大家互通有无,全在这一个地方搞定。
关键在于 Chubby 自己不是一台机器,而是一个 5 台机器组成的小组,内部立了条铁规矩:任何决定都要过半数点头才算数——5 台里至少 3 台同意,才认。为什么这条规矩能防住「两个都说了算」?因为一个决定要 3 票,两个互相矛盾的决定就得凑够 6 票,可总共只有 5 台,不够分——数学上就杜绝了同时冒出两个主。即使坏掉一两台,剩下的照样能过半、继续干活。这套「投票过半」的规矩,正是分布式领域最经典的 Paxos 共识算法。Chubby 的贡献不是发明它,而是把它埋进服务里、只做一次,让所有人白捡。
因为它把「难」藏起来了。程序员想给自己的系统加上「高可用选主」,本来得读一堆论文、写几个月还容易写错;有了 Chubby,只要会用「建文件、抢锁、读文件」这几个和操作系统里一模一样的动作就够了,改动极小。结果它火到被当成全公司的「通讯录」(名字服务)用——大家把「某某服务在哪台机器」也写进 Chubby 里互相查,这甚至超出了作者的预料。
Chubby 把「一群会宕机的机器如何公认一个主」这个硬核难题,打包成一个像共享网盘的「文件 + 锁」小服务:谁抢到锁谁当主。服务内部靠 5 台机器「过半数才算数」来保证锁不会被两人同时抢到。它让选主从一门手艺变成一次函数调用,成了 Google 几乎所有大系统的地基。
想看 Chubby 的架构图、租约与缓存机制、以及「为什么是锁服务而不是一个库」的完整论证? → 切到精读版
Chubby 是 Google 的一个分布式锁与协调服务(distributed lock service):它把难写易错的 Paxos 共识(consensus)封装进一个服务的内部,对外只暴露一套「文件系统 + 粗粒度建议锁 + 事件通知」的熟悉接口。应用只需像读写小文件、抢一把锁那样,就能可靠地完成主选举(leader election)、存放少量关键元数据、并互相通告。它优先保证的是可用性与可靠性而非高性能,是 GFS、Bigtable、MapReduce 等系统选主与协调的共同基石,也是后来 ZooKeeper、etcd 的思想原型。
作者是 Mike Burrows(Google,此前在 DEC 参与 AltaVista、后来又设计了 BigQuery 的底层),论文发表于 OSDI 2006。它上承 Lamport 的 Paxos(把「怎么达成共识」这一理论解决),下接工程现实:GFS(2003)、Bigtable(2006)都需要一个可靠的地方来选主、存元数据。Chubby 正是那个「地方」。它也直接启发了 Yahoo 开源的 ZooKeeper(2010)与后来基于 Raft 的 etcd——今天 Kubernetes 的协调层用的就是同一类东西。
一个大型分布式系统绕不开一件事:在一堆随时可能宕机的机器里,选出唯一一个「主」,并让全体公认它。这就是共识问题,Lamport 的 Paxos 早已在理论上给出答案。但 Paxos 有个现实痛点——它出了名地难懂、更难写对:真要落地成一段无 bug 的代码,充满边界情况,绝大多数团队没这个精力,也常常写错。
于是每个需要高可用的新系统,都得把这套烧脑的东西重造一遍。Burrows 的洞见是:与其给大家发一个「Paxos 库」让各自去啃,不如把共识做成一个中心化的服务,一次做对、所有人复用。他在论文里明确论证了「为什么是锁服务、而不是一个共识库」,理由很实在:
一句话:把最难的那部分收进服务里做一次,让难题对使用者「消失」。这就是 Chubby 的立意,也是它最被反复引用的思想。
一个 Chubby 单元(cell)通常由 5 台副本组成,放在不同机架/机房以抗故障。它们内部用 Paxos 选出一个主,并让主持有一份主租约(master lease):在租约有效期内,其余副本承诺不另选新主——这就从根上杜绝了脑裂。所有客户端只跟主打交道;主把每一次写操作,通过 Paxos 复制到多数副本的本地小数据库里,过半数落盘才算提交。主宕机后,剩下的副本用 Paxos 重新选主,客户端在一个宽限期(grace period,默认约 45 秒)内重连到新主,会话不中断。
要点是:Paxos 只在这 5 台服务器内部跑,客户端永远不碰共识。难的部分被关在盒子里。
Chubby 对外长得像一个极简的分布式文件系统。命名如 /ls/cell/foo/wombat(ls = lock service)。树上的每个节点(node)既是「文件」也是「目录」,可以:存一小段数据(整读整写,不支持部分读写,因为设计用途是小文件)、挂一把锁、订阅事件。节点分两种:永久节点与临时节点(ephemeral node)——后者在「没有客户端打开它」时自动消失,天然用来表示「某进程还活着」。
锁是建议性(advisory)且粗粒度(coarse-grained)的。「建议性」意味着锁只约束主动检查锁的人,不像操作系统那样硬性拦截——因为在分布式里,你根本拦不住一个不检查锁、直接去写数据的进程。「粗粒度」意味着锁一持有就是几小时甚至几天(如「我当主期间一直握着」),而不是每秒抢放上万次。这个取舍极其关键:正因为抢锁很稀疏、又能被客户端缓存,5 台服务器才扛得住数万客户端。
客户端与主之间维持一个会话(session),靠周期性的 KeepAlive 握手续命。每次 KeepAlive 相当于「我还在,请把会话租约再延一段」。这条握手是一举多得的通道:主可以在 KeepAlive 的回复里顺带捎上事件通知与缓存失效消息,省掉额外往返。若主在一段时间内没等到客户端的 KeepAlive,会话过期,它持有的锁、打开的临时节点都被释放;反过来若客户端联系不上主,它进入宽限期尝试重连(可能是重连到刚选出的新主),撑过去则会话无缝延续,撑不过才报错。用「租约到期」这种超时判断,替代了「必须收到明确的死亡通知」——这正是分布式容错的核心手法。
如果每次读文件都得发 RPC 问主,5 台机器早被数万客户端压垮了。Chubby 让客户端把读到的文件数据和元数据缓存在本地。难点是缓存怎么保证不脏。Chubby 的答案是严格一致 + 主动失效:主记录着「每个客户端缓存了哪些东西」;当某个文件将被修改时,主先阻塞这次修改,向所有缓存了它的客户端广播失效通知,等大家确认作废后,才真正落盘。
这样客户端要么读到缓存(保证是最新的),要么因失效而回源重取,绝不会读到过期数据。代价是每次写要等失效确认、写会略慢——但这套设计的整个赌注就是「读远多于写、锁很稀疏」,于是缓存把绝大部分读流量挡在了客户端本地,主只需处理少量写和 KeepAlive。这是它能以极少服务器服务海量客户端的关键。
建议锁有个隐患:进程 A 拿着锁去写存储,中途卡住(GC、网络延迟),锁超时被 A 释放、B 拿到了锁;此时 A 那条早先发出、姗姗来迟的写请求才到达存储——它以为自己还持锁,就可能覆盖 B 的数据。Chubby 给出两招:
这篇论文是一篇系统落地报告,说服力来自「真在 Google 大规模跑了」而非某个跑分。作者给出的运行画像很能说明设计取舍是否奏效:一个典型 Chubby 单元只用 5 台副本,却直接服务数以万计的客户端;RPC 流量里绝大多数是 KeepAlive,真正的读写占比很小——正是客户端缓存 + 粗粒度锁把读流量挡下来的直接证据。存储的多是小文件(元数据、选主登记),总量在很小的量级。Chubby 被 GFS、Bigtable、MapReduce 等系统广泛用于选主与元数据存储,成了 Google 基础设施里一个「谁都在用」的部件。
一个出乎意料的结果:Chubby 被大量当成名字服务(替代 DNS)用——开发者把「服务在哪」写进 Chubby、互相查询。它的缓存-失效模型比 DNS 的「定时过期」更适合这种场景,反倒显著减轻了内部 DNS 的压力。作者坦言这是设计时没料到的用法。
Chubby 确立了一个影响至今的范式:把分布式共识收进一个中心化协调服务里做一次,用「文件 + 锁 + 事件」这套人人会用的接口对外,让千千万万应用不必各自重造 Paxos。它是 Google「新三驾马车」之外、支撑老三件套(GFS/Bigtable)跑起来的隐形地基——没有一个可靠的选主与元数据存储,那些系统的 master 都无从谈起。
更深远的是它的开源回声:Yahoo 的 ZooKeeper(2010)几乎是 Chubby 思想的公开实现(接口略有不同,走无等待的 znode + watch),随后 etcd(基于更易懂的 Raft 共识)成了 Kubernetes 的协调核心。今天你在云原生世界里见到的「服务发现、配置中心、分布式锁、leader election」,血脉都能追到这篇论文。
① 一句话:Chubby 把难写易错的 Paxos 共识封装进一个服务,用「文件系统 + 粗粒度建议锁 + 事件」的熟悉接口对外,让应用轻松完成选主与元数据协调。
② 动机:共识(Paxos)理论已解决但极难写对;与其发库让人各自啃,不如做成中心服务、一次做对、所有人复用——且锁接口更符合直觉、还能顺带存「主是谁」。
③ 架构:一个单元 5 副本,内部用 Paxos 选主 + 复制(过半数才提交);主持主租约杜绝脑裂;客户端只连主、永不碰共识。
④ 接口:文件树 + 临时/永久节点,整读整写小文件;锁是建议性、粗粒度(一持有数小时)——正因稀疏才能以少服多。
⑤ 会话靠 KeepAlive 续租约,回复里捎带事件与缓存失效;用「租约超时」代替「明确死亡通知」,主宕机后客户端在宽限期(约 45 秒)无缝重连新主。
⑥ 客户端缓存 + 主动失效:主先阻塞写、广播失效、再落盘,保证严格一致;读远多于写的赌注让缓存挡下绝大部分流量。
⑦ 序列号 / lock-delay:治「旧锁持有者迟到的写请求覆盖新数据」,用带世代号的令牌校验或延迟抢锁窗口。
⑧ 影响与局限:GFS/Bigtable 的选主地基,还被当名字服务用;直接启发 ZooKeeper、etcd(Kubernetes 核心)。但不做高吞吐/细粒度锁、建议锁不防越界者、切换有短暂不可用、易被滥用需限流。