五年前我吐槽过 PG 的 32 位事务号问题,当时觉得这是个长期难解的坑。如今,金仓把这个坑补上了。
这篇文章一方面给刚接触 PG 的人讲清楚:XID 到底是什么、为什么麻烦;另一方面也给 PG 内核开发者提供一份对照:金仓 V9 改了什么、可能怎么改、还有哪些没覆盖。
一、PG 的 XID 为什么像“狗皮膏药”
PG 用 32 位无符号整数保存事务号,也就是 TransactionId 本质上是 uint32。这意味着一个集群最多同时容纳约 2³²,也就是 43 亿个事务号。听起来不少,但对高写入业务远远不够。典型电商库一天跑出 5 到 10 亿事务并不罕见。
事务号耗尽后会怎样?PG 的 MVCC 依赖 t_xmin < current_xid 之类的规则判断可见性。XID 用完,新事务无法继续分配,只能循环复用。问题在于:怎么保证“老 XID”和“新 XID”在 32 位空间里不打架?
PG 的方案是 frozen xid。它把 XID 空间看成一个圆:frozen xid 是圆上的一个点,顺时针是已经分配过的“过去”,逆时针是可分配的“未来”。每消耗约 20 亿事务号,frozen xid 就必须向前移动一次。移动方式是由 autovacuum 扫描整张表,把 t_xmin < frozen_xid 的行头信息标记为 frozen。这个特殊标记让该行永远可见,不再参与可见性判断。
代价也很明显:
- 冻结风暴:全表扫描,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,以及所有依赖 XID 的 hash join 等地方,影响面太大。
但 2026 年 8 月 28 日,金仓发布的 V009R002C016 明确写了:支持 64 位事务 ID,避免因 32 位事务 ID 回收不及时或长事务阻塞导致业务中断。
这是国产数据库首次把“64 位事务号”做成生产可用特性,意义不小。
二、64 位 XID 难在哪里
要理解金仓做了什么,先要理解 PG 为什么一直没做。64 位 XID 绝不是改个类型就完事,它牵动很多层面。
1. Tuple header 会变胖
PG 的 HeapTupleHeaderData 里原本有 32 位的 t_xmin、t_xmax、t_xvac、t_multi 等字段。若全部改成 64 位,每个 tuple 可能多出 16 字节。一张 10 亿行表,光 header 就多 16 GB。这是社区犹豫的第一点。
2. CLOG 不能继续照旧用 SLRU
PG 的 CLOG 用 8KB 页保存事务提交状态,每页约 512 个事务号,配两级 SLRU 缓存。64 位 XID 下,CLOG 必须换存储结构。可选思路包括 ring buffer,或改成基于 segment 的 mmap 文件。这块社区讨论过很多次,但没人真正动手,因为它直接牵动 vacuum 的可见性判断。
3. Vacuum 的“半个圆”语义失效
32 位 XID 下,vacuum 的 relfrozenxid < oldestXid - 2000000000 这类判断,本质是“freeze 点之前的事务号都该被冻结”。64 位 XID 下,这个机制基本不需要了——正常业务一辈子也写不到 2⁶³ 个事务。因此 freeze 核心逻辑要从“对抗循环”改成“无限寿命 + 普通清理”。
4. 子事务和 prepared transaction 受影响
PG 的子事务号是 uint32,分配时复用父事务的 XID 命名空间。64 位下,子事务可能需要单独命名空间,比如高 32 位用进程号、低 32 位用子事务号,pg_subtrans 也要重写。prepared transaction 在 2PC 中保存的 XID,也需要保证跨库持久化时宽度一致,这会改到 pg_prepared_xacts 系统表 schema。
5. 复制协议会受影响
流复制协议里的 WalDataMessage 含 xl_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 |
注意:这些参数的默认值没变,仍是原来的值,只是上限扩大了。这意味着金仓保留了一个“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 工作量减少,但单次事务的内存占用可能上升。金仓同步加入内存上限控制,避免引入新的 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 的“半圆”循环
32 位 XID 下,每一行 tuple 的 t_xmin 都落在圆上某一点。判断可见性时,大致逻辑是:
- 如果
t_xmin == frozen_xid,永远可见; - 如果
t_xmin在frozen_xid顺时针一侧,属于“过去”,通常不可见; - 否则属于“未来”,再看 commit log 判断是否已提交。
64 位 XID 改造后,这个圆会变成直线:t_xmin < 当前事务快照 就能直接判断可见性,不再有“循环”概念。freeze 也从“对抗循环”变成“普通 cleanup”。
4.2 freeze 到底在做什么
freeze 不是删除老数据,而是标记老数据。PG 在 heap tuple header 里预留了 bit 位,t_infomask 中有 HEAP_XMIN_FROZEN 和 HEAP_XMIN_INVALID。freeze 会把 t_xmin 替换成 FrozenTransactionId = 2,让所有事务都认为这行早已存在,t_xmin 字段因此可以被释放复用。
64 位 XID 下,这个机制仍然存在,因为 tuple header 字段宽度没变,但触发频率会大幅下降。因为 XID 不再循环,frozen 边界不再取决于“半个圆前的数据”,而是应用层主动清理的超长历史数据。
4.3 64 位 XID 的实际收益
对高写入业务,64 位 XID 直接缓解或消除了:
- freeze 风暴的根源:不再有“半个圆快耗尽”的焦虑;
- autovacuum 必须成功的强约束:XID 不循环,vacuum 慢一点也不致命;
- 长事务导致停库的极端情况:长事务不再挤占 freeze 空间;
- 从库延迟与 freeze 的耦合:freeze 不再高频,从库不再因主库 freeze 大幅延迟。
但 64 位 XID 不直接解决:
- 表膨胀,仍需要 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 压力会大幅降低,但 promote / switchover 仍需要确认主从 XID 兼容性。金仓这次没有在版本说明里展开这一点,可能意味着这块还没完全改完,或者金仓选择了“主从必须同版本”的限制,类似 PG 大版本升级策略。
五、专家关心的实现细节
如果你是 PG 内核开发者或 DBA 专家,这一节可以看作金仓的“实现笔记”。
5.1 数据类型层面的改造
金仓这次改造偏保守。基于 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,只是频率大幅降低;
- 业务层仍受 32 位 tuple header 的“半圆”约束,单表 43 亿事务上限不变。
5.2 autovacuum 调优建议:32 位到 64 位过渡期
对已经在用金仓 V009R002C016 的 DBA,可以分三阶段:
-- 第一阶段:迁移期,1-3 个月,保留 PG 经验值
-- 不改 freeze 参数,让系统按默认值跑
-- 监控 freeze 频率
-- 第二阶段:优化期,3-6 个月,开始利用 64 位 XID 优势
ALTER SYSTEM SET vacuum_freeze_min_age = 50000000;
ALTER SYSTEM SET autovacuum_freeze_max_age = 1000000000;
-- 第三阶段:平稳期,6+ 个月,根据实际负载调优
ALTER SYSTEM SET vacuum_freeze_table_age = 5000000000;
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 位”。这个分层设计值得学习:在不破坏 page 格式的前提下,把事务号分配空间从 43 亿扩到 2⁶⁴,代价只是引入一个“分配号到 tuple xmin”的映射。
6.2 默认值保留的兼容性设计
金仓升级 freeze 参数类型时,默认值跟 PG 完全一致,只扩展上限。这个细节看似不起眼,实际是降低 DBA 迁移心智成本的关键。PG 社区如果做类似改造,也应学习这种“默认值不变”的兼容性设计。
6.3 freeze 价值的重新定义
32 位 XID 下,freeze 是对抗循环的紧急任务。64 位 XID 下,freeze 是普通 cleanup。这个语义转变对 autovacuum 调优哲学影响深远:DBA 不再需要“算 freeze 窗口”,而是回归“按业务需要清理死元组”。
七、对应用开发者的建议
无论入门者还是专家,64 位 XID 改造后,应用开发实践也应调整:
- 不要在应用层做事务号运算:PG 系事务号不连续,committed 事务会跳号,应用层算
xid + 1没意义。金仓 64 位后这点仍然成立。 - freeze 不再是紧急任务:监控告警阈值可以放宽,从“剩余 1000 万事务”放宽到“剩余 100 万事务”,给 DBA 更多运维空间。
- 跨版本迁移测试更复杂:金仓 64 位 XID 后,从老版本升级时,主从 XID 兼容性需要测试。
八、给金仓的建议
作为 PG 老用户,我提两个期待:
- 配套最佳实践:运维管理层面,以往哪些需要特别小心的配置可以不再纠结?又有哪些新增注意事项?
- 配套监控指标:
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 认真参考。
五年前的吐槽,一个版本号,问题被国产数据库治好了。