第 10 章:OpenTenBase 深度解读——从 GTM 到贡献方向
结论先行:OpenTenBase 是腾讯开源的分布式 HTAP 数据库,基于 Postgres-XL 架构在 PostgreSQL 内核上”动手术”。它的核心组件 GTM(全局事务管理器)是理解整个系统的钥匙——所有分布式一致性保证都通过这个中心化组件实现。对犀牛鸟 2026 参赛者来说,OpenTenBase 的代码量适中(约 3,000 个 C 文件)、社区小(贡献可见度高)、有明确的优化方向,是做出有影响力贡献的理想入口。
改装汽车的故事
我们在第 3 章和第 7 章分别介绍了”扩展 PostgreSQL”的不同方式。现在把它们放在一起看:
- Citus 像给车装了一套高级导航系统(Extension)——车本身完全不变,导航系统通过蓝牙和车载电脑协同工作。
- Vitess 像在车队前面放了一个调度中心(Proxy)——每辆车(MySQL 实例)独立运行,调度中心决定任务分配。
- OpenTenBase 像是打开引擎盖,直接更换了发动机、加固了底盘、加装了四驱系统(内核改造)——你得到的是一辆真正的越野车,而不是加了配件的轿车。
这带来了最深层的能力:分布式 DDL、全局事务、全局序列。代价是每次 PostgreSQL 版本升级都意味着需要重新做一遍”手术”。
整体架构
OpenTenBase 集群架构:
┌─────────────────────────────────────────────────────┐
│ 客户端应用 │
│ (标准 PostgreSQL 客户端) │
└─────────────┬───────────────────────┬───────────────┘
│ │
┌─────────────▼───────────┐ ┌────────▼────────────────┐
│ Coordinator 1 │ │ Coordinator 2 │
│ (PG 进程 + 分布式扩展) │ │ (PG 进程 + 分布式扩展) │
│ - 解析 SQL │ │ - 解析 SQL │
│ - 生成分布式计划 │ │ - 生成分布式计划 │
│ - 汇总结果 │ │ - 汇总结果 │
└─────┬───────────────┬───┘ └──┬──────────────────┬───┘
│ │ │ │
┌─────▼─────┐ ┌──────▼───────▼──┐ ┌──────────────▼───┐
│ Datanode 1│ │ Datanode 2 │ │ Datanode 3 │
│ (PG 进程) │ │ (PG 进程) │ │ (PG 进程) │
│ 存储分片1 │ │ 存储分片2 │ │ 存储分片3 │
│ 行存+列存 │ │ 行存+列存 │ │ 行存+列存 │
└───────────┘ └─────────────────┘ └──────────────────┘
│ │ │
└───────────────┼───────────────────┘
│
┌─────────────────────▼───────────────────────────┐
│ GTM │
│ (全局事务管理器,独立进程) │
│ - 分配全局事务 ID (GXID) │
│ - 管理全局快照 (Global Snapshot) │
│ - 管理全局序列 (Global Sequence) │
│ - 主备模式 (GTM Standby) │
│ │
│ GTM Proxy (可选,部署在 Coordinator 旁边) │
│ - 批量聚合事务请求 │
│ - 减少 Coordinator → GTM 的网络往返 │
└──────────────────────────────────────────────────┘
四类角色的分工:
- Coordinator:面对客户端,是一个”增强版的 PostgreSQL 进程”。它接收 SQL、解析、生成分布式执行计划、把子查询分发到 Datanode、汇总结果返回客户端。客户端看到的就是一个普通的 PostgreSQL。
- Datanode:也是一个”增强版的 PostgreSQL 进程”,但它负责存储实际数据。每个 Datanode 存储表数据的一个分片。支持行存和列存两种引擎。
- GTM:全局事务管理器,一个独立的进程(不是 PostgreSQL 进程)。所有需要全局一致性的操作都要经过它——事务 ID 分配、快照获取、序列值获取。
- GTM Proxy:可选组件。部署在 Coordinator 旁边,把多个 Coordinator 的事务请求批量聚合后发给 GTM,减少网络开销。
GTM 深度剖析
GTM 是 OpenTenBase 最核心也是最独特的组件。理解 GTM 就理解了 OpenTenBase 的一致性模型。
为什么需要 GTM?
在单机 PostgreSQL 中,所有事务共享同一个进程的内存空间——事务 ID 分配(nextXid)、活跃事务列表(ProcArray)都在本地内存中,访问速度极快。但在分布式环境中,Coordinator 和 Datanode 运行在不同的机器上,它们需要一个”共同的参考点”来保证一致性。GTM 就是这个参考点。
单机 PostgreSQL 的事务管理:
PostgreSQL 进程
┌────────────────────────────┐
│ 共享内存 │
│ ├── nextXid: 1000 │ ← 下一个事务 ID
│ ├── ProcArray: [998,999] │ ← 当前活跃事务
│ └── Sequences: {...} │ ← 序列值
└────────────────────────────┘
所有本地访问,零网络开销
分布式 OpenTenBase 的事务管理:
Coordinator1 GTM Coordinator2
│ │ │
│── 申请事务ID ──►│ │
│◄── GXID=1000 ──│ │
│ │◄── 申请事务ID ─────│
│ │── GXID=1001 ─────►│
│── 获取快照 ────►│ │
│◄── [1000,1001] ─│ │
│ │ │
每次事务开始都要访问 GTM → 网络开销
GTM 的三大职责
职责一:全局事务 ID 分配
GTM 维护一个全局递增的事务 ID 计数器。每个事务开始时,Coordinator 向 GTM 申请一个唯一的 GXID(Global Transaction ID)。
源码位置:src/gtm/main/gtm_txn.c(4029 行)
核心数据结构(简化):
GTM_Transactions {
ControlXid: uint32 // 下一个可分配的事务 ID
gt_txn_array: [...] // 所有活跃事务的数组
gt_txn_count: int // 当前活跃事务数量
}
事务 ID 分配流程:
1. Coordinator 发送 MSG_TXN_BEGIN 到 GTM
2. GTM 锁住 gt_txn_array
3. ControlXid++ 生成新的 GXID
4. 在 gt_txn_array 中记录这个新事务
5. 解锁,返回 GXID 给 Coordinator
为什么 ControlXid 是瓶颈?
- 所有 Coordinator 的所有事务都要串行化获取 ID
- 高并发场景下,锁竞争严重
- 网络 RTT 叠加锁等待 → 延迟上升
职责二:全局快照管理
快照是 MVCC(多版本并发控制)的基础——一个事务需要知道”在我开始的那一刻,哪些事务已经提交、哪些还在进行中”。在单机 PG 中这是读本地内存;在 OpenTenBase 中,需要从 GTM 获取全局快照。
源码位置:src/gtm/main/gtm_snap.c
全局快照的内容:
GlobalSnapshot {
xmin: uint32 // 最小活跃事务 ID(比它小的都已提交)
xmax: uint32 // 最大活跃事务 ID + 1(比它大的都还没开始)
xcnt: int // 活跃事务数量
xip[]: uint32[] // 所有活跃事务 ID 列表
}
使用场景:
事务 T 读取一行数据,发现这行由事务 X 写入。
T 查看自己的快照:
- 如果 X < xmin → X 已提交,T 能看到这行
- 如果 X >= xmax → X 在 T 之后开始,T 看不到
- 如果 X 在 xip[] 中 → X 在 T 开始时还活跃,T 看不到
- 否则 → X 已提交,T 能看到
职责三:全局序列管理
PostgreSQL 的 SERIAL / SEQUENCE 在分布式环境中需要全局唯一。GTM 管理所有序列的当前值和增长步长。
源码位置:src/gtm/main/gtm_seq.c
每个序列的状态:
GTM_SeqInfo {
gs_key: name // 序列名称
gs_value: int64 // 当前值
gs_increment: int // 步长
gs_min: int64 // 最小值
gs_max: int64 // 最大值
}
优化:批量预取
Coordinator 不会每用一个序列值就请求 GTM 一次。
而是一次预取一批(比如 100 个),用完再请求。
→ 减少网络往返,代价是 Coordinator 重启时可能浪费一些序列值
GTM Proxy:缓解瓶颈的实战手段
源码位置:src/gtm/proxy/
没有 GTM Proxy 时:
Coordinator1 ──► GTM (每个请求一次网络往返)
Coordinator1 ──► GTM
Coordinator1 ──► GTM
Coordinator2 ──► GTM
Coordinator2 ──► GTM
有 GTM Proxy 时:
Coordinator1 ──► GTM Proxy ─┐
Coordinator1 ──► GTM Proxy ├──► GTM (批量发送)
Coordinator1 ──► GTM Proxy ─┘
Coordinator2 ──► GTM Proxy ─┐
Coordinator2 ──► GTM Proxy ├──► GTM (批量发送)
Coordinator2 ──► GTM Proxy ─┘
效果:
N 个独立请求 → 1 个批量请求
网络往返从 N 次减到 1 次
GTM 的锁竞争也从 N 次减到 1 次(批量分配事务 ID)
数据分片机制
Locator:分片路由
源码位置:src/backend/pgxc/locator/locator.c(2775 行)
Locator 结构体(简化):
Locator {
locatorType: char // 'H'=Hash, 'R'=Range, 'M'=Modulo, 'N'=RoundRobin
locatefunc: func_ptr // 分片函数指针(可插拔设计)
nodeCount: int // Datanode 节点数
nodeList: Oid[] // 节点列表
hashfunc: func_ptr // 哈希函数
}
分片策略:
Hash 分片:hash(分片键) % 节点数 → 目标 Datanode
Range 分片:按分片键范围划分
Modulo 分片:分片键值 % 节点数
RoundRobin:按行轮询分配(无分片键时使用)
建表时指定分片方式:
-- Hash 分片(最常用)
CREATE TABLE orders (
order_id BIGINT,
user_id INT,
amount DECIMAL
) DISTRIBUTE BY HASH(user_id);
-- 复制表(每个 Datanode 都存一份完整数据)
CREATE TABLE config (
key TEXT,
value TEXT
) DISTRIBUTE BY REPLICATION;
复制表(Replicated Table)是 OpenTenBase 处理小维度表的方式——把整张表复制到所有 Datanode,这样 JOIN 时不需要跨节点传输数据。类似 Citus 的 Reference Table。
远程查询执行
源码位置:src/backend/pgxc/pool/execRemote.c(12835 行,最大单文件)
查询执行流程:
1. Coordinator 收到 SQL
2. Parser 解析 SQL(标准 PG Parser)
3. pgxcship.c 判断:能否整条下推到单个 Datanode?
- 如果能 → 快路径:直接发送原始 SQL 到目标 Datanode
- 如果不能 → 走分布式执行计划
4. Planner 生成分布式计划
5. execRemote.c 把子计划发送到各 Datanode
6. 各 Datanode 执行并返回结果
7. Coordinator 汇总结果返回客户端
pgxcship.c 的可下推判断是性能的关键——如果一条查询能被完整下推到一个 Datanode,就避免了跨节点数据传输,性能接近单机 PostgreSQL。
可下推的典型查询:
SELECT * FROM orders WHERE user_id = 123;
→ user_id 是分片键,直接路由到一个 Datanode
不可下推的查询(需要在 Coordinator 汇总):
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id;
→ 需要从所有 Datanode 收集数据后在 Coordinator 聚合
SELECT o.*, u.name
FROM orders o JOIN users u ON o.user_id = u.id;
→ 如果 orders 和 users 的分片键都是 user_id,可以下推
→ 如果分片键不同,需要 Coordinator 做跨节点 JOIN
HTAP 双引擎
OpenTenBase 在每个 Datanode 上同时支持行存和列存:
Datanode 的双引擎:
┌─────────────────────────────┐
│ Datanode │
│ ┌───────────┐ ┌──────────┐│
│ │ 行存 │ │ 列存 ││
│ │ (Heap) │ │ (Column) ││
│ │ │ │ ││
│ │ OLTP 优先 │ │ OLAP 优先││
│ └───────────┘ └──────────┘│
│ 同一进程 │
└─────────────────────────────┘
对比 TiDB 的 HTAP:
TiDB:
TiKV (行存, 独立进程/机器) ←Raft Learner→ TiFlash (列存, 独立进程/机器)
物理隔离:TP 和 AP 完全不影响
OpenTenBase:
行存 + 列存 在同一个 Datanode 进程中
逻辑隔离:共享 CPU 和内存,资源竞争可能存在
这是一个重要的 trade-off:TiDB 的物理隔离更彻底(TP 绝对不受 AP 影响),但架构更复杂(需要维护 TiKV + TiFlash 两套集群)。OpenTenBase 的方式更简单(同一进程),但在高负载下可能出现资源竞争。
代码考古:#ifdef __OPENTENBASE__
OpenTenBase 的代码中大量使用条件编译标记来标注自己的改动:
/* 标准 PostgreSQL 代码 */
void standard_function() {
// 原始逻辑...
#ifdef __OPENTENBASE__
/* OpenTenBase 增强:加入分布式事务支持 */
if (IsDistributedTransaction()) {
GetGlobalTransactionId();
// 分布式逻辑...
}
#endif
// 原始逻辑继续...
}
这些标记让你可以精确地区分:哪些代码是 PostgreSQL 原始的、哪些是 Postgres-XL 的、哪些是 OpenTenBase 自己加的。对于想参与贡献的开发者来说,这相当于一份”改动地图”——搜索 __OPENTENBASE__ 就能找到所有 OpenTenBase 的增量改动。
犀牛鸟 2026 贡献方向
OpenTenBase 的 Star 数(约 2K)远低于 TiDB(41K)和 CockroachDB(32K),但这恰恰意味着更大的贡献空间和更高的可见度。
方向一:GTM 性能优化
GTM 是已知瓶颈。当前的事务 ID 分配路径使用全局锁,高并发下成为吞吐量天花板。
可能的优化思路:
1. 批量分配(Range-based allocation)
当前:每次分配 1 个 GXID
优化:一次分配一个范围(比如 1000-1099),Coordinator 本地消耗
效果:GTM 请求量降低 100 倍
参考:TiDB 的 PD 就是这样分配 TSO 的
2. 多 GTM 分片
当前:单个 GTM
优化:多个 GTM 各负责一部分事务 ID 范围
挑战:全局快照需要聚合所有 GTM 的信息
3. 减少快照获取频率
当前:每个事务开始时都获取全局快照
优化:对只读事务使用本地缓存的快照(有一定延迟)
适用:对时效性要求不高的查询
这个方向的入口文件是 src/gtm/main/gtm_txn.c(4029 行)。
方向二:查询优化器增强
当前的 pgxcship.c 可下推判断比较保守——很多理论上可以下推的查询被判定为不可下推,导致数据需要传到 Coordinator 汇总,浪费网络带宽和延迟。
可能的优化思路:
1. 更精细的 JOIN 下推
当前:只在分片键完全匹配时下推
优化:识别 co-located JOIN(两表同键分片),即使 JOIN 键不是分片键
也可以在某些条件下下推
2. 聚合下推
当前:GROUP BY 通常需要在 Coordinator 完成
优化:两阶段聚合——先在 Datanode 做局部聚合,再在 Coordinator 做全局聚合
参考:Citus 的五层规划器就有这个能力
3. 统计信息感知
当前:下推决策不考虑数据分布
优化:利用各 Datanode 的统计信息做更优的计划选择
入口文件是 src/backend/optimizer/util/pgxcship.c。
方向三:列存引擎完善
OpenTenBase 的列存引擎相比 Citus 的 columnar/ 实现还有完善空间——压缩算法、谓词下推、向量化执行等方面都有优化机会。
方向四:向量搜索增强
contrib/pgvector/ 的集成还处于早期阶段。随着 AI 应用的爆发,向量搜索能力对数据库越来越重要。这个方向时机好、技术门槛适中。
选择建议
贡献方向选择指南:
难度
↑
GTM 多分片 │ ★★★★★
│
查询优化器 │ ★★★★☆
│
列存引擎 │ ★★★☆☆
│
GTM 批量分配 │ ★★★☆☆
│
向量搜索增强 │ ★★☆☆☆
│
└──────────────────► 影响力
低 高
建议:
C 基础扎实 → GTM 性能优化(影响力最大)
对优化器感兴趣 → 查询下推增强(先读 Citus planner/README.md)
想快速出成果 → 向量搜索增强(门槛最低)
与其他 PG 系方案的对比
| 维度 | OpenTenBase | Citus | YugabyteDB (YSQL) |
|---|---|---|---|
| PG 兼容方式 | Fork 内核 | Extension | Fork 前端 |
| 分布式事务 | GTM 中心化 | 2PC + 协调节点 | Raft + HLC |
| 全局一致性保证 | GTM 全局快照 | 有限(依赖 2PC) | HLC 时间戳 |
| HTAP | 行+列双引擎 | 列存扩展 | 无专用列存 |
| PG 版本跟进 | 较慢(需重做手术) | 快(Extension 自动跟) | 较慢(需 merge 上游) |
| 代码规模 | ~3,000 文件 (C) | ~1,000 文件 (C) | ~10,000 文件 (C/C++) |
| 社区大小 | 小(~2K Star) | 中(~12.5K Star) | 中(~10.5K Star) |
| 适合贡献 | 门槛较低,可见度高 | 门槛中等 | 门槛较高 |
OpenTenBase 本地源码导航
OpenTenBase/OpenTenBase (C)
├── src/
│ ├── gtm/ ← GTM 全局事务管理器
│ │ ├── main/
│ │ │ ├── gtm_txn.c # 事务ID分配和管理(4029行)★ 入口
│ │ │ ├── gtm_snap.c # 全局快照管理
│ │ │ ├── gtm_seq.c # 全局序列管理
│ │ │ ├── gtm_xlog.c # GTM 的 WAL 日志
│ │ │ └── replication.c # GTM 主备复制
│ │ └── proxy/ # GTM Proxy 批量聚合
│ ├── backend/
│ │ ├── pgxc/ ← 分布式扩展层
│ │ │ ├── locator/
│ │ │ │ └── locator.c # 分片路由(2775行)
│ │ │ ├── pool/
│ │ │ │ └── execRemote.c # 远程查询执行(12835行)★ 最大文件
│ │ │ └── plan/
│ │ │ └── planner.c # 分布式查询计划
│ │ └── optimizer/
│ │ └── util/
│ │ └── pgxcship.c # 可下推判断 ★ 优化入口
│ └── ...
├── contrib/
│ ├── opentenbase_ai/ # AI 扩展
│ └── pgvector/ # 向量搜索扩展
└── doc/ # 文档
常见误区
误区一:”GTM 是单点故障,OpenTenBase 不可靠”
→ 正确理解:GTM 支持主备模式(replication.c 实现了 GTM Standby),主 GTM 宕机后备 GTM 可以接管。GTM 是性能瓶颈而非可靠性问题。而且在腾讯内部(微信支付等场景),OpenTenBase 有大规模生产验证。
误区二:”Fork PG 内核太重了,不如 Extension 模式” → 正确理解:Fork 确实维护成本高,但它能做到 Extension 做不到的事——全局事务 ID、全局快照、分布式 DDL。这些需要修改 PG 内核的事务子系统,Extension API 无法触及。选择哪种方式取决于你需要多深的分布式能力。
误区三:”代码少就意味着简单”
→ 正确理解:OpenTenBase 约 3,000 个 C 文件听起来比 CockroachDB(15,000+ Go 文件)少很多,但 C 语言的信息密度远高于 Go——单行 C 代码通常比单行 Go 代码做更多事情。execRemote.c 一个文件就有 12,835 行,理解它需要对 PostgreSQL 内核有相当的了解。
读完本章你能做什么
- 画出 OpenTenBase 的四角色架构图(Coordinator / Datanode / GTM / GTM Proxy)
- 解释 GTM 的三大职责及其性能瓶颈
- 用
#ifdef __OPENTENBASE__定位 OpenTenBase 的增量改动 - 评估四个贡献方向(GTM 优化 / 查询下推 / 列存 / 向量搜索)的难度和影响力
- 对比 OpenTenBase 与 Citus、YugabyteDB 在 PG 兼容方式上的差异
导读目录:README.md 上一章:第 9 章 · YugabyteDB——一个引擎两种语言 下一章:第 11 章 · 跨项目全景对比