📝 术语别名
  • cell: 一个故障隔离单元, 通常 1 master + 5 replica;
  • coarse-grained lock: 粗粒度锁, 持有时间小时/天级 (典型如"我是这个集群的 master"), 跟毫秒级临界区互斥的 fine-grained lock 相对;
  • advisory lock: 建议锁, 系统不强制, 靠"守规矩的应用"自觉遵守;
  • safety / liveness: 安全性(“坏事永不发生”, 比如绝不脑裂) / 活性(“好事最终发生”, 比如最终能达成决定);

论文定位

Mike Burrows, “The Chubby lock service for loosely-coupled distributed systems”, OSDI 2006, Google;

这不是一篇推公式的理论论文, 而是一篇工程经验总结: Chubby 在 Google 内部已经给 GFS、Bigtable 这些大家伙撑了好几年选主了, 所以全文都是踩坑之后的反思, 读它的正确姿势是"看一个老练的系统工程师, 怎么在一堆现实约束里做取舍"; 时间线上它比 Raft (2014) 还早了整整 8 年, 那会儿 Paxos 基本是异步共识的唯一选项;

Chubby 的核心主张乍一听有点反直觉; 当年要解决分布式选主/协调, "理论正确答案"是给每个应用塞一个 Paxos 库, 让它们自己投票去; 但 Chubby 偏不, 它把共识藏进一个中心化的服务里, 对外只露出一个长得像文件系统的锁接口;

"a lock service is more useful than a Paxos library": 对干活的团队来说, 锁服务比共识库更有用;

得留意这句话批的不是 Paxos 算法本身 (Chubby 底下用的正是 Paxos), 而是"把 Paxos 打包成库直接甩给开发者"这种交付方式; Chubby 对 Paxos 是真爱, 第二年还专门写了篇《Paxos Made Live》讲他们落地踩了多少坑;

为什么是锁服务而不是 Paxos 库

先看传统 Paxos library 是怎么活的: 共识逻辑被塞进每个应用/存储节点, 于是这个服务自己的每个 replica 都得:

  • 维护 Paxos 状态;
  • 参与提案和多数派投票;
  • 复制一致性日志;
  • 处理 leader 选举、故障恢复、成员变化;
  • 把达成共识的日志 apply 到自己的状态机;

说白了就是共识逻辑跟应用的数据存储、业务逻辑死死绑在一起, 每个想搞高可用的服务都得把这套从头扛一遍; Chubby 想破的就是这个局; 这一节给了四条理由, 是全篇的灵魂, 其实就是同一个立场的四个侧面:

# 核心论点 一句话
1 保留既有程序结构, 高可用可以事后补 改动小, 不用重写
2 选完主还得昭告天下, 所以得能存点小数据 光有锁不够, 还要能存小文件
3 锁/文件接口程序员熟 心智成本低
4 客户端不用自己凑多数派就能往前走 把 quorum 需求挪进服务内部

理由 1: 保留既有结构, 高可用可以事后补

开发者一开始基本不会为高可用做设计, 系统往往就是个原型, 负载又低, 代码压根没按共识协议那套来组织; 等它做大了、用户多了, 才回头想加副本、加选主;

这时候锁服务的好处就出来了: 你几乎不用动原来的结构就能把高可用挂上去; Burrows 举的例子很实在, 让一组进程选出 master、再由它去写一个已有的文件服务器, 用锁服务只要加两条语句和一个 RPC 参数 (“启动时抢个锁, 写的时候带上 lock 信息”) 就完事; 换共识库你就得把整个程序围着状态机重写一遍; 一个是增量改造, 一个是推倒重来;

理由 2: 选完主还得昭告天下, 所以得能存点小数据

很多服务选出 primary、或者把数据分了区之后, 还得有个办法把结果告诉所有人 (“现在谁是 master”、“分区表长啥样”); 光有加锁解锁干不了这事, 你还得能读写一点点数据; 所以 Chubby 顺手让每个 node 能存个小文件; 这活儿本来可以另起一个 name service 来干, 但既然一个强一致的锁服务已经在那杵着了, 把"存结果 + 广播结果"一并接过来最省事;

