标签 Undo引擎 下的文章

这两年数据库圈有点像3年前的云原生圈:"分布式"、"新一代内核"、"重构存储引擎"这些词突然又密集起来了。

前几天刷群,看到有人转了 OpenTeleDB 的开源消息,说是"基于 PostgreSQL 的新一代内核"。说实话,我第一反应是:又一个魔改 PG?

但看到里面提到一个点:原位更新 + Undo 引擎(XStore),我还是没忍住下了源码。 因为这恰好戳中我这些年被 PG 折磨得最狠的痛点:

表膨胀、autovacuum 抽风、性能像心电图一样忽高忽低。

所以这次我没看 PPT,也没看宣传稿,直接跑到机器上拆了半天,想看看它究竟动了 PG 的哪根"老筋"。

一、先说结论:XStore 不是快,而是"稳"

image.png

我装的是 OpenTeleDB 的 17.6 内核版。 创建方式很直观:

SELECT relname, amname
FROM pg_class c
JOIN pg_am a ON c.relam = a.oid
WHERE relname = 'test_xstore';

image.png

这一步其实就已经很有意思了------它不是 fork 了一套新引擎,而是作为插件挂进去的。 这个思路我很认可:

  • 不绑死 PG 版本
  • 能跟着大版本升级
  • 出问题可以随时回退

像 Citus、openHalo 这些"成功插件化路线"的项目,本质都是这个思路。

image.png

二、打开数据目录,我第一次意识到:它真不是换皮

$PGDATA 下面,多了一个非常显眼的目录:

drwx------ 2 postgres postgres  4096 Nov  3 20:15 undo

这就是 XStore 的核心: 它不是靠多版本链来维护 MVCC,而是靠 Undo 日志回滚。

这点和 Oracle、MySQL InnoDB 的逻辑更像。

也正是它敢说"原位更新"的底气来源。

三、插入测试:它不快,但很"诚实"

我用同样的参数,在同一台机器上跑了两组:

INSERT INTO test_xstore (name, value)
SELECT md5(random()::text), (random()*1000)::int
FROM generate_series(1,10000000);
INSERT INTO test_heap (name, value)
SELECT md5(random()::text), (random()*1000)::int
FROM generate_series(1,10000000);

image.png

结果是:

image.png

写慢了将近一倍。这点我反而觉得真实:因为 XStore 在写数据页的同时,还要写一份 Undo。物理写入翻倍,吞吐下降是必然的。如果一个系统告诉你"原位更新 + Undo 还更快",那我反而会不太信。

四、创新实验:模拟1千万数据的存储膨胀对比

我设计了一项创新实验:在 1000 万条级别的大数据量下,评估 XStore 与 Heap 表在高频更新下的空间膨胀、索引稳定性以及查询性能表现。该实验主要有两个创新点:

大规模数据模拟

  1. 使用 generate_series(1,10000000) 生成 1000 万条数据,保证数据量级对存储膨胀影响明显。
  2. 初始数据包括 idnamevalueupdated_at 四列,与前期实验一致,但数据量增加十倍,以模拟真实大规模 OLTP 系统负载。

多维度空间分析

  1. 不仅监控表总大小,还分别统计索引占用和 TOAST 表空间。
  2. 每轮更新后,通过 pg_relation_sizepg_total_relation_sizepg_indexes_size 获取精细化指标。
  3. 引入 可视化趋势分析,绘制表空间增长曲线,以直观展示 XStore 与 Heap 的差异。

image.png

4.1 实验设计

表结构

CREATE TABLE xstore_large (
    id SERIAL PRIMARY KEY,
    name TEXT,
    value INT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) USING XSTORE;

