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

第 7 章:轻量级方案——Citus 与 Vitess——不动内核的智慧

结论先行:Citus 和 Vitess 代表了”不重新造轮子”的两条路径——一个给 PostgreSQL 装插件,一个给 MySQL 加代理。它们的共同智慧是:利用现有数据库的成熟生态,用最小的侵入实现分布式能力。理解”做扩展”和”做代理”各自的边界,能帮你判断什么时候用轻量级方案就够了,什么时候需要上重量级方案。


两个手机用户的故事

小明和小红都觉得手机相机不够好。

小明的做法:在手机上安装了一个专业相机 App(Citus 的方式)。这个 App 利用手机硬件的全部能力,通过算法优化实现了接近专业相机的效果。手机系统升级后 App 自动适配,不影响手机原有功能。但 App 的能力受限于手机硬件——再怎么优化,手机的传感器尺寸就那么大。

小红的做法:买了一个外接镜头夹(Vitess 的方式)。镜头夹套在手机外面,不改动手机任何东西。拍照时镜头帮忙,不拍时可以取下来。缺点是两者之间的连接不够紧密,某些场景(如夜拍)外接镜头帮不上忙。

两人都没有换手机(都没有重写数据库内核),但用了不同的策略来增强能力。


Citus 深度剖析

一句话回顾

Citus 是 PostgreSQL 的原生 Extension,通过 PG 的 hook 机制拦截查询处理流程,不 fork 不魔改 PG 代码,实现分布式查询和水平扩展。被 Microsoft 收购后集成到 Azure Database for PostgreSQL。

从 PG Hook 说起

PostgreSQL 在设计时就预留了”扩展点”——在查询处理的关键环节提供 hook 函数指针。Extension 可以注册自己的函数来替换或增强默认行为。

Citus 利用的主要 hook:

PostgreSQL 查询处理流程与 Citus hook 位置:

客户端发送 SQL
    │
    ▼
[1] 解析(Parser)── PostgreSQL 原生,Citus 不介入
    │
    ▼
[2] 分析(Analyzer)── PostgreSQL 原生,Citus 不介入
    │
    ▼
[3] 规划(Planner)── ★ Citus 替换 planner_hook
    │                    distributed_planner.c 介入
    │                    判断是否涉及分布式表
    │                    生成分布式执行计划
    ▼
[4] 执行(Executor)── ★ Citus 替换 ExecutorStart_hook
    │                    adaptive_executor.c 介入
    │                    把子任务发送到 worker 节点
    │                    收集并合并结果
    ▼
返回结果给客户端

这意味着 Citus 完全复用了 PG 的 SQL 解析器和语义分析器——不需要自己写 SQL 解析,天然支持所有 PG 语法。

Adaptive Executor:Citus 最复杂的代码

executor/adaptive_executor.c(5359 行)是 Citus 中最复杂的单个文件。它实现了一个精巧的任务调度和连接管理系统。

它的设计灵感来自 TCP 的拥塞控制:

Slow Start 连接策略:

初始状态:每个 worker 节点开 1 个连接

  round 1: 1 个连接/worker → 如果处理正常...
  round 2: 2 个连接/worker → 如果仍然正常...
  round 3: 4 个连接/worker → 如果开始排队等待...
  round 4: 4 个连接/worker → 稳定(不再增加)

类似 TCP 慢启动:先用少量连接试探,
确认 worker 能处理后再增加,避免一次性打满。

为什么需要这种设计?因为每个连接都消耗 worker 节点的资源(内存、文件描述符)。如果一条查询涉及 100 个分片,一开始就对每个分片打开连接意味着 100 个并发连接——可能压垮 worker。

Adaptive Executor 用 WaitEventSet(PG 的事件驱动 IO 接口)多路复用所有连接,实现了单线程管理多个并发连接的高效调度。

列存引擎:Table AM 的典范

Citus 的列存引擎 (src/backend/columnar/) 是 PG Table Access Method 接口的典范运用。

-- 创建行存表(PG 默认)
CREATE TABLE events (
    id SERIAL,
    event_time TIMESTAMP,
    data JSONB
);

-- 一行命令切换到列存!
ALTER TABLE events SET ACCESS METHOD columnar;

