跳转到内容

COPS — 大规模跨地域存储如何用得起的代价拿到因果一致

是什么

COPS 是一个跨地域键值存储,它在「最终一致(便宜但乱)」和「强一致(贵到不能用)」之间,挖出了一档用得起、又有用的中间地带——因果一致(causal consistency)。

日常类比:群聊里 A 发了一张图,B 回了一句”哈哈”。

  • 最终一致:旁观者 C 可能先看到 B 的”哈哈”,再看到那张图。莫名其妙。
  • 强一致:所有人按同一时间线看,但要保证这件事,B 在另一个城市发”哈哈”前必须先和 A 所在城市同步一次——加 100ms 延迟。
  • 因果一致(COPS):保证只要 B 看到了 A 的图,那么任何看到 B 回复的人必然也已经看到了那张图。但两个没有因果关系的事件可以乱序——这够用了,又便宜。

COPS 把这个保证扩展到了跨数据中心、可水平扩展的工程系统里。

为什么重要

不理解 COPS,下面这些事都没法解释:

  • 为什么 Facebook 的评论不会”先看到回复后看到原帖”——背后是因果一致的工程化
  • 为什么 Spanner 这种强一致系统跨大洲写入要 100ms+,而 COPS 类系统能做到本地 ms 级
  • 为什么”最终一致”四个字在 2010 年代成为吐槽对象——因为它会让用户看到字面上的因果倒置
  • 一致性不是二选一:完全一致和完全不一致之间,有可形式化、可实现、用户能感知的中间档

核心要点

COPS 想同时满足 ALPS 四个性质:

  1. Available:单个数据中心宕机,其他数据中心继续服务
  2. Low latency:读写都打本地数据中心,ms 级
  3. Partition tolerant:网络分区时本地仍可写
  4. Scalability:数据中心内部 sharded,没有单点序列化

CAP 定理告诉你强一致 + 高可用 + 分区容忍三选二——所以 COPS 主动放弃强一致。但它不退到最终一致,而是停在因果一致 + 收敛冲突处理(causal+,简称 causal+)。

工程实现的关键三步:

  1. 依赖追踪:客户端每次读到一个值,记下 (key, version)。下次写入时,把这些 (key, version) 当作”我这次写依赖于这些版本”打包带上。
  2. 依赖检查:远端数据中心收到复制后,先调用 dep_check 检查所有依赖在本地都已可见,全部到齐才 apply——这就是因果保证的物理实现。
  3. 收敛冲突处理:两个客户端并发写同一个键,怎么办?默认 last-writer-wins,应用层可以注册合并函数(类似 CRDT 的思路)让两边收敛到同一个值。

实践案例

案例 1:因果倒置长什么样

客户端 A(纽约):PUT photo = "<照片二进制>"
客户端 A:PUT post = "看我这张照片" // 引用了 photo
客户端 B(东京):GET post → "看我这张照片"
客户端 B:PUT comment = "好看" // 看了 post 才评论
旁观者 C(伦敦):
GET comment → "好看"
GET post → 还没复制到,404

C 看到了”好看”这条评论,却看不到被评论的帖子。用户感知到的是系统出 bug。最终一致系统天然会出现这种情况。

案例 2:COPS 怎么消除案例 1 的问题

A.PUT(photo) → 返回 version v1
A.PUT(post, deps=[(photo, v1)]) → 返回 v2
B.GET(post) → 拿到 post,context 累积 (post, v2)、(photo, v1)
B.PUT(comment, deps=[(post, v2), (photo, v1)]) → v3
伦敦数据中心收到 comment 复制:
dep_check(post, v2) → 还没?等待
dep_check(photo, v1) → 还没?等待
两个都到了 → apply comment

C 永远不会先看到 comment 再看到 post——这是物理上保证的,不是概率上的。

案例 3:COPS-GT 的多键读快照

普通 COPS 一次只读一个键。COPS-GT(get_transaction,只读事务)允许:

get_trans([friends, wall_posts, photos])
# 保证返回的三个值来自一个因果一致的快照
# 不会出现 friends 看到了新好友 X,但 X 的 wall_posts 还看不到

注意:get_trans 不是写事务(没有 ACID 的 A 和 I),只是给”多键读”加因果一致快照。代价是元数据多带一份,论文测得 COPS-GT 比 COPS 慢约 2x

