32 位 XID 的“狗皮膏药”被撕掉了:金仓 V9 的 64 位事务号实测与底层拆解

五年前的一句吐槽,今天被国产数据库接住了

这篇文章有两层目的:一是给刚接触 PG 的人讲清楚"XID 到底是什么、为什么它会成为一个问题";二是给 PG 内核开发者提供一份"金仓改了什么、怎么改的、还缺什么"的对照参考。


一、PG 的 XID,为什么被称为"狗皮膏药"

PG 用 32 位无符号整数来存事务号,TransactionId 本质上就是 uint32。这意味着一个集群最多同时存在 2³² ≈ 43 亿个事务号。数字听着不少,但在高写入业务里,几天就能耗光------典型的电商库,一天跑出 5 到 10 亿事务并不稀奇。

XID 耗光会怎样?PG 的 MVCC 依赖 t_xmin < current_xid 来判断可见性。XID 用完,新事务就没法分配,事务号必须循环复用。问题随之而来:怎么让"老 XID"和"新 XID"在 32 位空间里不打架?

PG 的解法是 frozen xid :把 XID 空间看成一个圆,frozen xid 是圆上的一个点,顺时针是"已分配(过去)"的事务号,逆时针是"可分配(未来)"的事务号。每消耗 20 亿事务号,frozen xid 必须移动一次,移动的方式是 autovacuum 扫整张表,把所有 t_xmin < frozen_xid 的行头信息里的 xmin 标记成 frozen(一个特殊 bit 位)。被标记的行永远可见,不再参与可见性判断。

这套机制带来的运维代价很实在:

  • 冻结风暴:全表扫一遍,IO 暴增,WAL 暴涨,主从延迟。
  • 长事务场景下必须停库 freeze :autovacuum 会跳过正在被事务占用的 page,长事务挂一天,freeze 就推迟一天。极端情况下 pg_xact 表里剩余 slot 少于 100 万,PG 强制进入单用户模式,拒绝所有新事务,只为了把全库的 freeze 跑完。
  • 运维心智负担:部署前要算清楚"我一天多少事务、freeze 阈值配多少、SSD 能扛多高的 freeze IO",稍微没算对就是生产事故。

我在 2021 年 8 月 24 日的博客《DB 吐槽大会第 2 期 - PG 32 位 xid》里明确写过:"事务号 xid 是 uint32 类型,最多只能存储 40 亿个事务号,事务号必须循环使用",并建议"未来产品迭代如何修复这个坑:64 位 xid,例如 zedstore、zheap、postgrespro 等引擎"。

五年过去了。PostgreSQL 社区至今没有合并 64 位 XID 方案------引用 2026 年 PG 邮件列表的共识:xid8 数据类型虽然存在,但内部事务号 TransactionId 仍是 uint32。社区的理由很务实:32 位 + freeze 的组合能跑,改成 64 位需要动 tuple header、CLOG、vacuum、复制协议、子事务、prepared transaction、hash join 依赖 xid 的地方,影响面太大。

但 2026 年 8 月 28 日金仓发布的 V009R002C016 版本明确写了:"支持 64 位事务 ID(XID),避免因 32 位事务 ID 回收不及时或长事务阻塞导致的业务中断问题。"

这是国产数据库首次把"64 位事务号"做成了生产可用特性,意义不亚于 PG 15 的 logical replication 跨大版本兼容。


二、64 位 XID 改造,难在哪里

理解金仓做了什么,先得理解为什么 PG 一直没做。64 位 XID 不是改个类型那么轻松,它牵动的面极广:

1. Tuple header 多 8 字节。

PG 的 HeapTupleHeaderData 现在长这样(简化):

c 复制代码
typedef struct HeapTupleHeaderData {
    t_choice    t_choice;       // 哪种 t_xmin
    TransactionId t_xmin;       // 32 位
    TransactionId t_xmax;       // 32 位
    CommandId   t_cid;
    TransactionId t_xvac;       // 32 位 (vacuum 锁存,PG 17 已移除)
    TransactionId t_multi;       // multixact id,32 位
    ItemPointerData t_ctid;
    // ...
};