-- 切换回行存也是一行
ALTER TABLE events SET ACCESS METHOD heap;

列存 vs 行存的区别用一个例子解释:

行存(heap):每行数据连续存储
  行1: [id=1, time=10:00, data={...}]
  行2: [id=2, time=10:01, data={...}]
  行3: [id=3, time=10:02, data={...}]
  
  适合:读写单行(SELECT * FROM events WHERE id = 1)
  
列存(columnar):每列数据连续存储
  id列:   [1, 2, 3, 4, 5, ...]
  time列: [10:00, 10:01, 10:02, ...]
  data列: [{...}, {...}, {...}, ...]
  
  适合:扫描单列(SELECT AVG(id) FROM events)
       → 只需要读 id 列,跳过其他列
       → 压缩率高(同类数据连续存储)

这个设计让 Citus 在不改 PG 内核的情况下就实现了 HTAP 的雏形——行存表处理事务,列存表处理分析。

Citus 的局限性

尽管 Citus 的设计极其优雅,它仍然有明确的能力边界:

分布式事务能力有限:依赖 2PC(两阶段提交),不像 CockroachDB 有全局一致性保证。跨分片的复杂事务可能遇到问题。

受限于 PG hook 接口:PG 的 hook 点是有限的。某些深层优化(如修改 WAL 格式、修改缓冲区管理策略)在 Extension 层做不到。

协调节点是潜在瓶颈:所有查询都经过协调节点。虽然可以配置多个协调节点,但跨协调节点的元数据同步增加了复杂度。


Vitess 深度剖析

一句话回顾

Vitess 是 CNCF 毕业项目,在 MySQL 外面加一层智能代理实现水平分片和连接池管理。YouTube 十余年大规模生产验证。

VTGate 的 MySQL 伪装

Vitess 最巧妙的设计之一是 VTGate 伪装成一个 MySQL 服务端。应用程序连 VTGate 就像连普通 MySQL 一样——使用标准的 MySQL 客户端驱动,不需要特殊的 SDK。

应用程序使用标准 MySQL 连接:
  mysql -h vtgate-host -P 3306 -u user -p

VTGate 内部实现了完整的 MySQL 协议栈:
  go/mysql/server.go  — 服务端协议(接受连接、认证)
  go/mysql/client.go  — 客户端协议(连接后端 MySQL)
  go/mysql/query.go   — 查询编解码

这个设计的好处是零侵入——不需要修改应用代码、不需要换数据库驱动、不需要培训开发者学新 API。对于已经在线运行的系统,这是极其重要的。

Gen4 Planner:Vitess 的第四代查询规划器

Vitess 的查询规划经历了四代迭代,当前的 Gen4 Planner 位于 go/vt/vtgate/planbuilder/

Gen4 Planner 的架构:

SQL 字符串
    │
    ▼
sqlparser/ ── SQL 解析 → AST
    │
    ▼
semantics/ ── 语义分析
  analyzer.go  ── 类型推导
  binder.go    ── 名称绑定
  scoper.go    ── 作用域分析
    │
    ▼
operators/ ── 逻辑算子树
  route_planning.go     ── 路由规划
  aggregation_pushing.go ── 聚合下推
  hash_join.go          ── Hash JOIN
    │
    ▼
执行计划 ── engine/ 目录下的物理算子
  route.go      ── 路由到具体 shard
  join.go       ── 跨 shard JOIN
  aggregate.go  ── 跨 shard 聚合

Gen4 Planner 的核心决策是路由判断——一条查询应该发往一个 shard(单分片查询)还是所有 shard(散射查询)。

-- 单分片路由(快)
SELECT * FROM users WHERE user_id = 42;
-- VTGate: hash(42) → shard "-80" → 只发一个 shard

-- 散射路由(慢)
SELECT COUNT(*) FROM users WHERE city = '北京';
-- VTGate: city 不是分片键 → 发给所有 shard
-- 每个 shard 返回局部 COUNT → VTGate 合并求和

VReplication:在线数据迁移的核心

VReplication 是 Vitess 的”瑞士军刀”——它不仅用于 resharding,还用于 schema 迁移、跨数据中心复制、实时物化视图等场景。

