第 5 章开启 DDIA 第二部分"分布式数据"。复制意味着在多台经网络连接的机器上保留同一数据的副本。我们这么做有三个原因:让数据地理上靠近用户(更低延迟)、即便部分失败系统仍工作(可用性)、横向扩展服务读查询的机器数(吞吐)。若数据从不改变,复制就微不足道——拷一次就完事。全部困难、以及整章,都关于处理对复制数据的变更。Kleppmann 把一切围绕三种算法构建:单主、多主、无主复制。

⚡ 速览要点
  • 三种方法——单主(一个节点接受写)、多主(几个接受)、无主(客户端直接写多个节点)。几乎每个分布式数据存储都是这些的变体。
  • 同步 vs 异步是持久性/可用性取舍——同步复制保证一份最新副本但若 follower 挂了就阻塞;异步快但故障转移时可能丢失刚确认的写。
  • 故障转移危险——提升新 leader 冒丢写、脑裂(两个 leader)、坏超时调优的风险。自动故障转移真的难做对。
  • 复制滞后破坏直觉保证——你需要读己之写、单调读、一致前缀读来弥合一个落后的异步 follower。
  • 多主的核心问题是写冲突——同一记录在两个 leader 上被编辑必须被调和(LWW、CRDT、自定义合并)。
  • 无主用 quorum——若 w + r > n,读集和写集重叠,所以一次读看到最新写。读修复和反熵治愈陈旧副本。
tldr

单主复制简单且是默认(大多数关系 DB):写去一个 leader,它把变更日志流给 follower。多主跨数据中心增加写可用性,代价是冲突解决。无主(Dynamo 风格)完全去掉 leader,依赖 quorum 加修复。反复出现的主题是网络不可靠、复制异步时一致性与可用性间的张力。

单主复制(Single-Leader)

最常见的方法,被 PostgreSQL、MySQL、SQL Server、MongoDB 等许多系统使用,是基于 leader 的复制(也叫 active/passive 或 master/slave)。一个副本被指定为 leader。所有写必须去 leader,它先把新数据写到自己的本地存储。其他副本是 follower:每当 leader 写,它也把变更作为复制日志或变更流的一部分发给每个 follower。每个 follower 按 leader 处理的相同顺序应用变更。读能由 leader 或任何 follower 服务,但写仅 leader。

同步 vs 异步复制

一个关键配置旋钮是复制同步还是异步发生:

让每个 follower 都同步不切实际——一个慢节点会让整个系统停摆。一个常见折中是半同步(semi-synchronous):一个 follower 同步,若它太慢,另一个 follower 被替换为同步的。这保证至少两个节点有最新数据。实践中,许多基于 leader 的系统全异步运行,接受故障转移时丢失近期写的风险以换性能和可用性。

设置新 Follower

要不停机加 follower,你不能在写发生时只是拷文件。标准流程:取 leader 数据库的一致快照(大多数 DB 支持这个而不锁),拷到新 follower,然后让 follower 连到 leader 并请求自快照以来发生的所有变更——由复制日志中的精确位置标识(PostgreSQL 叫它日志序列号,MySQL 叫 binlog 坐标)。follower 追上后,它实时处理变更。

处理节点故障

任何节点都可能宕,复制的一个目标是让系统挺过单个故障。如何恢复取决于哪个节点失败。

Follower 故障:追赶恢复

这是容易的情况。每个 follower 保留它收到的变更日志。崩溃或网络抖动后,它知道它处理的最后一个事务;它重连到 leader 并请求自那以来的每个变更、应用它们,就重新同步了。无需人工介入。

Leader 故障:故障转移

这是难的情况。若 leader 失败,一个 follower 必须被提升为新 leader,客户端必须被重新配置去把写发给它,其他 follower 必须开始从它消费变更。这个过程——叫故障转移(failover)——可手动或自动,且布满陷阱:

为什么这要紧

这些故障转移问题没有简单答案——这正是为什么许多运维团队偏好手动故障转移,即便那意味更长的宕机。这些问题如此棘手的深层原因是网络、时钟的不可靠,以及就单一真相达成一致的困难——第 8、9 章的主题。

复制日志的实现

leader 实际如何把变更传达给 follower?有几种方法,各有取舍。

复制滞后的问题

从异步 follower 读让你便宜地扩展读,但 follower 可能落后 leader。若你从落后的 follower 读,你可能看到陈旧数据——系统是最终一致的,但"最终"刻意含糊,在负载下可能是数秒或数分钟。三个具体异常、以及修复它们的保证,在面试里不断出现:

读己之写一致性(Read-After-Write)

用户提交一个变更,然后重载——若他们的读命中一个还没收到该写的 follower,他们自己的更新看似消失了。读己之写(或 read-your-writes)一致性保证用户总看到自己的写。技术:从 leader 读用户可能修改过的东西;跟踪用户最后一次写的时间戳,只从追上它的 follower 读。

