第 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 进程内部意味着:
- 可以直接访问 PG 的内部数据结构(如 Plan 树、Catalog)
- 函数调用开销(纳秒级)vs Vitess 的网络调用开销(毫秒级)
- 但也意味着 Citus 的 bug 可能导致 PG 进程崩溃
Vitess 在 MySQL 外部意味着:
- MySQL 完全隔离——Vitess 的 bug 不会影响 MySQL
- 可以管理多个异构的 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
- 你已经在用 PostgreSQL,不想换数据库
- 需要分析能力(列存 + 分布式聚合)
- 数据量在几十 TB 级别
- 团队对 PG 生态熟悉
- 可以接受 2PC 级别的分布式事务
什么时候选 Vitess
- 你已经在用 MySQL,不想换数据库
- 负载以 OLTP 为主(大量简单查询)
- 需要成熟的在线 resharding 能力
- 运维团队有 Kubernetes 经验
- 对分布式事务要求不高
什么时候两个都不够用
当你需要以下能力时,可能需要考虑 TiDB、CockroachDB 或 OpenTenBase 等重量级方案:
- 强一致分布式事务(Serializable 隔离级别)
- 完整的 HTAP(事务 + 分析同时处理)
- 全球多活部署
- 自动分片和自动均衡(不想手动管理分片)
常见误区
误区一:”Citus 和 Vitess 能力太弱,不适合生产环境” → 正确理解:Citus 是 Azure Database for PostgreSQL 的核心技术,Vitess 管理着 YouTube 的数万个 MySQL 实例。”轻量级”不等于”能力弱”——它们在自己的设计目标范围内非常强大。
误区二:”扩展模式和代理模式可以互换” → 正确理解:两种模式有根本性的差异。扩展模式运行在数据库进程内部,可以直接操作内部数据结构;代理模式运行在数据库外部,只能通过 SQL 协议与数据库交互。这决定了它们各自的能力边界。
误区三:”VReplication 和 pg_dump + pg_restore 是同一个层次的东西” → 正确理解:VReplication 是在线、增量、持续的数据复制——系统在迁移过程中完全可用。pg_dump/restore 是离线、全量的备份恢复——需要停机或至少只读窗口。两者解决的问题和复杂度完全不同。
读完本章你能做什么
- 解释 Citus 如何通过 PG hook 机制实现分布式查询
- 描述 Adaptive Executor 的 Slow Start 连接策略
- 解释 Vitess VTGate 如何伪装成 MySQL 服务端
- 描述 VReplication 的在线 resharding 流程
- 在 Citus 和 Vitess 之间做出合理的技术选型
- 判断什么场景下轻量级方案不够用,需要上重量级方案
导读目录:README.md 上一章:第 6 章 · 分布式查询优化——比单机复杂 10 倍的执行计划 下一章:第 8 章 · TiDB 与 CockroachDB——从零打造分布式