暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

ZHEAP:重造PostgreSQL的存储引擎

4729

作者简介

Hans-Jürgen Schönig, 从90年代就开始使用PostgreSQL。他是CYBERTEC的首席执行官和技术主管,CYBERTEC是该领域的市场领导者之一,自2000年以来为全球无数客户提供服务。

译者简介    

朱君鹏,博士研究生。主要研究方向为数据库管理系统,尤其是内存数据库、事务处理系统、软硬件协同设计、日志系统。

校对者简介

赵全明 任职于华为技术有限公司,数据库内核开发工程师,曾参与云数据库及GaussDB多个项目的研发,致力于PostgreSQL在全行业的应用与推广。

在PostgreSQL中,自从最初的MVCC模型被构想出来以后,表膨胀一直是一个主要的问题。因此,我们决定写一系列的博客文章来详细地讨论这个问题。首先,什么是表膨胀?表膨胀指的是尽管存储在数据库中的数据并没有增长,但表或者索引的大小却在增长。如果想要支持事务(transactions),绝对有必要在数据被修改时不要覆盖数据,因为我们必须要知道,人们可能希望在修改或者回滚事务时依旧能够读取旧的数据行。
因此,表膨胀是PostgreSQL MVCC机制的一个固有特点。然而,PostgreSQL存储数据和操作事务的方法并不是数据库处理事务和并发的唯一方法。让我们看看还有哪些其它的思路:
MS SQL把旧版本数据存放在tempdb,然而在Oracle和MySQL会将旧版本数据放到重做日志(redo log)中。正如您知道的那样,PostgreSQL在更新(UPDATE)时会复制行,并将它们存储在同一个表中。Firebrid(译者注:一款开源数据库)也以内联的方式存储旧的行数据。
我在这里主要说两点:
l 删除旧行非常困难
l 没有不折衷的解决办法
删除旧行绝对是一个问题。在PostgreSQL中,删除旧行通常是通过VACUUM完成的。然而,在某些情况下,VACUUM跟不上或者由于一些其他原因(原因通常是由于长事务)表空间在增长。我们已经在CYBERTEC上已经发表了大量关于这种情况的文章。
“没有不折衷的解决办法”也是存储的一个重要方面。没有完美的存储引擎,只存在能够服务好特定工作负载的存储引擎。对于PostgreSQL也是如此:当前的表格式对于很多工作负载而言非常理想。然而,也存在不足的一面,就是我们开篇提到的:表膨胀。如果您正在运行更新(update)密集型的工作负载,那么一定会出现表的大小难以控制的情况,如果开发人员和系统管理员起初并不完全了解PostgreSQL的内部工作原理,则尤其如此。
Zheap:控制好膨胀
Zheap是一种控制表膨胀的方法,它实现了一个存储引擎,能够更有效地运行更新密集型的工作负载。该项目最初由EnterpriseDB发起,在这个项目已经投入了很多努力。
为了使zheap准备好投入生产,我们很自豪地宣布,我们在Heroic Lan的合作伙伴已经承诺为zheap的进一步开发提供资金,并向社区发布所有代码。CYBERTEC已经决定将资金增加一倍,并提供额外的专业知识和人力来推动这一项目的发展。如果有个人、公司等也有兴趣帮助我们推进zheap,我们渴望与所有愿意让这一伟大技术取得成功的任何人合作。
让我们来看看只要的设计目标:
l 执行原地更新(in-place update)
l 拥有更小的表(更小的元组头,改进的对齐)
l 尽可能减少写操作(除非数据被修改,否则避免弄脏页(dirtying pages))
l 更快地重用空间
因此,让我们看看这些目标总体上是如何实现的。
Zheap的基本设计
zheap是一个全新的存储引擎,因此深入研究它的基本架构是有意义的。三个重要组成部分必须协同工作::
l zheap:表格式
l undo’:处理事务回滚等
l WAL:保护重要写
让我们先看看zheap页面的布局。正如你所知道的PostgreSQL通常将表看作8k块序列,所以页面布局绝对是重要的:

乍一看,这个图看起来几乎像一个标准的PostgreSQL 8k页面,但实际上并不是。您可能注意到的第一件事是,tuple在页面的存储顺序与item(处在页面的前面,用来指向页面)相同,从而可以更快地扫描。第二件引起我们注意的是在页面末尾出现的槽位。在标准的PostgreSQL表中,可见性信息作为行的一部分被存储起来,这需要大量的空间。在zheap中,事务信息被移到特定页面上,这大大减少了数据的大小(它反过来转化为更好的性能)。一个事务占用16字节的存储空间,包含事务id、epoch和该事务最新的undo记录指针。一行指向一个事务槽。表中缺省的事务槽数是4,这对于大表来说通常是可以的。然而,有时需要更多的事务槽。在本例中,zheap有一个称为TPD的东西,它只不过是一个溢出区域,用于根据需要存储额外的事务信息。
这里是TPD的基本布局:

有时一个页面需要许多事务槽。TPD提供了一种灵活的方式来处理这个问题。问题是:zheap在哪里存储TPD数据?答案是:这些特殊页面与标准数据页面交交错在一起。它们只是用一种特殊的方式标记,以确保顺序扫描时不会扫描。为了跟踪这些特殊用途的页面,zheap使用一个元页面来跟踪它们:

TDP只是使事务槽更具伸缩性的一种方法。在块中设置一些槽可以减少过度触碰页面的需要。如果需要更多,那么TPD是一个很好的解决办法。在某种程度上,这是两全其美的。

当事务结束后,事务槽可以重用。

