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

第 8 章:重量级方案——TiDB 与 CockroachDB——从零打造分布式

结论先行:TiDB 和 CockroachDB 都是”从零开始”设计的原生分布式数据库,但它们走了完全不同的路——TiDB 选择了存算分离 + HTAP 双引擎,CockroachDB 选择了存算一体 + 强一致性。理解这两条路径的设计哲学和 tradeoff,能帮你理解分布式数据库设计中最深层的决策。


两家餐厅的故事

TiDB 像一家大型连锁餐厅集团。前厅(tidb-server/SQL 层)负责点菜和客户服务,后厨(TiKV)负责烹饪,甜品工坊(TiFlash)负责做蛋糕和甜点。三个部门各自独立运营,通过物流系统(PD 调度器)协调。需要更多服务员?加前厅。需要更快的出菜速度?加后厨。需要分析数据做经营报表?甜品工坊的团队同时也能帮忙(TiFlash 列存分析)。

CockroachDB 像一个”全能厨师”连锁店。每家店只有一个厨师,但这个厨师从接待客人、点菜、烹饪、上菜到结账全部搞定。任何一家店关门,客人去隔壁照常吃饭。不需要中央厨房(没有单点),不需要物流协调(没有独立的调度器),每家店完全独立。


TiDB 深度剖析

架构哲学:存算分离

TiDB 的核心设计决策是存算分离——计算和存储在不同的进程(甚至不同的机器)上运行,可以独立扩展。

TiDB 集群全景:

  ┌─────────────────────────────────────────────────┐
  │                  TiDB Server 集群               │
  │  tidb-server1  tidb-server2  tidb-server3      │
  │  (Go, SQL层)   (Go, SQL层)   (Go, SQL层)       │
  │  ┌──────────┐  ┌──────────┐  ┌──────────┐      │
  │  │ Parser   │  │ Parser   │  │ Parser   │      │
  │  │ Cascades │  │ Cascades │  │ Cascades │      │
  │  │ Executor │  │ Executor │  │ Executor │      │
  │  └────┬─────┘  └────┬─────┘  └────┬─────┘      │
  └───────┼──────────────┼──────────────┼───────────┘
          │              │              │
  ┌───────┼──────────────┼──────────────┼───────────┐
  │       └──────────────┼──────────────┘           │
  │                      │                          │
  │            PD (Placement Driver)                │
  │          Region 调度 / 负载均衡 / TSO           │
  │                      │                          │
  │       ┌──────────────┼──────────────┐           │
  │       │              │              │           │
  │  ┌────▼────┐   ┌────▼────┐   ┌────▼────┐      │
  │  │ TiKV 1  │   │ TiKV 2  │   │ TiKV 3  │      │
  │  │ (Rust)  │   │ (Rust)  │   │ (Rust)  │      │
  │  │ 行存    │   │ 行存    │   │ 行存    │      │
  │  │ RocksDB │   │ RocksDB │   │ RocksDB │      │
  │  └────┬────┘   └────┬────┘   └────┬────┘      │
  │       │Raft         │Raft         │Raft        │
  │       │Learner      │Learner      │Learner     │
  │  ┌────▼────┐   ┌────▼────┐   ┌────▼────┐      │
  │  │TiFlash 1│   │TiFlash 2│   │TiFlash 3│      │
  │  │ (C++)   │   │ (C++)   │   │ (C++)   │      │
  │  │ 列存    │   │ 列存    │   │ 列存    │      │
  │  │ Delta   │   │ Delta   │   │ Delta   │      │
  │  └─────────┘   └─────────┘   └─────────┘      │
  └────────────────────────────────────────────────┘

Raft Learner HTAP:TiDB 最精彩的设计

第 4 章已经介绍了 Raft Learner 的基本原理。这里我们深入看 TiDB 如何用它实现 HTAP。

传统做 HTAP 有两种方式,都有明显的缺点:

传统方式一:ETL(每晚批量导出)
  白天: OLTP 数据库处理事务
  深夜: 把数据导出(ETL)到分析数据库
  问题: 分析数据永远是"昨天的"

传统方式二:同一个引擎跑两种负载
  一台机器同时跑事务和分析
  问题: 分析查询(大量扫描)会严重影响事务处理(低延迟要求)

