第 9 章是第二部分的高潮。在编目了一切可能出错的东西(第 8 章)后,Kleppmann 现在构建积极的工具:带强保证的通用抽象,让应用能忽略它们底下的一些混沌。两个大想法是线性一致性(linearizability)(最强的单对象一致性模型)和共识(consensus)(让节点尽管有故障也就某事达成一致)。本章的妙处在于,出人意料的大量问题——leader 选举、唯一性约束、原子提交、锁服务——都等价于共识,所以解决共识一次(或外包给 ZooKeeper)就解决了它们全部。
- 线性一致性让一个复制系统看起来像单一副本带原子操作——一个新近度保证:一旦写完成,每个之后的读都看到它。
- 它不免费——CAP 定理说网络分区期间你必须在线性一致(一侧不可用)或可用(且非线性一致)间选。它也慢。
- 因果性是更弱、更便宜的顺序——因果一致性是挺过分区的最强模型,用版本向量或 Lamport 时间戳捕获。
- 全序广播(total order broadcast)(以相同顺序把相同消息投递给所有节点)等价于共识,是状态机复制的基础。
- 两阶段提交(2PC)给跨节点原子提交,但协调者死了就阻塞(in-doubt 问题)——一个单点故障。
- 共识算法(Paxos、Raft、Zab)用多数 quorum 安全达成一致;ZooKeeper/etcd 把它打包,这样你不自己实现。
线性一致性是金标准的"像一台机器一样行为"保证,但 CAP 和延迟使它昂贵,所以更弱的模型(因果一致性)常是正确选择。当你真正需要一致——谁是 leader、事务是否提交、这个名字是否唯一——你需要共识,它等价于全序广播。别自己造;用 ZooKeeper 或 etcd 这样的共识系统。
线性一致性(Linearizability)
线性一致性(也叫强一致性或原子一致性)背后的想法是让系统表现得仿佛只有一份数据副本,且仿佛对它的所有操作都是原子的。即便数据跨许多节点复制,客户端永远不该观察到复制:一个客户端的写完成的那一刻,每个后续读——来自任何客户端、在任何副本上——都必须返回那个新值(或更新的)。它从根本上是一个新近度保证。一个经典说明:两人在手机上看体育比分;若一人看到最终结果,另一人绝不能仍看到"进行中"。
线性一致性 vs 可串行化
这俩名字听起来相似且不断被混淆,但它们是不同的保证:
| 方面 | 线性一致性 | 可串行化 |
|---|---|---|
| 关于 | 单对象读/写的新近度 | 多对象事务的隔离 |
| 保证 | 看起来像单一副本、实时顺序 | 事务等价于某个串行顺序 |
| 关切 | 复制 / 新鲜度 | 并发 / 交错 |
| 合在一起 | 严格可串行化(strict serializability)= 同时两者(如两阶段锁、Spanner) | |
何时需要它
没有线性一致性,几样东西会坏:
- 锁和 leader 选举——锁必须线性一致,使只有一个节点相信它持有锁;这就是为什么系统用 ZooKeeper/etcd 做协调。
- 唯一性约束——保证用户名或 email 唯一需要一个线性一致的寄存器(否则两个节点都接受同一名字)。
- 跨通道时序依赖——如一个 web 服务器把图片写到存储,然后经一个单独通道给 resizer 发消息;没有线性一致性 resizer 可能读到旧或缺失版本。
它如何实现,以及 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 的致命弱点。
阶段 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 不可能性结果证明,若哪怕一个节点可能崩溃,在完全异步模型里没有算法能总达成共识。实践中的逃生口:真实系统被允许用超时(有时随机化)来取得进展,绕开理论不可能性,同时仍保证安全性。共识也有代价——它需要多数来取得进展且通常需要一个 leader,所以它对网络延迟敏感。
成员与协调服务
你很少自己实现共识。相反,你用一个协调服务如 ZooKeeper 或 etcd,它们内部运行一个共识算法并暴露一小组强大原语:线性一致的原子操作(如锁的 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 选举和锁。