Google Chubby: 解耦控制数据平面的分布式锁系统
- 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 | 共识库: App副本1 ┐ |
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 | DB1 → /locks/db1/master |
每个 DB 都不用自己再起一套 Paxos cluster 来选主, 全共享这一个 Chubby, 这就是理由 4 那个"quorum 摊薄"落到地上的样子;
为啥 Chubby 这种小集群特别好养
- 节点数量固定还少 (一个 cell 就 5 个), 不像应用节点动不动成千上万;
- 功能单一, 就管协调加一点点元数据, 出错的面小;
- 部署环境和网络都可控, 升级也不勤;
- 可以专门给它上监控、复制、容灾;
- 只要多数 Chubby replica 在线就行, 不要求所有应用节点都在线;
应用节点天天扩容、重启、升级、挂掉, 把高可用这副重担从"每个应用"收拢到"一小撮精心伺候的 Chubby 节点"身上, 整体反而更稳;
系统骨架 (§2 速写)
1 | 一个 cell (故障隔离单元) |
- 一个 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 可以提前放它返回, 顺手捎带:
- 续 lease (主业);
- 推事件通知 (events, 比如你订阅的文件变了);
- 推缓存失效 (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;
总结
它信奉的几条
- 可用性和正确性 > 性能: 它是给"谁是主"这种要命的决定兜底的, 慢一点无所谓, 但绝不能错、绝不能不可用; 所以一切设计先保这两样;
- 锁服务 > Paxos 库: 共识太难用对, 与其发个库让每个团队自己踩坑, 不如把共识藏进一个中心化服务, 对外只露熟悉的锁 + 文件接口;
- 只管低频但要命的协调: 选主、ownership、配置、服务发现这种"抢一次管很久"的活它接, 高频细锁它不碰, 那是 data plane 自己的事;
- 中心化共享, 把 quorum 摊薄: 全公司共用一套 5 副本 cell, 各应用退化成单实例就能高可用;
- 能少给保证就少给, 但给了的就死保: 锁是 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 出了卷子, 后人都在它这张卷子上答题;