TiDB 的方式:Raft Learner + 双存储引擎
  TiKV 处理事务(行存,低延迟随机读写)
  TiFlash 处理分析(列存,高吞吐顺序扫描)
  两者通过 Raft Learner 实时同步(秒级延迟)
  互不干扰(不同进程,不同机器)

查询优化器怎么决定用 TiKV 还是 TiFlash?这就是 Cascades 框架发挥作用的地方——优化器在生成执行计划时,会同时评估”下推到 TiKV”和”下推到 TiFlash”的成本,选成本更低的方案。

-- 查询示例
SELECT city, COUNT(*), AVG(amount)
FROM orders
WHERE order_date > '2026-01-01'
GROUP BY city;

-- 优化器的决策过程:
-- 方案A:下推到 TiKV(行存)
--   需要扫描所有行,逐行检查 order_date,逐行聚合
--   估算成本:高(行存不擅长大范围扫描和聚合)

-- 方案B:下推到 TiFlash(列存)
--   只读 order_date 和 city 和 amount 三列
--   列存压缩后数据量小,顺序扫描快
--   估算成本:低 ← 选择这个方案

Cascades 优化器的实现细节

TiDB 的 Cascades 实现在 pkg/planner/cascades/cascades.go

Cascades 优化器的三个核心组件:

1. Memo(记忆宫殿)
   存储所有探索过的等价执行计划
   避免重复计算
   
2. Rule Engine(规则引擎)
   转换规则:把一种执行方式转换成另一种等价方式
   例如:InnerJoin(A, B) → InnerJoin(B, A)(交换律)
   
3. Scheduler(调度器)
   决定下一步探索哪个 Group、应用哪条规则
   目标:以最少的探索找到成本最低的计划

规则引擎中有几十条优化规则,关键的包括:

谓词下推(Predicate Pushdown):
  Filter(Join(A, B), A.x > 10)
  → Join(Filter(A, A.x > 10), B)
  效果:减少 JOIN 的输入行数

JOIN 重排序(Join Reorder):
  Join(Join(A, B), C)
  → Join(A, Join(B, C))
  效果:可能找到更优的 JOIN 顺序

聚合消除(Aggregation Elimination):
  Aggregate(唯一键分组)
  → 直接返回(分组键唯一意味着不需要真正聚合)
  效果:减少不必要的计算

多仓库生态

TiDB 的一个显著特点是多仓库架构——不同组件在不同的 Git 仓库中,用不同的编程语言开发:

pingcap/tidb    → Go     → SQL 层(解析、优化、执行)
tikv/tikv       → Rust   → KV 存储层(Raft、RocksDB)
tikv/pd         → Go     → 调度器(Region 管理、负载均衡)
pingcap/tiflash → C++    → 列存引擎

优点:
  - 各组件可以独立开发、测试、部署
  - 用最适合的语言(Rust 写存储、Go 写 SQL 层)
  
缺点:
  - 跨仓库调试复杂
  - 版本兼容性管理困难
  - 新贡献者需要了解多个仓库

CockroachDB 深度剖析

架构哲学:存算一体

CockroachDB 的核心设计决策是存算一体——每个节点都是一个完整的数据库,既能处理 SQL 又能存储数据。没有单独的存储层、没有单独的调度器、没有单点。

CockroachDB 集群(每个节点结构完全相同):

  ┌─────────────────────────────────────────┐
  │             CockroachDB Node            │
  │  ┌───────────────────────────────────┐  │
  │  │  SQL Layer                        │  │
  │  │  ├── Parser (PG 兼容语法)         │  │
  │  │  ├── Optimizer (基于代价)          │  │
  │  │  └── DistSQL Execution            │  │
  │  ├───────────────────────────────────┤  │
  │  │  Transaction Layer                │  │
  │  │  ├── MVCC (多版本并发控制)         │  │
  │  │  ├── Latch Manager (本地锁)       │  │
  │  │  └── TxnCoordSender (事务协调)    │  │
  │  ├───────────────────────────────────┤  │
  │  │  Distribution Layer               │  │
  │  │  ├── DistSender (Range 路由)      │  │
  │  │  ├── Raft (共识复制)              │  │
  │  │  └── Range 管理 (分裂/合并/迁移)  │  │
  │  ├───────────────────────────────────┤  │
  │  │  Storage Layer (Pebble)           │  │
  │  │  └── LSM-Tree KV 存储             │  │
  │  └───────────────────────────────────┘  │
  └─────────────────────────────────────────┘
  
  所有节点结构完全相同 → 对等架构
  没有 master/slave → 没有单点

