Skip to content

Latest commit

 

History

History
193 lines (122 loc) · 12.4 KB

File metadata and controls

193 lines (122 loc) · 12.4 KB

LEC 9: More Replication, CRAQ

NotebookLM 生成的双人播客

https://notebooklm.google.com/notebook/fbe170ea-0599-4ea6-be39-1a53a487a005/audio

微信链接: https://mp.weixin.qq.com/s/MILHlCYkDPC1ZDNZ1L7lNQ

给我留下的较深的印象:

  • CRAQ 网络层的双向链表,数据一致性好管理(直观易懂)但是很难处理脑裂/网络分区,因此还需要外包容错给一个高可用服务(一个外部配置管理器)
  • 相比raft,读性能更高(链越长,提供 read 的服务器越多,负载越小),但是处理故障容错较慢,需要配置管理介入
  • 一种极端情况:如果数据一直传不到链尾(一直无法 commit ),那么一直是 dirty 的,此时,每个节点需要保留几代的历史数据?比如节点的数据是 100 代了,但是尾节点的数据还是第一代,那么这个节点就需要保留 100 代的历史数据?

Gemini 2.5 Pro 生成的“从链式复制到 CRAQ:深入理解高性能强一致性存储方案”

在构建大规模分布式系统时,我们总是面临一个经典难题:如何在保证数据强一致性(strong consistency)的同时,实现高吞吐量和高可用性?许多商业系统为了追求极致的性能和可用性,不得不在一致性上做出妥协,转而采用最终一致性模型。然而,链式复制(Chain Replication, CR)及其增强版 CRAQ(Chain Replication with Apportioned Queries)向我们证明,强一致性与高性能并非总是鱼与熊掌不可兼得。

链式复制(Chain Replication):简洁优雅的强一致模型

链式复制(CR)是一种旨在提供高吞-吐量和高可用性的数据复制方法。它的核心思想非常直观:将存储同一份数据的所有副本节点组织成一条线性的“链”。

这条链有两个特殊的角色:

  • 链头 (Head) :链的第一个节点,负责接收所有的写请求。
  • 链尾 (Tail) :链的最后一个节点,负责接收所有的读请求。
  Client                    Client
    |                         |
 (Write)                    (Read)
    |                         |
    V                         V
+-------+   +---------+   +------+
| Head  |-->| Replica |-->| Tail |
+-------+   +---------+   +------+

写操作流程

  1. 客户端将写请求(例如 write(obj, V) ) 发送给链头。
  2. 链头处理该请求,并将更新沿着链顺序传递给下一个节点。
  3. 当写操作最终到达链尾时,这个更新被认为是“已提交” (committed) 的。
  4. 此时,链尾会向客户端发送一个确认响应,告知写操作已成功。

读操作流程

所有读请求都直接发送给链尾,并由链尾直接响应。链上的其他节点不参与读操作。

为什么 CR 能保证强一致性?

CR 实现线性一致性(linearizability)的直觉非常简单: 链尾是所有操作的唯一仲裁点 。所有的写请求都必须经过链头的排序,并最终在链尾提交;所有的读请求也只能从链尾获取数据。这就好像整个系统只有一个服务器(链尾)在处理所有请求,从而自然地保证了所有操作的全局顺序。

CR 的优势

相较于 Paxos 或 Raft 这类共识算法,CR 在特定场景下性能更优:

  • 写路径开销低 :Raft 的领导者 (leader) 需要将操作日志并行发送给所有跟随者 (follower);而 CR 的链头只需将数据发送给链上的下一个节点,网络负载被分散到了整条链上。
  • 读写负载分离 :CR 的链头处理写,链尾处理读,负载天然分离;而 Raft 的 leader 通常需要处理所有客户端请求。
  • 故障恢复更简单 :CR 的故障恢复逻辑相比 Raft 的日志比对和冲突解决要简单得多。当一个节点失败时,它的后继节点可以接管其工作,无需重新执行所有操作。