zheap:元组格式

这个难题的下一个重要部分是单个元组的布局:在PostgreSQL中,标准的堆元组有一个20+字节的头,因为所有的事务信息都存储在一个元组中。但在这个例子中并非如此。所有事务信息已经被移动到页级结构(事务槽)中。这一点超级重要:元组头因此减少到只有5个字节。但是这里还有更多的优化:标准元组必须在元组头和实际数据行之间使用CPU对齐(填充),表中每一行都需要占用一些字节。zheap没有这样做,导致存储更紧凑。通过删除按值传递数据类型的填充可以节省额外的空间。所有这些优化都意味着我们可以在表的每一行中节省宝贵的空间。

注:左半图为标准PostgreSQL元组头的布局, 右半图为zheap元组头的布局

正如您所看到的,zheap元组比普通的堆元组要小得多。由于事务信息已经统一在事务槽机制中,我们不再需要处理行级别的可见性,反而可以在页面级别更高效地进行处理。

通过缩小存储空间,zheap将有助于提高性能。

UNDO:使事情井然有序

在谈论zheap时,最重要的事情之一是“undo”的概念。首先,这件事的目的是什么?让我们来看看:考虑以下操作:

为了确保事务能够正确地操作,更新不能只是覆盖旧的值并忘记它。原因有二:首先,我们希望支持并发。当数据被修改时,许多用户仍然应该能够读取数据(译者注:MVCC特性:读不阻塞写,写不阻塞读)。其次,更新一行并不一定意味着事务会被提交。因此,我们需要有效地处理回滚。经典的PostgreSQL存储格式将简单地复制标准堆中的行,这将导致我们已经在博客中讨论过的所有膨胀相关问题。

zheap处理事情的方法有点不同: 在进行了修改的情况下,系统会写撤消信息来修复它,以防事务由于某种原因不得不中止。这适用于插入、更新和删除。让我们一个一个地看一下这些操作,看看它是如何工作的:

INSERT:添加行

在INSERT的情况下,zheap必须分配一个事务槽,然后产生一个undo条目,用来处理error的情况。在插入时,TID是undo所需要的最相关的信息。在回滚INSERT后,空间可以立即回收,这是zheap和PostgreSQL中的标准堆表的主要差异。

UPDATE:修改数据

UPDATE语句要复杂得多:基本上有两种情况:

l 新行适合旧的空间

l 新行不适合旧的空间

如果旧行比新行短,我们可以简单地覆盖它,并产生一个包含完整旧行的undo条目。简而言之:我们在zheap中保存新行,在undo中保存旧行的副本,以便在需要时将其复制回来。

如果新行不适合怎么办?在这种情况下,性能会更差,因为zheap本质上不得不执行删除/插入操作,这当然不如就地UPDATE高效。

如下的情况下,可以立即回收空间:

l 将行更新为较短版本时

l 当执行非就地更新时

DELETE:删除行

最后是删除。为了处理行的删除,zheap必须产生一个undo记录,以便在回滚时将旧行放回原地。在删除期间,必须从zheap中删除行。

UNDO和ROLLBACK

到目前为止,我们已经讨论了很多关于UNDO和ROLLBACK的内容。但是,让我们再深入一点,看看UNDO、ROLLBACK等等是如何交互的。

如果发生ROLLBACK,UNDO必须确保恢复表的旧状态。因此,我们之前计划的UNDO操作必须执行。在出现错误的情况下,UNDO操作将作为新事务的一部分来执行,以确保成功。

理想情况下,与单个页面相关的所有UNDO操作都是一次性来应用的,以减少必须的WAL日志数量。这种策略的一个好处是,我们可以将页面级锁定减少到绝对最小,从而减少争用,因此有助于提高性能。

到目前为止,这听起来很简单,但是让我们考虑一个重要的用例:当一个长事务发生时,会发生什么?如果必须一次回滚万亿的数据,会发生什么情况?最终用户当然不喜欢无休止的回滚。同样值得记住的是,我们还必须为回滚期间的崩溃做好准备。

如果undo操作大于某个可配置的阈值,任务将由后台工作进程完成。这是一个非常优雅的解决方案,有助于维护良好的最终用户体验。

撤销(undo)本身可以在以下三种情况下删除:
l 当没有事务在读该数据
l 当所有undo操作完成
l 对于已提交的事务,直到它们都是可见的
让我们来看看一个基本的架构图:

正如你所看到的,这个过程非常复杂。

索引:简短说明

为了确保zheap可以替代当前堆的方式,重要的是保持索引代码不变。zheap可以使用PostgreSQL的标准访问方法。当然还有办法让事情变得更有效率。但是,此时不需要对索引代码进行更改。这也意味着PostgreSQL中所有可用的索引类型仍然都是完全可用的,没有已知的限制。

最后…

目前,该技术仍处于开发阶段,我们很高兴Heroic Lab为进一步发展该技术所做的贡献。到目前为止,我们已经实现了对zheap的逻辑解码,并添加了对PostgreSQL的支持。我们将继续投入更多的资源来推动该工具,使其能够用于生产。

如果你现在想阅读更多关于PostgreSQL和VACUUM的内容,请查看我们之前关于这个主题的文章。此外,我们也想邀请您继续定期访问我们的博客,以了解更多关于信息技术和其他有趣的技术。

相关链接

https://www.cybertec-postgresql.com/en/reasons-why-vacuum-wont-remove-dead-rows/




PostgreSQL中文社区欢迎广大技术人员投稿
投稿邮箱:press@postgres.cn

文章转载自PostgreSQL中文社区,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论