Parallel Commits:减少一轮 RTT

CockroachDB 对分布式事务的一个重大优化是 Parallel Commits——把传统 2PC 的两轮网络往返(prepare + commit)压缩到一轮。

传统两阶段提交:

传统 2PC(两轮 RTT):

  协调者                     参与者
    │                         │
    │── Phase 1: Prepare ──►  │
    │◄── 我准备好了 ─────────  │
    │                         │    第一轮 RTT(~1ms)
    │── Phase 2: Commit! ──►  │
    │◄── 提交完成 ──────────   │    第二轮 RTT(~1ms)
    │                         │
    总延迟 ≈ 2ms(2轮 RTT)

CockroachDB 的 Parallel Commits:

Parallel Commits(一轮 RTT):

  协调者                     参与者
    │                         │
    │── 写数据 + 标记 STAGING ──►  │
    │                              │   一轮 RTT(~1ms)
    │◄── 确认 ────────────────────  │
    │                              │
    事务状态从 STAGING → COMMITTED
    (异步完成,不需要额外 RTT)
    
    总延迟 ≈ 1ms(1轮 RTT)

关键创新:事务的最终写操作和 commit 决策并行发出。协调者把事务标记为 STAGING 状态(”如果所有写都成功了,这个事务就算提交”),然后并行地发出所有写操作。当所有写操作的确认回来后,事务自动从 STAGING 变成 COMMITTED——不需要额外的 commit 轮次。

CockroachDB 用 TLA+ 形式化验证了这个协议的正确性(docs/tla-plus/ParallelCommits/ParallelCommits.tla)。这不是简单的测试——TLA+ 会穷举所有可能的状态组合(包括网络分区、节点崩溃等极端场景),数学证明协议在所有情况下都是正确的。

Pebble:自研存储引擎

CockroachDB 最初使用 RocksDB(Facebook 开源的 LSM-Tree 引擎)作为底层存储,后来自研了 Pebble 来替代。

为什么要自研?

RocksDB 的问题(对 CockroachDB 来说):
  1. C++ 代码,Go 通过 CGo 调用 → 跨语言调用开销
  2. 丰富的功能(Column Family、Compaction Filter 等)
     CockroachDB 用不到,但仍然要承担复杂度
  3. Go 的内存管理和 C++ 的内存管理不协调

Pebble 的优势:
  1. 纯 Go 实现 → 消除 CGo 开销
  2. 针对 CockroachDB 的使用模式优化
  3. 更好地集成 Go 的内存管理和 GC

Pebble 是一个完整的 LSM-Tree 实现,性能已经超过了 RocksDB(在 CockroachDB 的使用场景下)。

Serializable 默认隔离级别

CockroachDB 做了一个大胆的设计决策:默认使用 Serializable 隔离级别。这是 SQL 标准定义的最严格隔离级别——事务的执行结果等价于某种串行顺序。

大多数数据库(包括 PostgreSQL 和 MySQL)默认使用较弱的隔离级别(如 Read Committed 或 Repeatable Read),因为 Serializable 的性能开销更大。CockroachDB 认为:正确性比性能更重要——绝大多数应用 bug 的根源是开发者不理解弱隔离级别的边界条件。

Serializable 保证的含义:

假设两个事务同时执行:
  T1: 读取账户A余额(100),写入账户B余额(B+100)
  T2: 读取账户B余额(0),写入账户A余额(A-100)

Serializable 保证结果等价于某种串行执行:
  要么 T1 先执行再 T2:A=0, B=100  ✓ 正确
  要么 T2 先执行再 T1:A=0, B=100  ✓ 正确

