PostgreSQL技术内幕4:PostgreSQL存储引擎内存管理
目录
- 0.简介
- 1.原理介绍
- 2.代码分析
- 2.0 整体结构
- 2.1 相关宏定义
- 2.2 MemoryContextData,内存管理使用的结构定义
- 2.3 AllocSetContext,负责真正的内存分配和管理
- 2.4 使用分析+层次分析
- 2.5 使用过程
- 3.共享内存区域介绍
- 4. 查看方法
0.简介
了解完PG的物理存储,本篇介绍PG的内存管理,主要包含内存管理方式,分配流程,各部分层级关系,以及如何通过函数指针的方式动态的进行分配方式选择,主要分析进程本地的内存管理,同时在最后第三节介绍多进程共享内存管理。本篇介绍完内存管理后,下一篇介绍物理存储到内存的转换过程。
1.原理介绍
对于操作系统来说,对于小块内存的管理,是利用不同大小的bucket链表串连相同大小的buf,如下图:
对于PG来说,利用和操作系统类似的原理,定义了11个大小不同的freelist,管理不同大小的内存。
2.代码分析
2.0 整体结构
其整体设计思路为两级结构MemoryContextData负责记录和管理内存树的信息,AllocSetContext负责真正的分配和管理内存,其中有空闲内存列表和MemoryContextData树的头节点。
2.1 相关宏定义
#define ALLOC_MINBITS 3 /* smallest chunk size is 8 bytes,2的3次方 */
#define ALLOCSET_NUM_FREELISTS 11 /*freelist的数量,如上图的freelist0到freelists10共11个*/
#define ALLOC_CHUNK_LIMIT (1 << (ALLOCSET_NUM_FREELISTS-1+ALLOC_MINBITS)) /* Size of largest chunk that we use a fixed size for */
2.2 MemoryContextData,内存管理使用的结构定义
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 */
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;
可以看到,内存分配方法methods使用函数指针表的方式定义,是c语言动态调用函数的一种方式,其结构如下。该结构还有父节点以及子节点,可以理解MemoryContextData构造了一个内存管理信息的树,某个节点清理可以清理它的子节点,方便了管理。
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);
#ifdef MEMORY_CONTEXT_CHECKING
void (*check) (MemoryContext context);
#endif
} MemoryContextMethods;
2.3 AllocSetContext,负责真正的内存分配和管理
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;
MemoryContextData是内存管理的基本结构,其只会记录内存管理的信息,而不直接管理内存分配,内存分配的管理实际采用AllocSetContext,其中header记录分配的Context信息,freelist就是上文介绍的内存切块的管理,数量为ALLOCSET_NUM_FREELISTS,和 AllocBlock分别对应内存的两个层次:内存块(Block)和内存片(Chunk)。通常,一个内存块会包含多个内存片,内存片则是PG分配内存的最小单元。
在向PG申请内存的时候,不会直接调用malloc分配内存,而是由PG先向系统通过malloc申请一块较大的内存块,然后由PG从该较大的内存块中切割出一块合适大小的内存片返回给申请者;申请者释放内存的时候也不会直接返还给操作系统,而是交由PG通过freelist保留不同大小的空闲碎片,在下次申请内存的时候,可以直接从freelist中寻找合适的内存片进行内存分配。这样做的目的主要是为了减少系统调用的次数和内存碎片的产生,提高内存分配和回收的效率。
allocChunkLimit为申请内存大小的最大值,如果需要分配的内存超过该值,显然无法从freelist中分配内存,将通过 malloc直接申请一整块内存块(内存对齐处理),并整体作为一个内存片返回给申请者。如果申请内存大小未超 allocChunkLimit 且freelist 中有合适空闲碎片,可直接通过计算,得到 freelistindex,并从freelist中的链表中返回合适的空闲碎片。
在内存释放时,如果ChunkSize > allocChunkLimit,直接调用 free() 进行释放;ChunkSize <= allocChunkLimit,将 Chunk直接添加至freelist空闲链表中。
2.4 使用分析+层次分析
在程序启动时会调用MemoryContextInit,其会初始化TopMemoryContext ,即顶层的MemoryContext,预先定义的MemoryContext如下所示:
MemoryContext CurrentMemoryContext = NULL;
/*
* Standard top-level contexts. For a description of the purpose of each
* of these contexts, refer to src/backend/utils/mmgr/README
*/
MemoryContext TopMemoryContext = NULL;
MemoryContext ErrorContext = NULL;
MemoryContext PostmasterContext = NULL;
MemoryContext CacheMemoryContext = NULL;
MemoryContext MessageContext = NULL;
MemoryContext TopTransactionContext = NULL;
MemoryContext CurTransactionContext = NULL;
层次如下:
1)TopMemoryContext:位于MemoryContext树状管理结构的最上层,其它所有MemoryContext都是其直接或间接子节点。
2)CacheMemoryContext:RelCache、CatCache以及相关模块的持久存储,无法重置或删除。
3)MessageContext:此Context保存有传递过来的当前命令和当前命令创建出的和当前命令生存周期一致的存储空间。
4)TopTransactionContext:此内存环境一直持续到最高层事务结束的时候。在每一次最高层事务结束的时候,这个内存环境都会被重设,其所有的子内存环境都会被删除。注意:此内存环境不会在出错时立即清除,而是直到事务块通过调用COMMIT/ROLLBACK时清除。
5)CurTransactionContext:此内存环境持有当前事务的数据,直到当前事务结束,特别是在最高层事务提交时需要此内存环境。当处于一个最高层事务中时,此内存环境与TopTransactionContext一致,但是在子事务中,CurTransactionContext则指向一个子内存环境。
6)ErrorContext:这是一个持久性的内存环境,会在错误恢复过程中切换,在恢复结束时重设。这里安排了8K的空间,保证在其他所有内存用尽之后,也可以顺利地把错误恢复。
2.5 使用过程
创建:
cxt = AllocSetContextCreate(CurrentMemoryContext,
"AttachPartitionEnsureIndexes",
ALLOCSET_DEFAULT_SIZES);
删除:
out:
/* Clean up. */
for (i = 0; i < list_length(attachRelIdxs); i++)
index_close(attachrelIdxRels[i], AccessShareLock);
MemoryContextSwitchTo(oldcxt);
MemoryContextDelete(cxt);
创建删除的调用代码如上,下面对其中调用palloc的部分的分配进行分析,
主要分为以下两个步骤:
1)调用palloc申请内存。
2)palloc中调用虚拟表(即函数指针的列表)中的alloc方法。
其中底层使用的为AllocSetAlloc:
static const MemoryContextMethods AllocSetMethods = {
AllocSetAlloc,
AllocSetFree,
AllocSetRealloc,
AllocSetReset,
AllocSetDelete,
AllocSetGetChunkSpace,
AllocSetIsEmpty,
AllocSetStats
#ifdef MEMORY_CONTEXT_CHECKING
,AllocSetCheck
#endif
};
3.共享内存区域介绍
分为以下几个部分:
1)Shared buffer pool:存储PG表和索引的页面。
2)WAL buffer:WAL(事务日志)写入前的存储区域。
3)Commit log buffer:提交日志为并发控制(CC)机制保存了所需的所有事务状态(例如进行中、已提交、已中止等)。
4. 查看方法
在PG14提供了pg_log_backend_memory_contexts函数,查看MemoryContext使用情况,可以使用select pg_log_backend_memory_contexts(进程id),然后可以在log中查看信息。