这也正是 etcd 为啥既是协调原语、又是个 KV 存储: 选主和"把选主结果发出去"本来就是一个需求的两半; 见 12.etcd;

理由 3: 锁和文件, 程序员熟

程序员都懂锁、都懂文件; 真没几个懂怎么把 Paxos 用对; 拿大家本来就有的心智模型 (acquire/release、open/read/write) 把共识包起来, 上手快, 还不容易用错; 哪怕分布式锁的语义跟单机锁有些微妙的不一样 (这正是后面 sequencer、lock-delay 要补的坑), 这层熟悉的壳还是把采用门槛压得很低;

理由 4: 客户端不用自己凑多数派就能往前走

最容易卡住的地方是: “Chubby 自己不也得 3/5 多数派吗, 怎么会只要 1 台在线?” 问题在于, 这个"1 台在线"说的根本不是 Chubby 的服务器, 而是你自己应用的副本;

[!external lock server 设计理念]
Chubby 在这里想指出个一个设计理念就是把数据复制、存储流程和共识、增删查改的控制逻辑解耦,前者就是我们常见的 Data Plane 数据平面, 而后者就是我们常见的 Control Plane 控制平面;
在这种解耦设计下,数据平面的 replica 数量并不会影响 control plane 内部的一致性问题,也就是说,data plane 的逻辑正确性依靠 control plane 内部维护的 paxos cluster 的 safety 保障, 并且即使 data plane 服务器非常多有 1000 台,这里的 control palne 服务器也只需要 5 台就可以控制,出现 failure 的概率比 1000 台低很多, 共识速度相对也会快不少;
后文会对这部分有总结

1
2
3
4
5
6
7
共识库:  App副本1 ┐
App副本2 ├─ 互相投票, 要 2/3 在线才能决定谁是主
App副本3 ┘ 挂到剩 1 台就废了

lock server: App副本1 (活着) ──抢锁──┐
App副本2 (挂了) ├──▶ Chubby cell (5副本, 内部自己凑 3/5)
App副本3 (挂了) ┘ 只要还剩 1 个实例 + 能连上 Chubby 就能往前走

quorum 并没有凭空消失, 只是被搬了个地方、还顺带摊薄了: 从"每个应用各养一套 5 副本 Paxos", 挪进了大家共享的 Chubby 那一套 (3/5):

谁背 quorum 代价
共识库 每个应用各背一套, N 个应用 = N 套 5 副本 重复、贵、每个团队都得懂 Paxos
锁服务 大家共享 Chubby 这一套 一套 5 副本被全公司成百上千应用复用

粗细粒度锁设计取舍

前面四条说的是"为啥要做成锁服务", 这后半段 Burrows 接着掰扯一个更细的取舍: 既然做成锁服务了, 那这锁该做成粗的还是细的? 先把两种锁摆一块儿对比 (粗粒度到底啥意思、跟 Multi-Paxos 那个稳定 leader 怎么区分, 放在后面系统骨架那节细抠, 这里只关心粗 vs 细怎么选):

粗粒度 coarse-grained 细粒度 fine-grained
持有时长 长, 小时/天级 短, 秒级甚至更短k
典型用途 选主、primary ownership 保护某一条数据/某个事务
对 lock server 的负载 小: 抢锁频率跟业务事务率几乎没关系 大: 抢锁频率约等于业务事务率
server 短暂挂掉的影响 小: 反正很少来抢 大: 一票 client 立刻卡住
failover 丢锁的代价 高, 所以非保住锁状态不可 低, 丢了重抢就行, 可以接受

为啥 failover 时, 粗粒度比细粒度容易丢更多数据