弱隔离级别可能产生的结果:
  T1 和 T2 同时读到旧值:A=0, B=100  ... 还是正确?
  不一定!某些交叉执行可能导致 A=0, B=0(钱消失了)

代价是:当 CockroachDB 检测到两个事务可能违反 Serializable 时,它会自动中止并重试其中一个事务。应用需要做好重试逻辑。


TiDB vs CockroachDB:深层对比

存算分离 vs 存算一体

维度 TiDB(存算分离) CockroachDB(存算一体)
扩展方式 计算和存储独立扩展 整体扩展
弹性 高(可以只加计算节点) 中(加节点=加计算+存储)
网络依赖 高(计算和存储之间有网络) 低(本地访问)
单查询延迟 较高(跨网络访问存储) 较低(本地存储访问)
故障域 更细粒度(计算层故障不影响存储) 整节点故障
运维复杂度 高(多种组件) 低(所有节点相同)

HTAP 能力对比

TiDB 的 HTAP:
  ┌─────────┐    Raft Learner    ┌─────────┐
  │  TiKV   │ ────────────────► │ TiFlash │
  │ (行存)  │    秒级同步       │ (列存)  │
  │ OLTP    │                   │ OLAP    │
  └─────────┘                   └─────────┘
  两个独立引擎,物理隔离
  
CockroachDB 的"HTAP":
  ┌──────────────────┐
  │    Pebble        │
  │   (行存 only)    │
  │ OLTP ✓  OLAP △  │
  └──────────────────┘
  一个引擎,OLAP 能力有限

TiDB 在 HTAP 上有明显优势——独立的列存引擎 TiFlash 让分析查询不影响事务处理。CockroachDB 不支持列存,复杂分析查询性能有限。

一致性模型对比

TiDB 默认: Snapshot Isolation (SI)
  - 每个事务看到一个一致的数据快照
  - 允许 Write Skew 异常(两个事务基于同一快照做不同的写入)
  - 性能较好

CockroachDB 默认: Serializable
  - 事务结果等价于串行执行
  - 不允许任何异常
  - 可能需要事务重试

工程文化对比

TiDB:
  - 多仓库(tidb/tikv/pd/tiflash)
  - Go + Rust + C++ 多语言
  - 中文社区为主
  - PingCAP 主导

CockroachDB:
  - 单仓库(cockroachdb/cockroach)
  - 纯 Go(Pebble 也是 Go)
  - 英文社区
  - 极致的测试文化(TLA+、Data-driven Test、Chaos Test)
  - doc.go 文档详细程度惊人

学习价值对比

想理解分布式数据库的不同方面,两个项目各有优势:

想学...                       推荐看...
─────────────────────────────────────────
HTAP 架构设计                 TiDB(Raft Learner + TiFlash)
查询优化器框架                TiDB(完整 Cascades 实现)
分布式事务协议                CockroachDB(Parallel Commits + TLA+)
共识协议工程化                CockroachDB(深度定制 Raft)
存储引擎设计                  CockroachDB(Pebble 自研)
形式化验证实践                CockroachDB(TLA+ 文件夹)
多语言微服务架构              TiDB(Go + Rust 协作)

常见误区

误区一:”原生分布式比 Citus/Vitess 好” → 正确理解:原生分布式能力更强,但复杂度也更高。部署一套 TiDB 集群(tidb-server + TiKV + PD + TiFlash)比部署 Citus(PG + Extension)复杂得多。如果你的需求 Citus 能满足,用 Citus 可能是更好的选择。

误区二:”TiDB 兼容 MySQL,迁移过去就行” → 正确理解:TiDB 的 MySQL 兼容性虽高但不是 100%。存储过程、触发器支持有限,某些 MySQL 特有的行为可能不同。迁移前需要做兼容性评估。

误区三:”CockroachDB 的 Serializable 默认会导致性能很差” → 正确理解:对于大多数 OLTP 负载,Serializable 的性能开销在 5-10% 左右——这是正确性换来的小代价。真正的性能影响在于事务冲突率——如果冲突率高,重试开销确实会很大。

读完本章你能做什么


导读目录:README.md 上一章:第 7 章 · Citus 与 Vitess——不动内核的智慧 下一章:第 9 章 · YugabyteDB——一个引擎两种语言