由 John Doe 十二月 10, 2024
摘要:在本文中,我们将了解 PostgreSQL 要实现大版本轻松升级的挑战。
目录
介绍
PostgreSQL 大版本之间的升级是出了名的烦人。您不可能简单地安装服务器二进制文件,并重新启动,因为数据目录的格式不兼容。
为什么我们不能做到保持数据格式兼容呢?
也许令人惊讶的是,数据格式实际上大部分兼容,但并不完全兼容。只是缺少一些很难解决的事情。
让我们看看可能的升级过程:
- pg_upgrade
- 使用 pg_dumpall 进行转储和还原
- 逻辑复制到新实例
2 和 3 本质上是相同的思路:构建一个新实例,并以一种更高级别的兼容格式在实例之间传输数据。
什么是 pg_upgrade?
pg_upgrade 更有意思。pg_upgrade 的作用是:
- 使用 pg_dump 转储旧数据库的结构,并将其还原到新数据库。实际上,要处理实例中的每个数据库,需要结合使用 pg_dumpall,但这在这里并不重要。
- 将数据文件直接从旧实例复制到新实例。
因此,包含表和索引数据的数据文件,实际上在主要版本之间是兼容的。自从 pg_upgrade 发布以来,它们已经存在了很长时间。
如何管理这些信息取决于这些文件里的内容。例如,B 树有一个简单的版本机制:
#define BTREE_VERSION 4 /* current version number */
#define BTREE_MIN_VERSION 2 /* minimum supported version */
堆表则更复杂,它只需要与旧版本可能存储的任何内容保持兼容性。但无论如何,这样都可以工作。
不兼容的是结构(数据定义、表的元数据等)的存储方式。这就是 pg_upgrade 必须处理的问题。
为什么大版本之间的元数据不兼容呢?
PostgreSQL 数据库的元数据存储在所谓的系统目录中,这些目录本身就是表。出于初始引导和一些效率的原因,这些系统目录的结构在服务器中是硬编码的。例如,当系统想知道一个列的名称时,它会加载pg_attribute
中的行,并且它知道带有列名称的attname
字段的偏移位置。这些必须进行硬编码,因为您无法通过查询pg_attribute
,来查找有关pg_attribute
的信息。
每当实现一个新功能,需要在系统目录中存储一些信息时,这些硬编码的逻辑就会变得不正确。例如,在 PostgreSQL 17 中,订阅增加了一个新的故障转移标志。该故障转移标志需要存储在某个位置。它存储在 pg_subscription 系统表的subfailover
字段中。因此,pg_subscription
表中行的硬编码大小会发生变化,在新的subfailover
字段后的某些字段的偏移位置也会发生变化。由于其他复杂的原因,并不能将所有的新字段都添加到末尾。
对于该问题,似乎有一个明显的解决方案:服务器源代码只需要针对旧的元数据结构有条件处理的代码。但是这个代码非常普遍,粗略估计显示有一千多个地方。因此,实现这一目标将是一项艰巨的工作,并且需要大量的持续维护。所以我认为在实现这个目标之前,需要有一些新的想法,先对其中的一些地方进行重构。
另一个问题是,这仍然无法实现将系统目录升级到新的结构。如果您升级到 PostgreSQL 17,并希望创建一个支持故障转移的订阅,会无法创建,因为系统目录仍采用的是旧格式。然后,您仍然需要以某种方式来升级它们,并且需要用工具来管理所有这些事情。
现在,在不同的数据库中,系统目录的设计都会有所不同,同时要考虑兼容性和可升级性。系统目录用普通表实现的想法,可能源自于最初的 Berkeley Postgres 的设计。这样确实很好,因为它为您提供了一些有用的功能,尤其是事务性 DDL,而额外的工作量相对较小。但它确实也让格式更难升级了。
所以,我倾向于认为,这是由这些历史性架构决策的权衡产生的:轻松地进行升级,还是易于支持事务性 DDL。
需要明确的是,这并不是要实现轻松升级的唯一障碍。但我认为,这是最重要的一个。另一个问题是,在系统目录中的一些内部数据结构的序列化格式没有管理好,以实现跨大版本的兼容性,比如存储的视图规则或默认值表达式。因为到目前为止还没有必要。但我认为,这是可以找到解决方案的。有很多这样的事情,我们甚至没有认真考虑过,因为没有需求。例如,预写式日志(WAL)的格式在大版本之间不兼容。由于 pg_upgrade 和其他升级过程都不会保留 WAL,因此这也不是问题。在这里提到它,只是为了表明,如果我们所知道的问题得到解决后,可能还有哪些较少发现的问题。