CREATE TABLE heap_large (
    id SERIAL PRIMARY KEY,
    name TEXT,
    value INT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

image.png

初始化 1000 万条数据

INSERT INTO xstore_large (name, value)
SELECT 'name_' || g, g
FROM generate_series(1, 10000000) AS g;

INSERT INTO heap_large (name, value)
SELECT 'name_' || g, g
FROM generate_series(1, 10000000) AS g;

image.png

先对现在存入1000w数据的空间监控与记录一下如下。

image.png

SELECT
    pg_size_pretty(pg_total_relation_size('xstore_large')) AS xstore_total,
    pg_size_pretty(pg_indexes_size('xstore_large')) AS xstore_index,
    pg_size_pretty(pg_total_relation_size('heap_large')) AS heap_total,
    pg_size_pretty(pg_indexes_size('heap_large')) AS heap_index;
 xstore_total | xstore_index | heap_total | heap_index
--------------+--------------+------------+------------
 985 MB       | 388 MB       | 789 MB     | 214 MB

多轮全表更新

  • 连续 5 轮更新,每轮更新 valueupdated_at,模拟写入密集场景:
UPDATE xstore_large
SET value = value + 1, updated_at = CURRENT_TIMESTAMP;

UPDATE heap_large
SET value = value + 1, updated_at = CURRENT_TIMESTAMP;

image.png

空间监控与记录

SELECT
    pg_size_pretty(pg_total_relation_size('xstore_large')) AS xstore_total,
    pg_size_pretty(pg_indexes_size('xstore_large')) AS xstore_index,
    pg_size_pretty(pg_total_relation_size('heap_large')) AS heap_total,
    pg_size_pretty(pg_indexes_size('heap_large')) AS heap_index;

第一轮:

image.png

 xstore_total | xstore_index | heap_total | heap_index
--------------+--------------+------------+------------
 985 MB       | 388 MB       | 1578 MB    | 428 MB

第五轮:

image.png

 xstore_total | xstore_index | heap_total | heap_index
--------------+--------------+------------+------------
 985 MB       | 388 MB       | 1628 MB    | 428 MB

4.2 千万数据更新膨胀可视化

image.png

image.png

五、实验结论

这组 1000 万级数据 + 多轮全表更新的实验,其实把 PG 传统 Heap 表的"老问题"放大得非常清楚。

最核心的对比结果只有一句话:

XStore 的空间是线性的、可预测的;Heap 表的空间是失控的、不可预测的。

具体来看:

  1. 表空间膨胀

a. Heap 表在第一次更新后,表体空间直接翻倍,从 789MB 飙到 1578MB。

b. 之后每一轮更新,虽然增长幅度趋缓,但空间再也回不到初始状态。

c. XStore 从头到尾不变: 985MB → 985MB → 985MB

  1. 索引体积稳定性

a. Heap 表索引从 214MB 膨胀到 428MB,且在后续更新中保持"高位横盘"。

b. XStore 的索引尺寸始终维持在 388MB 左右,没有明显漂移。

  1. 更新行为本质差异

a. Heap:每一次 UPDATE,本质都是 DELETE + INSERT → 老版本残留 → 表膨胀 → 索引碎片 → autovacuum 压力。

b. XStore:真正的原位更新 → 历史版本进 Undo → 主表物理页不变 → 无膨胀。

  1. 长期可运维性

a. 在 Heap 表上,如果你不 VACUUM,它一定会慢; 如果你 VACUUM,系统一定会抖。

b. 在 XStore 上,这两件事都不再是必选项。

这意味着什么?

它不是让你飞起来,而是让你不再塌方

六、我的心得

说实话,这几年我已经对"新一代数据库内核"这类说法有点免疫了。大多数项目,要么是在 PG 上糊一层分布式壳; 要么就是换个名字,重新卖一遍 MVCC。而 XStore 给我的感觉不一样。它没有试图掩盖代价。写入更慢, IO 更多,架构更复杂。

但它正面承认了一个事实:

PostgreSQL 的 MVCC,在高频更新场景下已经接近物理极限。

这不是参数调优能解决的事,也不是加机器能扛住的事,而是存储模型本身的问题。这些年我见过太多系统:白天 QPS 很稳,半夜 autovacuum 开始清垃圾,延迟突然拉长,业务报警,DBA 开始手工 VACUUM / REINDEX / CLUSTER,第二天继续循环。

这不是运维水平的问题,而是模型在和现实硬扛。XStore 让我第一次意识到:原来 PG 也可以选择不走这条老路。它没有追求"更快",而是选择了一个更难、但更稳的方向:

  • 用 Undo 换空间可控
  • 用写放大换性能平滑
  • 用工程复杂度换系统长期可预期性

如果你是写多、更新密集型 OLTP 系统,如果你被表膨胀、索引碎片、autovacuum 抽风折磨过,那你会和我一样---不一定立刻用它,但你会开始认真看它。这大概就是我这次拆源码、跑实验,最大的收获。