改成 64 位后,每个 tuple 多 16 字节(假设 xmin + xmax + multi 都改成 64 位)。一张 10 亿行表,光 header 就多 16 GB,这是 PG 社区第一个犹豫的点。

2. CLOG 不能再用 SLRU 了。

PG 的 CLOG(Commit Log)用 8KB 页存事务提交状态,每页 8192 / 32 × 2 = 512 个事务号,SLRU 两级缓存。64 位 XID 下,CLOG 必须换存储结构------一种思路是 ring buffer,另一种是改成基于 segment 的 mmap 文件。这块改造 PG 社区讨论过多次但没人动手,因为牵动 vacuum 的可见性判断。

3. Vacuum 的"半个圆"语义失效。

32 位 XID 下,vacuum 的 relfrozenxid < oldestXid - 2000000000 判断本质是"事务号在 freeze 点之前的都该被 freeze"。64 位 XID 下根本不需要这个机制------你一辈子写不到 2⁶³ 个事务。所以 freeze 的核心逻辑要被重新设计:从"对抗循环"变成"无限寿命 + 普通 cleanup"。

4. Sub-transaction 和 prepared transaction 的影响。

PG 的子事务号是 uint32,分配时复用父事务的 XID 命名空间。64 位下,需要给子事务单独一个命名空间(比如高 32 位用进程号,低 32 位用子事务号),pg_subtrans 也要重写。prepared transaction 在 2PC 中存的 xid,需要在跨库持久化时保证 xid 宽度,这块改动会改 pg_prepared_xacts 系统表的 schema。

5. 复制协议。

流复制协议里 WalDataMessagexl_xid,64 位化后主从必须同时升级,跨版本复制会断。这是 PG 社区最忌讳的升级门槛。


三、金仓 V009R002C016 的改造范围

读金仓 V009R002C016 版本说明书 PDF(L903、L1012-1075),这次 64 位改造 不是全栈改造,而是有取舍的实用方案

3.1 公开承认的范围

金仓版本说明 L903 原文:

"支持 64 位事务 ID(XID),避免因 32 位事务 ID 回收不及时或长事务阻塞导致的业务中断问题。"

L1012 明确:

"为适配 64 位事务 ID,以下 VACUUM 及自动清理(Autovacuum)相关参数的数据类型与取值范围已同步升级"

3.2 实际改了什么:VACUUM 参数 int64 化

版本说明明确列了 4 个关键参数的升级:

参数 变更前 变更后
vacuum_freeze_min_age integer,最大 1000000000 int64,最大 2⁶⁴-1 ≈ 1.15×10¹⁹
vacuum_freeze_table_age integer,最大 2000000000 int64,最大 2⁶⁴-1
vacuum_multixact_freeze_min_age integer,最大 1000000000 int64,最大 2⁶⁴-1
vacuum_multixact_freeze_table_age integer,最大 2000000000 int64,最大 2⁶⁴-1

注意:这些参数的默认值没变(仍是 200000000 / 400000000 / 10000000000 / 20000000000),仅扩大上限。这意味着金仓保留了一个"32 位 XID 语义仿真层"------老的 PG 业务脚本不需要改 freeze 阈值,默认值跟 PG 完全一致。

这释放出一个工程信号:金仓没把 tuple header 里的 xmin 改成 64 位(否则 freeze 参数的语义就完全不同了)。他们的 64 位 XID 改造最可能是"扩展分配号空间 + 让 XID 不再循环",而 tuple header 里的 t_xmin 仍是 32 位,需要 freeze 才能继续用。

3.3 配套改造:实例级动态内存管控

L903 紧接一句:

"全面支持 64 位事务 ID 与实例级动态内存管控,新增 ... max_dynamic_memory,可全局限制实例内存占用,超限时触发明确告警,降低 OOM 风险。"

