第 9 章是第二部分的高潮。在编目了一切可能出错的东西(第 8 章)后,Kleppmann 现在构建积极的工具:带强保证的通用抽象,让应用能忽略它们底下的一些混沌。两个大想法是线性一致性(linearizability)(最强的单对象一致性模型)和共识(consensus)(让节点尽管有故障也就某事达成一致)。本章的妙处在于,出人意料的大量问题——leader 选举、唯一性约束、原子提交、锁服务——都等价于共识,所以解决共识一次(或外包给 ZooKeeper)就解决了它们全部。

⚡ 速览要点
  • 线性一致性让一个复制系统看起来像单一副本带原子操作——一个新近度保证:一旦写完成,每个之后的读都看到它。
  • 它不免费——CAP 定理说网络分区期间你必须在线性一致(一侧不可用)或可用(且非线性一致)间选。它也慢。
  • 因果性是更弱、更便宜的顺序——因果一致性是挺过分区的最强模型,用版本向量或 Lamport 时间戳捕获。
  • 全序广播(total order broadcast)(以相同顺序把相同消息投递给所有节点)等价于共识,是状态机复制的基础。
  • 两阶段提交(2PC)给跨节点原子提交,但协调者死了就阻塞(in-doubt 问题)——一个单点故障。
  • 共识算法(Paxos、Raft、Zab)用多数 quorum 安全达成一致;ZooKeeper/etcd 把它打包,这样你不自己实现。
tldr

线性一致性是金标准的"像一台机器一样行为"保证,但 CAP 和延迟使它昂贵,所以更弱的模型(因果一致性)常是正确选择。当你真正需要一致——谁是 leader、事务是否提交、这个名字是否唯一——你需要共识,它等价于全序广播。别自己造;用 ZooKeeper 或 etcd 这样的共识系统。

线性一致性(Linearizability)

线性一致性(也叫强一致性或原子一致性)背后的想法是让系统表现得仿佛只有一份数据副本,且仿佛对它的所有操作都是原子的。即便数据跨许多节点复制,客户端永远不该观察到复制:一个客户端的写完成的那一刻,每个后续读——来自任何客户端、在任何副本上——都必须返回那个新值(或更新的)。它从根本上是一个新近度保证。一个经典说明:两人在手机上看体育比分;若一人看到最终结果,另一人绝不能仍看到"进行中"。

线性一致性 vs 可串行化

这俩名字听起来相似且不断被混淆,但它们是不同的保证:

方面线性一致性可串行化
关于单对象读/写的新近度多对象事务的隔离
保证看起来像单一副本、实时顺序事务等价于某个串行顺序
关切复制 / 新鲜度并发 / 交错
合在一起严格可串行化(strict serializability)= 同时两者(如两阶段锁、Spanner)

何时需要它

没有线性一致性,几样东西会:

它如何实现,以及 CAP 代价

哪些复制方法线性一致?单主复制(若你只从 leader 读,且它真的是 leader)。共识算法。多主不是。无主(Dynamo 风格)即便用严格 quorum 通常也线性一致,因为时钟偏移和读修复的时序破坏新近度保证。线性一致性难的深层原因被 CAP 定理捕获:网络分区发生时,系统必须选——通过在够不到 quorum 的那侧拒绝请求来保持一致(线性一致)(牺牲可用性),或通过服务可能陈旧的数据保持可用(牺牲线性一致)。CAP 常被表述得太宽;它只关切一个故障(分区)和一个模型(线性一致)。但实际教训成立:线性一致性需要网络往返,所以它,许多系统刻意放弃它以换性能和可用性。

排序与因果性

排序不断重现,因为它与因果性深深相连。因果性给事件强加一个顺序:问题必须在答案之前;一行必须在被更新前创建。因果性定义一个偏序(partial order)——一些事件有序(因果相关),其他不可比(并发)。线性一致性相反,强加一个全序(total order):每个操作都在一条时间线上。线性一致性蕴含因果性,但它比必要的更强(更贵)。

因果一致性(causal consistency)需要等网络、能在分区期间保持可用的最强一致性模型——使它很有吸引力。要跟踪因果性你能用版本向量Lamport 时间戳给一个与因果性一致的全序(每个节点保留一个计数器、附到消息上,并把它升到它见过的最大值)——但单个 Lamport 时间戳无法告诉你一个顺序何时被最终确定(是否还有另一个更低编号的操作在途)。

全序广播(Total Order Broadcast)

