PostgreSQL内存上下文系统设计概述

PostgreSQL内存上下文系统设计概述

原文:src/backend/utils/mmgr/README

背景

我们在"内存上下文"中进行大部分内存分配,通常是AllocSets由src/backend/utils/mmgr/aset.c实现。在没有大量开销的情况下成功进行内存管理的关键是定义一组具有适当生命周期的有用上下文。

内存上下文的基本操作是:

  • 创建一个上下文

  • 在上下文中分配一块内存(相当于标准C库的malloc())

  • 删除一个上下文(包括释放其中分配的所有内存)

  • 重置上下文(释放上下文中分配的所有内存,但不释放上下文对象本身)

  • 查询分配给上下文的内存总量(上下文从中分配块的原始内存;而不是块本身)

给定先前从上下文分配的一块内存,可以释放它或重新分配更大或更小的内存(对应于标准C库的free()和realloc()进程)。这些操作将内存返回到块最初分配的同一上下文或从该上下文中获取更多内存。

始终存在由CurrentMemoryContext全局变量表示的"当前"上下文。palloc()在该上下文中隐式分配空间。MemoryContextSwitchTo()操作选择一个新的当前上下文并返回之前的上下文,以便调用者可以在退出之前恢复之前的上下文)。

与普通使用malloc/free相比,内存上下文的主要优点是可以轻松释放内存上下文的全部内容,而无需请求释放其中的每个单独块。这比按块记录更快、更可靠。我们利用在事务结束时进行清理:通过重置事务的所有活动上下文或缩短生命周期,我们可以回收所有瞬态内存。类似地,我们可以在每个查询结束时或在查询期间处理每个元组之后进行清理。

关于pallocAPI与标准C库的一些注意事项

palloc及其其它pg内存分配函数的行为与标准C库的malloc及其它内存分配函数类似,但也有一些故意的差异。以下是一些澄清行为的注释。

  • 如果内存不足,palloc和repalloc通过elog退出(错误)。它们从不返回NULL,并且测试这样的结果没有必要也没有用。通过palloc_extended(),可以使用MCXT_ALLOC_NO_OOM标志覆盖该行为。

  • palloc(0)是明确有效的操作。它不返回NULL指针,而是返回一个不能使用任何字节的有效块。然而该块稍后可能会被重新分配得更大;它也可以被释放而不会出现错误。类似地,repalloc允许重新分配到零大小。

  • pfree和repalloc不接受NULL指针(故意这样设计的)。

当前内存上下文(TheCurrentMemoryContext)

CurrentMemoryContext的基本概念。如果没有它,copyObject进程将需要传递上下文,返回引用传递数据类型的函数执行进程也是如此。同样,对于在内部临时分配空间但不将其返回给调用者的进程?我们当然不希望系统中的每个调用都因"这里是用于您可能想要执行的任何临时内存分配的上下文"而变得混乱。

不过这种推理的结果是,如果可能的话CurrentMemoryContext通常应该指向短生命周期上下文。在查询执行期间,它通常指向一个在每个元组之后重置的上下文。只有在"非常"受限的代码中,它才应该指向具有大于事务生命周期的上下文,因为这样做会带来永久内存泄漏的风险。

pfree/repalloc不依赖于CurrentMemoryContext

pfree()和repalloc()可以应用于任何块,无论它是否属于CurrentMemoryContext------无论如何该块的所属上下文都将被调用来处理操作。

"父"和"子"上下文("Parent" and "Child" Contexts)

如果所有上下文都是独立的,那么就很难跟踪它们,尤其是在错误情况下。这是通过创建"父"和"子"上下文树来解决的。创建内存上下文时,可以将新上下文指定为某个现有上下文的子上下文。一个上下文可以有多个子级,但只能有一个父级。通过这种方式,上下文形成了一个森林(不一定是一棵树,因为可能有多个顶级上下文;尽管在当前实践中只有一个顶级上下文,即TopMemoryContext)。

删除上下文也会删除其所有直接和间接子级。重置上下文时,删除子上下文几乎总是更有用,因此MemoryContextReset()意味着,如果您确实想要一个空上下文树,则需要调用MemoryContextResetOnly()加上MemoryContextResetChildren()。

