犀牛鸟 2026 研究笔记 estelledc.github.io

第 9 章:YugabyteDB——一个引擎两种语言

结论先行:YugabyteDB 用一个统一的分布式存储引擎 DocDB,同时支持 PostgreSQL 和 Cassandra 两种 SQL 接口。它的核心创新在于把 Raft + LSM-Tree + MVCC + HLC 四个技术融合成一个紧密耦合的存储层——所有分布式复杂性都收敛在 DocDB 里,上层的 SQL 兼容层可以相对”轻薄”。


一个仓库,两个门面

想象一家超市后面有一个巨大的自动化仓库(DocDB)。这个仓库管理所有商品的进出、库存盘点、分仓调度。超市前面有两个入口:

两扇门通向同一个仓库。无论从哪扇门进来,商品的管理方式完全一样——分仓(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 自研的方式兼容性稍低,但代码更可控。


读完本章你能做什么


导读目录:README.md 上一章:第 8 章 · TiDB 与 CockroachDB——从零打造分布式 下一章:第 10 章 · OpenTenBase 深度解读——从 GTM 到贡献方向