David Rowley 的这篇博文最初发表在 Microsoft TechCommunity上的 Azure Database for PostgreSQL 博客上。
我在 PostgreSQL 14 中关注的性能项目之一是加速 PostgreSQL 恢复和真空。在Microsoft的PostgreSQL 团队中,我大部分时间都与社区的其他成员一起致力于 PostgreSQL 开源项目。在 Postgres 14(由于在 2021 年第三季度发布)中,我提交了一项更改以优化 compactify_tuples 函数,以降低 PostgreSQL 恢复过程中的 CPU 使用率。PostgreSQL 14 中的这种性能优化使我们的崩溃恢复测试用例快了大约 2.4 倍。
该compactify_tuples函数在 PostgreSQL 内部使用:
- 当 PostgreSQL 在非干净关闭后启动时——称为崩溃恢复
- 物理备用服务器用来重放从主服务器到达的更改(如预写日志中所述)的恢复过程
- 由 VACUUM
所以好消息是,改进compactify_tuples将: 提高崩溃恢复性能;减少备用服务器的负载,使其能够更快地重放来自主服务器的预写日志;并提高 VACUUM 性能。
在这篇博文中,我们将compactify_tuples介绍对的更改、该函数过去如何工作,以及为什么 Postgres 14 中新重写的版本更快。
分析恢复过程突出了一个性能问题
在 PostgreSQL 中,预写日志(称为WAL)包含带有指令的记录集和与指令对应的数据。这些 WAL 记录描述了要对基础数据进行的更改。WAL 用于确保在将底层数据写入磁盘之前 PostgreSQL 停止运行的情况下,对数据的每个更改都是持久的。当 PostgreSQL 在关闭后重新启动时,自上次检查点以来的所有 WAL 都必须作为恢复过程的一部分进行重播。WAL 被重播,以便将更改重新应用于在数据库关闭之前可能尚未写出到磁盘的页面。
在运行 UPDATE-heavy OLTP 类型工作负载的基准测试后故意使数据库崩溃后,我们对 PostgreSQL 恢复过程进行了一些分析。这些配置文件表明,大部分 CPU 负载来自“HEAP2 CLEAN”WAL 记录的重放。HEAP2 CLEAN 在表中记录碎片整理页以删除死元组留下的可用空间。每当页面变满并且页面上需要更多空间时,就会将 HEAP2 CLEAN 记录添加到 WAL。
元组是 PostgreSQL 对表中一行的内部表示。单行可能有许多元组来表示它,但这些元组中只有一个适用于任何单个时间点。较旧的运行事务可能需要访问该行的较旧版本(另一个元组),以便他们可以访问事务开始时的行。对行的更新会导致创建元组的新版本。以这种方式为每一行创建多个版本称为多版本并发控制(MVCC)。
从堆页面中删除未使用的空间
要了解 HEAP2 CLEAN 在 PostgreSQL 中的作用,我们需要深入了解堆页面格式。让我们看一个典型的(简化的)堆页面,其中包含一些元组碎片:

