PostgreSQL14加速恢复与VACUUM
我关注的PG14的性能项目其中一个是加速恢复与VACUUM。在微软的PG团队中,我和PG社区其他成员大部分时间一起致力于PG开源项目。在PG14中(的Q3),我提交了一个patch优化compactify元组函数,减少恢复时CPU的使用。这项性能优化可以使PG14的恢复快2.4倍。
compactify_tuples函数在PG内部使用的地方:
1)崩溃恢复时
2)备机回放
3)VACUUM
因此,好消息是,改进compactify_tuples可以提高崩溃恢复性能;减少备机负载,使其更快回放来自主的日志;提高VACUUM性能。
本文中,我们将介绍compactify_tuples函数的改进,该函数过去如何工作,以及PG14改写后为什么更快。
剖析恢复进程突出一个性能问题
PG中WAL日志包含指令及数据。描述了对基础数据进行修改。WAL确保数据持久化。当PG重启时,恢复进程将最近执行检查点位置之后的日志全部回放。顺序回放WAL日志,将数据库关闭时未刷写到磁盘的更改回放到数据页中。
在运行UPDATE密集型工作负载后,故意使数据库崩溃,我们对恢复过程进行了一些分析。CPU负载大部分来自“HEAP2 CLEAN”WAL记录的回放。HEAP2 CLEAN记录通过清除死记录占用空间来整理页内碎片。当页面变满,并需要更多空间时,就会产生HEAP2 CLEAN日志。
一个tuple是表中行的内部表示。一行可能有很多tuple,但任何一个时间点,仅一个tuple有效。老事务可能访问旧版本,以便访问事务开始时的行。UPDATE会产生新版本。创建每个行的多个版本被叫做多版本并发控制(MVCC)。
从heap页中清理未使用的空间
为理解HEAP2 CLEAN在PG中如何工作,需要首先了解下heap页结构。我们看下带有元组碎片的heap页:
图1 由于删除了元组而出现碎片的heap页
我们可以看到,每个页头后面都有一个“items”数组。这些item指向每个元组。PG从页尾开始将元组写入页面,当items数组和元组空间重叠时,页面变满。需要注意,页尾处的元组和item指针的顺序并不是完全反向相反。元组2和3在这里出现了乱序。在页面更新了一些记录并旧的item指针被重用后,元组就会变得乱序。我们还可以看到,图1中的页面有很多未使用的空间。未使用的空间是由于VACUUM删除了元组。HEAP2 CLEAN操作将清除这些未使用的空间。
在PG13及更早的版本中,HEAP2 CLEAN操作会将上述内容转换为:
图2 PG14之前碎片整理的heap页
我们可以看到空白区域小时了,元组紧凑移动到了页尾。注意,元组和原来保持相同顺序。元组2和3保持交叉顺序。
PG14之前heap页compacification怎么工作
comactify_tuples函数为我们处理页面compact工作。更改前,compactify_tuples函数会对item副本执行排序。这种排序允许元组移动到页尾。本案例中,先移动tuple1,然后tuple3,接着tuple2,最后tuple4。为决定哪个tuple先移动,compactify_tuples使用qsort函数用于比较并排序。以反向偏移顺序对item数组进行排序,允许从页面尾部的元组开始移动。
由于heap页面最多可以版本几百个元组,并且由于update负载中compact的频率导致qsort耗费大量CPU。当然,如果页面仅有几个元组,排序的代价就不高了。
那么是否需要排序?是,也不是。如果以item数组的顺序移动元组,不进行排序,我们可以在稍后的页面中覆盖元组。例如,图2中,如果在移动tuple3前移动tuple2,那么我们将覆盖tuple3。
当像这样移动元组时,必须确保先将该位置的元组移动到页尾。因此对于这种本地移动必须进行排序。
怎样使HEAP2 CLEAN更快?
为了在执行HEAP2 CLEAN时,加快页面compact,我们需要写一个自定义qsort函数内联操作符函数。创建一个通用qsort函数会减少一些函数调用代价,但是不管怎么做qsort的复杂度都是O(n log n)。完全摆脱这种函数会更好。
使用qsort仅保证不会覆盖。因此可以将这些元组拷贝到一个临时缓冲区,这样移动元组的顺序就无关紧要了。
PG14中compactify_tuples函数完全不需要使用qsort。不用排序,可以使我们以item数组的顺序移动tuple到页尾。临时内存避免了元组在移动前被覆盖的风险,也意味着元组以正确的顺序放回到页面尾处。PG14进行compact后新的heap页为:
图3 PG14性能提升后新的compact heap页
注意,元组2和3交换了位置,并且元组现在处于item反向顺序。
新的PG14代码通过预检查进一步优化,看元组是否已将在正确的反向item指针顺序中。如果元组顺序不正确,则不需要使用临时缓冲区。然后仅移动比第一个空白空间更早的元组。其他元组已经在正确位置。现在我们再次将元组以item指针反向顺序放回元组,我们更加频繁地遇到这种预先排序的情况。平均而言,我们金辉移动页面上一半元组。新元组产生新的item指针也会维护这样的顺序。
与元组在页面中的随机顺序相比,让元组以反向顺序还可以帮助某些CPU架构更有效地预取。
现在PG14有多快?
我们的测试用力使用了包含2个INT列,填充因子为85的1000万行数据。考虑元组头,允许在每8KB页面存储最多226个元组。为了生成一些WAL来回放,使用Pgbench,随机执行1200万次更新。1000万行中每行平均有12次更新。然后非正常关闭,重启进行崩溃恢复。在性能提升前,崩溃恢复需要148秒才能重放2.2GB的WAL。
新版本的compactify_tuples代码进行同样的测试,耗时60.8秒。崩溃恢复的性能提升约2.4倍。
以前在compacity_tuples更改前,具有大量元组的页面进行compact很慢,因为qsort需要很长时间。更改后,不同数量的页面进行compact基本一致。即使每个页有很少元组,这种变化仍然会小幅提升性能。但是,当元组数量巨大时,更加有效。
PG14中的VACUUM性能也有所提升
VACUUM和(autovacuum)在heap页面中删除死元组也使用相同代码。因此加速compactify_tuples也意味着VACUUM性能有很好改进。我们尝试对之前更新的基准测试表执行VACUUM,发现PG14中运行速度比更改前快25%。以前需要4.1秒,现在仅2.9秒。
加速恢复过程还意味着备机更有可能跟上主,并在产生日志后很快回放掉。这意味着可以帮助备不落后主。
因此恢复进程和VACUUM在PG14中更快--而且还有很多工作正在进行中
compacity_tuples在很多情况下确实有助于提高恢复性能。但是恢复过程在IO上遇到瓶颈而不是CPU也很场景。当恢复的数据库大于可用内存时,必须等待从磁盘中读取页然后进行回放。幸运的是,我们还在研究一种方法,让恢复进程将页面预取到内核的页面缓冲中,这样物理IO就可以在后台并发进行,而不是让恢复进程等待。
更多信息可以查看邮件列表的讨论:
/message-id/CA+hUKGKMQFVpjr106gRhwk6R-nXv0qOcTreZuQzxgpHESAL6dw@
原文
/t5/azure-database-for-postgresql/speeding-up-recovery-and-vacuum-in-postgres-14/ba-p/2234071