这些功能使我们能够管理大量上下文,而不必担心某些上下文会被泄露;我们只需要跟踪我们将在事务结束时删除的一个顶级上下文,并确保我们创建的任何短期上下文都是该上下文的后代。由于树可以有多个级别,因此我们可以轻松处理存储的嵌套生命周期,例如每个事务、每个语句、每个扫描、每个元组。仅部分重叠的存储生命周期可以通过从上下文林的不同树进行分配来处理(下一节中有一些示例)。

为了方便起见,我们还提供"重置/删除给定上下文的所有子级,但不重置或删除该上下文本身"之类的操作。

内存上下文重置/删除回调

Postgres9.5中引入的一项功能允许使用内存上下文来管理更多资源,而不仅仅是普通的分配内存。这是通过为内存上下文注册"重置回调函数"来完成的。这样的函数将在下次重置或删除上下文之前调用一次。它可用于放弃在某种意义上与上下文中分配的对象关联的资源。可能的用例包括

  • 关闭与元组排序对象关联的打开文件;
  • 释放长期缓存对象的引用计数,这些对象由正在重置的上下文中的某个对象持有;
  • 释放与某些palloc对象关联的malloc管理的内存。
    最后一种情况仅代表Postgres代码的不良编程实践;最好在目标上下文或某个子上下文中使用palloc进行所有分配。然而,对于与非Postgres库交互的代码来说,它很可能派上用场。

可以为内存上下文建立任意数量的重置回调;它们以与注册相反的顺序调用。此外,如果重置或删除上下文树,则附加到子上下文的回调会在附加到父上下文的回调之前调用。

此API要求调用者提供MemoryContextCallback内存块来保存回调的状态。通常,应将其分配在逻辑附加的同一上下文中,以便在使用后自动释放。要求调用者提供此内存的原因是,在大多数使用场景中,调用者将在目标上下文中创建一些更大的结构,并且可以通过包含以下内容来"免费"创建MemoryContextCallback结构,而无需单独的palloc()调用它在这个更大的结构中。

实践中的内存上下文

全局已知的上下文

有一些众所周知的上下文通常是通过全局变量引用的。在任何时刻,系统都可能包含许多附加上下文,但所有其他上下文应该是这些上下文之一的直接或间接子级,以确保它们在发生错误时不会泄漏。

  • TopMemoryContext---这是上下文树的实际顶层;所有其他上下文都是该上下文的直接或间接子上下文。这里的分配本质上与"malloc"相同,因为这个上下文永远不会被重置或删除。这是为了那些应该永远存在的东西,或者是为了控制模块将在适当的时间删除的东西。一个例子是fd.c的打开文件表。除非确实必要,否则请避免在此处分配内容,尤其是避免使用指向此处的CurrentMemoryContext来运行。

  • PostmasterContext---这是postmaster的正常工作上下文。后端启动后删除PostmasterContext释放postmaster启动时使用的不需要的内存副本。请注意,在非EXEC_BACKEND版本中,postmaster的pg_hba.conf和pg_ident.conf数据副本在后端进程的身份验证期间直接使用;因此,在完成之前后端无法删除PostmasterContext。(postmaster只有TopMemoryContext、PostmasterContext和ErrorContext---其余的顶级上下文在启动期间在每个后端中设置。)

  • CacheMemoryContext---relcache、catcache和相关模块的永久存储。它也永远不会被重置或删除,因此没有必要将其与TopMemoryContext区分开来。但为了调试目的而保持这种区别是值得的。(注意:CacheMemoryContext的子上下文的生命周期较短。例如,子上下文是保存与relcache条目关联的辅助存储的最佳位置;这样我们就可以轻松地释放规则解析树等,而不必依赖于构造freeObject()的可靠版本。)

  • MessageContext---此上下文保存来自前端的当前命令消息,以及仅需要与当前消息一样长的任何派生存储(例如,在简单查询模式下,解析树和计划树可以存在于此)。在PostgresMain外循环的每个循环的顶部,将重置此上下文,并删除所有子级。这与每个事务和每个门户上下文分开,因为查询字符串可能需要比任何单个事务或门户生存更长或更短的时间。

  • TopTransactionContext---这保存了直到顶级事务结束为止的所有内容。在每个顶级事务周期结束时,此上下文将被重置,并删除其所有子上下文。在大多数情况下,您不想直接在此处分配内容,而是在CurTransactionContext中;这里真正属于的是显式存在的控制信息,用于管理跨多个子事务的状态。注意:出现错误时不会立即清除此上下文;其内容将一直存在,直到事务块通过COMMIT/ROLLBACK退出。

  • CurTransactionContext------它保存必须存活到当前事务结束的数据,特别是在顶级事务提交时需要的数据。当我们处于顶级事务中时,这与TopTransactionContext相同,但在子事务中它指向子上下文。重要的是要理解,如果子事务中止,则其CurTransactionContext在完成中止处理后将被丢弃;但已提交的子事务CurTransactionContext一直保留到顶级提交(当然,除非子事务的中间级别之一中止)。这确保了我们不会将失败的子事务中的数据保留超过必要的时间。由于这种行为,您必须在子事务中止期间小心地进行正确清理------子事务的状态必须与上层事务中保存的任何指针或列表解除链接,否则您将有悬空指针,导致顶级提交时崩溃。这里保存的数据的一个示例是挂起的NOTIFY消息,这些消息在顶级提交时发送,但前提是生成的子事务没有中止。

  • PortalContext---这实际上不是一个单独的上下文,而是一个指向当前活动执行门户的每个门户上下文的全局变量。如果需要分配与当前门户执行所需时间一样长的存储空间,则可以使用此方法。

  • ErrorContext---这个永久上下文被切换到错误恢复处理,然后在恢复完成时重置。我们安排始终有几KB的可用内存。这样,即使后端内存不足,我们也可以确保有一些内存可用于错误恢复。这允许将内存不足视为正常错误情况,而不是致命错误。

