Skip to content

Memory Readme

Memory Context System Design Overview

背景

我们的绝大多数内存分配都在 “内存上下文” 中完成,内存上下文通常是由 src/backend/utils/mmgr/aset.c 实现的 AllocSet 结构。实现低开销且可靠的内存管理,关键在于定义一套生命周期合理的内存上下文集合。

内存上下文的基本操作包括:

  • 创建一个上下文
  • 在上下文内分配一块内存(等价于标准 C 库的 malloc ())
  • 删除一个上下文(同时释放其中分配的所有内存)
  • 重置一个上下文(释放上下文内分配的所有内存,但不销毁上下文对象本身)
  • 查询分配给该上下文的总内存大小(指上下文用于分配的原始内存,而非单个内存块)

对于已从某个上下文分配的内存块,可以对其进行释放,或者进行扩容、缩容(对应标准 C 库的 free() 和 realloc())。这些操作都会将内存归还到最初分配该块的上下文,或从该上下文申请更多内存。

系统始终存在一个由全局变量 CurrentMemoryContext 标识的 “当前” 内存上下文。palloc() 会隐式在当前上下文中分配空间。MemoryContextSwitchTo() 用于切换新的当前上下文,并返回切换前的上下文,以便调用者在退出前恢复原上下文。

相比直接使用 malloc/free,内存上下文的主要优势在于可以一次性释放整个上下文的所有内存,无需逐个释放内部的每一块内存。这种方式比单独管理每块内存更快、更可靠。我们在事务结束时利用这一特性进行内存清理:通过重置所有事务级或更短生命周期的活动上下文,即可回收所有临时内存。同理,也可以在每条查询结束时,或查询处理完每一行元组后完成清理。

关于 palloc API 与标准 C 库的区别说明

palloc 及其相关函数的行为与标准 C 库的 malloc 系列函数类似,但也存在一些刻意设计的差异。以下说明用于明确其行为特性。

  • 若内存不足,palloc 和 repalloc 会通过 elog(ERROR) 直接退出程序。它们永远不会返回 NULL,因此检测返回值是否为 NULL 是不必要且无意义的。在使用 palloc_extended() 时,可以通过 MCXT_ALLOC_NO_OOM 标志覆盖该行为。

  • palloc(0) 是明确合法的操作。它不会返回 NULL 指针,而是会返回一个有效的内存块,只是该内存块不允许使用任何字节。不过,该内存块后续可以通过 repalloc 扩容,也可以无错误地通过 pfree 释放。同理,repalloc 允许将内存重分配为 0 大小。

  • pfree 和 repalloc 不接受 NULL 指针,这是刻意设计的规则。 (对于 repalloc 而言,这是必要的:如前所述,repalloc 不依赖当前内存上下文,因此必须知道在哪个内存上下文中执行分配。所以首次分配必须在 repalloc 之外完成。对于 pfree 而言,该行为主要是历史原因,部分原因是额外的空指针检查会影响性能。)

当前内存上下文

由于总是将合适的内存上下文传递给被调用函数会带来过大的代码编写开销,因此系统中始终存在一个 当前内存上下文(CurrentMemoryContext) 的概念。 如果没有它,例如 copyObject 函数就需要额外传递一个上下文参数,返回引用传递数据类型的函数执行函数同样也需要。对于那些内部临时分配内存、却不会将内存返回给调用者的函数来说也是如此。我们显然不希望让系统中的每一处调用都充斥着“请使用这个上下文进行你可能需要的任何临时内存分配”这样的冗余代码。

不过,基于上述考虑得出的结论是:CurrentMemoryContext 应尽可能指向一个生命周期较短的上下文。在查询执行期间,它通常指向一个每处理完一个元组就会被重置的上下文。只有在极其有限的代码中,才可以让它指向生命周期超过事务的上下文,因为这样做存在造成永久性内存泄漏的风险。

pfree/repalloc 不依赖当前内存上下文

pfree()repalloc() 可作用于任意内存块,无论该内存块是否属于当前内存上下文——系统都会找到该内存块所属的上下文,并由其负责处理对应的操作。

父、子上下文

如果所有上下文都是相互独立的,将会很难对它们进行管理,尤其是在出错的场景下。这一问题通过构建“父-子”上下文的树形结构来解决。创建内存上下文时,可以将新上下文指定为某个已有上下文的子节点。一个上下文可以拥有多个子上下文,但只能有一个父节点。通过这种方式,所有上下文构成一片森林(并非一定是单棵树,因为可以存在多个顶层上下文;不过在当前实际实现中,只有一个顶层上下文 TopMemoryContext)。

删除一个上下文时,会同时删除其所有直接和间接子上下文。重置一个上下文时,删除其子上下文通常更符合实际需求,因此 MemoryContextReset() 就是这样设计的;如果你确实需要保留树形结构、只清空上下文内容,则需要调用 MemoryContextResetOnly() 再配合 MemoryContextResetChildren()。

这些机制让我们可以安全地管理大量上下文,不必担心出现泄漏。我们只需要维护一个会在事务结束时删除的顶层上下文,并确保创建的所有生命周期更短的上下文都是它的后代即可。由于树形结构可以有多层,我们可以轻松处理嵌套的存储生命周期,例如事务级、语句级、扫描级、元组级。对于仅部分重叠的存储生命周期,可以通过从上下文森林的不同树中分配内存来处理(下一节会给出一些示例)。

为方便使用,系统还提供了一类操作:重置或删除指定上下文的所有子节点,但不改动该上下文本身。

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