VReplication 的工作原理:

VReplication 的核心机制:

1. 初始全量复制(CopyPhase)
   源 shard ──全量数据──► 目标 shard
   
2. 增量追赶(RunningPhase)
   源 shard ──binlog 事件──► 目标 shard
   (通过 MySQL binlog 持续同步增量变更)
   
3. 追赶完成后,执行切换(SwitchTraffic)
   VTGate 路由规则原子更新:
     读流量:源 → 目标
     写流量:源 → 目标(短暂只读窗口)

这个设计让 resharding 的停机时间从”小时级”(传统方案)降到了”秒级”。

Vitess 的局限性

分布式事务支持有限:跨 shard 事务需要应用层自行管理。Vitess 提供了 vtgate_transaction_mode 配置项,但完整的分布式 ACID 不是 Vitess 的设计目标。

SQL 兼容性天花板:VTGate 需要在中间件层重新实现 SQL 语义——这永远是一个”追赶”的过程。某些复杂 SQL(嵌套子查询、窗口函数跨 shard 等)可能不支持或行为与原生 MySQL 不同。

运维复杂度:Vitess 的组件较多(VTGate + VTTablet + 拓扑服务 + VTOrc),部署和运维的学习曲线较陡。


Citus vs Vitess:扩展 vs 代理

现在让我们把这两个项目放在一起对比。它们代表了”不改内核”的两种哲学:

架构哲学对比

Citus(扩展模式):
  ┌────────────────────────┐
  │     PostgreSQL 进程    │
  │  ┌──────────────────┐  │
  │  │  Citus Extension │  │  ← 运行在 PG 进程内部
  │  │  (同一个进程)    │  │
  │  └──────────────────┘  │
  └────────────────────────┘

Vitess(代理模式):
  ┌──────────┐     ┌──────────┐
  │  VTGate  │────►│  MySQL   │  ← 独立进程,通过网络连接
  │ (独立进程)│     │ (独立进程)│
  └──────────┘     └──────────┘

Citus 在 PG 进程内部意味着:

Vitess 在 MySQL 外部意味着:

详细对比表

维度 Citus Vitess
基于 PostgreSQL MySQL
集成方式 Extension(进程内) Proxy(独立进程)
SQL 解析 复用 PG 解析器 自研解析器(sqlparser/)
对底层数据库的影响 零(hook 机制) 零(代理层)
列存 内建(Table AM)
连接管理 Adaptive Executor ScatterConn
分布式事务 2PC 有限
在线 resharding 有限 VReplication(成熟)
代码量 ~1000 文件(C) ~3500 文件(Go)
商业支持 Microsoft (Azure) Google 背景 + CNCF
Star 数 ~12.5K ~21K
最佳场景 PG 生态 + 分析负载 MySQL 生态 + OLTP

什么时候选 Citus

什么时候选 Vitess

什么时候两个都不够用

当你需要以下能力时,可能需要考虑 TiDB、CockroachDB 或 OpenTenBase 等重量级方案:

常见误区

误区一:”Citus 和 Vitess 能力太弱,不适合生产环境” → 正确理解:Citus 是 Azure Database for PostgreSQL 的核心技术,Vitess 管理着 YouTube 的数万个 MySQL 实例。”轻量级”不等于”能力弱”——它们在自己的设计目标范围内非常强大。

误区二:”扩展模式和代理模式可以互换” → 正确理解:两种模式有根本性的差异。扩展模式运行在数据库进程内部,可以直接操作内部数据结构;代理模式运行在数据库外部,只能通过 SQL 协议与数据库交互。这决定了它们各自的能力边界。

误区三:”VReplication 和 pg_dump + pg_restore 是同一个层次的东西” → 正确理解:VReplication 是在线、增量、持续的数据复制——系统在迁移过程中完全可用。pg_dump/restore 是离线、全量的备份恢复——需要停机或至少只读窗口。两者解决的问题和复杂度完全不同。

读完本章你能做什么


导读目录:README.md 上一章:第 6 章 · 分布式查询优化——比单机复杂 10 倍的执行计划 下一章:第 8 章 · TiDB 与 CockroachDB——从零打造分布式