我们可以看到,在页眉之后是一个“items”数组。这些项充当指向每个元组开头的指针。PostgreSQL 从末尾开始将元组写入页面并反向工作。当项目数组和元组空间重叠时,页面已满。
您还应该注意到页面末尾的元组与项目指针的顺序并不完全相反。元组 2 和 3 在这里出现乱序。在页面中的某些记录已更新并且旧的项目指针被重用后,元组可能会变得乱序。
我们还可以看到,图 1 中页面上有相当多的未使用空间。未使用空间是由于 VACUUM 删除了元组。HEAP2 CLEAN 操作将清除这个未使用的空间。
在 PostgreSQL 13 及更早版本中,HEAP2 CLEAN 操作会将上述内容转换为:
我们可以看到空白区域消失了,元组现在被推到了页面的末尾。请注意,元组保持与原来相同的顺序,元组 2 和 3 保持相同的交换顺序。
在 PostgreSQL 14 之前,堆页面压缩是如何工作的
该compactify_tuples函数为我们处理页面压缩操作。所做的所有更改都在compactify_tuples函数中。在此更改之前,该compactify_tuples函数将对 items 数组的副本执行排序。这种排序允许元组从页面末尾的元组开始移动。在这种情况下,元组从 tuple1 开始移动,然后是 tuple3、tuple2 和 tuple4。要确定首先移动哪些元组,请compactify_tuples使用通用 Cqsort函数和自定义比较器函数。以反向元组偏移顺序对项目数组进行排序允许从页面末尾的元组开始移动元组。
由于堆页面最多可以包含几百个元组,并且由于在 UPDATE 繁重的工作负载中可以压缩页面的频率,该qsort调用可能会使用大量 CPU。对于只包含几个元组的页面,排序开销并没有那么糟糕。
我们真的需要排序吗?......好吧,是和不是。如果我们按项目数组顺序移动元组并且没有排序,那么我们可以稍后在页面中覆盖元组。例如,在图 2 中,如果我们在移动 tuple3 之前将 tuple2 移向页面末尾,那么我们将覆盖 tuple3。当像这样就地移动元组时,我们必须确保首先移动页面末尾的元组。因此,我们在进行这种就地移动时必须进行排序。
如何使 HEAP2 CLEAN 更快?
为了加快在 HEAP2 CLEAN 期间完成的页面压缩,我们可以编写一个自定义qsort函数来内联比较器函数。创建自定义qsort会减少一些函数调用开销,但是,无论我们做什么,qsort平均复杂度仍为 O(n log n)。完全摆脱这种排序会很好。
该qsort只需要让我们不覆盖任何尚未要被移动的元组。因此,我们可以将这些元组复制到一个临时的内存缓冲区,而不是排序,这样我们移动元组的顺序就无关紧要了。
compactify_tuplesPostgreSQL 14 中的新版本完全消除了使用qsort. 删除排序允许我们以项数组顺序将元组移动到页面的末尾。临时内存缓冲区消除了元组在移动之前被覆盖的危险,也意味着元组以正确的顺序放回页面末尾的第一个元组。
新的堆页面格式,在 PostgreSQL 14 中压缩后,如下所示:
请注意,元组 2 和 3 现在交换了位置,并且元组现在处于反向项目数组顺序。
新的 Postgres 14 代码通过预检查进一步优化,看看元组是否已经在正确的反向项指针偏移顺序中。如果元组的顺序正确,则不需要使用临时缓冲区。然后我们只移动页面中比第一个空白区域更早(向后工作)的元组。其他元组已经在正确的位置。此外,现在我们再次将元组放回反向项指针顺序,我们更频繁地遇到这种预先排序的情况。平均而言,我们只会移动页面上的一半元组。生成新项目指针的新元组也将为我们维护这个顺序。
与元组在页面中的随机顺序相比,将元组按相反的项目顺序还可以帮助某些 CPU 架构更有效地预取内存。
现在 PostgreSQL 14 中的恢复过程有多快?
我的测试用例使用了一个包含两个 INT 列和一个 85 的“填充因子”和 1000 万行的表。考虑到元组标头,这允许在每 8 KB 页面上容纳最多 226 个元组。
为了生成一些 WAL 来重放,我使用了pgbench,一个 PostgreSQL 附带的简单基准测试程序,对随机行执行 1200 万次更新。1000 万行中的每一行将平均收到 12 次更新。然后我对 PostgreSQL 进行了非正常关闭并再次启动它,迫使数据库执行崩溃恢复。在性能提升之前,崩溃恢复需要 148 秒才能重放 2.2 GB 的 WAL。
新版本的compactify_tuples代码进行了同样的测试,耗时 60.8 秒。通过此更改和给定的测试用例,PostgreSQL 的崩溃恢复速度提高了约 2.4 倍。
| 在更改为压缩 | 更改为压缩 | 紧凑化 | |
|---|---|---|---|
| 是时候重放 2.2 GB WAL 作为崩溃恢复的一部分了 | 148 秒 | 60.8 秒 | 快 2.4 倍 |
以前,在compactify_tuples更改之前,具有大量元组的页面是压缩最慢的。这是因为qsort大型阵列需要更长的时间。更改后compactify_tuples性能与页面上不同数量的元组更加一致。即使每页只有很少的元组,这种变化仍然会导致页面上的小幅加速。但是,当每页的元组数量很大时,这种更改最有帮助。
PostgreSQL 14 中的 VACUUM 性能也有所提高
VACUUM(和 autovacuum)在从堆页面中删除死元组后碰巧使用相同的代码。因此,加速compactify_tuples也意味着真空和自动真空的良好性能改进。我们尝试对先前更新的基准表执行 VACUUM,发现 VACUUM 现在在 PostgreSQL 14 中的运行速度比compactify_tuples更改前快 25% 。以前清空表需要 4.1 秒,更改后时间减少到 2.9 秒。
加快恢复过程还意味着物理备用服务器更有可能跟上步伐,并在生成主 WAL 时尽快重放它。因此,进行此更改compactify_tuples也意味着备用服务器不太可能落后于主服务器。
因此,PostgreSQL 14 中的恢复过程和真空将更快——而且还有更多的工作正在进行中
compactify_tuples在许多情况下,更改确实有助于提高恢复过程的性能。但是,恢复过程在 I/O 而不是 CPU 上遇到瓶颈也很常见。当恢复大于系统可用 RAM 的数据库时,恢复通常必须等待从磁盘读取页面,然后才能对其应用任何更改。幸运的是,我们还在研究一种方法,让恢复进程将页面预取到内核的页面缓存中,这样物理 I/O 就可以在后台并发进行,而不是让恢复进程等待它。
文章来源:https://www.citusdata.com/blog/2021/03/25/speeding-up-recovery-and-vacuum-in-postgres-14/