预备语句和执行平台(portal)的上下文

预备语句对象具有关联的私有上下文,其中存储查询的解析树和计划树。由于这些树对于执行器来说是只读的,因此预备语句可以多次重复使用,而无需进一步复制这些树。

execution-portal(执行平台)对象具有一个私有上下文,当execution-portal处于活动状态时,该上下文由PortalContext引用。对于由DECLARE CURSOR创建的平台,此私有上下文包含查询解析和计划树(没有其他对象可以容纳它们)。从预备语句创建的平台仅引用准备好语句树,不需要在其私有上下文中分配任何存储。

逻辑复制工作线程上下文

ApplyContext------在应用工作的整个生命周期内永久存在。此处也可以使用TopMemoryContext,但为了简化内存使用分析,我们启动不同的上下文。

ApplyMessageContext---在处理每个逻辑复制协议消息后重置的短期上下文。

执行期间的瞬态上下文

创建预备语句时,解析树和计划树将构建在MessageContext子级的临时上下文中(以便在出错时它将自动消失)。成功后,完成的计划将被复制到预备语句的私有上下文中,并释放临时上下文;这允许规划器在执行开始之前恢复临时空间。(在简单查询模式下,不需要额外的复制步骤,因此规划器临时空间会一直保留到查询结束。)

顶级执行程序进程以及大多数"计划节点"执行代码通常将在由ExecutorStart创建并由ExecutorEnd销毁的上下文中运行;此上下文还保存在ExecutorStart期间构建的"计划状态"树。这些进程中分配的大部分内存旨在一直存在到查询结束。执行器的顶级上下文是PortalContext的子级,即代表查询执行的平台上下文。

执行器中的主要内存管理考虑因素是表达式求值(无论是用于质量测试还是用于目标列表条目的计算)都不能发生内存泄漏。为此,执行器中创建的每个ExprContext(表达式求值上下文)都有一个与之关联的私有内存上下文,并且在计算该ExprContext中的表达式时,我们会切换到该上下文。拥有ExprContext的计划节点负责在不再需要表达式求值结果时将私有上下文重置为空。通常,重置是在计划节点中每个元组获取周期开始时完成的。

请注意,此设计为每个计划节点提供了自己的表达式评估内存上下文。这对于正确处理嵌套连接是必要的,因为外部计划节点可能需要保留它在从内部节点获取下一个元组时计算出的表达式结果------但内部节点可能会在返回之前执行许多元组循环和许多表达式。内部节点必须能够在每个外部元组循环中多次重置其自己的表达式上下文。幸运的是内存上下文足够便宜为每个计划节点提供一个内存上下文不是问题。