单主系统真正需要的是一种决定固定且已知的操作全序的方式。全序广播(原子广播)是正式版:一个可靠地且以相同顺序把消息投递给所有节点的协议。它是状态机复制(state machine replication)的基础——若每个副本以相同顺序应用相同操作,它们都最终处于相同状态。结果证明,全序广播等价于共识,且正是 ZooKeeper 和 etcd 这样的系统内部提供的。

分布式事务与共识

共识——让若干节点就某事达成一致——是分布式计算中最重要、最微妙的问题之一。它听起来简单但布满风险,尤其因为第 8 章的故障。

两阶段提交(2PC)

跨多个节点原子提交的标准算法——确保要么所有节点提交一个事务、要么全部中止。一个协调者驱动两阶段:首先它问每个参与者"你能提交吗?"(prepare);每个只在确定能时才回是。若全投是,协调者发 commit;若任何投否,它发 abort。参与者投"是"时做的承诺不可撤销——这造成 2PC 的致命弱点。

2PC and the in-doubt problem
阶段 1 (prepare):  协调者 → 参与者: "你能提交吗?"
                   参与者 → 协调者: "能"(现在它们必须服从)

阶段 2 (commit):   协调者 → 参与者: "提交!"

  ✗ 协调者在阶段 1 后、阶段 2 前崩溃
    参与者卡在 "in doubt" — 它们承诺了提交,
    无法单方面决定,必须持锁直到协调者恢复。
    → 阻塞;协调者是 SPOF。

若协调者在参与者投了是之后、但在它发决定之前崩溃,那些参与者in doubt(存疑):它们无法自行提交或中止,所以它们阻塞、持锁,直到协调者回来。这使协调者成为单点故障。(XA 标准跨异构系统实现 2PC,并恰好遭受这些运维头痛。)

容错共识

共识算法比 2PC 做得更好,通过容忍协调者失败。一个共识算法必须满足四个属性:一致同意(uniform agreement)(没有两个节点决定不同)、完整性(integrity)(没有节点决定两次)、有效性(validity)(被决定的值由某个节点提议过),和终止(termination)(每个未崩溃节点最终决定——容错属性)。众所周知的算法——Paxos、Raft、Zab、Viewstamped Replication——实际实现全序广播(决定一个值的序列)。它们依赖多数 quorum 和一个 epoch/ballot 号:每轮领导有一个唯一递增的号,leader 必须在决定前从一个 quorum 收集投票,这保证任何两个 quorum 重叠、且一个旧 leader 无法覆盖一个更新的。

FLP,简述

著名的 FLP 不可能性结果证明,若哪怕一个节点可能崩溃,在完全异步模型里没有算法能达成共识。实践中的逃生口:真实系统被允许用超时(有时随机化)来取得进展,绕开理论不可能性,同时仍保证安全性。共识也有代价——它需要多数来取得进展且通常需要一个 leader,所以它对网络延迟敏感。

成员与协调服务

你很少自己实现共识。相反,你用一个协调服务ZooKeeperetcd,它们内部运行一个共识算法并暴露一小组强大原语:线性一致的原子操作(如锁的 compare-and-set)、操作全序(ZooKeeper 的 zxid 充当 fencing token)、故障检测(会话和心跳),和变更通知(watch)。有了这些你构建 leader 选举、分区/分配管理、服务发现和分布式锁。模式是把难的共识工作外包给这些久经考验的服务之一,并把它挡在你自己应用逻辑的关键路径之外。

总结

本章的大领悟是这张等价之网:线性一致的 compare-and-set、全序广播、原子提交、leader 选举和共识全都能相互归约。所以你无需分别解决每个——解决共识一次。而既然正确的共识极难实现,工程答案几乎总是倚靠 ZooKeeper、etcd,或一个内建共识的数据库,而非自己造。

🎯 面试速答

线性一致性 vs 可串行化?线性一致性 = 单对象的新近度(看起来像单一副本);可串行化 = 多对象事务的隔离。严格可串行化是两者。
CAP 实际说什么?网络分区期间,选一致(线性一致,拒绝一些请求)或可用(服务陈旧数据)。它很窄——一个故障、一个模型——但取舍真实。
2PC 为什么脆弱?若协调者在 prepare 阶段后崩溃,参与者存疑——它们无法独自决定并阻塞持锁。协调者是单点故障。
为什么用 ZooKeeper 而非自己写共识?共识(Paxos/Raft)以难做对著称;ZooKeeper/etcd 打包线性一致操作、全序和故障检测,这样你在其上构建 leader 选举和锁。

← 上一篇
分布式系统的麻烦