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 四个性质:
- Available:单个数据中心宕机,其他数据中心继续服务
- Low latency:读写都打本地数据中心,ms 级
- Partition tolerant:网络分区时本地仍可写
- Scalability:数据中心内部 sharded,没有单点序列化
CAP 定理告诉你强一致 + 高可用 + 分区容忍三选二——所以 COPS 主动放弃强一致。但它不退到最终一致,而是停在因果一致 + 收敛冲突处理(causal+,简称 causal+)。
工程实现的关键三步:
- 依赖追踪:客户端每次读到一个值,记下
(key, version)。下次写入时,把这些(key, version)当作”我这次写依赖于这些版本”打包带上。 - 依赖检查:远端数据中心收到复制后,先调用
dep_check检查所有依赖在本地都已可见,全部到齐才 apply——这就是因果保证的物理实现。 - 收敛冲突处理:两个客户端并发写同一个键,怎么办?默认 last-writer-wins,应用层可以注册合并函数(类似 CRDT 的思路)让两边收敛到同一个值。
实践案例
案例 1:因果倒置长什么样
客户端 A(纽约):PUT photo = "<照片二进制>"客户端 A:PUT post = "看我这张照片" // 引用了 photo客户端 B(东京):GET post → "看我这张照片"客户端 B:PUT comment = "好看" // 看了 post 才评论旁观者 C(伦敦): GET comment → "好看" GET post → 还没复制到,404C 看到了”好看”这条评论,却看不到被评论的帖子。用户感知到的是系统出 bug。最终一致系统天然会出现这种情况。
案例 2:COPS 怎么消除案例 1 的问题
A.PUT(photo) → 返回 version v1A.PUT(post, deps=[(photo, v1)]) → 返回 v2B.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 commentC 永远不会先看到 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。
踩过的坑
-
依赖元数据膨胀:长时间运行的客户端 deps 集合越积越大,每次写都要传一长串。论文做了两件事:(a) nearest dependency 优化——只保留每个键的最近一个版本(其他可由因果传递推出);(b) dependency GC——一旦版本在所有数据中心都已稳定,可丢。
-
客户端必须正确累积 context:如果客户端 SDK bug 漏记了某次读,因果链就断了。这是个**“对了感觉不到,错了找不到”**的隐患——论文没强制方案,依赖 SDK 实现质量。
-
跨客户端的因果靠不上:A 的客户端读了 X,把 X 的内容复制粘贴给 B。B 不会知道这是来自 X 的。因果一致只追代码内的因果,不追用户大脑里的因果。
-
写入冲突的合并函数难写:默认 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 这条线则把”收敛冲突处理”做到了类型级别。
学到什么
- 一致性是连续光谱:最终一致 ↔ 因果一致 ↔ 顺序一致 ↔ 线性一致。COPS 证明了中间档不仅理论上有意义,工程上也跑得动。
- 元数据是货币:因果一致的代价不是同步延迟,而是带宽 + 内存——你用元数据换来了性能。所有跨地域系统都在做类似交易。
- 客户端库是一致性的承担者:服务端只做 dep_check,因果链的累积全在客户端 context。这种”协议参与者”角色分配决定了系统的可演化性。
- 够用 + 可达 > 完美 + 不可达:因果一致是工程界对 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 给全球数据库发时间戳