踩过的坑

  1. 依赖元数据膨胀:长时间运行的客户端 deps 集合越积越大,每次写都要传一长串。论文做了两件事:(a) nearest dependency 优化——只保留每个键的最近一个版本(其他可由因果传递推出);(b) dependency GC——一旦版本在所有数据中心都已稳定,可丢。

  2. 客户端必须正确累积 context:如果客户端 SDK bug 漏记了某次读,因果链就断了。这是个**“对了感觉不到,错了找不到”**的隐患——论文没强制方案,依赖 SDK 实现质量。

  3. 跨客户端的因果靠不上:A 的客户端读了 X,把 X 的内容复制粘贴给 B。B 不会知道这是来自 X 的。因果一致只追代码内的因果,不追用户大脑里的因果

  4. 写入冲突的合并函数难写:默认 last-writer-wins 在购物车这种场景会丢东西。要写自定义合并函数,应用层负担转移到合并逻辑——这正是 CRDT 想替你解决的问题。

适用 vs 不适用场景

适用

  • 跨地域只读为主、写为辅的社交类负载(评论、点赞、好友关系)
  • 用户能感知到因果但能容忍并发不一致的场景
  • 需要单数据中心低延迟的全球部署
  • 已知最终一致不够、但又付不起 Paxos 跨地域 quorum 的延迟

不适用

  • 银行转账、库存扣减这种需要严格隔离的场景——因果一致不防止 lost update
  • 强读自己写之后立刻被另一个用户看到(read-your-writes 在跨用户场景不保证)
  • 需要全局唯一序号(订单号、自增 ID)——必须真正强一致
  • 单数据中心场景——直接用 Paxos / Raft 更简单,没必要这套机器

历史脉络(可跳过)

  • 1978 年:Lamport 定义 happens-before 关系,给”因果”一个数学骨架。
  • 1990s:Bayou、Coda、Lazy Replication 这一波系统都在客户端追因果,但没扩展到大规模。
  • 2007 年:Amazon Dynamo——最终一致的工业代表,CAP 选 AP。
  • 2011 年:COPS 在 SOSP 提出因果一致 + 水平扩展两者兼得的工程实现。
  • 2013 年:同作者推出 Eiger,扩展到列存储模型。
  • 2017 年:Occult 进一步压缩元数据;CRDT 这条线则把”收敛冲突处理”做到了类型级别。

学到什么

  1. 一致性是连续光谱:最终一致 ↔ 因果一致 ↔ 顺序一致 ↔ 线性一致。COPS 证明了中间档不仅理论上有意义,工程上也跑得动。
  2. 元数据是货币:因果一致的代价不是同步延迟,而是带宽 + 内存——你用元数据换来了性能。所有跨地域系统都在做类似交易。
  3. 客户端库是一致性的承担者:服务端只做 dep_check,因果链的累积全在客户端 context。这种”协议参与者”角色分配决定了系统的可演化性。
  4. 够用 + 可达 > 完美 + 不可达:因果一致是工程界对 CAP 定理的优雅妥协——挑出”用户最在意的那条因果序”,剩下的让位给延迟。

延伸阅读

  • 论文 PDF:COPS SOSP 2011(14 页,前 6 页是 motivation 必读)
  • 后续工作:Eiger SOSP 2013(COPS 的列存储版)
  • Bolt-on Causal Consistency(不重写存储层也能加因果一致的层)
  • lamport-1978 —— happens-before 关系给”因果”一个数学定义,COPS 是其工程化
  • dynamo —— 最终一致的代表,COPS 想超越的对象

关联

  • lamport-1978 —— happens-before 定义因果,COPS 的因果一致以此为骨架
  • dynamo —— 最终一致的工业代表,COPS 直接对标的退路
  • spanner-2012 —— 强一致用 TrueTime 跨地域同步的代价:COPS 想避开的极端
  • cassandra-2010 —— 同期工业级最终一致 KV,提供对比基线
  • crdt-shapiro-2011 —— 收敛冲突处理的另一条路(数据类型级别 vs 协议级别)
  • sequential-consistency-1979 —— 比因果一致更强的层级
  • paxos-1998 —— 跨地域强一致的协议基础,COPS 的不归路

反向链接

  • cassandra-2010 —— Cassandra 2010 — 把 Dynamo 的 P2P 骨架和 Bigtable 的列族数据模型拼成一个东西
  • crdt-shapiro-2011 —— CRDT — 让多副本各改各的,最终自动合一
  • dynamo —— Dynamo — 让购物车永远能写入的分布式存储
  • lamport-1978 —— Lamport 1978 — 分布式系统里没有”绝对的同时”
  • paxos-1998 —— Paxos 1998 — 古希腊议会寓言里藏的共识协议
  • spanner-2012 —— Spanner 2012 — 用原子钟和 GPS 给全球数据库发时间戳