在查询生命周期上下文中运行索引访问和排序的一个问题是,这些操作会调用特定的数据类型比较函数,并且如果比较器泄漏任何内存,那么该内存将不会在查询结束之前恢复。比较器函数都返回bool或int32,因此它们的结果数据没有问题,但可能存在内部临时数据泄漏的问题。特别是,对支持TOAST的数据类型进行操作的比较器函数需要小心,不要泄漏其输入的detoast版本。这很烦人,但使比较器符合要求比修复索引和排序进程容易得多,所以这就是7.1所做的事情。这仍然是btree和哈希索引中的情况,因此btree和哈希支持函数仍然需要不泄漏内存。大多数其他索引AM已被修改为在短期上下文中运行opclass支持函数,因此内存泄漏没有问题;鉴于它们的支持职能往往要复杂得多,这是必要的。

有一些特殊情况,例如聚合函数。nodeAgg.c需要记住从一个元组循环到下一个元组循环的聚合转换函数的评估结果,因此它不能在循环中丢弃元组状态。处理这个问题的最简单方法是在聚合节点中拥有两个元组上下文,并在它们之间进行ping-pong操作,以便在每个元组中,一个是当前活动分配上下文,另一个上一循环时上下文。

切换CurrentMemoryContext的执行程序进程可能需要在返回之前将数据复制到调用者的当前内存上下文中。然而我们已经最大限度地减少了这种需要,因为按照惯例,在执行周期的"开始"而不是结束时重置每个元组上下文。根据该规则,执行节点可以返回一个在其每个元组上下文中分配的元组,并且该元组将保持良好状态,直到该节点被调用另一个元组或被告知结束执行。这与表扫描级别的按引用传递值的情况类似,因为扫描节点可以返回指向磁盘缓冲区中的元组的直接指针,而该元组只能保证在那么长时间内保持良好状态。

复制数据的一个更常见的原因是将结果从每个元组上下文传输到每个查询上下文;例如,Unique节点将在其每个查询上下文中保存最后一个不同的元组值,需要复制步骤。

允许多种类型上下文的机制

为了有效地允许不同的分配模式并进行实验,我们允许具有不同分配策略但相似外部行为的不同类型的内存上下文。为了处理这个问题,内存分配函数是通过函数指针访问的,并且我们要求所有上下文类型都遵守此处给出的约定。

MemoryContextData(请参阅memnodes.h)。此结构体标识上下文的确切类型,并包含不同类型的MemoryContext之间通用的信息,例如父上下文和子上下文以及上下文的名称。

这本质上是一个抽象超类,其行为由"methods"指针决定,即它的虚函数表(struct MemoryContextMethods)。特定的内存上下文类型将使用将这些字段作为其第一个字段的派生结构。特定类型的所有上下文都将具有指向相同的函数指针静态表的方法指针。

虽然从上下文分配和重置等操作将相关的MemoryContext作为参数,但像free和realloc这样的操作则更加棘手。为了实现这些工作,我们要求所有内存上下文类型生成分配的块,这些块立即没有任何填充,前面是指向相应MemoryContext的指针。

如果某种类型的分配器需要有关其块的附加信息(例如分配的大小),则该信息又可以先于MemoryContext。这意味着内存上下文机制隐含的唯一开销是指向其上下文的指针,因此我们不会对上下文类型设计者产生太多限制。

鉴于此,像pfree这样的进程通过类似的操作确定它们相应的上下文(尽管通常封装在GetMemoryChunkContext()中)

c 复制代码
MemoryContext context = *(MemoryContext*) (((char *) pointer) - sizeof(void *));
#然后调用上下文对应的方法
context->methods->free_p(pointer);

aset.c行为的更多控制

默认情况下,aset.c始终在上下文中第一次分配时分配8K块,并为每个连续的块请求将该大小加倍。对于可能保存"大量"数据的上下文来说,这是一个很好的行为。但是,如果系统中有数十个甚至数百个较小的上下文,我们就需要能够更好地进行微调。

上下文的创建者能够指定初始块大小和最大块大小。选择较小的值可以防止在预计不会容纳太多内容的上下文中浪费空间(一个例子是relcache的每个关系上下文)。

