第 9 章:YugabyteDB——一个引擎两种语言
结论先行:YugabyteDB 用一个统一的分布式存储引擎 DocDB,同时支持 PostgreSQL 和 Cassandra 两种 SQL 接口。它的核心创新在于把 Raft + LSM-Tree + MVCC + HLC 四个技术融合成一个紧密耦合的存储层——所有分布式复杂性都收敛在 DocDB 里,上层的 SQL 兼容层可以相对”轻薄”。
一个仓库,两个门面
想象一家超市后面有一个巨大的自动化仓库(DocDB)。这个仓库管理所有商品的进出、库存盘点、分仓调度。超市前面有两个入口:
-
正门(YSQL)面向习惯逛超市的顾客——走进来按货架分区(SQL 的表和索引),可以做复杂的购物清单查询(JOIN)。这扇门和普通超市(PostgreSQL)长得一模一样,老顾客不用学新路线。
-
侧门(YCQL)面向批量采购的商户——他们知道每件商品的编号,直接报编号拿货(Key-Value 模式),速度极快。这扇门和另一家仓库(Cassandra)长得一样。
两扇门通向同一个仓库。无论从哪扇门进来,商品的管理方式完全一样——分仓(Sharding)、备份(Raft 复制)、调度(Tablet 管理)全部由仓库统一处理。
DocDB:统一的分布式存储引擎
为什么叫 DocDB?
DocDB 的全称是 Document DB——”文档数据库”。这个名字可能让你困惑:YugabyteDB 不是关系数据库吗?为什么存储引擎叫”文档数据库”?
答案在于内部数据模型。DocDB 在内部把所有数据(无论来自 YSQL 还是 YCQL)统一编码成文档结构——一种类似 JSON 的嵌套键值对。
SQL 表:
users (id INT, name TEXT, email TEXT)
INSERT INTO users VALUES (1, '小明', 'ming@example.com');
DocDB 内部存储(简化表示):
Key: (table_id, 1)
Value: {
"name": "小明",
"email": "ming@example.com",
"timestamp": HLC_T1 ← 每个版本带混合逻辑时钟时间戳
}
Cassandra 表:
users (id INT PRIMARY KEY, name TEXT, email TEXT)
INSERT INTO users (id, name, email) VALUES (1, '小明', 'ming@example.com');
DocDB 内部存储(完全相同的格式):
Key: (table_id, 1)
Value: {
"name": "小明",
"email": "ming@example.com",
"timestamp": HLC_T1
}
这意味着:无论从 YSQL(PG 兼容)还是 YCQL(Cassandra 兼容)写入的数据,在存储层都是同一种格式。
Tablet:DocDB 的数据分片单位
类比 TiDB 的 Region 和 CockroachDB 的 Range,DocDB 使用 Tablet 作为数据的最小管理单元。
一张表的数据在 DocDB 中的组织:
users 表
┌──────────────────────────────────────────┐
│ Tablet 1 Tablet 2 Tablet 3 │
│ id: 1-100 id: 101-200 id: 201-300│
│ │
│ 每个 Tablet 都是独立的 Raft Group │
└──────────────────────────────────────────┘
Tablet 1 的 Raft Group:
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Node A │ │ Node B │ │ Node C │
│ Leader │ │ Follower │ │ Follower │
│ RocksDB │ │ RocksDB │ │ RocksDB │
└───────────┘ └───────────┘ └───────────┘
Tablet 2 可能在不同的节点上有 Leader
→ 负载自然分散
一个核心设计:每个 Tablet 有自己独立的 RocksDB 实例。这与 TiKV(整个节点共享一个 RocksDB)不同。
YugabyteDB(每个 Tablet 一个 RocksDB):
Node A:
Tablet 1 → RocksDB_1
Tablet 5 → RocksDB_5
Tablet 9 → RocksDB_9
优点:Tablet 迁移时只需移动对应的 RocksDB 文件
缺点:更多的 RocksDB 实例 → 更多的 LSM Compaction 开销
TiKV(整个节点一个 RocksDB):
Node A:
Region 1 ─┐
Region 5 ──── 共享 → RocksDB
Region 9 ─┘
优点:统一的 Compaction 管理
缺点:Region 迁移需要在 KV 层面拷贝数据
HLC:混合逻辑时钟
第 4 章介绍了全局时钟的三种方案。YugabyteDB 选择了 HLC(Hybrid Logical Clock,混合逻辑时钟)——一个不依赖专用硬件(GPS/原子钟)的全局时间方案。
HLC 的核心思想是把物理时钟和逻辑时钟结合起来。
HLC 时间戳结构:
┌──────────────────────┬─────────────┐
│ 物理部分(高位) │ 逻辑部分 │
│ Wall Clock Time │ Counter │
│ 微秒级精度 │ 0-65535 │
└──────────────────────┴─────────────┘
总计 64 位:高 48 位是物理时间,低 16 位是逻辑计数器
HLC 更新规则(每个节点维护自己的 HLC):
发送消息时:
physical = max(本地物理时钟, 当前HLC物理部分)
if physical == 当前HLC物理部分:
logical = 当前HLC逻辑部分 + 1
else:
logical = 0
HLC = (physical, logical)
接收消息时:
physical = max(本地物理时钟, 当前HLC物理部分, 消息HLC物理部分)
if physical == 当前HLC物理部分 == 消息HLC物理部分:
logical = max(当前HLC逻辑部分, 消息HLC逻辑部分) + 1
elif physical == 当前HLC物理部分:
logical = 当前HLC逻辑部分 + 1
elif physical == 消息HLC物理部分:
logical = 消息HLC逻辑部分 + 1
else:
logical = 0
HLC = (physical, logical)
HLC 的关键保证:
1. 因果一致性:如果事件 A 导致了事件 B,
那么 HLC(A) < HLC(B)
2. 物理时钟近似:HLC 的物理部分始终接近真实的物理时钟
偏差不超过 NTP 的误差范围(通常 < 250ms)
3. 单调递增:同一节点上的 HLC 永远递增
4. 无需中心化:不需要像 TiDB 的 PD(TSO) 那样的中心时钟分配器
HLC vs 其他时钟方案:
| 维度 | TrueTime (Spanner) | TSO (TiDB) | HLC (YugabyteDB) |
|---|---|---|---|
| 依赖 | GPS + 原子钟 | 中心化 PD | NTP 时钟同步 |
| 精度 | ~7μs | 逻辑递增 | NTP 误差范围 |
| 单点 | 无 | 有(PD) | 无 |
| 成本 | 高(专用硬件) | 低 | 低 |
| 外部一致性 | 完美保证 | 保证 | 依赖 NTP 精度 |
HLC 的代价:由于依赖 NTP 同步,如果两个节点的时钟偏差过大,可能出现短暂的一致性异常。YugabyteDB 通过 max_clock_skew 参数(默认 500ms)来限制可容忍的最大时钟偏差。如果检测到节点时钟偏差超过这个值,节点会拒绝服务。
YSQL:PostgreSQL 兼容层
YugabyteDB 的 YSQL 不是”协议兼容”或”语法兼容”——它直接复用了 PostgreSQL 的前端代码。
YSQL 架构:
┌─────────────────────────────────────────────────┐
│ YSQL (PostgreSQL Fork) │
│ ┌─────────────────────────────────────────┐ │
│ │ PostgreSQL 代码(几乎未修改) │ │
│ │ ├── Parser(SQL 解析) │ │
│ │ ├── Analyzer(语义分析) │ │
│ │ ├── Planner(查询规划) │ │
│ │ └── Executor(执行器) │ │
│ ├─────────────────────────────────────────┤ │
│ │ PgGate 适配层(C++ shim) │ │
│ │ 把 PG 的存储 API 调用翻译成 DocDB API │ │
│ └──────────────────────┬──────────────────┘ │
└─────────────────────────┼───────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ DocDB (分布式存储) │
└─────────────────────────────────────────────────┘
关键组件:PgGate
- C++ 实现的适配层
- 拦截 PostgreSQL 对本地存储(heap tuples)的调用
- 翻译成 DocDB 的分布式 KV 操作
这种方式的好处是:每次 PostgreSQL 发布新版本,YugabyteDB 可以相对容易地 merge 上游的更新(新语法、新功能)。但代价是需要维护一个持续分叉的 PostgreSQL 代码库。
YCQL:Cassandra 兼容层
YCQL 提供了 Cassandra 查询语言的兼容接口,但底层用 DocDB 替代了 Cassandra 原生的存储引擎。
YCQL vs 原生 Cassandra:
特性 YCQL 原生 Cassandra
─────────────────────────────────────────────────────
查询语言 CQL 兼容 CQL
一致性模型 强一致(Raft) 最终一致(Gossip)
事务支持 分布式 ACID 事务 轻量级事务(LWT)
底层存储 DocDB (RocksDB) SSTable
集群管理 Raft + Master Gossip 协议
为什么同时支持两种接口?YugabyteDB 的定位是替代多种数据库。如果你的团队同时使用 PostgreSQL(事务场景)和 Cassandra(高写入场景),可以用一套 YugabyteDB 替代两套系统。
事务实现
单 Tablet 事务
如果一个事务的所有读写都在同一个 Tablet 内,事务由该 Tablet 的 Raft Leader 直接处理,不需要任何跨节点协调——这是快路径。
单 Tablet 事务(快路径):
客户端 Tablet Leader
│ │
│── BEGIN ──────────────────► │
│── INSERT (key=5) ────────► │ ← key=5 属于本 Tablet
│── UPDATE (key=7) ────────► │ ← key=7 也属于本 Tablet
│── COMMIT ────────────────► │
│◄── OK ─────────────────────│
│ │
整个事务在一个 Tablet 内完成
通过 Raft 复制保证持久性
不需要 2PC
跨 Tablet 分布式事务
当事务涉及多个 Tablet 时,YugabyteDB 使用一个事务管理器来协调。
分布式事务流程:
客户端 事务管理器 Tablet A Tablet B
│ │ │ │
│── BEGIN ─► │ │
│ │ │ │
│── INSERT (key in A) ──────► │
│── INSERT (key in B) ──────────────►
│ │ │ │
│── COMMIT ► │ │
│ │── Prepare ─► │
│ │── Prepare ─────────────►
│ │◄── OK ───── │
│ │◄── OK ─────────────────│
│ │── Commit ──► │
│ │── Commit ──────────────►
│◄── OK ─── │ │
事务管理器本身也是通过 Raft 复制的——如果管理器节点宕机,其他节点可以接管。这避免了事务协调器成为单点。
事务冲突解决
YugabyteDB 使用 Provisional Records(临时记录) 来检测和解决事务冲突。
两个事务同时修改同一行:
T1: UPDATE users SET name = 'Alice' WHERE id = 1;
T2: UPDATE users SET name = 'Bob' WHERE id = 1;
DocDB 中的存储:
Key: (users, id=1)
├── Intent (T1, HLC=100): name = 'Alice' ← T1 的临时写入
├── Intent (T2, HLC=101): name = 'Bob' ← T2 的临时写入
└── Committed (HLC=50): name = '小明' ← 上一个已提交的值
冲突检测:
T2 在写入 Intent 时发现已经有 T1 的 Intent
→ 检查 T1 的事务状态
→ 如果 T1 还在进行中:根据优先级决定谁等待谁
→ 如果 T1 已提交:T2 需要重启(基于 T1 的提交时间戳之后)
→ 如果 T1 已中止:T2 清理 T1 的 Intent,继续
YugabyteDB 的集群管理
YB-Master 和 YB-TServer
YugabyteDB 集群角色:
YB-Master(元数据管理):
- 管理表的 Schema
- 管理 Tablet 到 TServer 的映射
- 协调 Tablet 分裂和负载均衡
- 自身通过 Raft 复制(通常 3 或 5 个 Master)
YB-TServer(数据服务):
- 承载 Tablet(DocDB 实例)
- 处理读写请求
- 运行 YSQL 和 YCQL 接口
- 可以有很多个(弹性扩展)
┌──────────────────────────────┐
│ YB-Master Raft │
│ M1(Leader) M2 M3 │
│ 表元数据 + Tablet 映射 │
└──────────┬───────────────────┘
│
┌──────────▼───────────────────┐
│ YB-TServer 集群 │
│ TS1 TS2 TS3 │
│ ┌────────┐ ┌────────┐ ┌────────┐
│ │Tablet 1│ │Tablet 1│ │Tablet 1│
│ │(Leader)│ │(Follow)│ │(Follow)│
│ ├────────┤ ├────────┤ ├────────┤
│ │Tablet 2│ │Tablet 2│ │Tablet 2│
│ │(Follow)│ │(Leader)│ │(Follow)│
│ ├────────┤ ├────────┤ ├────────┤
│ │Tablet 3│ │Tablet 3│ │Tablet 3│
│ │(Follow)│ │(Follow)│ │(Leader)│
│ └────────┘ └────────┘ └────────┘
└──────────────────────────────┘
Leader 均匀分布在不同 TServer 上
→ 读写负载自然分散
Tablet 分裂
当一个 Tablet 的数据量超过阈值时,YugabyteDB 会自动分裂。
Tablet 自动分裂:
分裂前:
┌────────────────────────────┐
│ Tablet 1 (id: 1-1000) │
│ 大小: 20GB (超过阈值10GB) │
└────────────────────────────┘
分裂后:
┌──────────────────┐ ┌──────────────────┐
│ Tablet 1a │ │ Tablet 1b │
│ (id: 1-500) │ │ (id: 501-1000) │
│ 大小: ~10GB │ │ 大小: ~10GB │
└──────────────────┘ └──────────────────┘
分裂过程中:
1. Master 选择分裂键(尽量均分)
2. 在 Leader 上创建两个新 Tablet
3. 通过 Raft 将分裂操作复制到 Follower
4. 所有副本完成分裂后,旧 Tablet 标记为已分裂
5. 对客户端透明(自动重定向到新 Tablet)
YugabyteDB 与竞品对比
与 CockroachDB 的对比
两者经常被放在一起比较——都是 Spanner 启发的、支持 PostgreSQL 兼容的分布式 SQL 数据库。
架构层面:
CockroachDB:
┌────────────────────┐
│ SQL + KV + Raft │ ← 全部在一个 Go 进程内
│ (单一仓库/纯 Go) │
└────────────────────┘
YugabyteDB:
┌────────────────────┐
│ YSQL (PG Fork) │ ← C 代码(来自 PostgreSQL)
├────────────────────┤
│ PgGate (C++ shim) │ ← C++ 适配层
├────────────────────┤
│ DocDB (C++) │ ← C++ 存储引擎
└────────────────────┘
| 维度 | CockroachDB | YugabyteDB |
|---|---|---|
| PG 兼容方式 | 自研兼容层 | Fork PG 前端 |
| PG 兼容度 | 中(核心 SQL 功能) | 高(直接复用 PG 代码) |
| 存储引擎 | Pebble (Go) | RocksDB (C++) |
| 全局时钟 | HLC | HLC |
| 默认隔离级别 | Serializable | Snapshot Isolation |
| 多模型 | 仅 SQL | SQL + CQL |
| 编程语言 | Go | C / C++ |
与 TiDB 的对比
| 维度 | TiDB | YugabyteDB |
|---|---|---|
| 兼容协议 | MySQL | PostgreSQL + Cassandra |
| 架构 | 存算分离 | 存算一体 |
| HTAP | TiFlash 列存 | 无专用列存 |
| 全局时钟 | TSO(中心化) | HLC(去中心化) |
| 编程语言 | Go + Rust | C + C++ |
YugabyteDB 的 Geo-Distribution(地理分布)
YugabyteDB 对多区域部署有原生支持,这是它的一个亮点。
三种地理分布模式:
1. Multi-Zone(多可用区):
同一地域的不同机房
延迟:< 2ms
适用:同城容灾
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Zone A │ │ Zone B │ │ Zone C │
│ (机房1) │ │ (机房2) │ │ (机房3) │
└─────────┘ └─────────┘ └─────────┘
距离: ~50km
2. Multi-Region(多区域):
不同地理区域(如北京、上海、广州)
延迟:10-50ms
适用:异地多活
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 北京 │ │ 上海 │ │ 广州 │
│ Region │ │ Region │ │ Region │
└──────────┘ └──────────┘ └──────────┘
距离: ~1000-2000km
3. Geo-Partitioned(地理分区):
数据按地理位置分区,每个区域只存本区域的数据
适用:数据主权合规(如 GDPR)
欧洲用户数据 → 只在欧洲节点
亚洲用户数据 → 只在亚洲节点
代码结构导览
yugabyte-db/
├── src/
│ ├── yb/ ← DocDB 核心(C++)
│ │ ├── consensus/ ← Raft 共识实现
│ │ ├── tablet/ ← Tablet 管理
│ │ ├── docdb/ ← 文档存储引擎
│ │ ├── master/ ← YB-Master 逻辑
│ │ ├── tserver/ ← YB-TServer 逻辑
│ │ └── common/ ← 公共组件(HLC 等)
│ └── postgres/ ← PostgreSQL Fork(YSQL)
│ └── src/
│ └── backend/
│ └── utils/
│ └── pggate/ ← PgGate 适配层
├── java/ ← Java 客户端
├── python/ ← Python 测试工具
└── docs/ ← 文档
常见误区
误区一:”YugabyteDB 同时支持 SQL 和 NoSQL,所以它是万能数据库” → 正确理解:YSQL 和 YCQL 共享存储引擎,但它们的适用场景不同。YSQL 适合需要复杂查询和事务的场景,YCQL 适合高写入吞吐、简单查询的场景。混用两种接口访问同一张表会带来一致性管理上的复杂性。
误区二:”HLC 不需要任何外部依赖,所以比 TSO/TrueTime 更好” → 正确理解:HLC 不需要专用硬件或中心化服务,但它依赖 NTP 时钟同步的精度。如果 NTP 配置不好(时钟偏差过大),可能导致读到过期数据。TiDB 的 TSO 虽然有单点风险,但时间戳分配是精确的——没有”偏差”问题。
误区三:”Fork PostgreSQL 比 CockroachDB 自研兼容层好” → 正确理解:各有优劣。Fork PG 的优势是兼容性更高(直接复用 PG 的 Parser、Planner),代价是需要持续跟踪 PG 上游版本更新——每次 PG 大版本升级都是一次大工程。CockroachDB 自研的方式兼容性稍低,但代码更可控。
读完本章你能做什么
- 解释 DocDB 的”文档”数据模型如何统一 SQL 和 NoSQL 两种接口
- 画出 YugabyteDB 的 Tablet 分布和 Raft 复制结构
- 比较 HLC 和 TSO/TrueTime 的优缺点
- 理解 YSQL 的 PgGate 适配层在做什么
- 说出 YugabyteDB 与 CockroachDB 的三个关键差异
导读目录:README.md 上一章:第 8 章 · TiDB 与 CockroachDB——从零打造分布式 下一章:第 10 章 · OpenTenBase 深度解读——从 GTM 到贡献方向