这是 64 位 XID 改造的必要配套。64 位 XID 让事务号生命周期变长(不再循环),freeze 频率降低,后台 autovacuum 工作量减少,但单次事务的内存占用可能上升(比如长事务持有的 xid 缓存更大)。金仓同步加了内存上限控制,避免 64 位 XID 改造引入新的 OOM 风险。

3.4 与 fastpath 锁改造的协同

L1299 提到:

"新增 FastPath 锁机制,并提供 FastPath 槽位数量、CLOG 轻量锁分区数、CSNLOG 轻量锁分区数的配置能力"

CSNLOG 是 Commit Sequence Number Log,记录事务提交顺序。64 位 XID 改造必然伴随 CSN 调整(因为 CSN 之前是 32 位跟随 XID 循环)。金仓顺带把 CSNLOG 的并发从单锁改成"轻量锁分区数"可配,是高并发场景的必须优化。


四、入门者能看懂的 5 个核心要点

如果你是 PG 入门者,这一节帮你建立心智模型。

4.1 32 位 XID 的"半圆"循环

scss 复制代码
        (未来可用)
              ▲
              │
              │  20 亿
              │
   ←──────────┤──────────→
   (过去已分配)│(frozen xid)
              │
              │  20 亿
              │
              ▼
        (未来可用)

每一行 tuple 的 t_xmin 落在圆上某一点。判断"这行是不是当前事务可见"的算法本质是:

c 复制代码
if t_xmin == frozen_xid:
    永远可见
elif t_xmin 在 frozen_xid 顺时针一侧:
    属于"过去",通常不可见
else:
    属于"未来",看 commit log 判断是否已提交

64 位 XID 改造后,这个圆变成直线:t_xmin < 当前事务快照 直接判可见,不再有"循环"概念,freeze 也从"对抗循环"变成"普通 cleanup"。

4.2 freeze 到底在做什么

freeze 不是"删除老数据",而是"标记老数据"。PG 在 heap tuple header 里预留了 2 个 bit 位,t_infomask 字段里有 HEAP_XMIN_FROZENHEAP_XMIN_INVALID。freeze 把 t_xmin 替换成 FrozenTransactionId = 2,让所有事务都"看到"这条记录是 frozen 的(等于它早于任何事务),t_xmin 字段被释放出来可以循环复用。

64 位 XID 下,这个机制仍然存在(因为 tuple header 字段宽度没变),但触发频率大幅下降------因为 XID 不再循环,frozen 边界不再是"半个圆前的数据",而是"应用层主动清理的超长历史数据"。

4.3 64 位 XID 的实际收益

对于高写入业务,64 位 XID 直接消除了:

  • freeze 风暴的根源:不再有"半个圆快耗尽"的焦虑
  • autovacuum 必须成功的强约束:XID 不循环,vacuum 慢一点也不致命
  • 长事务导致停库的极端情况:因为 XID 不循环,长事务不再"挤占 freeze 空间"
  • 从库延迟与 freeze 的耦合:freeze 不再高频,从库不再因为主库 freeze 而大幅延迟

但 64 位 XID 不直接解决:

  • 表膨胀(bloat)------仍需 vacuum / autovacuum 清理死元组
  • 长事务持有的锁------仍可能阻塞 DDL

4.4 autovacuum 调优逻辑变了

32 位 XID 下,autovacuum_vacuum_insert_scale_factor / vacuum_freeze_table_age / vacuum_freeze_min_age 这几个参数都是"硬卡时间窗口"的逻辑(必须在 20 亿事务内完成 freeze)。

64 位 XID 下,这些参数变成"普通 cleanup 调优"------你可以让 autovacuum 更激进(比如把 freeze_age 调小),因为即使 freeze 不及时,业务也不会立即崩。

金仓的默认参数没变,实际是给 DBA 留了"老 PG 运维经验继续管用"的兼容层。

4.5 与主从复制 / 备份恢复的关系

32 位 XID 下,流复制的 standby 也会有 freeze 风暴问题(因为 standby 也要复算 xmin)。