从 CR 到 CRAQ:解锁中间节点的潜力

CR 虽然设计优雅,但它有一个明显的性能瓶颈: 所有的读负载都压在了链尾这一个节点上 。这意味着系统的读吞吐量受限于单个节点的处理能力,而链中的其他中间节点在处理读请求时完全处于空闲状态,其计算资源被白白浪费。

为了解决这个问题,CRAQ (Chain Replication with Apportioned Queries) 应运而生。它的核心目标是: 在维持强一致性的前提下,允许链上的任何节点都能处理读请求 ,从而将读负载“分摊”(apportion)到整条链上,实现读性能的线性扩展。

CRAQ 的核心机制:Dirty 位与版本查询

CRAQ 的魔法在于它如何巧妙地处理来自任意节点的读请求,同时又不会破坏线性一致性。

1. 引入版本和状态

在 CRAQ 中,每个副本节点可以为一个对象存储多个版本。每个版本除了版本号,还有一个关键的状态属性: 洁净 (clean)肮脏 (dirty)

2. 写操作流程的变化

  • 当一个写请求从链头开始传递时,每经过一个中间节点,该节点就会为对象创建一个新版本,并将其标记为 dirty
  • 当写操作到达链尾时,链尾同样创建新版本,但它直接将版本标记为 clean,此时该版本才算正式 committed
  • 随后,链尾会沿着链向前发送一条“确认”消息。收到确认的节点会将其对应的 dirty 版本变为 clean,并可以安全地删除更旧的版本。
写请求 W(v2) 到达:

初始状态:
  Head(v1, clean), Replica(v1, clean), Tail(v1, clean)

W(v2) 到达 Head:
  Head(v1, clean; v2, dirty), Replica(v1, clean), Tail(v1, clean)
  |
  +--> W(v2) 传播

W(v2) 到达 Replica:
  Head(v1, clean; v2, dirty), Replica(v1, clean; v2, dirty), Tail(v1, clean)
                                 |
                                 +--> W(v2) 传播

W(v2) 到达 Tail (提交):
  Head(v1, clean; v2, dirty), Replica(v1, clean; v2, dirty), Tail(v1, clean; v2, clean)
                                                                |
                       <-- ACK(v2) 回传 -------------------------+

ACK(v2) 到达 Replica:
  Head(v1, clean; v2, dirty), Replica(v2, clean), Tail(v2, clean)
                                     |
  <-- ACK(v2) 回传 -------------------+

ACK(v2) 到达 Head:
  Head(v2, clean), Replica(v2, clean), Tail(v2, clean)

3. 读操作的智能处理

现在,当任意节点收到一个读请求时,它会:

  • 洁净读 (Clean Read) : 如果该对象最新的版本是 clean 的,那么节点可以直接返回这个版本的数据。这是最高效的情况。
  • 肮脏读 (Dirty Read) : 如果最新版本是 dirty 的,情况就复杂了。节点不能直接返回 dirty 数据,因为它还未提交,可能会因故障而丢失。也不能想当然地返回上一个 clean 版本,因为可能已经有更新的版本在链尾提交了。

CRAQ 的解决方案是:当节点遇到 dirty 数据时,它会向链尾发送一个轻量级的 版本查询 (version query) ,询问:“对于这个对象,你那里最新的已提交版本号是多少?”

链尾收到查询后,会返回最新的 clean 版本的版本号。由于写操作是顺序传播的,中间节点保证拥有这个已提交的版本。于是,它就可以根据链尾返回的版本号,在本地找到对应版本的数据并返回给客户端。

为什么这个机制只在 CRAQ 中有效?

CRAQ 的这个巧妙设计依赖于其线性的链式结构,它保证了 所有节点在写操作提交前都必然会看到这个写操作 。因此,节点能明确知道自己何时持有 dirty 数据,何时需要向链尾求证。

