PostgreSQL模式更改指南

2022-02-26 09:05:31

Braintree Payments使用PostgreSQL作为其主要数据存储。我们严重依赖传统关系数据库提供的数据安全性和一致性保证,但这些保证会带来一定的操作困难。为了让事情变得更有趣,我们允许我们的主要支付处理服务实现零计划功能停机。

几年前,我们发表了一篇博客文章,详细介绍了我们在不中断生产API流量的情况下安全运行DDL操作的一些经验。

从那时起,PostgreSQL经历了相当多的主要升级周期——其中有几个增加了对并发DDL的改进支持。我们';我们还进一步完善了我们的流程。考虑到变化有多大,我们认为是时候发布一篇博客文章了。

实时代码和模式与更新后的代码和模式保持前向兼容:这使我们能够在一组应用服务器和数据库集群中逐步展开部署。

新的代码和模式与实时代码和模式向后兼容:这允许我们在发生意外错误时回滚到以前版本的任何更改。

在表或索引上获取的任何独占锁最多可保持2秒。

PostgreSQL支持事务性DDL。在大多数情况下,可以在显式数据库事务中执行多个DDL语句,并使用";要么全无";一系列改变的方法。然而,在一个事务中运行多个DDL语句有一个严重的缺点:如果您更改多个对象,您';我们需要在单个事务中获取所有这些对象的独占锁。由于多个表上的锁会造成死锁,并增加长时间等待的风险,因此我们不会将多个DDL语句组合到一个事务中。PostgreSQL仍将以事务方式执行每个单独的DDL语句;每条语句要么被干净地应用,要么失败,事务被回滚。

注意:并发索引创建是一种特殊情况。Postgres不允许在显式事务中同时执行创建索引;相反,Postgres自己管理交易。如果由于某种原因,索引生成在完成之前失败,您可能需要在重试之前删除该索引,但如果索引未成功完成生成,则该索引将永远不会用于常规查询。

PostgreSQL有许多不同级别的锁定。我们';我们主要关注以下表级锁,因为DDL通常在这些级别上运行:

SHARE ROW EXCLUSIVE:阻止对锁定表中的行进行并发DDL和行修改(允许读取)。

所有DDL操作通常都需要在被操纵的对象上获取其中一个锁。例如,运行时:

PostgreSQL试图获取表foos上的访问独占锁。试图获取此锁会导致此表上的所有后续查询排队,直到释放锁为止。在实践中,DDL操作可能会导致其他查询在执行最长运行查询所需的时间内进行备份。由于传入查询的任意长排队与中断是无法区分的,因此我们试图避免在支持支付处理应用程序的数据库中出现任何长时间运行的查询。

但有时查询所需的时间比您预期的要长。或者你可能有一些你已经知道需要很长时间的特殊情况查询。PostgreSQL提供了一些额外的运行时配置选项,使我们能够保证查询队列背压不会';不会导致停机。

在执行DDL语句时,我们不依赖Postgres来锁定对象,而是自己显式地获取锁。这使我们能够仔细控制查询排队的时间。此外,当我们无法在几秒钟内获得锁时,我们会在重试之前暂停,以便在不显著增加负载的情况下执行任何排队查询。最后,在尝试锁获取之前,我们检查(此处的查询)是否存在任何当前长时间运行的查询,以避免在锁获取不太可能成功的情况下,不必要地将查询排队几秒钟。

从Postgres 9.3开始,您可以调整lock_timeout参数,以控制Postgres在返回而不获取锁之前允许获取锁的时间。如果您碰巧使用的是9.2或更早版本(这些版本不受支持,您应该升级!),然后,您可以通过在显式锁周围使用statement_timeout参数来模拟这种行为<;表>;陈述

在许多情况下,访问独占锁只需保持很短的时间,即Postgres更新其";目录";(想想元数据)表。下面我们';我们将讨论较低的锁级别足以避免阻塞SELECT/INSERT/UPDATE/DELETE的长锁的情况或替代方法。

注:有时,在表大小相对较小的情况下,即使对目录更新以外的内容(例如,完整表扫描或甚至重写)持有访问独占锁,在功能上也可以接受。我们建议根据实际的数据大小和硬件测试您的特定用例,以查看特定操作是否为";足够快";。在好的硬件上,表很容易加载到内存中,对数千行(甚至可能是100到数千行)的完整表扫描或重写可能是";足够快";。

一般来说,添加一个表是我们不做的少数操作之一';I don’我们不必想得太多,因为根据定义,我们';re";修改";可以';目前还不可能投入使用D

虽然创建表所涉及的大多数属性都不涉及其他数据库对象,但在初始表定义中包含外键将导致Postgres获取对引用表的共享行独占锁,阻止任何并发DDL或行修改。虽然该锁应该是短期的,但它需要与获取此类锁的任何其他操作一样的谨慎。我们更愿意将它们分成两个独立的操作:创建表,然后添加外键。