关键就一个词: 一把锁的 blast radius (波及面), 也就是"这把锁要是在 failover 里出了岔子, 背后有多少东西跟着遭殃";

先把 master failover 这个场景在脑子里过一遍: Chubby master 挂了, 新 master 顶上来, 中间会有一小段时间, "谁持有哪把锁"这件事是模糊的: 老的持锁方可能还没反应过来自己的处境变了, 新 master 那边也还在重建状态; 这段模糊窗口就是一切风险的来源;

现在看两种锁掉进这个窗口分别会咋样:

  • 粗粒度: 一把锁背后, 是一个攥着它好几个小时、正连续往 DB1 哐哐写几百万条的 primary; 它敢这么写的全部底气就是"我手里有这把锁 = 我是唯一的 primary = 我可以放心写"; 可一旦 failover 把"谁是主"弄模糊了, 老 primary 还没回过神、继续闷头写, 新 primary 又被选出来开始写, 俩主同时往同一份数据上招呼, 写出来的东西互相冲突、互相覆盖, 这就是 split-brain, 直接就是数据损坏/丢更新; 而且它持锁那段时间本来就在海量 I/O, 这个窗口里押在台面上的数据量大得吓人, 一把锁出事就能糊掉一整个 DB 的在途写入;
  • 细粒度: 持锁方就在一个毫秒级的小临界区里干一件小事; 就算 failover 把它的锁弄丢了, 它顶多损失正在做的这一个小操作, 重试一遍就完事了; 波及面约等于 0;

所以同样是 failover 丢一把锁, 粗粒度那把锁的"含金量"高得离谱: 它代表的是一个长命 primary 加上一大坨在途数据的所有权, 丢一把就是丢一片; 细粒度那把锁就是个一锤子买卖, 丢了不疼;

[!Motivation 总结]
所以粗细粒度的锁都有问题,但是在这里的应用场景中我们主要是让整个 cluster 的对外读写性能不要受太大影响,所以坚持用 粗粒度锁,然后下文会介绍一些方法来避免 master failover 之后系统损失太大

control plane / data plane 解耦

我们上面也说过这个解耦的理念了,这里再总结一下:

control plane (Chubby) data plane (应用/数据库)
干什么 选主、分配资源所有权、维护锁、存少量元数据、给出一致的协调结果 真正的数据读写、高吞吐业务、海量存储与计算
频率 低频但要命 高频又重
一致性 强一致 (绝不能两个主) 看业务取舍

Chubby 只管"低频但要命"的协调操作, 应用那一大坨数据它根本不碰; 这也解释了它为啥敢做得又小又慢, 但一致性上一步都不让;

一条容易踩的边界: 协调共识 ≠ 数据复制共识

有个地方特别容易想当然: Chubby 都替我做共识了, 那数据库是不是就不用自己搞 Paxos 了? 不是的;

Chubby 替你做的是应用的"协调共识" (谁是主、哪个 shard 归谁), 它不替你做数据库内部的"数据复制共识"; 一个数据库靠 Chubby 选出了 primary 之后, 它自己那几个副本之间怎么复制数据、怎么保证不丢, 该用自己的 Paxos/Raft 还得用;

也正因为这条边界, 一个 Chubby cluster 能同时伺候一大堆数据库, 各用各的锁互不打架:

1
2
3
DB1 → /locks/db1/master
DB2 → /locks/db2/master
DB3 → /locks/db3/master

每个 DB 都不用自己再起一套 Paxos cluster 来选主, 全共享这一个 Chubby, 这就是理由 4 那个"quorum 摊薄"落到地上的样子;

为啥 Chubby 这种小集群特别好养

  • 节点数量固定还少 (一个 cell 就 5 个), 不像应用节点动不动成千上万;
  • 功能单一, 就管协调加一点点元数据, 出错的面小;
  • 部署环境和网络都可控, 升级也不勤;
  • 可以专门给它上监控、复制、容灾;
  • 只要多数 Chubby replica 在线就行, 不要求所有应用节点都在线;