此外,还可以指定最小上下文大小,以防由于某种原因应与附加块的初始大小不同。aset.c上下文将始终包含至少一个大小为minContextSize的块(如果已指定),否则为initBlockSize。

我们预计每个元组上下文将被频繁重置,并且通常不会为每个元组周期分配太多空间。为了使这种使用模式更便宜,在重置期间不会将上下文中分配的第一个块返回给malloc(),而只是清除。这可以避免malloc抖动。

替代内存上下文实现

aset.c是我们默认的通用实现,在大多数情况下工作正常。我们还有两个针对特殊用例优化的实现,与aset.c相比(或两者)提供更好的性能或更低的内存使用量。

  • slab.c(SlabContext)是为固定长度的块的分配而设计的,不允许分配不同大小的块。

  • generation.c(GenerationContext)专为将块分配到具有相似生命周期的组中或大致按FIFO顺序分配的情况而设计。

两个内存上下文都旨在将内存释放回操作系统(与aset.c不同,它将释放的块保留在freelist中,并且仅在重置/删除时返回内存)。

这些内存上下文最初是为ReorderBuffer开发的,但只要分配模式匹配,就可能在其他地方有用。

内存核算

基本内存上下文操作之一是确定上下文(及其子上下文)中使用的内存量。我们有多个地方实现了自己的临时内存核算,这是为了提供统一的方法。临时计算解决方案适用于对分配有严格控制的地方,或者当很容易确定分配块的大小时(例如,仅使用元组的地方)。

内存上下文中内置计算是透明的,并且对所有分配都透明地工作,只要它们最终位于正确的内存上下文子树中。

例如,考虑聚合函数-聚合状态通常由从转换函数分配的任意结构表示,因此临时计算不太可能起作用。然而,内置的计算功能可以很好地处理这种情况。

为了最大限度地减少开销,会计是在块级别完成的,而不是针对单个分配块。

记账是惰性的------分配(或释放)块后,仅更新拥有该块的上下文。这意味着当查询给定上下文中的内存使用情况时,我们必须递归地遍历所有子上下文。这意味着内存核算不适用于内存上下文过多(在相关子树中)的情况。

个人对内存上下文的总结

内存上下文就是控制已分配内存的生命周期,例如以下代码

c 复制代码
oldcxt = MemoryContextSwitchTo(TopMemoryContext);
object = palloc(1024*1024*8);
MemoryContextSwitchTo(oldcxt);

object是分配在TopMemoryContext,生命周期为PostgreSQL服务启动至停止.

同时pg fork进程时,object也会复制一个至新的进程.例如当有新连接pg创建进程时也会复制.

相关推荐
在努力的前端小白2 分钟前
Spring Boot 敏感词过滤组件实现:基于DFA算法的高效敏感词检测与替换
java·数据库·spring boot·文本处理·敏感词过滤·dfa算法·组件开发
未来之窗软件服务4 分钟前
自建知识库,向量数据库 (九)之 量化前奏分词服务——仙盟创梦IDE
数据库·仙盟创梦ide·东方仙盟·自建ai·ai分词
冒泡的肥皂3 小时前
MVCC初学demo(一
数据库·后端·mysql
.Shu.4 小时前
Redis Reactor 模型详解【基本架构、事件循环机制、结合源码详细追踪读写请求从客户端连接到命令执行的完整流程】
数据库·redis·架构
薛晓刚7 小时前
当MySQL的int不够用了
数据库
SelectDB技术团队7 小时前
Apache Doris 在菜鸟的大规模湖仓业务场景落地实践
数据库·数据仓库·数据分析·apache doris·菜鸟技术
星空下的曙光8 小时前
mysql 命令语法操作篇 数据库约束有哪些 怎么使用
数据库·mysql
小楓12018 小时前
MySQL數據庫開發教學(一) 基本架構
数据库·后端·mysql
染落林间色8 小时前
达梦数据库-实时主备集群部署详解(附图文)手工搭建一主一备数据守护集群DW
数据库·sql
颜颜yan_8 小时前
企业级时序数据库选型指南:从传统架构向智能时序数据管理的转型之路
数据库·架构·时序数据库