Author: mohen
PostgreSQL 是多进程架构,它的内存架构可以划分为两大类:共享内存(shared memory area)和本地内存(local memory area)。除了动态申请的共享内存外,绝大多数共享内存是在 Postmaster 启动时分配,具有固定大小,因此内存管理相对简单,不易发生内存泄漏。而每个后端进程还必须管理自己的本地内存来完成请求的处理,这部分的内存管理比较棘手。这是因为 PostgreSQL 内核主要采用 C 语言编写,程序必须显式释放所有动态分配的内存,并且 PostgreSQL 中需要处理大量的以指针传值的逻辑,很容易产生难以排查的内存泄漏问题。内存泄漏对于多进程架构的影响是致命的,所以更好地管理本地内存是 PostgreSQL 实现中的一个重要环节。
从 7.1 版本开始,PostgreSQL 引入了内存上下文(MemoryContext)机制来管理本地内存。这个机制很好地解决了内存泄漏问题;同时也提高了内存分配效率,避免了内存碎片化的产生;也让内存管理有了生命周期。
MemoryContext 本质上是对内存进行分层和分类。一个 MemoryContext 实际上是一个内存池,代表一类内存。不同的 MemoryContext 组成了一个树状结构,代表了不同种类内存之间的联系。在运行过程中,开发者可以根据自己的需要创建和删除 MemoryContext。删除 MemoryContext,会让 MemoryContext(包括子 MemoryContext)中申请的内存都被释放,而不必去关心每一块内存的释放。
PostgreSQL 中有几个被熟知的 MemoryContext,它们之间的关系如下图所示:
它们的作用为:
MemoryContext 的基本操作包括:创建 context、删除 context(释放所有获得的内存块)、重置 context(仅保留创建时申请的内存块)、在 context 中申请/释放内存片(palloc()/pfree())以及查询 context 中内存分配的情况等。
为了降低管理负担,PostgreSQL 中提供了 CurrentMemoryContext 全局变量表示当前的 MemoryContext。palloc() 会隐式地从这个 context 中分配空间。MemoryContextSwitchTo() 操作可以选择一个新的当前 context,并返回之前的 context,以便调用者最后恢复之前的 context。需要注意,pfree() 和 repalloc() 在处理内存块时,不依赖 CurrentMemoryContext,会直接调用内存块所属的 context 进行操作。 PostgreSQL 9.5 之后,MemoryContext 中引入了 reset 回调函数机制。注册的函数能够在 context 被删除或者重置时调用,调用的顺序和注册的顺序相反。回调函数用于释放与这个 context 相关的资源,比如关闭某些操作打开的文件或者释长生命对象被 context 的引用计数等。 MemoryContext 实际上是一个抽象类型,它的底层可以有多种实现方式,但是能让用户只使用同一套接口来管理不同的内存分配机制,只是内存申请释放由原来的 malloc/free 变为了 palloc/pfree 。为了实现这个目的:
接下来看下 MemoryContext 具体的数据结构(代码主要来自 PostgreSQL 14.11 src/include/nodes/memnodes.h):
typedef struct MemoryContextData *MemoryContext;
typedef struct MemoryContextMethods
{
void *(*alloc) (MemoryContext context, Size size);
/* call this free_p in case someone #define's free() */
void (*free_p) (MemoryContext context, void *pointer);
void *(*realloc) (MemoryContext context, void *pointer, Size size);
void (*reset) (MemoryContext context);
void (*delete_context) (MemoryContext context);
Size (*get_chunk_space) (MemoryContext context, void *pointer);
bool (*is_empty) (MemoryContext context);
void (*stats) (MemoryContext context,
MemoryStatsPrintFunc printfunc, void *passthru,
MemoryContextCounters *totals,
bool print_to_stderr);
#ifdef MEMORY_CONTEXT_CHECKING
void (*check) (MemoryContext context);
#endif
} MemoryContextMethods;
typedef struct MemoryContextData
{
NodeTag type; /* identifies exact kind of context */
/* these two fields are placed here to minimize alignment wastage: */
bool isReset; /* T = no space alloced since last reset */
bool allowInCritSection; /* allow palloc in critical section */
Size mem_allocated; /* track memory allocated for this context */
const MemoryContextMethods *methods; /* virtual function table */
MemoryContext parent; /* NULL if no parent (toplevel context) */
MemoryContext firstchild; /* head of linked list of children */
MemoryContext prevchild; /* previous child of same parent */
MemoryContext nextchild; /* next child of same parent */
const char *name; /* context name (just for debugging) */
const char *ident; /* context ID if any (just for debugging) */
MemoryContextCallback *reset_cbs; /* list of reset/delete callbacks */
} MemoryContextData;
可以看到 MemoryContext 实际上是指向 MemoryContextData 的指针。MemeoryContext 之间的联系是一个树状结构:
parent 指向父 context,firstchild 指向第一个子 context,而 prevchild 和 nextchild 则是指向同层级的 context。
MemoryContext 底层有多种实现方式(aset.c/slab.c/generation.c),其中 slab.c/generation.c 会将释放的内存直接还给操作系统,而 aset.c 则会保留一部分已释放内存在一个 freelist 中,只有在 context 被删除或者重置时归还给操作系统。aset.c(AllocSet)是 MemoryContext 的标准实现(参考 src/backend/utils/mmgr/aset.c) ,接下来介绍它的详细实现。
首先看下 AllocSet 的数据结构组织关系,如下图所示:
typedef struct AllocSetContext
{
MemoryContextData header; /* Standard memory-context fields */
/* Info about storage allocated in this context: */
AllocBlock blocks; /* head of list of blocks in this set */
AllocChunk freelist[ALLOCSET_NUM_FREELISTS]; /* free chunk lists */
/* Allocation parameters for this context: */
Size initBlockSize; /* initial block size */
Size maxBlockSize; /* maximum block size */
Size nextBlockSize; /* next block size to allocate */
Size allocChunkLimit; /* effective chunk size limit */
AllocBlock keeper; /* keep this block over resets */
/* freelist this context could be put in, or -1 if not a candidate: */
int freeListIndex; /* index in context_freelists[], or -1 */
} AllocSetContext;
AllocSetContext 是 AllocSet 的核心管理模块, 所以单独介绍下其中每个成员的作用:
/* typedef AllocSetContext *AllocSet; */
aset = (AllocSet) mcxt;
typedef struct AllocBlockData
{
AllocSet aset; /* aset that owns this block */
AllocBlock prev; /* prev block in aset's blocks list, if any */
AllocBlock next; /* next block in aset's blocks list, if any */
char *freeptr; /* start of free space in this block */
char *endptr; /* end of space in this block */
} AllocBlockData;
AllocSet 从 malloc() 中获取一块连续内存的单位是 AllocBlock。而用户调用 palloc() 获取一块连续内存的单位是 AllocChunk。一个 AllocBlock 包含 1 个或者多个 AllocChunk。用户 pfree() 释放 AllocChunk 后,可能不会直接返还操作系统,如果匹配了 freelist 中的大小,将会由对应的 freelist 管理。 AllocBlockData 是一个 AllocBlock 的 header 数据,可分配的空间是从下一个内存对齐边界开始。
typedef struct AllocChunkData
{
/* size is always the size of the usable space in the chunk */
Size size;
#ifdef MEMORY_CONTEXT_CHECKING
/* when debugging memory usage, also store actual requested size */
/* this is zero in a free chunk */
Size requested_size;
#define ALLOCCHUNK_RAWSIZE (SIZEOF_SIZE_T * 2 + SIZEOF_VOID_P)
#else
#define ALLOCCHUNK_RAWSIZE (SIZEOF_SIZE_T + SIZEOF_VOID_P)
#endif /* MEMORY_CONTEXT_CHECKING */
/* ensure proper alignment by adding padding if needed */
#if (ALLOCCHUNK_RAWSIZE % MAXIMUM_ALIGNOF) != 0
char padding[MAXIMUM_ALIGNOF - ALLOCCHUNK_RAWSIZE % MAXIMUM_ALIGNOF];
#endif
/* aset is the owning aset if allocated, or the freelist link if free */
void *aset;
/* there must not be any padding to reach a MAXALIGN boundary here! */
} AllocChunkData;
AllocChunkData 是一个 AllocChunk 的 header 数据,可使用空间也是从下一个内存对齐边界开始。这里的 size 是表示可以使用的空间大小(不包括 metadata 的空间)。
typedef struct AllocSetFreeList
{
int num_free; /* current list length */
AllocSetContext *first_free; /* list header */
} AllocSetFreeList;
/* context_freelists[0] is for default params, [1] for small params */
static AllocSetFreeList context_freelists[2] =
{
{
0, NULL
},
{
0, NULL
}
};
为了避免频繁地创建和删除 AllocSet,PG 中同样使用了 AllocSetFreeList 来管理部分被释放的 AllocSet,便于减少再次申请 AllocSet 的工作量。每一类 AllocSetFreeList 的候选集中的 AllocSet 必须要有 相同 的 minContextSize 和 initBlockSize,而 maxBlockSize 则不相关,因为不影响最开始分配块(initial AllocBlock)的大小。放入 AllocSetFreeList 之前,AllocSet 都会被进行 Reset 操作,只会让 keeper 指向 initial AllocBlock,然后删除其余的 block。
PG 提供了两类 AllocSetFreeList,一类是 ALLOCSET_DEFAULT_SIZES,另一类 ALLOCSET_SMALL_SIZES(ALLOCSET_START_SMALL_SIZES 也可以使用这里的候选集,因为只有 maxBlockSize 与 ALLOCSET_SMALL_SIZES 不同)。
AllocSetFreeList 中 AllocSet 的取用方式是 LIFO,但是每个 AllocSetFreeList 的个数有一个上限(MAX_FREE_CONTEXTS),超过这个上限,PG 会倾向删除最近创建的 AllocSet(即末尾的节点),为了保持进程的 MMAP 紧凑。
PG 在实现时,采用的方案是 一旦发现 AllocSetFreeList 个数溢出,就直接删除 AllocSetFreeList 所有 节点,来近似达到上面的想法。而这个方案是基于 一个会分配很多 MemoryContext 的查询,或多或少的可能会以申请相反的顺序释放 MemoryContext。实际实现时简化为,一旦发现某个 AllocSetFreeList 中数量超过 100,则会将这些 AllocSet 全部释放。在 AllocSetFreeList 中,每个 AllocSet 的 nextchild 指向下一个 AllocSet。
MemoryContext
AllocSetContextCreateInternal(MemoryContext parent,
const char *name,
Size minContextSize,
Size initBlockSize,
Size maxBlockSize)
这个函数创建一个 AllocSet,会分配一个 initial AllocBlock,而 minContextSize / initBlockSize / maxBlockSize 会影响 initial AllocBlock 的大小,以及再次分配 block 的大小。主要的流程:
确定 firstBlockSize 时,需要至少包含头部的开销,然后再去和 minContextSize/initBlockSize 比较:
/* Determine size of initial block */
firstBlockSize = MAXALIGN(sizeof(AllocSetContext)) +
ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;
if (minContextSize != 0)
firstBlockSize = Max(firstBlockSize, minContextSize);
else
firstBlockSize = Max(firstBlockSize, initBlockSize);
确定 allocChunkLimit 时,会有一个默认值 ALLOC_CHUNK_LIMIT(8 KB),但是不能超过 maxBlockSize 的 1/4,否则 allocChunkLimit 就要循环缩减一倍,直到满足要求:
set->allocChunkLimit = ALLOC_CHUNK_LIMIT;
while ((Size) (set->allocChunkLimit + ALLOC_CHUNKHDRSZ) >
(Size) ((maxBlockSize - ALLOC_BLOCKHDRSZ) / ALLOC_CHUNK_FRACTION))
set->allocChunkLimit >>= 1;
从上面的分析可以看到,minContextSize/initBlockSize/maxBlockSize 会直接影响 AllocSet 的内存申请规则,而了解具体的规则是比较有挑战的。为方便调用,PG 定义了三个分配 size 的宏,同时也定义了一个调用函数的宏:
#ifdef HAVE__BUILTIN_CONSTANT_P
#define AllocSetContextCreate(parent, name, ...) \
(StaticAssertExpr(__builtin_constant_p(name), \
"memory context names must be constant strings"), \
AllocSetContextCreateInternal(parent, name, __VA_ARGS__))
#else
#define AllocSetContextCreate \
AllocSetContextCreateInternal
#endif
#define ALLOCSET_DEFAULT_SIZES \
ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE
#define ALLOCSET_SMALL_SIZES \
ALLOCSET_SMALL_MINSIZE, ALLOCSET_SMALL_INITSIZE, ALLOCSET_SMALL_MAXSIZE
#define ALLOCSET_START_SMALL_SIZES \
ALLOCSET_SMALL_MINSIZE, ALLOCSET_SMALL_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE
因此,常见的调用方式就是:
source_context = AllocSetContextCreate(CurrentMemoryContext,
"CachedPlanSource",
ALLOCSET_START_SMALL_SIZES);
文章开头介绍 Memorycontext 时提到了作为抽象类的两个条件,我们已经看到 MemoryContextData 确实是 AllocSetContext 第一个成员,而 MemoryContextMethods 指向具体的 method 实现是在 AllocSetContextCreateInternal 函数中完成的:
MemoryContextCreate((MemoryContext) set,
T_AllocSetContext,
&AllocSetMethods,
parent,
name);
AllocSetMethods 包含的具体方法为:
static const MemoryContextMethods AllocSetMethods = {
AllocSetAlloc,
AllocSetFree,
AllocSetRealloc,
AllocSetReset,
AllocSetDelete,
AllocSetGetChunkSpace,
AllocSetIsEmpty,
AllocSetStats
#ifdef MEMORY_CONTEXT_CHECKING
,AllocSetCheck
#endif
};
static void *
AllocSetAlloc(MemoryContext context, Size size)
这个函数是 palloc() 的底层调用,具体的逻辑流程为:
当请求的 size 超过了 allocChunkLimit,就会直接向 malloc() 申请一个 AllocBclok,并且整个 block 作为一个 AllocChunk 返回给用户。而在 freelist 请求固定大小的 chunk 时,对应 freelist 没有可用 chunk,并且 block 剩余空间也不足,会先将剩余的空间分配到 合适大小的 freelist 中,然后再向 malloc() 申请一个 nextBlockSize 的 block 用于 chunk 分配。
static void
AllocSetFree(MemoryContext context, void *pointer)
这个函数是 pfree() 的底层调用,具体的逻辑流程为:
执行的逻辑和 AllocSetAlloc 相关,超过 allocChunkLimit 的内存直接归还给操作系统,不超过的内存,交给合适的 freelist 管理。
static void *
AllocSetRealloc(MemoryContext context, void *pointer, Size size)
这个函数是 repalloc() 的底层调用,具体的逻辑流程为:
当原来分配的内存超过 allocChunkLimit,会调整新需求的 size 至少超过 allocChunkLimit,然后调用 realloc()。原来分配 chunksize 能满足新需求 size,则只调整 chunk 的可用空间即可。否则就会调用 AllocSetAlloc 分配新的需求,AllocSetFree 释放老的空间。
只保留 AllocSet->keeper 的中 block(initial block),其余的 block 都调用 free() 释放掉。
删除一个 AllocSet,将所有资源都 free(),如果是可以放入 AllocSetFreeList 中,则会进行 MemoryContextResetOnly 后再放入。
对分配的一个 chunk,返回包括 AllockChunk header 在内的占用空间。
简单判断 context->isReset 是否被置位。
MemoryContextStatsInternal 的底层调用,统计 AllocSet 的 totalspace,freespace 和 block/freechunks 个数。
debug 调试使用,主要是根据分配时规则,做一些检查。