删除表需要在该表上使用独占锁。只要桌子不是';t在当前使用中,你可以安全地放下桌子。在允许放下桌子之前。。。为了进入我们的生产环境,我们需要文档来显示表的所有引用何时从代码库中删除。要再次检查这种情况,可以查询PostgreSQL';s table statistics view pg_stat_user_tables确认返回的统计数据不';不要在合理的时间内改变。

而它';不足为奇的是,表重命名需要获取表上的访问独占锁,即';这远非我们最大的担忧。除非该表没有被读取或写入,否则它';it’应用程序代码不太可能安全地处理在其下重命名的表。

我们几乎完全避免表重命名。但如果重命名是绝对必要的,那么安全的方法可能如下所示:

在旧表上使用INSERT和UPDATE触发器来维护新表中的奇偶校验。

其他涉及视图和/或规则的方法也可能可行,具体取决于所需的性能特征。

向现有表中添加列通常需要在更新目录表时在表上保持短访问独占锁。但有几个潜在的问题:

默认值:在添加列的同时引入默认值将导致表被锁定,而表中所有行的默认值都将被分配。相反,你应该:

注意:在最近发布的PostgreSQL 11中,非易失性默认值不再是这种情况。相反,添加一个带有默认值的新列只需要更新目录表,任何没有新列值的行读取都会神奇地拥有它";填写";在飞行中。

Not null约束:只有在没有现有行或还提供了默认值的情况下,才能添加带有Not约束的列。如果没有现有行,那么该更改实际上相当于仅目录更改。如果存在现有行,并且您也指定了默认值,那么对于默认值,上述警告同样适用。

注意:添加列将导致所有SELECT*FROM。。。引用表以开始返回新列的样式查询。确保所有当前运行的代码安全地处理新列是很重要的。为了避免应用程序中出现这种情况,我们要求查询避免*扩展,以支持显式列引用。

在一般情况下,更改列';s类型要求在使用新类型重写整个表时,在表上保持独占锁。

注意:尽管9.1中添加了上述异常之一,但更改索引列的类型始终会重写索引,即使避免了表重写。在9.2中,任何避免表重写的列数据类型也可以避免重写相关的索引。如果你';I’我想确认一下,您的零钱赢了';如果不重写表或任何索引,可以查询pg_类并验证relfilenode列不';不要改变。

如果您需要更改列的类型,且上述异常之一不存在';如果不适用,那么安全的替代方案是:

重命名<;列>;致老uu<;列>;和新的<;列>;在单个事务和显式锁定中<;表>;陈述

不言而喻,删除一篇专栏文章应该非常小心。删除列需要表上的独占锁来更新目录,但不会重写表。只要列不是';t在当前使用中,你可以安全地放下柱子。它';确认列未被任何可能不安全的依赖对象引用也很重要。特别是,任何使用该列的索引都应该单独安全地删除,同时删除索引,否则它们将在访问独占锁下与该列一起自动删除。您可以查询pg_depend中的任何依赖对象。

在允许更改表格之前。。。删除列。。。为了进入我们的生产环境,我们需要文档来显示何时从代码库中删除了对该列的所有引用。这个过程使我们能够安全地回滚到丢弃列之前的版本。

注意:删除列需要更新所有依赖该列的视图、触发器、函数等。

创建索引的标准形式。。。在使用单个表扫描构建索引时,针对正在编制索引的表获取访问独占锁。相比之下,表单会同时创建索引。。。获取共享更新独占锁,但必须完成两次表扫描(因此速度稍慢)。这个较低的锁级别允许在构建索引时继续对表进行读写。

单个表上的多个并发索引创建不会从两个创建索引中同时返回。。。语句,直到最慢的语句完成。

同时创建索引。。。可能不会在事务内部执行,但会在内部维护事务。这种保持打开事务的方式意味着,在索引构建开始之后,直到构建完成之前,自动清空(针对系统中的任何表)都无法清理引入的死元组。如果有一个更新量很大的表(如果更新到非常小的表,情况尤其糟糕),这可能会导致非常不理想的查询执行。

同时创建索引。。。必须等待使用该表的所有事务完成后才能返回。

下降指数的标准形式。。。在删除索引时获取对具有索引的表的访问独占锁。对于小索引,这可能是一个短期操作。但是,对于大型索引,文件系统断开链接和磁盘刷新可能需要大量时间。相比之下,表单删除索引。。。获取共享更新独占锁以执行这些操作,允许在删除索引时继续对表进行读写。

同时删除索引。。。不能用于删除任何支持约束(例如主键或唯一)的索引。

同时删除索引。。。可能不会在事务内部执行,但会在内部维护事务。这种保持打开事务的方式意味着,在索引构建开始之后,直到构建完成之前,自动清空(针对系统中的任何表)都无法清理引入的死元组。如果有一个更新量很大的表(如果更新到非常小的表,情况尤其糟糕),这可能会导致非常不理想的查询执行。

同时删除索引。。。必须等待使用该表的所有事务完成后才能返回。