单调读(Monotonic Reads)

若用户从不同副本做几次读,他们可能看到一个值,片刻后看到一个更旧的值——时间看似倒流。单调读保证一旦你读了更新的数据,你之后不会读更旧的。一个简单实现:每个用户总从同一副本读(如按其用户 ID 的哈希选)。

一致前缀读(Consistent Prefix Reads)

若一连串写以某顺序发生,任何读它们的人都应以同样顺序看到。违反令人不安——你可能在问题本身之前看到一个答案。一致前缀读保证因果相关的写被按序读。这在分区(分片)数据库里尤其是问题,那里不同分区以不同速度复制。

关键要点

"最终一致性"是一个弱保证,把大量复杂度推给应用开发者。与其希望滞后保持小、然后在它没保持时惊讶,不如用这些具体保证思考——并认识到提供更强保证正是事务和共识(后续章节)的用途。

多主复制(Multi-Leader)

单主有一个明显缺点:所有写漏斗式通过一个节点。多主复制允许多于一个节点接受写;每个 leader 同时充当其他 leader 的 follower。主要用例:

写冲突

多主引入的大问题是写冲突:同一数据能在两个不同 leader 上并发被修改,而冲突直到变更被异步合并才被检测到。方法:

复制拓扑

多于两个 leader 时,你必须决定写在它们之间走的路径。全互联(all-to-all)(每个 leader 发给每个其他)最健壮但若某些链路比其他快可能有因果问题。环形星形拓扑减少流量但引入单点故障,且若一个节点宕可能断。为防无限循环,每个写被标上它已经过的节点标识符。

无主复制(Leaderless)

第三种方法完全放弃 leader 概念:任何副本都能直接从客户端接受写。这种风格由 Amazon 的 Dynamo 推广,被 Cassandra、Riak、Voldemort 使用(常叫 Dynamo 风格)。客户端(或代表它的协调者节点)把每个写发给几个副本并从几个副本并行读。

Quorum 读写

要知道一次读尽管有些节点宕了也看到最新写,无主系统用 quorum。有 n 个副本,一个写必须被 w 个节点确认、一个读必须查询 r 个节点。只要 w + r > n,被写的节点集和被读的集保证至少重叠一个节点——所以一次读会看到至少一份最新副本。

quorum condition
n = 3 副本

  w + r > n   保证写集和读集重叠

  例:  w = 2, r = 2,  n = 3
        2 + 2 = 4 > 3   ✓  每次读看到最新写

  为读调优:   w = 3, r = 1   (读快,写慢/脆弱)
  为写调优:  w = 1, r = 3   (写快,必须广泛读)

保持副本同步

当一个宕掉的节点回来,它错过了写。两个机制治愈这个:

Quorum 的局限与检测并发写

Quorum 不是密不透风的。宽松 quorum(sloppy quorum)(带 hinted handoff)让写在"家"节点不可用时去其他可达节点,增加可用性但削弱重叠保证。且因为多个客户端能并发写一个无主存储,冲突像在多主里一样出现。检测它们需要推理 "happens-before" 关系:若两个操作都不知道对方,它们是并发的。系统用每 key 版本号,或跨副本的版本向量(version vector),来判断一个写是否取代另一个,还是它们真正并发(那种情况下冲突值,叫 sibling,必须由应用合并)。

维度单主多主无主
谁接受写一个节点几个节点任何副本
写冲突无(串行化)有——必须解决有——必须解决
处理节点丢失故障转移(难)其他 leader 继续quorum 容忍丢失
一致性较强(若读 leader)最终最终(可调 quorum)
示例PostgreSQL、MySQL多 DC、CouchDBCassandra、Riak、Dynamo
总结

复制是每个分布式数据存储的基础,三种策略是写可用性与解决冲突成本之间取舍的谱系。单主避免冲突但成瓶颈、失败别扭;多主和无主用可用性换取你必须调和并发写。知道一个给定数据库坐落在哪——以及它对滞后和冲突做什么——是强大的分布式系统面试答案的核心。

🎯 面试速答

同步 vs 异步复制?同步保证一份持久的最新副本但在慢 follower 上阻塞;异步快但故障转移时可能丢已确认的写。半同步(一个同步 follower)是常见折中。
为什么故障转移难?丢失异步写、脑裂(两个 leader)、坏超时调优,以及与外部系统的危险交互——这就是为什么许多团队手动做它。
w + r > n 给你什么?读 quorum 和写 quorum 至少重叠一个节点,所以一次读保证看到最新写——无主一致性的核心旋钮。
怎么检测并发写?用版本号或版本向量跟踪 happens-before 关系;两个都没看到对方的写是需要合并的并发 sibling。

← 上一篇
编码与演化