PostgreSQL 9.5 引入的一项特性,允许内存上下文不仅用于管理普通的 palloc 内存,还能管理更多类型的资源。实现方式是为内存上下文注册“重置回调函数”。该函数会在上下文下一次被重置或删除之前被调用一次,可用于释放那些与上下文中分配的对象存在关联的资源。典型应用场景包括:

  • 关闭与元组排序对象相关联的已打开文件;
  • 释放被待重置上下文中的对象所持有的、长生命周期缓存对象的引用计数;
  • 释放与某些 palloc 对象关联的、由 malloc 管理的内存。

最后一种场景在纯 PostgreSQL 代码中属于不良编程习惯;更好的做法是统一在目标上下文或其子上下文中使用 palloc 完成所有内存分配。不过,在与非 PostgreSQL 库交互的代码中,这种方式会非常实用。

一个内存上下文可以注册任意数量的重置回调,调用顺序与注册顺序相反。当一整棵上下文树被重置或删除时,子上下文的回调会先于父上下文的回调执行。

对应的 API 要求调用者提供一个 MemoryContextCallback 内存块,用于保存回调的状态信息。通常这块内存应分配在逻辑上与之关联的同一个上下文中,以便使用后能自动释放。要求调用者自行提供这段内存的原因是:在大多数使用场景下,调用者会在目标上下文中创建一个更大的结构体,将 MemoryContextCallback 结构体嵌入其中,无需单独执行 palloc() 即可“免费”获得该结构体空间。

Memory Contexts in Practice

全局已知内存上下文

系统中存在若干广泛使用、通过全局变量引用的内存上下文。在任意时刻,系统可能还包含许多其他上下文,但所有这些上下文都必须是下列上下文的直接或间接子节点,以确保在发生错误时不会发生内存泄漏。

TopMemoryContext —— 这是上下文树真正的顶层节点,其他所有上下文都是它的直接或间接子节点。在此分配内存本质上等同于使用 malloc,因为该上下文永远不会被重置或删除。它用于存放需要永久存活的数据,或由对应管理模块负责在合适时机删除的数据。例如 fd.c 中的打开文件管理表。除非绝对必要,否则应避免在此分配内存,尤其要避免将 CurrentMemoryContext 指向此处。

PostmasterContext —— 这是 Postmaster 主进程的常规工作上下文。在衍生出后端进程后,后端进程可以删除此上下文,以释放不需要的、从 Postmaster 继承的内存。注意在非 EXEC_BACKEND 编译模式下,Postmaster 持有的 pg_hba.conf 和 pg_ident.conf 配置数据会在后端进程认证阶段被直接使用,因此后端进程必须在认证完成后才能删除此上下文。(Postmaster 仅拥有 TopMemoryContext、PostmasterContext 和 ErrorContext,其余顶层上下文均在各个后端进程启动时创建。)

CacheMemoryContext —— 用于关系缓存、系统表缓存及相关模块的永久存储空间。该上下文同样永远不会被重置或删除,因此从功能上看它与 TopMemoryContext 并无本质区别。但为了便于调试,保留这一区分是有意义的。(注意:CacheMemoryContext 拥有生命周期更短的子上下文。例如,与关系缓存条目相关的辅助存储最适合放在子上下文中,这样可以轻松释放规则解析树等资源,而不必依赖实现可靠的 freeObject()。)

MessageContext —— 该上下文用于存放来自前端的当前命令消息,以及仅需存活至当前消息处理完成的临时存储(例如在简单查询模式下,语法解析树和执行计划树可存放在此处)。在 PostgresMain 主循环的每一轮处理开始前,此上下文都会被重置,其所有子节点都会被删除。它与事务级、Portal 级上下文相互独立,因为查询字符串的存活周期可能长于或短于单个事务或 Portal。

TopTransactionContext —— 存放所有需要存活至顶层事务结束的数据。该上下文会在每次顶层事务周期结束时被重置,其所有子节点都会被删除。大多数情况下不应直接在此分配内存,而应在 CurTransactionContext 中分配;此处仅用于存放明确需要跨多个子事务管理状态的控制信息。注意:该上下文在出错时不会立即清空,其内容会保留到事务块通过 COMMIT/ROLLBACK 退出为止。

CurTransactionContext —— 存放必须存活至当前事务结束、且在顶层事务提交时需要使用的数据。在顶层事务中,它与 TopTransactionContext 指向同一个上下文;在子事务中,它指向一个子节点上下文。需要重点注意:如果子事务中止,其 CurTransactionContext 会在中止处理完成后被丢弃;而已提交的子事务的 CurTransactionContext 会被保留至顶层事务提交(除非中间某层子事务中止)。这一机制确保不会长期保留失败子事务产生的数据。基于此行为,在子事务中止时必须正确清理状态:子事务的数据结构必须从上层事务的指针或链表中解除关联,否则会产生悬空指针并导致顶层提交时进程崩溃。典型例子是待发送的 NOTIFY 消息,它们仅在生成该消息的子事务未中止时,才会在顶层事务提交时发送。

PortalContext —— 这并非一个实际独立的上下文,而是一个全局变量,指向当前活跃执行 Portal 的专属上下文。当需要分配仅存活于当前 Portal 执行周期的内存时,可以使用该上下文。

ErrorContext —— 这是一个永久上下文,专门用于错误恢复处理,并在恢复完成后被重置。系统始终保证该上下文中有几 KB 的可用内存。这样即使后端进程已耗尽其他内存,仍能确保错误恢复流程拥有可用内存,从而将内存不足处理为普通 ERROR 级别错误,而非 FATAL 致命错误。

Contexts For Prepared Statements And Portals

Logical Replication Worker Contexts

Transient Contexts During Execution

Mechanisms to Allow Multiple Types of Contexts

More Control Over aset.c Behavior

Alternative Memory Context Implementations

Memory Accounting