分布式 SQL 引擎:CockroachDB 架构
CockroachDB 是 NewSQL 阵营的代表作:对外提供标准 PostgreSQL 协议与 SQL 语义,对内把整个数据库打散成按 key 范围切分的 Range,用 Raft 保证每个 Range 的多副本一致性,再用 MVCC + 混合逻辑时钟(HLC)+ 分布式事务协议支撑跨分片的 ACID。它想同时回答两个问题:像 NoSQL 一样水平扩展、异地多活,又像传统单机关系库一样强一致、支持事务。本文拆解它的分层架构与事务协议,并用一段零依赖 Python 代码把『Range 路由 + 跨分片两阶段提交 + 写意图清理』跑给你看。
一、整体分层:从 SQL 到磁盘的四层
- SQL 层:无状态的网关。解析、优化(CBO)、执行 SQL,把表数据映射到底层 KV:
/tableID/indexID/主键列 -> 行列值。任意节点都能处理任意查询。 - 事务层:实现分布式 ACID。核心产物是事务记录(Transaction Record)与写意图(Write Intent)。
- 分布层(Distribution):维护 Range 的元信息与路由(Range Cache / Range Descriptor),决定每个 key 落在哪个 Range、该 Range 的副本在哪些节点。
- 复制层与存储层:每个 Range 是一个 Raft Group(默认 3 副本);单个副本落在本机的 Pebble(CockroachDB 自研的 LSM-Tree 存储引擎,受 RocksDB 启发)。
一句话概括数据流:SQL -> KV 操作 -> 按 key 定位 Range -> 走该 Range 的 Raft 提交 -> 写入 Pebble。
二、Range:分片与扩展的基本单元
整个 key 空间被切成连续不重叠、左闭右开的 Range,默认约 512MB 触发分裂(split),过小则合并(merge)。每个 Range 的副本分散在不同节点,Raft Leader 负责该 Range 的读写协调。路由信息被客户端(网关节点)缓存在 Range Cache 里,分片分裂/迁移后通过元信息查找修正。
['', 'm') 与 ['m', 'z'),那么 zebra 这种以 z 开头的 key 会因不满足 zebra < 'z' 而路由失败——真实系统用 KeyMax 表示无上界,本文用 ASCII 中 z 之后的字符 { 模拟。
三、MVCC 与混合逻辑时钟(HLC)
CockroachDB 的每个写都带时间戳,同一 key 的多个版本共存(MVCC),快照读直接按时间戳读历史版本,避免读写互相阻塞。时间戳用的是 HLC:物理时钟(PT)与逻辑计数器(L)的组合,既贴近真实时间,又保证因果顺序。各节点时钟偏移被限制在 --max-offset(默认 500ms)内;读操作如果遇到『现在 + max_offset』窗口内的不确定写,需要等待(不确定性重启/等待),以此换取无全局时钟的线性一致性。
四、分布式事务协议:写意图 + 事务记录 + 并行提交
跨 Range 的事务是 CockroachDB 最硬核的部分,简化理解为一个『以写意图为参与者』的变种两阶段提交:
- 阶段一(并发写意图):事务把每个要写 key 的写意图(未提交的值 + 指向事务记录的指针)并发地写到各自所在的 Range,并创建事务记录(状态 PENDING/STAGING)。写意图本身就是『参与者的 prepared 状态』。
- 提交决策:客户端把事务记录原子地标记为 COMMITTED——这一落点就是整个事务的原子提交点。
- 阶段二(异步清理意图):把每个写意图原地解析为已提交的 MVCC 版本。清理是异步的,不影响事务已提交的事实。
- 冲突处理:其他事务读到写意图时,检查其事务记录:已提交则帮忙解析;已中止则回滚该意图;仍在进行则推动(push)其时间戳或等待。
现代版本还引入并行提交(Parallel Commits):把『写齐所有意图』与『标记 COMMITTED』流水线化,跨分片事务提交延迟从两个 RTT 压到一个 RTT。
五、动手实验:模拟 Range 路由与跨分片事务
下面代码用两个 Range、一个支持写意图的 KV 存储和一个简化事务管理器,完整演示:路由定位、跨分片写意图暂存、提交前读冲突、提交清理、回滚清理。零依赖,Python 3 直接运行:
# -*- coding: utf-8 -*-
# 简化模拟 CockroachDB 架构核心机制:
# 1) Range 分片 + 路由 2) 跨 Range 分布式事务 2PC 与写意图(intent)清理
# 注意: 最后一个 Range 的上界必须覆盖 key 空间末尾,
# '{' 是 ASCII 中 'z' 之后的字符, 用来模拟 KeyMax
RANGES = [
('', 'm', 'range-1'), # 前缀 a~l
('m', '{', 'range-2'), # 前缀 m~z (含 z 开头, 覆盖到末尾)
]
def locate(key):
for lo, hi, rid in RANGES:
if lo <= key < hi:
return rid
raise KeyError(key)
class Store:
def __init__(self):
self.data = {} # key -> (committed_value, commit_ts)
self.intents = {} # key -> (txn_id, value, ts) 未提交的写意图
def get(self, key):
if key in self.intents:
i = self.intents[key]
raise RuntimeError(
f'READ_CONFLICT: key={key!r} 上有未提交写意图 '
f'txn={i[0]}, 需等待或推动其时间戳')
return self.data.get(key)
def put_intent(self, key, value, txn_id, ts):
self.intents[key] = (txn_id, value, ts)
def resolve_commit(self, key, ts):
txn_id, value, its = self.intents.pop(key)
self.data[key] = (value, ts)
def resolve_abort(self, key):
self.intents.pop(key, None)
store = Store()
class Txn:
_seq = 0
def __init__(self, ts):
Txn._seq += 1
self.id = f'txn-{Txn._seq}'
self.ts = ts
self.writes = {}
self.status = 'PENDING'
def stage_intents(self, store):
for k, v in self.writes.items():
store.put_intent(k, v, self.id, self.ts)
self.status = 'STAGING'
def commit(self, store):
self.status = 'COMMITTED'
for k in self.writes:
store.resolve_commit(k, self.ts)
def abort(self, store):
self.status = 'ABORTED'
for k in self.writes:
store.resolve_abort(k)
print('== 分片路由 ==')
for k in ['apple', 'mongo', 'zebra']:
print(f' key={k!r:>10} -> {locate(k)}')
print('\n== 事务1: 提交(跨两个 Range) ==')
t1 = Txn(ts=100)
t1.writes = {'apple': 5, 'zebra': 9}
t1.stage_intents(store)
print(' 阶段一完成, 写意图分布到: ' +
', '.join(sorted(locate(k) for k in t1.writes)))
try:
store.get('apple')
except RuntimeError as e:
print(f' 提交前读 apple 触发: {e}')
t1.commit(store)
print(f' 阶段二完成; apple={store.get("apple")}, '
f'zebra={store.get("zebra")}')
print('\n== 事务2: 回滚 ==')
t2 = Txn(ts=200)
t2.writes = {'mongo': 7}
t2.stage_intents(store)
t2.abort(store)
print(f' 回滚后 mongo 意图已清理, 读取结果={store.get("mongo")}')
assert store.data == {'apple': (5, 100), 'zebra': (9, 100)}
assert store.intents == {}
print('\n校验通过: 提交数据已生效, 无残留写意图')
在沙箱(Python 3,无第三方库)上的真实运行输出:
== 分片路由 ==
key= 'apple' -> range-1
key= 'mongo' -> range-2
key= 'zebra' -> range-2
== 事务1: 提交(跨两个 Range) ==
阶段一完成, 写意图分布到: range-1, range-2
提交前读 apple 触发: READ_CONFLICT: key='apple' 上有未提交写意图 txn=txn-1, 需等待或推动其时间戳
阶段二完成, 提交并清理意图; apple=(5, 100), zebra=(9, 100)
== 事务2: 回滚 ==
回滚后 mongo 意图已清理, 读取结果=None
校验通过: 提交数据已生效, 无残留写意图
六、结果解读
- 路由正确性:
zebra正确落到range-2。值得一提,本文初版脚本把右边界写成'z',zebra直接抛出KeyError——这正是分片系统设计里最容易漏的边界条件:最后一个 Range 不能有上界缺口。 - 读撞上写意图:事务提交前读
apple触发READ_CONFLICT,对应真实系统里的『intent resolution』:读者必须检查意图背后的事务记录,决定等待、帮忙提交还是回滚它。这正是 MVCC 数据库『读不被写阻塞、但要能感知未提交写』的具体形态。 - 原子提交点:事务记录标记 COMMITTED 后,两个 Range 上的数据才对外可见;清理意图只是『收尾保洁』,即便节点在清理前宕机,后续读者也能依据事务记录代为完成解析——这是崩溃恢复正确性的关键。
- 回滚的代价:事务2 回滚只需删除意图,已提交数据不受影响,最终存储中无任何残留,断言校验通过。
七、与同类方案的对照
| 维度 | CockroachDB | Google Spanner | 传统分库分表 |
|---|---|---|---|
| 时间戳 | HLC(无需专用硬件) | TrueTime(原子钟/GPS) | 各库本地时钟 |
| 分片 | Range 自动分裂/合并/再平衡 | Split 自动管理 | 人工维护,扩缩容痛苦 |
| 跨分片事务 | 写意图 + 事务记录 + 并行提交 | 2PC + Paxos | 应用层或 XA,性能差 |
| 一致性读 | MVCC 快照读,不确定性窗口等待 | TrueTime 提交等待(commit-wait) | 难以保证全局一致 |
八、工程应用
- 异地多活与合规部署:用 REGIONAL BY ROW / 副本放置约束把数据钉在用户所在区域,兼顾低延迟与数据主权合规。
- 跟随读(Follower Reads):历史快照读可以走非 Leader 副本,跨地域只读查询延迟大幅下降。
- 热点缓解:单调递增主键(如时间戳开头)会把写集中到末尾 Range,官方建议用 UUID 或 hash-sharded index 打散。
- 变更数据捕获(Changefeed):基于 Range 级 Raft 日志的变更流,直接对接 Kafka 做数仓同步。
九、常见误区
- 把 CockroachDB 当『自动分片的 Postgres』而忽略事务开销:跨 Range 写事务天然比单机贵(至少一次共识 + 意图清理),热点小事务应通过表设计尽量落在单 Range。
- 用单调自增 ID 做主键:在分布式 KV 上这是制造末尾 Range 热点的经典反模式。
- 认为 HLC 能消除时钟问题:HLC 只保证因果序;超出 max-offset 的时钟漂移会直接导致节点被集群拒绝。
- 忽视写意图清理失败的影响:意图长期不清理不会破坏正确性,但会拖慢所有冲突读写,真实集群需要关注 intent 积压指标。
- 盲目追求 SERIALIZABLE 之外的隔离级别:CockroachDB 只提供可串行化(SER 隔离级别),很多『读已提交够用』的经验在这里反而多余。
十、参考文献
- Taft, R. et al. CockroachDB: The Resilient Geo-Distributed SQL Database. SIGMOD 2020.
- Corbett, J. et al. Spanner: Google's Globally-Distributed Database. OSDI 2012.
- Kulkarni, S. et al. Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases. PaPoC 2014(HLC 原始论文).
- Ongaro, D., Ousterhout, J. In Search of an Understandable Consensus Algorithm (Raft). USENIX ATC 2014.
- CockroachDB 官方文档: Transaction Layer & Parallel Commits. https://www.cockroachlabs.com/docs/stable/architecture/transaction-layer