第 3 章:四大架构模式——从改装到重造
结论先行:6 个分布式数据库项目采用了 5 种不同的架构策略,本质上是在回答同一个问题:”如何把单机数据库变成分布式?”。从”给现有数据库装插件”到”从零打造新数据库”,侵入程度越深,能力越强,但代价也越大。理解这个光谱,是理解所有后续项目设计决策的关键。
一个汽车改装的故事
假设你有一辆普通家用轿车,现在需要它能跑越野路段。你有几种选择:
方案一:给轿车装一个车顶行李架和越野轮胎(扩展插件)。车的发动机、变速箱、底盘都不动,只在外面加东西。优点是改装成本低、不影响日常用车、随时可以拆掉恢复原样。缺点是越野能力有限——轿车底盘还是那么低,过不了深水区。
方案二:在轿车前面放一辆越野引导车(中间件代理)。引导车负责探路和导航,轿车只需要跟着走。优点是轿车完全不用改、引导车可以换、多辆轿车可以共用一辆引导车。缺点是速度受限于两车的协调,遇到真正恶劣的路况引导车也帮不上忙。
方案三:打开发动机盖,换一台更强的引擎,加装底盘护甲(内核改造)。改装很深入,越野能力大幅提升。但每次原厂推出新车型(数据库版本升级),你都得重新做一遍改装。
方案四:不改装了,直接买一辆专业越野车(原生分布式)。从设计之初就为越野而生。性能最强,但你需要重新学习驾驶方式,而且价格不菲。
这四种策略,对应了分布式数据库领域的四大架构模式(加上 YugabyteDB 的双协议变体,共 5 种)。
模式一:扩展插件模式——Citus 的极简哲学
核心思路
一句话:不改 PostgreSQL 的一行代码,通过 PG 原生的 Extension 机制,把分布式查询能力”安装”进 PG。
想象 Citus 就是手机上的一个 App。你的手机(PostgreSQL)出厂时并不支持”控制多台设备协同工作”,但装了这个 App 之后就可以了。关键是——手机的操作系统完全没变,你原来的所有应用和数据都不受影响。
工作原理
Citus 利用了 PostgreSQL 提供的 hook 机制。PostgreSQL 在查询处理的几个关键环节留了”钩子”——如果有 extension 注册了这些钩子,PostgreSQL 会在执行到对应环节时调用 extension 的代码。
用户发送 SQL
|
v
PostgreSQL 收到查询
|
v
解析 SQL(PostgreSQL 原生)
|
v
[hook] 查询规划 ← Citus 在这里介入!
| 把单机计划改成分布式计划
v
[hook] 查询执行 ← Citus 在这里介入!
| 把子查询发送到 worker 节点
v
返回结果给用户
具体来说,Citus 的入口文件 shared_library_init.c 在 PostgreSQL 启动时注册了多个 hook:
// Citus 注册 hook 的简化示意
void _PG_init(void) {
// 替换 PostgreSQL 的标准查询规划器
prev_planner_hook = planner_hook;
planner_hook = distributed_planner; // ← Citus 的分布式规划器
// 替换执行器的相关 hook
prev_ExecutorStart = ExecutorStart_hook;
ExecutorStart_hook = CitusExecutorStart;
}
当一条 SQL 进来时,Citus 的 distributed_planner 会判断:这条查询涉及的表是不是分布式表?如果是,就生成一个分布式执行计划,把子查询发送到各个 worker 节点执行;如果不是,就交还给 PostgreSQL 的标准规划器处理。
架构图
┌─────────────────────┐
│ Coordinator Node │
│ ┌───────────────┐ │
用户 SQL ──────────►│ │ PostgreSQL │ │
│ │ + Citus Ext │ │
│ └──────┬────────┘ │
└─────────┼───────────┘
分布式计划
┌─────────┼───────────┐
│ │ │
┌─────▼───┐ ┌──▼──────┐ ┌──▼──────┐
│ Worker 1 │ │ Worker 2│ │ Worker 3│
│ PG+Citus │ │ PG+Citus│ │ PG+Citus│
│ (分片1,4)│ │ (分片2,5)│ │ (分片3,6)│
└─────────┘ └─────────┘ └─────────┘
关键设计决策和取舍
决策:用 PG Extension 而不是 Fork
收益:
- PG 版本升级时,Citus 几乎不需要改代码——因为 hook 接口是稳定的
- 用户可以继续使用 PG 的全部生态(备份工具 pg_dump、监控工具 pg_stat、各种 FDW 等)
- 安装简单——
CREATE EXTENSION citus;一条命令搞定
代价:
- 只能在 PG 暴露的 hook 点做事,某些底层优化做不了(比如修改存储格式、修改锁机制)
- 分布式事务能力受限——依赖 2PC,不像原生分布式数据库那样有全局一致性保证
一个精彩的例子:Citus 的列存引擎也是通过 PG 的 Table Access Method 接口实现的——用户只需要 ALTER TABLE orders USING columnar; 就能把一张表从行存切换到列存,不需要迁移数据、不需要改应用代码。这是 PG 扩展机制的极致运用。
模式二:中间件代理模式——Vitess 的流量调度
核心思路
一句话:不改 MySQL 一行代码,在外面加一层智能代理,负责查询路由和分片管理。
想象一个快递分拣中心。它不生产货物(不改 MySQL),也不开运输车(不管存储),它只做一件事:接收快递员送来的包裹(SQL 查询),根据收件地址(分片键)分拣到正确的运输车上(MySQL 实例),然后把收集到的回执(查询结果)合并后交还给快递员。
工作原理
Vitess 的核心是 VTGate——一个伪装成 MySQL 服务器的代理。应用程序连接 VTGate 就像连接普通的 MySQL 一样,完全感知不到背后是分布式集群。
应用程序(以为在连 MySQL)
|
v
VTGate(伪装成 MySQL 服务端)
|
├── 解析 SQL(自研解析器 sqlparser/)
├── 根据 VSchema 确定分片路由
└── 生成执行计划(Gen4 Planner)
|
├──────────────────┬──────────────────┐
v v v
VTTablet + MySQL VTTablet + MySQL VTTablet + MySQL
(shard -80) (shard 80-c0) (shard c0-)
VTGate 的关键工作流程:
- 接收连接:
go/mysql/server.go实现了完整的 MySQL 服务端协议,包括认证握手 - 解析 SQL:
go/vt/sqlparser/sql.y定义了 MySQL 兼容的语法规则 - 确定路由:根据 VSchema 中定义的 Vindexes(分片函数),计算 SQL 应该发往哪些 shard
- 生成计划:Gen4 Planner 决定是路由到单个 shard 还是需要散射到多个 shard 再合并
- 执行和合并:通过 ScatterConn 并行发送到多个 shard,合并结果后返回
架构图
┌─────────────┐
│ Topology │
│ Service │
│ (etcd/ZK) │
└──────┬──────┘
│ 分片映射
┌──────────────────────────┼──────────────────────────┐
│ │ │
v v v
┌──────────┐ ┌──────────┐ ┌──────────┐
│ VTGate │ │ VTGate │ │ VTGate │
│ (无状态) │ │ (无状态) │ │ (无状态) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
v v v
┌──────────┐ ┌──────────┐ ┌──────────┐
│ VTTablet │ │ VTTablet │ │ VTTablet │
│ + MySQL │ │ + MySQL │ │ + MySQL │
│ shard-80 │ │ shard │ │ shard c0-│
│ │ │ 80-c0 │ │ │
└──────────┘ └──────────┘ └──────────┘
关键设计决策和取舍
决策:VSchema 声明式分片
传统的数据库中间件(如 MyCAT、ShardingSphere)通常需要在配置文件或代码中写分片规则。Vitess 独创了 VSchema 的概念——用 JSON 声明每张表的分片策略和索引关系:
{
"sharded": true,
"vindexes": {
"hash": { "type": "hash" }
},
"tables": {
"users": {
"column_vindexes": [
{ "column": "user_id", "name": "hash" }
]
},
"orders": {
"column_vindexes": [
{ "column": "user_id", "name": "hash" }
]
}
}
}
这样做的好处是分片策略与应用代码完全解耦——改分片规则不需要改一行应用代码,甚至可以在线做 resharding。
收益:
- MySQL 完全不用改,享受 MySQL 的全部生态(备份工具、监控工具、DBA 经验)
- VTGate 是无状态的,可以水平扩展——请求量大了就多部署几个 VTGate
- 在线 resharding 经过 YouTube 十年验证
代价:
- 分布式事务支持有限——跨 shard 事务需要应用层自行管理
- SQL 兼容性有天花板——再怎么做也受限于”中间件层能重写的 SQL 范围”
- 能力上限受限于 MySQL 单实例——Vitess 不能让单个查询在 MySQL 实例上跑得更快
模式三:内核改造模式——OpenTenBase 的深度手术
核心思路
一句话:直接打开 PostgreSQL 的源码,在里面植入分布式能力——全局事务管理、数据分片路由、远程查询执行。
回到汽车改装的类比:OpenTenBase 是直接打开发动机盖,换更强的引擎、加底盘护甲、改装悬挂系统。改装完成后,这辆车的越野能力远超方案一(加行李架)和方案二(用引导车),但每次原厂推出新车型时,你都得重新做一遍改装。
OpenTenBase 基于 Postgres-XL(一个更早的 PostgreSQL 分布式 fork),在此基础上做了 HTAP 增强。它的代码中大量使用 #ifdef __OPENTENBASE__ 来标记自己对 Postgres-XL 基线的修改,这让代码考古变得非常容易——你可以精确看到哪些代码是 OpenTenBase 自己加的。
工作原理
OpenTenBase 引入了三个关键组件,植入到 PostgreSQL 的内核中:
GTM(Global Transaction Manager):独立进程,负责分配全局事务 ID 和全局快照。所有分布式事务都必须向 GTM 申请事务 ID,这保证了全局一致性,但也让 GTM 成为潜在瓶颈。
Coordinator:接收用户的 SQL 请求,做查询规划和路由。Coordinator 本质上是一个增强版的 PostgreSQL,多了分布式查询计划生成的能力。
Datanode:实际存储数据和执行查询的节点。每个 Datanode 也是一个增强版的 PostgreSQL,能执行 Coordinator 下发的子查询。
用户发送 SQL
|
v
Coordinator(增强版 PG)
|
├── 解析 SQL(PG 原生解析器)
├── 向 GTM 申请事务 ID(全局一致性的关键)
├── 查询 Shard Map 确定数据在哪些 Datanode
├── 生成分布式执行计划(pgxcship 判断是否可下推)
└── 把子查询发送到 Datanode
|
├──────────────────┬──────────────────┐
v v v
Datanode 1 Datanode 2 Datanode 3
(增强版 PG) (增强版 PG) (增强版 PG)
行存+列存 行存+列存 行存+列存
| | |
└──────────────────┼──────────────────┘
v
GTM
(全局事务管理器)
事务 ID 分配 / 快照管理
架构图(详细版)
┌─────────────────────────┐
│ GTM │
│ ┌───────────────────┐ │
│ │ gtm_txn.c (4029行)│ │ 全局事务 ID
│ │ gtm_snap.c │ │ 全局快照
│ │ gtm_seq.c │ │ 全局序列
│ └───────────────────┘ │
│ ↕ 主备复制 │
│ GTM Standby │
└────────┬────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
GTM Proxy GTM Proxy GTM Proxy
(批量聚合) (批量聚合) (批量聚合)
│ │ │
v v v
┌──────────┐ ┌──────────┐ ┌──────────┐
│Coordinator│ │Coordinator│ │ (可选) │
│ 1 │ │ 2 │ │Coordinator│
│ (路由+ │ │ (路由+ │ │ 3 │
│ 规划) │ │ 规划) │ │ │
└────┬─────┘ └────┬─────┘ └──────────┘
│ │
┌────┴──────────────┴────┐
│ │ │
┌────▼────┐┌───▼────┐┌───▼────┐
│Datanode │ │Datanode ││Datanode │
│ 1 ││ 2 ││ 3 │
│行存+列存││行存+列存││行存+列存│
└─────────┘└────────┘└────────┘
关键设计决策和取舍
决策:中心化的 GTM vs 去中心化的共识
CockroachDB 和 TiDB 使用去中心化的 Raft 协议来保证一致性——没有单点。OpenTenBase 选择了中心化的 GTM——所有事务必须经过 GTM。
这个选择背后的逻辑是:OpenTenBase 基于 Postgres-XL,而 Postgres-XL 的设计目标是”在 PG 内核上做最小改动实现分布式”。GTM 是实现全局一致性最直接的方式——不需要改动 PG 的存储引擎或复制协议,只需要加一个外部协调器。
收益:
- 实现简单直接——GTM 就是一个全局的事务 ID 分配器
- 保持了完整的 PostgreSQL 内核兼容性——PG 的存储过程、触发器、自定义类型全都能用
- 行存 + 列存双引擎在同一个 Datanode 上,数据同步延迟为零
代价:
- GTM 是单点瓶颈——所有事务都要经过它,高并发下可能成为吞吐天花板
- 每次 PG 大版本升级,需要重新合并所有内核改动——工作量大、风险高
#ifdef __OPENTENBASE__散落在整个代码库中,维护成本随修改量递增
与 Citus 的对比
同样基于 PostgreSQL,OpenTenBase 和 Citus 代表了两种截然不同的策略:
| 维度 | Citus(扩展) | OpenTenBase(Fork) |
|---|---|---|
| PG 兼容性 | 通过 hook,不改 PG 代码 | Fork PG,深度修改 |
| PG 版本跟进 | 快(hook 接口稳定) | 慢(需要重新合并改动) |
| 分布式事务 | 2PC,能力有限 | GTM 全局协调,能力更强 |
| HTAP | 列存扩展(有限) | 行存+列存双引擎 |
| DDL 分布式 | 有限 | 更完整 |
| 代码入门难度 | 低(~1000 文件) | 中(~3000 文件) |
模式四:原生分布式模式——TiDB 和 CockroachDB
核心思路
一句话:不基于任何现有数据库,从零开始设计一个天生的分布式数据库。
这就是直接买一辆专业越野车。它从设计之初就为分布式而生——存储引擎、查询引擎、事务管理、复制协议全都是为分布式场景定制的。
但 TiDB 和 CockroachDB 虽然都是原生分布式,却走了两条不同的路。
TiDB:存算分离 + 双引擎 HTAP
TiDB 的设计哲学是存算分离——计算层(tidb-server)和存储层(TiKV + TiFlash)完全解耦。
想象一家大公司把”接待前台”和”仓库管理”彻底分开。前台只管理解客户需求(SQL 解析和优化),仓库只管存取货物(数据读写),中间靠物流系统(PD 调度器)协调。需要更多接待能力?加前台。需要更多存储空间?加仓库。两者可以独立扩展。
┌────────────────────────────────────────────┐
│ TiDB Server (Go) │
│ ┌──────────────────────────────────┐ │
用户 ──►│ │ SQL Parser → Cascades Optimizer │ │
│ │ → Executor → DistSQL Client │ │
│ └──────────────┬───────────────────┘ │
└───────────────────┼────────────────────────┘
│
PD (调度器,Go)
管理 Region 分布
负载均衡调度
│
┌─────────────┼─────────────┐
│ │ │
┌────▼────┐ ┌───▼────┐ ┌───▼────┐
│ TiKV │ │ TiKV │ │ TiKV │
│ (Rust) │ │ (Rust) │ │ (Rust) │
│ 行存 │ │ 行存 │ │ 行存 │
│ Raft │ │ Raft │ │ Raft │
└────┬────┘ └───┬────┘ └───┬────┘
│ Raft │ │
│ Learner │ Raft │ Raft
│ 异步复制 │ Learner │ Learner
v v v
┌─────────┐ ┌─────────┐ ┌─────────┐
│ TiFlash │ │ TiFlash │ │ TiFlash │
│ (C++) │ │ (C++) │ │ (C++) │
│ 列存 │ │ 列存 │ │ 列存 │
└─────────┘ └─────────┘ └─────────┘
TiDB 最精彩的设计是用 Raft Learner 打通 TP(事务处理)和 AP(分析处理):
- TiKV 之间通过 Raft 协议同步数据(正常的多数派投票)
- TiFlash 作为 Raft Learner 加入复制组——它只接收数据但不参与投票
- Learner 不参与投票意味着它不会影响写入延迟——写入只需要等 TiKV 的多数派确认
- 查询优化器可以自动决定每个操作是下推到 TiKV(点查快)还是 TiFlash(扫描聚合快)
CockroachDB:存算一体 + 强一致
CockroachDB 的设计哲学是存算一体——每个节点既能处理 SQL 又能存储数据,没有单独的存储层。
想象一个连锁便利店,每家店既能收银又能管库存。总部不需要专门的”存储中心”,任何一家店倒闭,客户去隔壁照常买东西。这种设计消除了单点依赖。
┌────────────────────────────────────┐
│ CockroachDB Node │
│ ┌──────────────────────────────┐ │
用户 ──►│ │ SQL Layer │ │
│ │ Parser → Optimizer → │ │
│ │ DistSQL Execution │ │
│ ├──────────────────────────────┤ │
│ │ KV Layer │ │
│ │ Transaction → Raft → │ │
│ │ Range Management │ │
│ ├──────────────────────────────┤ │
│ │ Storage Layer (Pebble) │ │
│ │ MVCC → LSM-Tree │ │
│ └──────────────────────────────┘ │
└──────────────────┬─────────────────┘
│ Raft 复制
┌──────────────────┼──────────────────┐
│ │ │
┌────▼──────┐ ┌────▼──────┐ ┌────▼──────┐
│ Node 2 │ │ Node 3 │ │ Node 4 │
│ SQL+KV │ │ SQL+KV │ │ SQL+KV │
│ +Storage │ │ +Storage │ │ +Storage │
└───────────┘ └───────────┘ └───────────┘
CockroachDB 最精彩的设计是 Parallel Commits——传统两阶段提交需要两轮网络往返(prepare → commit),CockroachDB 把 commit 决策与最后一批写操作合并在一轮中完成,并用 TLA+ 数学证明了正确性。
TiDB vs CockroachDB 的核心差异
| 维度 | TiDB | CockroachDB |
|---|---|---|
| 计算存储 | 分离(tidb-server / TiKV / TiFlash) | 一体(每个节点全功能) |
| 兼容协议 | MySQL | PostgreSQL |
| HTAP | 强(TiKV 行存 + TiFlash 列存) | 弱(只有行存) |
| 一致性 | Snapshot Isolation(默认) | Serializable(默认) |
| 编程语言 | Go + Rust + C++ | Go |
| 仓库结构 | 多仓库(tidb + tikv + pd) | 单仓库 |
| 存储引擎 | RocksDB(in TiKV) | Pebble(自研) |
| 形式化验证 | 无 | TLA+(ParallelCommits 等) |
| 社区 | 中文为主 | 英文为主 |
| Stars | ~41K | ~32K |
模式五:双协议兼容模式——YugabyteDB 的两面性
核心思路
一句话:一个存储引擎(DocDB),上面跑两种查询语言——YSQL(PostgreSQL 兼容)和 YCQL(Cassandra 兼容)。
想象一个翻译官同时会中文和英文。不管你用哪种语言问他问题,他内部的思考过程是一样的(DocDB),但他能用你的语言给你答复。这意味着一个团队里——需要复杂 SQL 查询的后端开发者和需要低延迟 KV 访问的实时系统开发者——可以用同一个数据库。
用户 A(需要 SQL) 用户 B(需要 NoSQL)
| |
v v
┌──────────┐ ┌──────────┐
│ YSQL │ │ YCQL │
│ (PG 兼容)│ │(Cassandra│
│ pggate/ │ │ 兼容) │
└────┬─────┘ └────┬─────┘
│ │
└───────────┬───────────┘
v
┌──────────────┐
│ DocDB │
│ (RocksDB + │
│ Raft) │
│ HLC 时钟 │
└──────────────┘
关键设计决策和取舍
决策:双协议并存
收益:
- 一个数据库替代 PostgreSQL + Cassandra,减少运维成本
- 对于需要混合 workload 的团队非常有吸引力
- 基于 Spanner 架构,强一致事务
代价:
- YSQL 和 YCQL 的事务语义不完全相同,用户需要理解差异
- YSQL 层是修改过的 PG fork,PG 版本跟进可能滞后
- 代码量庞大(C/C++ + Java,~10,000 文件),学习曲线陡峭
五种模式的全景对比
现在让我们把五种模式放在一起,看看它们在各个维度上的差异:
侵入程度光谱
侵入性:低 ◄────────────────────────────────────► 高
Citus Vitess OpenTenBase TiDB/CRDB
(PG扩展) (MySQL代理) (PG Fork) (从零造)
│ │ │ │
│ 不改一行 │ 不改一行 │ 深度改造 │ 完全自研
│ PG代码 │ MySQL代码 │ PG代码 │
│ │ │ │
能力:有限 ◄────────────────────────────────────► 强大
维护:简单 ◄────────────────────────────────────► 复杂
对比矩阵
| 维度 | Citus (扩展) | Vitess (代理) | OpenTenBase (Fork) | TiDB (原生) | CockroachDB (原生) | YugabyteDB (双协议) |
|---|---|---|---|---|---|---|
| 基于 | PostgreSQL | MySQL | Postgres-XL/PG | 从零 | 从零 | 从零 |
| 改动程度 | 零(hook) | 零(代理) | 深度(#ifdef) | 全新 | 全新 | 全新 |
| PG/MySQL 兼容 | 极高 | 高 | 极高 | 中(MySQL) | 中(PG) | 中(PG+CQL) |
| 分布式事务 | 2PC | 有限 | GTM 全局 | Raft+MVCC | Raft+MVCC | Raft+MVCC |
| HTAP | 列存扩展 | 无 | 行+列双引擎 | TiKV+TiFlash | 有限 | 有限 |
| 版本跟进 | 快 | 快 | 慢 | 不依赖 | 不依赖 | 不依赖 |
| 代码量 | ~1K 文件 | ~3.5K | ~3K | ~5K+ | ~15K+ | ~10K |
| 入门难度 | 低 | 中 | 中 | 中 | 高 | 高 |
选型决策树
你需要分布式数据库吗?
│
├── 只需要 MySQL 水平扩展,不想改应用代码
│ └── Vitess
│
├── 已经在用 PostgreSQL,想最小改动实现分布式
│ └── Citus
│
├── 需要 PG 兼容 + HTAP + 分布式事务
│ └── OpenTenBase
│
├── 需要 MySQL 兼容 + HTAP
│ └── TiDB
│
├── 需要最强一致性 + 全球多活
│ └── CockroachDB
│
└── 需要同时支持 SQL 和 NoSQL
└── YugabyteDB
常见误区
误区一:”原生分布式一定比扩展/中间件模式好” → 正确理解:每种模式有自己的最佳场景。如果你的数据量在几十 TB 以内、已经在用 PostgreSQL、团队不想学新技术,Citus 可能是最好的选择。”最好”取决于你的约束条件,不是技术的先进程度。
误区二:”Fork 模式已经过时了” → 正确理解:OpenTenBase 在腾讯内部(微信支付等)有大规模生产验证。Fork 模式的优势在于能做更深层的优化(如 GTM 全局事务),这是扩展模式做不到的。代价是版本跟进的维护成本。
误区三:”中间件模式能力太有限,不值得考虑” → 正确理解:Vitess 管理着 YouTube 的数万个 MySQL 节点,处理着全球最大视频平台的数据库负载。中间件模式的”限制”恰恰是它的”边界”——不碰内核意味着不引入新的 bug,不影响已有的 DBA 工具链。
读完本章你能做什么
- 用”汽车改装”的类比向朋友解释四种架构模式的区别
- 看到一个新的分布式数据库项目,能快速判断它属于哪种架构模式
- 解释 Citus(扩展)和 OpenTenBase(Fork)基于同一个 PostgreSQL 却走了完全不同路径的原因
- 解释 TiDB(存算分离)和 CockroachDB(存算一体)的核心设计差异
- 理解侵入程度与能力强度之间的 tradeoff
导读目录:README.md 上一章:第 2 章 · 阅读路线图与前置知识 下一章:第 4 章 · 共识与复制——让多台机器达成一致