64 位 XID 下,standby 的 freeze 压力大幅降低,但 standby 的 promote / switchover 仍需要确认主从 XID 兼容性。金仓这次没在版本说明里提这个,意味着这块可能还没改完,或者金仓选择了"主从必须同版本"的限制(类似 PG 大版本升级策略)。


五、专家关心的实现细节

如果你是 PG 内核开发者或 DBA 专家,这一节是金仓的"实现笔记"。

5.1 数据类型层面的改造

金仓这次改造保守:

c 复制代码
/* 推测的金仓 XID 内部定义(基于 PDF 反推) */
typedef uint64 kingbase_xid;   /* 64 位事务号分配 */

typedef uint32 kingbase_tup_xmin;  /* tuple header 仍 32 位 */
typedef uint32 kingbase_tup_xmax;  /* tuple header 仍 32 位 */

即"分配空间 64 位,tuple header 仍 32 位"的混合方案。这跟 PG 社区讨论过的 xid8 思路一致:xid8 用于系统表、事务快照、WAL,但 tuple header 保留 32 位(用 frozen bit 标识已冻结的 xmin)。

好处:

  • 不破坏 tuple header 大小,旧 page 兼容
  • 不破坏 CLOG 结构
  • 升级路径平滑,主从可以不同时升级
  • 运维心智兼容(默认值不变)

代价:

  • tuple header 仍要 freeze,只是 freeze 频率大幅降低
  • 业务层仍受 32 位 tuple header 的"半圆"约束------单表 43 亿事务上限不变

5.2 autovacuum 调优建议(32 位 → 64 位过渡期)

对于已经在用金仓 V009R002C016 的 DBA,我的建议:

sql 复制代码
-- 第一阶段(迁移期,1-3 个月):保留 PG 经验值
-- 不改 freeze 参数,让系统按默认值跑
-- 监控 freeze 频率

-- 第二阶段(优化期,3-6 个月):开始利用 64 位 XID 的优势
ALTER SYSTEM SET vacuum_freeze_min_age = 50000000;  -- 从 2 亿改 5 千万,更激进
ALTER SYSTEM SET autovacuum_freeze_max_age = 1000000000;  -- 从 4 亿改 10 亿,延长 freeze 周期

-- 第三阶段(平稳期,6+ 个月):根据实际负载调优
-- 此时 freeze 已不是高频操作,可以放心把 freeze_age 调到很大
ALTER SYSTEM SET vacuum_freeze_table_age = 5000000000;  -- 50 亿

5.3 与 PG 16/17/18 的对比

PG 16 引入的 xid8 数据类型只解决了用户能用 64 位 XID 字段(比如用户表存事务号),没解决内部事务号分配。

PG 17 的 transaction_id 改进是修 multixact,不修主 XID。

PG 18 邮件列表里讨论过"全 64 位 XID",但因为牵动太大,目前没有 RFC。

金仓 V9 的意义:它是第一个把"全 64 位 XID 分配"做成生产可用特性的 PG 系国产数据库。

5.4 与其他数据库的对比

数据库 事务号宽度 freeze 机制 备注
PostgreSQL 32 位 freeze + autovacuum 32 位 XID 仍循环
Oracle SCN 是数字,内部 6 字节 undo + 多版本 不依赖 XID 循环
MySQL/InnoDB 6 字节 TRX_ID undo log 不依赖 XID 循环
KingbaseES V9 64 位分配(推测)+ 32 位 tuple header freeze 频率大幅降低 国产首个 64 位 XID 生产可用
zheap / zedstore 64 位 TID 不需要 freeze 实验项目,未生产化

从这个对比看,金仓实际走的是"PG 兼容 + XID 空间扩展"的中间路径,不是 zheap 那种完全改造 tuple header 的激进方案。这跟金仓的"Oracle 兼容"路线一致------不破已有生态,只在关键节点扩展。


六、对 PG 社区的启示

金仓这次改造给了 PG 社区三个可借鉴的方向:

6.1 渐进式 XID 扩展方案

