多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

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

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 暴涨主从延迟。长事务场景下必须停库 freezeautovacuum 会跳过正在被事务占用的 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 位事务 IDXID避免因 32 位事务 ID 回收不及时或长事务阻塞导致的业务中断问题。”这是国产数据库首次把“64 位事务号”做成了生产可用特性意义不亚于 PG 15 的 logical replication 跨大版本兼容。二、64 位 XID 改造难在哪里理解金仓做了什么先得理解为什么 PG 一直没做。64 位 XID 不是改个类型那么轻松它牵动的面极广1. Tuple header 多 8 字节。PG 的HeapTupleHeaderData现在长这样简化typedefstructHeapTupleHeaderData{t_choice t_choice;// 哪种 t_xminTransactionId 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 的 CLOGCommit 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. 复制协议。流复制协议里WalDataMessage含xl_xid64 位化后主从必须同时升级跨版本复制会断。这是 PG 社区最忌讳的升级门槛。三、金仓 V009R002C016 的改造范围读金仓 V009R002C016 版本说明书 PDFL903、L1012-1075这次 64 位改造不是全栈改造而是有取舍的实用方案。3.1 公开承认的范围金仓版本说明 L903 原文“支持 64 位事务 IDXID避免因 32 位事务 ID 回收不及时或长事务阻塞导致的业务中断问题。”L1012 明确“为适配 64 位事务 ID以下 VACUUM 及自动清理Autovacuum相关参数的数据类型与取值范围已同步升级”3.2 实际改了什么VACUUM 参数 int64 化版本说明明确列了 4 个关键参数的升级参数变更前变更后vacuum_freeze_min_ageinteger最大 1000000000int64最大 2⁶⁴-1 ≈ 1.15×10¹⁹vacuum_freeze_table_ageinteger最大 2000000000int64最大 2⁶⁴-1vacuum_multixact_freeze_min_ageinteger最大 1000000000int64最大 2⁶⁴-1vacuum_multixact_freeze_table_ageinteger最大 2000000000int64最大 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 的“半圆”循环(未来可用) ▲ │ │ 20 亿 │ ←──────────┤──────────→ (过去已分配)│(frozen xid) │ │ 20 亿 │ ▼ (未来可用)每一行 tuple 的t_xmin落在圆上某一点。判断“这行是不是当前事务可见”的算法本质是ift_xminfrozen_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_FROZEN和HEAP_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 清理死元组长事务持有的锁——仍可能阻塞 DDL4.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 数据类型层面的改造金仓这次改造保守/* 推测的金仓 XID 内部定义(基于 PDF 反推) */typedefuint64 kingbase_xid;/* 64 位事务号分配 */typedefuint32 kingbase_tup_xmin;/* tuple header 仍 32 位 */typedefuint32 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我的建议-- 第一阶段(迁移期,1-3 个月):保留 PG 经验值-- 不改 freeze 参数,让系统按默认值跑-- 监控 freeze 频率-- 第二阶段(优化期,3-6 个月):开始利用 64 位 XID 的优势ALTERSYSTEMSETvacuum_freeze_min_age50000000;-- 从 2 亿改 5 千万,更激进ALTERSYSTEMSETautovacuum_freeze_max_age1000000000;-- 从 4 亿改 10 亿,延长 freeze 周期-- 第三阶段(平稳期,6 个月):根据实际负载调优-- 此时 freeze 已不是高频操作,可以放心把 freeze_age 调到很大ALTERSYSTEMSETvacuum_freeze_table_age5000000000;-- 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 机制备注PostgreSQL32 位freeze autovacuum32 位 XID 仍循环OracleSCN 是数字内部 6 字节undo 多版本不依赖 XID 循环MySQL/InnoDB6 字节 TRX_IDundo log不依赖 XID 循环KingbaseES V964 位分配推测 32 位 tuple headerfreeze 频率大幅降低国产首个 64 位 XID 生产可用zheap / zedstore64 位 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 改造后应用开发实践应该调整不要在应用层做“事务号”运算PG 系的事务号不连续committed 事务跳号应用层算xid 1没意义。金仓 64 位后这点仍然成立。freeze 不再是“紧急任务”监控告警阈值可以放宽从“剩余 1000 万事务”放宽到“剩余 100 万事务”给 DBA 留更多运维空间。跨版本迁移测试更复杂金仓 64 位 XID 后从老版本升级时主从 XID 兼容性需要测试。八、给金仓的建议写完这篇我作为 PG 老用户提 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 个版本号问题被国产数据库治好了。
返回列表