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 代的历史数据?
在构建大规模分布式系统时,我们总是面临一个经典难题:如何在保证数据强一致性(strong consistency)的同时,实现高吞吐量和高可用性?许多商业系统为了追求极致的性能和可用性,不得不在一致性上做出妥协,转而采用最终一致性模型。然而,链式复制(Chain Replication, CR)及其增强版 CRAQ(Chain Replication with Apportioned Queries)向我们证明,强一致性与高性能并非总是鱼与熊掌不可兼得。
链式复制(CR)是一种旨在提供高吞-吐量和高可用性的数据复制方法。它的核心思想非常直观:将存储同一份数据的所有副本节点组织成一条线性的“链”。
这条链有两个特殊的角色:
- 链头 (Head) :链的第一个节点,负责接收所有的写请求。
- 链尾 (Tail) :链的最后一个节点,负责接收所有的读请求。
Client Client
| |
(Write) (Read)
| |
V V
+-------+ +---------+ +------+
| Head |-->| Replica |-->| Tail |
+-------+ +---------+ +------+写操作流程
- 客户端将写请求(例如
write(obj, V)) 发送给链头。 - 链头处理该请求,并将更新沿着链顺序传递给下一个节点。
- 当写操作最终到达链尾时,这个更新被认为是“已提交” (
committed) 的。 - 此时,链尾会向客户端发送一个确认响应,告知写操作已成功。
读操作流程
所有读请求都直接发送给链尾,并由链尾直接响应。链上的其他节点不参与读操作。
为什么 CR 能保证强一致性?
CR 实现线性一致性(linearizability)的直觉非常简单: 链尾是所有操作的唯一仲裁点 。所有的写请求都必须经过链头的排序,并最终在链尾提交;所有的读请求也只能从链尾获取数据。这就好像整个系统只有一个服务器(链尾)在处理所有请求,从而自然地保证了所有操作的全局顺序。
CR 的优势
相较于 Paxos 或 Raft 这类共识算法,CR 在特定场景下性能更优:
- 写路径开销低 :Raft 的领导者 (
leader) 需要将操作日志并行发送给所有跟随者 (follower);而 CR 的链头只需将数据发送给链上的下一个节点,网络负载被分散到了整条链上。 - 读写负载分离 :CR 的链头处理写,链尾处理读,负载天然分离;而 Raft 的
leader通常需要处理所有客户端请求。 - 故障恢复更简单 :CR 的故障恢复逻辑相比 Raft 的日志比对和冲突解决要简单得多。当一个节点失败时,它的后继节点可以接管其工作,无需重新执行所有操作。
CR 虽然设计优雅,但它有一个明显的性能瓶颈: 所有的读负载都压在了链尾这一个节点上 。这意味着系统的读吞吐量受限于单个节点的处理能力,而链中的其他中间节点在处理读请求时完全处于空闲状态,其计算资源被白白浪费。
为了解决这个问题,CRAQ (Chain Replication with Apportioned Queries) 应运而生。它的核心目标是: 在维持强一致性的前提下,允许链上的任何节点都能处理读请求 ,从而将读负载“分摊”(apportion)到整条链上,实现读性能的线性扩展。
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,但它们对网络分区的容忍度更低,强依赖于一个外部的配置服务。理解这些核心差异,才能帮助我们在真实世界的复杂场景中做出最恰当的架构决策。