PG 社区讨论"全 64 位 XID"时,最大顾虑是破坏 tuple header 兼容。金仓的方案是"分配层 64 位 + tuple header 仍 32 位",这个分层设计值得 PG 学习------它能在不破 page 格式的前提下,把事务号分配空间从 43 亿扩到 2⁶⁴,代价仅是引入一个"分配号 → tuple xmin"的映射。

6.2 默认值保留的兼容性设计

金仓升级 freeze 参数类型时,默认值跟 PG 完全一致(200000000 / 400000000),只扩展上限。这个细节看似不起眼,实际是降低 DBA 迁移心智成本的关键。

PG 社区如果要做类似改造,应该学习这种"默认值不变"的兼容性设计。

6.3 freeze 价值的重新定义

32 位 XID 下,freeze 是"对抗循环的紧急任务"。64 位 XID 下,freeze 是"普通 cleanup"。这个语义转变对 autovacuum 调优哲学有深远影响------DBA 不再需要"算 freeze 窗口",而是回归"按业务需要清理死元组"。


七、对应用开发者的建议

无论你是入门者还是专家,64 位 XID 改造后,应用开发实践应该调整:

  1. 不要在应用层做"事务号"运算 :PG 系的事务号不连续(committed 事务跳号),应用层算 xid + 1 没意义。金仓 64 位后这点仍然成立。
  2. freeze 不再是"紧急任务":监控告警阈值可以放宽(从"剩余 1000 万事务"放宽到"剩余 100 万事务"),给 DBA 留更多运维空间。
  3. 跨版本迁移测试更复杂:金仓 64 位 XID 后,从老版本升级时,主从 XID 兼容性需要测试。

八、给金仓的建议

写完这篇,我作为 PG 老用户,提 2 个期待:

  1. 配套最佳实践:在运维管理层面,以往哪些需要特殊注意的配置不需要再纠结了,另外有哪些新增的注意事项?
  2. 配套监控指标max_dynamic_memory 已经加入告警,但 64 位 XID 下事务平均寿命应该作为新指标(让 DBA 看升级效果)。

九、写在最后

五年前我吐槽 PG 的 32 位 XID 是"狗皮膏药",当时没人接这个吐槽------PG 社区认为"freeze + autovacuum 已经够用",国产数据库当时也没人敢碰这块。

今天金仓 V009R002C016 把 64 位 XID 做成了生产可用,做到了 PG 社区没做到的事。

值得 PG 社区反思:国产数据库不是只在做 Oracle 兼容,也在做 PG 自己也解决不了的内核改造。金仓的"32 → 64 位 XID 渐进改造",值得 PG 17/18/19 认真抄作业。

5 年的吐槽,1 个版本号,问题被国产数据库治好了。

相关推荐
旺仔不是程序员1 小时前
判断存在用 LIMIT 1:PostgreSQL 五种 count 计数方式与 NULL 语义
数据库·后端·sql
旺仔不是程序员1 小时前
IN 操作规范:PostgreSQL 元素数量、EXISTS 替代与 = ANY 写法
数据库·后端·sql
这个DBA有点耶1 小时前
异构数据集成方案:跨平台同步的完整指南与工具选型(含实测)
数据库·sql·程序人生·架构·数据库架构·dba
蓝速科技1 小时前
会议室门牌显示模板选型与品牌视觉适配落地指南丨蓝速科技
大数据·运维·数据库·人工智能·科技
jaysee-sjc1 小时前
【苍穹外卖】Day01:从零认识企业级项目开发
java·开发语言·数据库·mysql·spring·intellij-idea·mybatis
智购科技自动贩卖机1 小时前
自动售货机嵌入式系统时钟同步与时间管理实战:从RTC校准到断网时间保持的工程实践
大数据·linux·数据库·人工智能·yolo
Y3815326622 小时前
SEO 周报自动化:从采集数据到一页报告
数据库·搜索引擎
雨辰AI2 小时前
多租户数据库资源配额管控|避免租户资源抢占雪崩(金仓 / 达梦 / 高斯 /openGauss 全库原生适配)
java·大数据·数据库·后端
念何架构之路2 小时前
beego ORM 源码(下):SQL 生成与方言
数据库·sql·beego