Skip to content
Go back

数据复制的三条路:单主、多主与无主

用户刚修改收货地址,页面刷新后却显示旧地址。写入已经成功,读取也没有报错,两个结果仍然互相矛盾。

常见原因很简单:写请求进入主库,紧接着的读请求被分配到尚未追上进度的只读副本。复制让系统多了一份可用数据,也让“哪一份数据算最新”成为必须回答的问题。

Harshit Khosla 的分布式系统系列第三篇介绍了单主、多主和无主复制。本文沿着这三条路径,进一步讨论它们分别把协调成本放在哪里,以及工程中容易被简化掉的边界。

复制解决了什么,又增加了什么

保存多个副本通常有三个目的:

复制一次并不难。持续有写入、网络时断时续、节点随时重启时,让副本保持可解释的状态才是难点。

设计复制方案时,真正需要决定的是:一次写入要等待多少副本,谁有权接受写入,冲突由谁发现和处理,以及节点恢复后如何补齐遗漏数据。三类复制模型给出的答案不同。

单主复制:先从最容易解释的方案开始

单主复制只允许一个节点接受写入。主节点按顺序记录变更,再把变更发送给从节点。读取可以留在主节点,也可以分配到从节点。

它的主要优点是写入入口唯一。同一条记录的更新先后通常由主节点确定,应用不必在每次写入时处理多个来源产生的冲突。对多数业务系统,这是一条稳妥的起点。

代价集中在两个地方。

第一处是复制确认。异步复制让主节点在本地提交后立即响应,延迟较低;如果主节点在变更传给从节点前永久损坏,最近的已确认写入可能丢失。同步复制等待指定从节点确认,能提高已提交数据的持久性,写入延迟也会增加。PostgreSQL 还允许按事务设置 synchronous_commit,说明“同步或异步”可以是一次事务的选择,无需成为整个集群唯一的固定模式。

第二处是主节点故障转移。系统需要判断旧主是否真的失去服务能力、选择进度足够新的从节点并完成提升。如果旧主仍在局部网络中接受写入,新主又已经开始工作,就会出现两个主同时存在的 split-brain(脑裂)。防止它通常需要租约、仲裁、隔离旧主或共识协议,单靠“检测不到心跳”并不够。

副本延迟会直接进入用户体验

把读取交给异步从节点后,系统可能遇到:

常见处理方式包括让刚写过数据的会话暂时读主节点、记录写入位置并等待从节点追上,或只把允许短暂陈旧的数据分给从节点。选择前应先定义业务可以接受多旧的数据,不能只看平均复制延迟。

多主复制:用冲突换取就近写入

多主复制允许多个节点同时接受写入,常见形式是每个地区部署一个写入节点。东京用户可以写东京节点,欧洲用户可以写欧洲节点,跨洲网络延迟不再出现在每次本地写入的关键路径上。

难点随之转到冲突处理。两个地区在互相断开时都修改同一条记录,恢复连接后会得到两个并发版本。系统必须先识别它们没有明确的因果先后,再选择处理方式:

向量时钟能帮助判断两个版本是祖先关系还是并发关系,但它只发现冲突,不替业务决定正确答案。账户余额、库存和用户名需要的合并规则完全不同。

因此,多主复制适合确实需要多地低延迟写入、离线写入或多个独立写入中心的系统。若业务无法清楚说明每类冲突如何处理,增加主节点只会把故障从“写不进去”变成“写进去了但无法解释”。

无主复制:让法定人数承担协调

无主复制没有固定写入主节点。客户端或协调节点把一次写入发送给多个副本,收到足够多确认后返回成功。Amazon Dynamo 推广了这类设计,Cassandra、Riak 等系统继承了其中许多思想。

假设一份数据有 N = 3 个副本:

写入等待 W = 2 个副本确认
读取等待 R = 2 个副本返回
W + R > N

读集合和已成功写入的集合至少会重叠一个副本。协调节点比较返回的版本,便有机会取得已确认写入的最新值。节点少量离线时,系统仍能继续工作。

W + R > N 不是一张万能保证书

这条公式经常被写成“读取一定看到最新写入”,实际还需要多个前提:

Dynamo 为了提高可用性采用 sloppy quorum(宽松法定人数):目标副本不可用时,把写入暂存在其他健康节点。此时两次操作选择的临时节点可能没有传统法定人数预期的交集。Cassandra 的官方文档也使用“通常可见”描述 R + W > N 的效果,并把具体保证交给一致性级别、修复机制和版本协调共同完成。

法定人数是副本集合相交的数学条件,完整的一致性保证仍然来自整个协议。

读修复和提示移交负责追赶

无主系统允许部分副本短暂落后,因此需要持续收敛。

读修复发生在读取路径。协调节点比较多个副本的摘要或数据,发现差异后把较新的版本写回落后的副本。热门数据被频繁读取,通常会更快修复;长期无人读取的数据不能依赖这一机制自动恢复。

提示移交发生在写入路径。目标副本离线时,协调节点保存一份带目标信息的临时写入;目标恢复后,再把遗漏变更交还给它。它能缩短短暂故障造成的不一致时间。

两者都属于尽力而为。Cassandra 明确说明,提示有保存期限,读修复也只覆盖本次读取涉及的副本和数据范围。要保证副本长期收敛,仍需定期运行基于 Merkle Tree 的反熵修复。把“节点回来后会自己追上”当成无需维护的承诺,会让冷数据中的差异长期潜伏。

三类复制如何选择

模型写入入口主要收益主要成本常见场景
单主一个主节点顺序清楚、冲突少、运维成熟主节点故障转移、从节点延迟事务业务、读多写少的常规应用
多主多个主节点多地就近写入、可容忍地区断连并发冲突、合并规则、调试复杂多地区写入、离线协作
无主多个副本节点故障时仍可读写、一致性可调版本协调、后台修复、陈旧读取高可用键值存储、会话与购物车

可以先按以下顺序判断:

  1. 单主能否满足写入延迟和可用性目标?能满足时优先使用它。
  2. 多地都必须接受低延迟写入吗?若必须,先写清每种冲突的合并规则。
  3. 业务是否愿意用短暂不一致换取节点故障期间持续写入?若愿意,再评估无主方案。
  4. 哪些读取必须看到自己的写入,哪些允许陈旧几秒或几分钟?
  5. 节点恢复、数据校验和全量修复由谁执行,如何监控积压?

上线前检查这七件事

复制模型选定后,还需要把抽象保证变成可观测的运行规则:

  1. 明确写入成功的条件:本地落盘、一个远端确认,还是法定人数确认。
  2. 定义可接受的数据丢失量和恢复时间,并用故障演练验证。
  3. 监控复制延迟、未发送日志、提示积压、修复进度和副本差异。
  4. 为需要写后读一致性的接口指定读取路径。
  5. 记录版本、写入位置或因果信息,避免只靠墙上时间判断新旧。
  6. 测试网络分区、旧主复活、写入超时和节点长期离线。
  7. 给冲突和无法自动合并的数据准备审计与人工处理入口。

复制不会消除故障,它把一台机器的故障转化为副本之间的协调、延迟与修复问题。单主把顺序集中在一个入口,多主把冲突交给业务处理,无主用法定人数和后台修复维持可用性。先选择最简单且能满足目标的方案,再为真实故障补齐监控和恢复机制,通常比追求更复杂的拓扑可靠。

Aide Hub 会继续分享 AI 助手、开发工具和软件工程实践。

参考


Tags


Previous

.NET 11 Runtime Async:异步为何更快

Next

分布式系统时间:Lamport、向量时钟与 HLC