相比之下,Raft/Paxos 无法做到这一点。它们的 leader 只需要得到多数派 (majority) 的确认就可以提交一个日志条目,这意味着少数派的 follower 可能完全不知道某个已提交数据的存在。如果此时允许这个不知情的 follower 处理读请求,就可能返回陈旧的数据,从而破坏线性一致性。

生产实践中的常见问题与解决方案

1. 网络分区与裂脑 (Split-Brain)

这是 CR/CRAQ 面临的最严峻挑战。协议本身无法处理网络分区。如果链上的一个节点与邻居失联,它只能无限等待。如果此时允许失联的节点自作主张(例如,链上的第二个节点因联系不上链头,就自己“晋升”为新链头),就可能导致“裂脑”:系统中出现两个链头,各自接受写请求,造成数据不一致。

解决方案:独立的配置服务 (Configuration Service)

CR/CRAQ 依赖一个外部的、自身容错的配置服务(例如使用 Paxos、Raft 或 ZooKeeper 构建)来管理链的成员信息。这个服务是全系统对于“谁是链头、谁是链尾、链上有哪些成员”的唯一权威。

  • 配置服务会监控所有节点的健康状况。
  • 当检测到节点故障时,它会决定新的链配置(例如,将故障节点摘除)。
  • 然后,它将新的配置通知给链上的所有幸存成员以及客户端。
  • 所有组件都必须无条件服从配置服务的指令。

这个模式在很多大型系统中都有应用,比如 GFS 的 Master 节点就扮演了类似的角色。

2. 跨数据中心部署 (Geo-Replication)

当链需要跨越广域网部署在不同地理位置的数据中心时,CR 的弊端会进一步放大。如果链尾恰好在一个遥远的数据中心,那么本地数据中心的所有读请求都必须承受高昂的跨洋延迟。

CRAQ 的优势

客户端可以优先从本地的副本读取数据。

  • 在写操作不频繁时,本地副本大概率是 clean 的,读请求可以被快速响应,几乎没有延迟。
  • 即使在写操作频繁导致本地副本 dirty 的情况下,也只需要向远端的链尾发送一个轻量级的“版本查询”请求,其网络开销远小于传输整个数据对象。

实验数据显示,在广域网环境下,CRAQ 的读延迟显著低于传统的 CR。

3. 负载均衡的另一种思路:多链交错

在 CRAQ 出现之前,其实还有一种方法可以缓解 CR 的链尾读瓶颈,那就是使用多条链,并将它们交错地分布在服务器上。

假设有三台服务器 S1, S2, S3,我们可以构建三条链:

C1: S1(Head) -> S2 -> S3(Tail)
C2: S2(Head) -> S3 -> S1(Tail)
C3: S3(Head) -> S1 -> S2(Tail)

通过这种方式,每台服务器都同时扮演着链头、中间节点和链尾的角色,从而将读写的负载大致均匀地分摊开。这种方法在负载比较均衡的场景下是相当合理的。但如果某些对象或链变得异常火爆(即“热点”问题),这种静态的负载均衡策略就会失效,而 CRAQ 动态分摊读负载的能力则能更好地应对这种情况。

总结

链式复制 (CR) 以其简洁的设计,提供了一种不同于 Raft/Paxos 的强一致性复制方案。它通过严格的读写路径分离,实现了较高的吞吐量。

CRAQ 则是 CR 的一次精妙进化。它抓住了 CR 中间节点资源浪费的核心痛点,通过引入 clean/dirty 状态和版本查询机制,成功地将读负载分摊到整条链上,极大地提升了读密集型场景下的系统吞吐量,同时完美地保持了强一致性。

在选择技术方案时,理解它们之间的权衡至关重要。CR/CRAQ 在原始吞吐量上可能优于 Raft,但它们对网络分区的容忍度更低,强依赖于一个外部的配置服务。理解这些核心差异,才能帮助我们在真实世界的复杂场景中做出最恰当的架构决策。