注意:同时删除索引。。。在Postgres 9.2中添加。如果你';如果仍在运行9.1或更早版本,则可以通过将索引标记为无效且未准备好写入、使用pgfincore扩展刷新缓冲区以及删除索引来获得类似的结果。

改变索引。。。重命名为。。。需要索引上的访问独占锁,阻止对基础表的读取和写入。然而,最近的一项承诺预计将成为Postgres 12的一部分,降低了共享独家更新的要求。

重新索引索引。。。需要索引上的访问独占锁,阻止对基础表的读取和写入。相反,我们使用以下程序:

注意:如果需要重建的索引支持一个约束,请记住也要重新添加约束(根据我们记录的所有注意事项)。

从列中删除现有的NOTNULL约束需要在执行简单目录更新时对表进行独占锁定。

相反,向现有列添加NOTNULL约束需要表上的独占锁,而完整表扫描验证不存在空值。相反,你应该:

使用ALTER TABLE添加一个CHECK约束,要求列不为空<;表>;添加约束<;名称>;检查(<;column>;不为空)无效;。无效项告诉Postgres它没有';不需要扫描整个表来验证所有行是否满足条件。

使用ALTER TABLE验证约束<;表>;验证约束<;name>;;。使用此语句,PostgreSQL将阻止获取表的其他独占锁,但不会阻止读取或写入。

额外好处:目前有一个补丁正在开发中(可能它会进入Postgres 12),如果已经存在检查约束(如我们上面创建的)的话,它将允许您在不进行完整表扫描的情况下创建一个非空约束。

改变桌子。。。ADD FOREIGN KEY要求在修改的表和引用的表上都有一个共享行独占锁(从9.5开始)。而这场胜利';t阻塞SELECT查询,长时间阻塞行修改操作对于我们的事务处理应用程序来说同样是不可接受的。

改变桌子。。。添加外键。。。无效:添加外键并开始对所有新的INSERT/UPDATE语句强制执行约束,但不验证所有现有行是否符合新约束。此操作仍然需要共享行独占锁,但这些锁仅短暂保持。

改变桌子。。。验证约束<;约束>;:此操作将检查所有现有行,以验证它们是否符合指定的约束。验证需要共享更新独占,因此可以与行读取和修改查询同时运行。

改变桌子。。。添加约束。。。检查(…)需要访问独占锁。然而,与外键一样,Postgres支持将操作分为两个步骤:

改变桌子。。。添加约束。。。检查(…)无效:添加检查约束,并开始对所有新的INSERT/UPDATE语句强制执行该约束,但不验证所有现有行是否符合新约束。此操作仍需要访问独占锁。

改变桌子。。。验证约束<;约束>;:此操作将检查所有现有行,以验证它们是否符合指定的约束。验证需要对修改后的表进行独占的共享更新,因此可以与行读取和修改查询同时运行。引用表上有一个行共享锁,它将在验证约束时阻止任何需要独占锁的操作。

改变桌子。。。添加约束。。。独特的(…)需要访问独占锁。然而,Postgres支持将操作分为两个步骤:

同时创建一个唯一的索引。此步骤将立即强制唯一性,但如果需要声明的约束(或主键),则继续单独添加约束。

使用已有的带有ALTER TABLE的索引添加约束。。。添加约束。。。唯一使用索引<;索引>;。添加约束仍然需要访问独占锁,但该锁将仅用于快速目录操作。

注意:如果指定主键而不是唯一键,则索引中的任何非空列都将变为非空。这需要一个完整的表格扫描,目前可以';这是不可避免的。有关更多详细信息,请参见非空约束。

改变桌子。。。添加约束。。。排除使用。。。需要访问独占锁。添加排除约束会生成支持索引,不幸的是,目前不支持使用现有索引(可以使用唯一约束)。

创建类型<;名称>;作为(…)和下拉式<;名称>;(在验证数据库中没有现有用法后)这两种方法都可以安全地完成,而不会出现意外锁定。

改变类型<;enum>;重命名值<;旧的>;到<;新>;在10级后加上。此语句不需要使用枚举类型的锁定表。

枚举在内部存储为整数,不支持有效范围内的间隙,删除一个值将导致当前值移位,并使用这些值重写所有行。PostgreSQL目前不支持从现有枚举类型中删除值。

我们';我们也很高兴地宣布,我们已经开放了我们的内部库pg_ha_迁移。这个Ruby gem在使用Ruby on Rails和/或ActiveRecord的项目中加强了DDL安全性,重点是明确选择折衷方案,避免不必要的魔法(以及相应的意外)。你可以在项目#39;这是自述。

[^locks hold]您可以找到活动的长期运行查询,以及它们通过以下查询锁定的表:https://gist.github.com/30b4779cb101c133859a1a11247233f1

[^relation Rewrited]通过查看运行语句后relfilenode值是否发生变化,可以查看DDL是否会导致关系被重写:https://gist.github.com/67687738c11f1f5ba8a04a7198d92715

[^dependent objects]通过运行以下语句,可以找到依赖于特定列的对象(例如索引):https://gist.github.com/5fac44d798bbce1d5d4f9c0bd57abb21