应用节点天天扩容、重启、升级、挂掉, 把高可用这副重担从"每个应用"收拢到"一小撮精心伺候的 Chubby 节点"身上, 整体反而更稳;

系统骨架 (§2 速写)

1
2
3
4
5
6
7
8
9
10
     一个 cell (故障隔离单元)
┌──────────────────────────────┐
│ master ← 唯一对客户端服务 │ 持有 master-lease
│ ┌──┐ ┌──┐ ┌──┐ ┌──┐ │
│ │R │ │R │ │R │ │R │ 5 副本 │ 用 Paxos 选 master + 复制状态
│ └──┘ └──┘ └──┘ └──┘ │ 5 副本容忍 2 个同时挂 (3/5 可用即上线)
└──────────────┬───────────────┘
│ 客户端只跟 master 说话, 维持 session + KeepAlive
┌─────┴─────┐
client client ← 抢锁 / 读小文件 / 订阅事件
  • 一个 cell = 1 master + 通常 5 个副本, 3/5 在线就能干活;
  • 只有 master 对客户端服务 (副本只管参与 Paxos 投票, 随时准备顶上来); 这种主从不对称是为了让一致性和顺序好保证, 跟 12.etcd 里"只有 apiserver 直连 etcd"是一个套路: 把入口收窄换简单;
  • 接口是个阉割版文件系统: 每个文件/目录是一个 node, 能加锁、能存小文件、能订阅事件; 但它故意砍掉了部分读写、seek、rename、move、符号链接这些, 它本来就不是真文件系统, 是"披了层文件皮的协调原语";

Paxos 的 safety / probable liveness 和 FLP

"Paxos maintains safety without timing assumptions, but clocks must be introduced to ensure liveness; this overcomes the impossibility result of Fischer et al."

要拆它, 先把两种"正确"分清楚:

Safety 安全性 Liveness 活性
口号 坏事永远不发生 好事最终会发生
共识里 绝不会有两个节点对同一位置达成矛盾决定 (不脑裂) 系统最终一定能达成某个决定, 不会永远卡住
坏了咋样 数据不一致, 灾难, 不可恢复 系统挂起不进展, 但数据还是对的

记住这个不对称: safety 坏了是灾难, liveness 坏了只是卡一下; 这一整句话的精髓全在这个不对称上;

FLP (1985) 说的是: 在 pure async 系统里 (消息延迟没有上界), 只要有一个节点可能崩 (fail), 就不存在任何确定性算法能同时保住 safety 和 liveness; 病根还是那句老话, 你分不清它是真死了还是只是慢;

Paxos 的破法是把这两半拆开, 各用各的招:

  • safety 这边无条件成立: 靠的是 ballot number + quorum 交集这套纯逻辑, 不管消息怎么延迟乱序、时钟怎么乱跑, 它就是不会产生冲突决定;
  • liveness 这边就没法无条件了, 必须请时钟出场: 纯异步下 Paxos 真有可能永远不进展, 经典的就是 dueling proposers, 俩提案者拿越来越大的编号互相抢占, 谁也提交不了; 破法是选一个唯一的 leader 只让它发提案, 而判断"老 leader 死没死"靠的就是超时, 也就是时钟, 这对应后半句; (这个很像后面 Raft 的策略)

那它凭啥就 overcome 了 FLP? FLP 的前提是"纯异步", 现实做法就是把这个前提松一松, 加一个很弱的时间假设: 靠超时比较靠谱地判 leader 死活, timeout ⟹ 死了;
它允许误判: 一个只是 GC 卡了一下的活节点可能被错判成死, 但这个误判只伤 liveness (多选一轮、临时冒出俩 leader), 永远伤不到 safety (safety 是 ballot + quorum 独立死保的); 正因为"判错了也死不了人", 这个粗暴的检测器才敢这么用;

session + KeepAlive + failover

session 是啥: 一段"我俩还连着"的关系

