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

第 10 章:OpenTenBase 深度解读——从 GTM 到贡献方向

结论先行:OpenTenBase 是腾讯开源的分布式 HTAP 数据库,基于 Postgres-XL 架构在 PostgreSQL 内核上”动手术”。它的核心组件 GTM(全局事务管理器)是理解整个系统的钥匙——所有分布式一致性保证都通过这个中心化组件实现。对犀牛鸟 2026 参赛者来说,OpenTenBase 的代码量适中(约 3,000 个 C 文件)、社区小(贡献可见度高)、有明确的优化方向,是做出有影响力贡献的理想入口。


改装汽车的故事

我们在第 3 章和第 7 章分别介绍了”扩展 PostgreSQL”的不同方式。现在把它们放在一起看:

这带来了最深层的能力:分布式 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 的网络往返             │
  └──────────────────────────────────────────────────┘

四类角色的分工:


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 内核有相当的了解。


读完本章你能做什么


导读目录:README.md 上一章:第 9 章 · YugabyteDB——一个引擎两种语言 下一章:第 11 章 · 跨项目全景对比