专业书籍精读 · DDIA · 第 9 章
Designing Data-Intensive Applications · Ch 9 · Martin Kleppmann · 2017
你刷的每个大网站,背后都不是一台电脑,而是成百上千台机器一起干活。它们必须对一些「事实」达成一致:现在谁是主管(谁说了算)、这笔订单到底成没成、这个用户名是不是已经被占了。这一章讲的就是:一群各自可能宕机、消息可能丢的电脑,怎么可靠地「说到一块儿去」——这就是一致性和共识。
把这套系统想成一屋子人合用一个记事本,但每人手里其实是一份复印件。理想情况是:谁写一笔,所有人下一眼看到的都是同一个最新值,没人会读到旧的——这种「一堆复印件用起来就像只有一本」的效果,就是这章最重要的概念,叫线性一致。难点在于:复印件之间同步要时间,一不小心你翻到的就是别人三秒前的旧版本。
如果电脑们各说各话,就会出乱子:两台机器都以为自己是主管(叫脑裂),一笔钱被扣两次;两个人同时抢到同一个用户名。更麻烦的是网络会断——一半机器忽然联系不上另一半,而且谁也分不清对方是真死了还是只是暂时失联。在这种半明半暗里还要做出统一决定,是分布式系统最难的地方。
秘诀出奇地简单——投票,而且要过半数。想让大家认定一件事,就让超过一半的机器投票通过。为什么非得过半?因为任意两个「过半数」的人群一定有重叠的成员,于是不可能同时选出两个互相打架的结果——脑裂被从根上堵死。选主管是一次这样的投票;给事件排顺序也是——大家把发生的事按同一个顺序抄进一本共用的流水账,谁都不许插队,顺序自然就统一了。这套「靠过半数达成一致」的机制,就叫共识。
好消息:这套东西极难写对,所以别自己造——现成的 ZooKeeper、etcd 这类「协调服务」已经把它封装好,选主、加锁、排顺序,直接调用就行。还要记住一条著名的取舍(叫 CAP):网络一旦断裂,「保持一致」和「保持可用」只能二选一。诚实地说,达成一致是有代价的——每件事都等过半数点头,天生比单机慢,这份「慢」买来的是「不会算错」。
一群会宕机、会失联的电脑要可靠地「达成一致」,靠的是过半数投票——它一口气解决了选主、排顺序、抢唯一名额。代价是更慢,且网络分裂时一致与可用二选一。别自己造,用 ZooKeeper / etcd。
想进到线性一致、全序广播、共识算法与 2PC 的具体机制? → 切到精读版
这一章是 DDIA「分布式数据」部分的顶点:它把前几章的麻烦(复制滞后、部分失效、不可靠时钟)收拢成一个问题——在不可靠的地基上,最强能建立起多强的保证?答案落在三块基石:线性一致性(linearizability)——让一堆副本用起来「像只有一份」;因果与全序(ordering)——给分散发生的事件排出统一顺序;以及全书的皇冠明珠共识(consensus)——让一群会宕机的节点对某件事达成一致。关键洞见是:线性一致存储、全序广播、共识其实是同一个问题的三种伪装;解决了它,选主、唯一性约束、原子提交就全到手了。
本章是 Part II「分布式数据」的收官与升华。它上承第 5 章复制(多副本带来的一致性坑)、第 7 章事务(单机上的强保证)、第 8 章分布式系统的麻烦(部分失效、不可靠网络与时钟)——把那些「坏消息」收拢成一个正面命题:我们究竟能建立多强的保证,又要为之付出什么。落到现实,它正是 ZooKeeper、etcd、Google Chubby / Spanner、Raft / Paxos 这一整类系统的理论内核。读懂这一章,你就握住了「分布式协调」的钥匙。
第 8 章讲透了坏消息:网络会丢包会延迟,节点会崩,时钟不可信,而且你无法区分「一个节点死了」和「它只是慢 / 失联」。在这种半明半暗里,最朴素的需求都会崩坏——想选一个主,可能选出两个(脑裂),两主同时接写、数据对不上;想保证用户名唯一,两个请求打到不同副本、都以为「没人用过」,于是双双注册成功;想让跨服务的下单要么全成要么全不成,却可能扣了库存没收到钱。
核心问题因此是:在不可靠的地基上,能不能造出一块「可靠的抽象」,让上层假装底下没那么糟?就像事务(第 7 章)让应用假装「没有并发、没有崩溃」,这一章要给出分布式版的同款魔法——线性一致的存储、统一的事件顺序、可靠的共识。不解决它,就只能忍受脆弱,或把一致性问题一个个手搓、反复踩坑。
是什么。线性一致性是最强的单对象一致性保证。它的承诺一句话:让整个分布式系统表现得好像只有一份数据、每个读写都在某个瞬间原子地生效。它本质是一条新鲜度(recency)保证——你读到的一定是「最近一次写入」的结果,而不是某个副本上的陈旧快照。
直觉与判据。关键规则是:一旦有任何一次读返回了新值,此后所有读都不许再返回旧值——哪怕它们打到了不同副本。换句话说,存在一个「翻牌」的瞬间,值从旧原子地变成新;这一刻之前读到旧、之后必须读到新,中间不许反复横跳。下图用一个寄存器 x 的读写时间线说明这条铁律。
别和可串行化搞混(常见误区)。可串行化(serializability)是事务的隔离性质——多个跨对象事务的效果等价于「按某种串行顺序逐个执行」,不关心这顺序合不合真实时间。线性一致性是单个对象上的新鲜度保证。两者正交,同时满足叫严格可串行化,Spanner 之类才提供。
谁离不开它。三类场景对线性一致有硬需求:①加锁与选主——所有节点必须对「谁持有锁 / 谁是主」达成唯一、最新的共识,否则脑裂;②唯一性约束——用户名、账号、库存不能超卖,需要「此刻全局只有一个赢家」;③跨渠道时序依赖——书里的经典例子:用户传图后,Web 服务器把「缩放任务」丢进消息队列,若队列比数据库先把消息送到缩放器、而缩放器读到的却是还没这张图的旧副本,任务就会因「文件不存在」失败。这种跨渠道竞态,只有线性一致的存储能根治。
线性一致很强,但很贵(下一节算账)。退一步,很多问题的本质其实是排序。这里要分清两种序。
因果序是偏序。因果关系规定「有因才有果」:你先看到一条消息、才可能回复它。但两件互不相干的事(张三改了个人简介、李四发了条动态)是并发的,因果不要求它们分先后——所以因果是一种偏序:有的能比、有的并发。线性一致则是全序:它假装只有一份数据,任意两个操作都能排出唯一先后。全序一定尊重因果,所以线性一致蕴含因果一致。
能不能只要因果、不要那么贵的全序?能,而且这是个甜点:因果一致性(causal consistency)是网络延迟和分区面前仍能保持高性能、高可用的最强一致性模型——它不像线性一致那样每次操作都要全网协调。实现上给每个操作打逻辑时钟(Lamport 时间戳:每个节点维护一个计数器 + 节点号,取较大者递增),就能造出一个尊重因果的全序。
但光有全序号还不够。Lamport 时间戳能事后把事件排成唯一的序,可唯一性约束需要当场拍板:两人同时抢 @alice,你必须此刻就知道谁在前——不能等「以后消息收齐了再排」。这就要求把定序做成实时、随发生随定序的机制。它就是——
全序广播(total order broadcast),又叫原子广播。它是节点间交换消息的协议,保证两条安全性质:可靠投递(消息不丢,一个节点收到、所有正常节点最终都收到)与全序投递(所有节点以完全相同的顺序收到所有消息)。把它想成一台全网共用、只能追加的流水账:谁想记事就投一条进去,系统给它定一个全局唯一的槽位,再一模一样地播给每个节点。它一次办三件事:数据库复制(各副本按同一顺序重放 = 状态机复制)、可串行化事务(事务塞进同一条日志顺序执行)、栅栏令牌(槽位号天然单调递增,就是防僵尸锁的令牌)。
本章最深的洞见在这里:全序广播、线性一致的比较并设置(compare-and-set)、共识,是等价的问题——能解其一,就能解其余。全序广播「决定下一条消息是谁」,正是在对「下一个值」反复达成共识;反过来,反复运行共识,就得到一串定好序的值 = 全序广播。所以攻克共识,等于一把钥匙开三把锁。
共识要什么。让若干节点就一个值达成一致,形式上要满足四条:协商一致(没有两个节点决定出不同的值)、诚实(一个节点不能反悔已决定的值)、有效(决定的值必是某节点提议过的)、可终止(只要多数节点存活,就一定能决定,不会永远卡住)。前三条保证安全(不出错),第四条保证活性(有进展)——正是这第四条把共识和「只会脑裂的两阶段提交」区分开。
怎么做到(白话)。主流容错共识算法——Paxos、Raft、Zab、Viewstamped Replication——套路惊人地一致:分两轮投票。第一轮选主,附一个递增的纪元号(epoch),每换一任主就 +1;第二轮由当选主提议值、收集多数派投票。防脑裂的魔法藏在「两次投票的多数派必然相交」:假如老主没死又选出了新主,新主的选举多数派里一定有节点见过更高纪元号,会拒绝老主的提议——老主的写凑不齐多数,自动作废。于是同一时刻只有一个主能推进。
共识的一个近亲问题是原子提交:一笔事务横跨多台机器(比如同时改两个分片),必须要么全提交、要么全回滚,不许一半成一半败。经典解法两阶段提交(2PC)引入一个协调者:阶段一「准备」——协调者问所有参与者「你能提交吗?」,参与者若答「能」,就等于立下不可反悔的承诺(哪怕随后崩溃,恢复后也必须能提交);阶段二「提交 / 中止」——只要有一个说「不能」,协调者就通知全体回滚;全说「能」,才通知全体提交。
2PC 的阿喀琉斯之踵:协调者是单点。若协调者在「所有人都答了能、但提交令还没发出」的那一刻崩溃,参与者就陷入存疑(in doubt)——它已承诺、不能自行回滚,又收不到指令,只能攥着锁死等协调者复活,相关数据这段时间被锁住、无法读写。这正是 2PC 和容错共识的分水岭:2PC 缺了共识的第四条「可终止」——协调者一倒可能无限期阻塞。(三阶段提交想补这个洞,却依赖「网络延迟有上界」这个现实里不成立的假设,故极少采用。)
这一章的灵魂是「保证越强、代价越大」这条光谱。先看一致性模型的谱系,再看 CAP 逼出的取舍,最后对比 2PC 与容错共识。
表 1 · 一致性模型谱系:强度 ↔ 性能 / 可用性的此消彼长
| 模型 | 保证什么 | 网络分区时可用? | 受网络延迟拖累? | 代表系统 |
|---|---|---|---|---|
| 线性一致 linearizable | 看起来只有一份、读到的一定最新 | 否(须停等,CP) | 是,每次操作都要协调,慢 | ZooKeeper、etcd、Spanner(读写) |
| 因果一致 causal | 尊重因果先后,并发操作不强排 | 是(可继续服务) | 否,是「不牺牲性能的最强模型」 | 部分数据库的会话 / 因果模式 |
| 最终一致 eventual | 只承诺「早晚收敛」,中途可读旧值 | 是(AP) | 否,最快 | Cassandra、Dynamo 风格无主库 |
表 2 · CAP:网络分区时,一致性 vs 可用性只能保一个
| CP:保一致、舍可用 | AP:保可用、舍一致 | |
|---|---|---|
| 分区时行为 | 少数派一侧拒绝服务(宁可不可用也不返回旧值) | 两侧都继续服务,事后再收敛 / 冲突合并 |
| 适合 | 选主、锁、唯一性、余额不能为负 | 购物车、点赞数、可容忍暂时不一致的读多写 |
| 代表 | ZooKeeper、etcd、HBase | Cassandra、Riak、DynamoDB(可调) |
| 易错认知 | CAP 只在「分区已发生」时才逼你二选一;没有分区时两者可兼得。它也没考虑延迟——而线性一致即便不分区也慢,很多系统弃用它是为了性能而非容错。 | |
表 3 · 分布式原子提交:两阶段提交 vs 容错共识
| 两阶段提交 2PC | 容错共识(Raft / Paxos) | |
|---|---|---|
| 目标 | 跨节点事务的全体一致提交 / 回滚 | 对一串值达成一致 = 全序广播 |
| 协调者 / 主 | 单一协调者,单点 | 选举出的主,可故障转移 |
| 协调者 / 主崩溃 | 参与者存疑、持锁阻塞(缺「可终止」) | 多数派存活即可选新主、继续推进 |
| 需要多少节点 | 全部参与者都要投「能」 | 只需多数派存活 |
| 现实代价 | XA 事务实测可比单机慢一个数量级;持锁期间放大争用 | 频繁选主 / 网络抖动时性能差;成员固定、靠超时探活 |
选型心法:需要「全局唯一决定」(选主、锁、唯一约束)→ 交给线性一致的协调服务(ZooKeeper / etcd),别手搓;能容忍暂时不一致、要极致可用与低延迟 → 走最终一致的无主库;两者之间、想要「不牺牲性能的最强一致」→ 因果一致。跨库事务能免则免;非要,就认清 2PC 的阻塞风险、把协调者本身也做成高可用。
这一章是「分布式协调」的面试与架构公共底本。任何「谁是主」「锁归谁」「操作什么顺序」的问题最终都归约到共识;而工业界的共识实现几乎都收敛成同一套零件:把一小份关键元数据(谁是主、分片归属、配置)放进内存,用容错的全序广播复制到多个节点——这正是 ZooKeeper(用 Zab)与 etcd(用 Raft)的骨架。它们对外提供四样东西:线性一致的原子操作(compare-and-set 实现锁与选主)、操作全序(zxid / index 即栅栏令牌)、故障检测(会话 + 临时节点,客户端一断连锁自动释放)、变更通知(watch)。Kafka、HBase、Kubernetes 靠它们做选主、分片分配与服务发现——把最难的共识只解一次、复用到全局。
① 一句话:在不可靠地基上造「可靠的抽象」——线性一致、全序、共识,是本章三块基石。
② 线性一致:让多副本看起来只有一份、每个操作原子生效;一旦读到新值就不许再退回旧值。它是新鲜度保证,不同于可串行化(事务隔离)。
③ 谁离不开它:选主 / 锁、唯一性约束、跨渠道时序依赖(图片缩放竞态)。
④ 排序:因果是偏序(并发不排),线性一致是全序并蕴含因果;因果一致是「不牺牲性能的最强一致」。
⑤ 全序广播 = 全网共用的追加日志,一次办三件事:状态机复制、可串行化事务、栅栏令牌(槽位号)。
⑥ 皇冠明珠:全序广播 ≡ 线性一致 CAS ≡ 共识,三者等价。共识靠两轮投票 + 递增纪元 + 多数派相交杜绝脑裂。
⑦ 原子提交用 2PC,但协调者是单点、崩溃即存疑阻塞——它缺共识的「可终止」,不是容错共识。
⑧ CAP 只在分区时逼你在一致与可用间二选一,且没算延迟;别当「三选二」。
⑨ 落地:ZooKeeper(Zab)/ etcd(Raft)把共识只解一次,对外给锁、选主、全序、故障检测、watch;Chubby、Spanner、Raft 生态、Jepsen 是四组硬实证。
⑩ 心法:要全局唯一决定 → 用现成协调服务,别手搓;能忍不一致 → 走最终一致换可用与低延迟。