session 就是一个客户端和一个 Chubby cell 之间的一段关系, 靠周期性的 KeepAlive 握手;
客户端第一次连上 master 时开一个 session, 正常退出时显式关掉, 或者 idle 太久 (没开任何 handle、也没调用, 大概一分钟) 被隐式收走;

每个 session 挂一个 lease (租约): 这段时间里 master 保证不单方面把这个 session 干掉, 租约的截止点叫 lease timeout; master 会在三种时候把 lease 有效时间延长: session 刚建时、master failover 时 (注意我们前面提及过,coarse grained lock 在 master failover 的时候性能会比较差)、它回应一个 KeepAlive 时;

KeepAlive: 阻塞式握手 + 一鱼三吃

KeepAlive 不是那种 “客户端每隔几秒 ping 一下、master 立刻回 pong” 的傻轮询, 而是:

客户端发个 KeepAlive 过去, master 故意把它压住不回 (block 住), 一直拖到客户端的旧 lease 快到期了, 才放它返回, 并在返回里捎上 “新的 lease timeout”; 客户端一收到回复, 立马又甩一个 KeepAlive 过去;

所以每个 session 在 master 那儿几乎总挂着一个还没返回的 KeepAlive; 这么干省掉了海量空转的心跳包; 而且这条返回的 KeepAlive 是一鱼三吃, master 可以提前放它返回, 顺手捎带:

  1. 续 lease (主业);
  2. 推事件通知 (events, 比如你订阅的文件变了);
  3. 推缓存失效 (cache invalidation, 告诉你本地缓存哪块作废了);
    • 这里应该是 look-aside cache, 每次都是 client 自行缓存,master 负责进行 write-dirty 之后的无效声明

客户端那一侧: 自己揣一个更保守的 lease (spanner 的时候讨论过 true time 下的 保守设计)

客户端不会傻信 master 报的 lease timeout, 它自己维护一个更保守的本地 lease, 比 master 那个再缩一点, 把时钟漂移和消息在路上的时间都扣掉; 所以客户端永远比 master 更早认为"我的租约快没了"; 这个"宁可自己先慌"的保守差, 是后面一切安全的本钱: 绝不能比 master 还乐观;

断联了咋办: jeopardy → grace period → safe / expired

万一客户端本地 lease 都到期了还没等到 master 回应 (网断了、或者 master 挂了), 它就拿不准自己的 session 还在不在了, 于是:

  • 立刻清空并禁用本地缓存 (缓存可能已经不一致, 不敢再用), session 进入 jeopardy (危险期), 给应用抛一个 jeopardy 事件;
  • 然后再多扛一段 grace period, 赌在这段时间里能跟 master (可能是新选出来的) 重新握上手;
  • 赌赢了: 重建 KeepAlive, 抛 safe 事件, 缓存重新启用, 啥事没有;
  • 赌输了 (grace period 耗完还没连上): 认定 session 真没了, 抛 expired 事件, 它手里的锁、临时节点全释放;

这三个事件 (jeopardy / safe / expired) 是给应用的, 让应用能在危险期先把手里的危险操作停一停, 而不是傻乎乎当啥都没发生; 这点对粗粒度锁尤其要命, 正接上前面那个 failover 容易丢数据的 motivation;

总结

它信奉的几条

  1. 可用性和正确性 > 性能: 它是给"谁是主"这种要命的决定兜底的, 慢一点无所谓, 但绝不能错、绝不能不可用; 所以一切设计先保这两样;
  2. 锁服务 > Paxos 库: 共识太难用对, 与其发个库让每个团队自己踩坑, 不如把共识藏进一个中心化服务, 对外只露熟悉的锁 + 文件接口;
  3. 只管低频但要命的协调: 选主、ownership、配置、服务发现这种"抢一次管很久"的活它接, 高频细锁它不碰, 那是 data plane 自己的事;
  4. 中心化共享, 把 quorum 摊薄: 全公司共用一套 5 副本 cell, 各应用退化成单实例就能高可用;
  5. 能少给保证就少给, 但给了的就死保: 锁是 advisory (不强制), 暴露的语义尽量少, 但凡承诺过的一致性, 用 Paxos 死死兜住;

在每个岔路口选了哪条

岔路口 两条路 Chubby 选谁 代价 / 为啥认
一致性 vs 吞吐 强一致 / 高吞吐 强一致 牺牲吞吐, 反正粗粒度负载本来就低
服务 vs 库 中心化服务 / Paxos 库 中心化服务 多了个关键依赖, 但换来易采用、好运维
粗 vs 细粒度 coarse / fine coarse server 负载低, 但单锁 blast radius 大, failover 得格外小心
锁的强制力 advisory / mandatory advisory 灵活、不挡正常 I/O, 但得靠应用守规矩
缓存 客户端缓存 / 每次问 master 缓存 + 强一致失效协议 写要等失效 ack, 换来读几乎零延迟、master 不被打爆
怎么判活 lease(时钟) / 完美故障检测 lease 押了个弱时钟假设, 但换来"超时即失效"这种本地可判定

串起来就一句: 它在每个岔路口都挑了"对工程团队更省心、对正确性更保险"的那条, 哪怕代价是吞吐、是多一个依赖、是 failover 得多费心;

有意思的是, Chubby 后来在 Google 内部最大的用途竟然不是锁, 而是当名字服务 (替代 DNS): 它那套"缓存 + 主动失效"天生比 DNS 的"TTL 到了再轮询"更新更快、更省, 一不留神就成了全公司的服务发现底座; 算是粗粒度协调服务的一个意外彩蛋;

说到底, Chubby 不是个炫技的系统, 它的牛在于把"分布式共识"这个最难用对的东西, 包装成了一个连原型项目都能两行代码接上的、又慢又稳又强一致的中心化锁服务; etcd / ZooKeeper 今天的样子, 基本就是照着这张图描的;

ZooKeeper(ZAB) 和 etcd(k8s)

Chubby 是这个物种的祖宗, 但它是 Google 内部闭源的; 真正把这套带到全世界的是两个后人: Yahoo 的 ZooKeeper (2008 开源, 几乎就是冲着 Chubby 来的) 和后来云原生时代的 etcd; 这俩把 Chubby 的概念几乎全盘继承, 只在几个点上各自动了刀; 先把"全继承的"和"各自改的"分开看;

把 Chubby 的核心概念一字排开, ZooKeeper 和 etcd 基本是一一对上的, 只是换了名字:

Chubby 的概念 ZooKeeper etcd
共识复制 Paxos ZAB Raft
命名节点 node (文件树) znode (节点树) key (扁平 keyspace)
临时状态绑会话 ephemeral node ephemeral znode key 绑到 lease
会话+租约+心跳 session + KeepAlive session + heartbeat Lease + keepalive
防 stale leader 的代号 master epoch + 锁代号 zxid 的 epoch 位 Raft term (任期)
免轮询的通知 events (捎在 KeepAlive) watch (一次性) Watch (从 revision 流式重放)
定位 只存小元数据/协调, 不碰大数据

所以你看, "协调服务 + 共识复制 + session/lease + 临时节点 + epoch 防脑裂 + watch 免轮询 + 只存小数据"这套基因, 三家一个没丢; 真正有意思的是它们在哪儿改了;

动刀点 1: 共识协议, Paxos 太难, 后人各找替身

Chubby 自己在第二年的《Paxos Made Live》里就哭诉过 Paxos 落地有多坑; 后人干脆都不碰裸 Paxos:

  • ZooKeeper 自创了 ZAB (ZooKeeper Atomic Broadcast): 一个 leader-based 的原子广播协议, 天生给"主序 (primary order)", 同一个 primary 提的 a 在 b 前, 所有人就按这个序交付, 跨换主靠 zxid 里的 epoch 续上; 为啥非要主序? 因为 ZK/Chubby 的状态是增量叠加的 (在上一个状态上接着改), 不像经典 Paxos 每个 slot 各决各的, 所以必须钉死一条全局连续的顺序;
  • etcd 用 Raft: 本质也是 leader-based + term 续序, 卖点就俩字"好懂";

共同点很清楚: 仨都从"裸 Paxos"挪到了"leader-based + epoch/term 续序"这个形态, 因为这正好对上 Chubby 当年要的那条"增量叠加的有序日志";

动刀点 2: 接口, 从"直接给你锁"到"给你积木自己拼"

  • Chubby 大方, 直接把锁 + 文件当原语塞给你;
  • ZooKeeper 抠门一点, 不给现成的锁, 只给更底层的积木 (znode + ephemeral + sequential + watch), 锁和选主你自己用经典 recipe 拼: “建一个 ephemeral sequential 节点, 谁序号最小谁当主, 其他人就 watch 排在自己前面那个”, 前面那个一没 (会话断了 ephemeral 自动删) 就轮到你;
  • etcd 给的是 KV + lease + txn(CAS) + watch, 同样靠 recipe 拼锁 (也顺手封了个 lock / election API);

趋势是: 暴露可组合的原语, 而不是焊死一把锁; 灵活了, 但"拼对"的责任也甩给了用户;

动刀点 3: 读一致性, 给不给你"读旧值换吞吐"的旋钮

  • Chubby: 读全走 master, 线性一致 (master 攥着 lease, 知道自己是权威);
  • ZooKeeper: 读可以走任意 follower, 所以快、能水平扩, 但代价是可能读到旧值, 它只保证 sequential consistency (读不是 quorum 操作); 想读到最新得先 sync() 一下、或者干脆先写一笔再读; 写仍旧走 leader 保证线性;
  • etcd: 干脆给你个旋钮自己拧, 要线性一致就走 ReadIndex (读之前先过一轮 quorum 确认自己还是 leader), 要快就走 serializable 读 (读本地、可能旧);

趋势是: 从 Chubby 那种"一刀切线性", 走到"把一致性强度的旋钮交到用户手里";

动刀点 4: watch, 从"通知你一次"到"从某个时间点可靠重放"

这条是后人改得最狠、也最值钱的一处:

  • Chubby: events 捎在 KeepAlive 回包里推给你;
  • ZooKeeper: watch 是一次性的 (触发一下就失效), 你处理完得重新注册; 触发到重注册这段空档里的变化你可能直接漏掉, 而且它只告诉你"这 key 变了", 不告诉你"变成啥、变了几次";
  • etcd: Watch 背靠 MVCC, 能从指定 revision 流式重放、有序不丢 (就是我们 12.etcd 笔记里讲透的那套); 这是整条线最大的一次进化, 因为 MVCC 那条时间轴让"从过去某个点续传"成了可能, 一把填上了 ZK 那个"会漏事件"的坑;

趋势是: 从"通知一次"进化到"从某个 revision 起的可靠有序事件流";

落到 k8s: 为啥偏偏挑了 etcd

k8s 那套 reconcile (level-triggered 对账) 恰好就吃 etcd 这几个升级: MVCC 给时间轴、Watch 能可靠重放 (漏事件也能靠 relist 兜回来)、lease 管组件存活、读还能按需挑线性一致; 这套其实就是 Chubby 当年那个"协调服务"概念长大成人的样子, 只不过把 file + lock 接口换成了 MVCC KV、把 events 换成了能重放的 Watch; 细节见 12.etcd;

收口一句: Chubby 提出的那套基因, ZooKeeper 和 etcd 谁都没推翻, 只是各自照着自己的场景, 在共识协议、接口抽象、读一致性、watch 模型这四个旋钮上拧了拧; 说白了, Chubby 出了卷子, 后人都在它这张卷子上答题;