PostgreSQL 内存管理深度解析
摘要
PostgreSQL 拥有精心设计的分层内存管理架构,包括进程本地内存上下文、共享内存、缓冲区缓存和查询执行内存。本文深入剖析 PostgreSQL 源码中的内存管理实现,涵盖内存上下文机制、共享内存初始化、缓冲区管理、查询内存分配策略等核心主题,帮助读者全面理解 PostgreSQL 如何高效管理内存资源。
1. PostgreSQL 内存架构总览
┌─────────────────────────────────────────────────────────────────────────────┐
│ PostgreSQL 内存架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 共享内存 (Shared Memory) │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ 缓冲区缓存 │ │ WAL 缓冲区 │ │ 锁管理器 │ │ │
│ │ │ (8KB * N) │ │ (16MB) │ │ (Lightweight Locks)│ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ 进程/连接信息 │ │ 事务管理器 │ │ 缓存表 │ │ │
│ │ │ (per backend) │ │ (XID, clog) │ │ (syscache) │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 进程本地内存 (Per-Backend) │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ 内存上下文 │ │ 查询执行内存 │ │ 临时文件 │ │ │
│ │ │ (Memory Cxt) │ │ (work_mem) │ │ (spill to disk) │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘1.1 内存区域划分
| 区域类型 | 生命周期 | 作用范围 | 配置参数 |
|---|---|---|---|
| 共享内存 | 服务启动到关闭 | 所有进程共享 | shared_buffers |
| 本地内存 | 连接生命周期 | 单个后端进程 | work_mem |
| 查询内存 | 查询执行期间 | 单个操作 | work_mem |
| 维护内存 | 维护操作期间 | VACUUM/索引创建 | maintenance_work_mem |
| 临时内存 | 临时表访问 | 会话级别 | temp_buffers |
2. 内存上下文 (Memory Context)
2.1 核心数据结构
内存上下文是 PostgreSQL 本地内存管理的基石。定义于 src/include/nodes/memnodes.h:
// src/include/nodes/memnodes.h:42-57
typedef struct MemoryContextData {
NodeTag type; // 上下文类型标识
bool isReset; // 上次重置后是否分配过空间
bool allowInCritSection; // 关键代码段中是否允许分配
Size mem_allocated; // 此上下文已分配的内存总量
const MemoryContextMethods *methods; // 虚拟函数表
MemoryContext parent; // 父上下文 (NULL 表示根上下文)
MemoryContext firstchild; // 子上下文链表头
MemoryContext prevchild; // 同父上下文中的前一个
MemoryContext nextchild; // 同父上下文中的后一个
const char *name; // 上下文名称
const char *ident; // 上下文标识符
MemoryContextCallback *reset_cbs; // 重置/删除回调函数列表
} MemoryContextData;2.2 内存上下文方法接口
// src/include/nodes/memnodes.h:58-72
typedef struct MemoryContextMethods {
void *(*alloc) (MemoryContext context, Size size, int flags);
void (*free_p) (void *pointer);
void *(*realloc) (void *pointer, Size size, int flags);
void (*reset) (MemoryContext context); // 释放所有块,保留上下文
void (*delete_context) (MemoryContext context); // 彻底删除上下文
MemoryContext (*get_chunk_context) (void *pointer);
Size (*get_chunk_space) (void *pointer);
bool (*is_empty) (MemoryContext context);
void (*stats) (MemoryContext context, ...);
} MemoryContextMethods;2.3 上下文层次结构
TopMemoryContext (根上下文)
│
├── MessageContext // 当前消息处理
│ └── PortalContext // Portal 执行上下文
│ └── QueryDesc // 查询描述符
│
├── ExecutorState (es_query_cxt) // 查询执行上下文
│ ├── SortState
│ ├── HashJoinState
│ └── AggState
│
└── CurTransactionData // 当前事务数据2.4 AllocSet 实现详解
AllocSet 是最常用的内存上下文实现 (src/backend/utils/mmgr/aset.c):
// src/backend/utils/mmgr/aset.c
typedef struct AllocSetContext {
MemoryContextData header; // 标准内存上下文头部
AllocBlock blocks; // 此上下文的所有内存块链表
// 空闲链表数组 - 11 个,分别对应 2^3 到 2^13 字节
MemoryChunk *freelist[ALLOCSET_NUM_FREELISTS];
uint32 initBlockSize; // 初始块大小
uint32 maxBlockSize; // 最大块大小
uint32 nextBlockSize; // 下一个要分配的块大小
uint32 allocChunkLimit; // 有效分块大小限制
int freeListIndex; // 在全局空闲链表中的索引
} AllocSetContext;
// 空闲链表配置
#define ALLOC_MINBITS 3 // 最小块: 8 字节 (2^3)
#define ALLOCSET_NUM_FREELISTS 11 // 最多 8192 字节 (2^13)
#define ALLOC_CHUNK_LIMIT (1 << ALLOCSET_NUM_FREELISTS) // 81922.5 内存块结构
// src/backend/utils/mmgr/aset.c
typedef struct AllocBlockData {
AllocSet aset; // 拥有此块的 AllocSet
AllocBlock prev; // 同 aset 中前一个块
AllocBlock next; // 同 aset 中后一个块
char *freeptr; // 此块中空闲空间的起始位置
char *endptr; // 此块空间的结束位置
} AllocBlockData;2.6 分配策略图解
┌─────────────────────────────────────────────────────────────────┐
│ AllocSet 分配策略 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 分配请求 (size) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 大小判断: size <= 8192 ? │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 是: 检查对应空闲链表 [freelist[idx]] │ │ │
│ │ │ 向上取整到 2 的幂: 8, 16, 32, 64, ... │ │ │
│ │ │ │ │ │
│ │ │ 空闲链表命中? ──→ 返回空闲块 │ │ │
│ │ │ │ │ │ │
│ │ │ └──→ 无空闲块 ──→ 从当前块分配 │ │ │
│ │ │ │ │ │ │
│ │ │ └──→ 当前块不足 ──→ 新块 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 否: 大块分配 (直接 malloc) │ │ │
│ │ │ 不使用空闲链表 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘3. palloc/pfree 实现
3.1 palloc 函数
// src/backend/utils/mmgr/mcxt.c:1317-1344
void *
palloc(Size size)
{
void *ret;
MemoryContext context = CurrentMemoryContext; // 使用当前上下文
Assert(MemoryContextIsValid(context));
AssertNotInCriticalSection(context);
context->isReset = false;
// 通过函数指针调用上下文的 alloc 方法
ret = context->methods->alloc(context, size, 0);
Assert(ret != NULL); // 内存不足时抛出 ERROR
VALGRIND_MEMPOOL_ALLOC(context, ret, size);
return ret;
}3.2 palloc 变体
// src/include/utils/palloc.h
// 标准 palloc
void *palloc(Size size);
// 释放内存 (实际上是标记为空闲,不会真正释放)
void pfree(void *pointer);
// 重新分配
void *repalloc(void *pointer, Size size);
// 分配零初始化的内存
void *palloc0(Size size);
// 分配对齐的内存
void *palloc0fast(Size size);
// 分配数组 (自动计算元素数量)
#define pallocArray(ctx, type, n) \
((type *) MemoryContextAlloc(ctx, sizeof(type) * (n)))
// 分配并零初始化数组
#define palloc0Array(ctx, type, n) \
((type *) MemoryContextAllocZero(ctx, sizeof(type) * (n)))3.3 上下文切换
// src/include/utils/palloc.h:123-131
static inline MemoryContext
MemoryContextSwitchTo(MemoryContext context)
{
MemoryContext old = CurrentMemoryContext;
CurrentMemoryContext = context;
return old; // 返回旧上下文,便于恢复
}
// 使用示例:
MemoryContext old_ctx = MemoryContextSwitchTo(MyQueryContext);
result = palloc(size);
/* ... */
MemoryContextSwitchTo(old_ctx);4. 上下文生命周期管理
4.1 创建内存上下文
// src/backend/utils/mmgr/memutils.c
MemoryContext
AllocSetContextCreate(
MemoryContext parent, // 父上下文
const char *name, // 上下文名称
Size minContextSize, // 最小上下文大小
Size initBlockSize, // 初始块大小
Size maxBlockSize) // 最大块大小
{
AllocSetContext *set;
// 1. 从父上下文分配上下文结构
set = (AllocSetContext *)
MemoryContextAlloc(parent, sizeof(AllocSetContext));
// 2. 初始化结构
set->header.methods = &AllocSetMethods;
set->header.parent = parent;
set->header.name = name;
set->blocks = NULL;
// 3. 初始化空闲链表
for (int i = 0; i < ALLOCSET_NUM_FREELISTS; i++)
set->freelist[i] = NULL;
// 4. 设置块大小参数
set->initBlockSize = MAXALIGN(initBlockSize);
set->maxBlockSize = MAXALIGN(maxBlockSize);
set->nextBlockSize = set->initBlockSize;
// 5. 添加到父上下文的子链表
set->header.prevchild = NULL;
set->header.nextchild = parent->firstchild;
if (parent->firstchild != NULL)
parent->firstchild->prevchild = (MemoryContext) set;
parent->firstchild = (MemoryContext) set;
return (MemoryContext) set;
}4.2 上下文重置 vs 删除
// 重置上下文 - 释放所有块,保留上下文结构
void
MemoryContextReset(MemoryContext context)
{
// 调用上下文的 reset 方法
context->methods->reset(context);
context->isReset = true;
}
// 删除上下文 - 递归删除所有子上下文和块
void
MemoryContextDelete(MemoryContext context)
{
// 1. 递归删除所有子上下文
while (context->firstchild != NULL)
MemoryContextDelete(context->firstchild);
// 2. 调用上下文的 delete_context 方法
context->methods->delete_context(context);
}4.3 回调机制
// src/include/nodes/memnodes.h:77-85
typedef void (*MemoryContextCallbackFunction) (void *arg);
typedef struct MemoryContextCallback {
MemoryContextCallbackFunction func; // 回调函数
void *arg; // 回调参数
struct MemoryContextCallback *next; // 链表下一项
} MemoryContextCallback;
// 注册回调
void
MemoryContextRegisterResetCallback(
MemoryContext context,
MemoryContextCallback *cb)
{
cb->next = context->reset_cbs;
context->reset_cbs = cb;
}
// 典型使用场景
void my_cleanup_callback(void *arg)
{
/* 清理资源,如关闭文件句柄 */
}
MemoryContextCallback cb;
cb.func = my_cleanup_callback;
cb.arg = my_data;
MemoryContextRegisterResetCallback(ctx, &cb);5. 共享内存 (Shared Memory)
5.1 共享内存头部结构
// src/include/storage/pg_shmem.h:31-44
typedef struct PGShmemHeader {
int32 magic; // 魔数: 679834894,用于验证
pid_t creatorPID; // 创建进程的 PID
Size totalsize; // 整个共享内存段大小
Size freeoffset; // 第一个空闲空间的偏移量
dsm_handle dsm_control; // 动态共享内存控制段 ID
void *index; // ShmemIndex 哈希表指针
dev_t device; // 数据目录所在设备
ino_t inode; // 数据目录的 inode 号
} PGShmemHeader;5.2 共享内存索引
所有共享内存结构通过 ShmemIndex 追踪:
// src/backend/storage/ipc/shmem.c
typedef struct {
char key[SHMEM_INDEX_KEYSIZE]; // 结构名称字符串
Size size; // 结构大小
Size allocated_size; // 实际分配大小
void *location; // 起始地址
} ShmemIndexEnt;5.3 共享内存初始化流程
CreateSharedMemoryAndSemaphores()
│
├── RequestAddinShmemSpace() // 请求额外的 addin 空间
│
├── PGSharedMemoryCreate()
│ │
│ ├── 创建系统 V 共享内存段
│ │
│ └── 初始化 PGShmemHeader
│
├── InitShmemAccess()
│ │
│ └── 设置基本指针
│
├── InitShmemAllocation()
│ │
│ └── 初始化自旋锁用于 ShmemAlloc
│
├── InitShmemIndex()
│ │
│ └── 创建 shmem index 哈希表
│
└── CreateOrAttachShmemStructs()
│
├── InitBufferPool() // 初始化缓冲区缓存
├── InitLockTables() // 初始化锁表
├── InitXactGlobals() // 初始化事务全局变量
├── CreateSharedMemoryGraph() // 创建共享内存图
└── ... 其他子系统6. 缓冲区缓存 (Buffer Pool)
6.1 缓冲区描述符
// src/include/storage/buf_internals.h:244-255
typedef struct BufferDesc {
BufferTag tag; // 缓冲区中页面的 ID
int buf_id; // 缓冲区索引号 (0 到 N-1)
// 状态组合: refcount(18位) + usage_count(4位) + flags(10位)
pg_atomic_uint32 state;
int wait_backend_pgprocno; // 等待 pin 计数的 backend
int freeNext; // 空闲链表链接
LWLock content_lock; // 锁定缓冲区内容
} BufferDesc;6.2 缓冲区标签
// src/include/storage/buf_internals.h:92-99
typedef struct buftag {
Oid spcOid; // 表空间 OID
Oid dbOid; // 数据库 OID
RelFileNumber relNumber; // 关系文件号
ForkNumber forkNum; // 分叉号 (main, fsm, vm, etc)
BlockNumber blockNum; // 相对于关系起始的块号
} BufferTag;6.3 缓冲区状态标志
// src/include/storage/buf_internals.h:59-69
#define BM_LOCKED (1 << 0) // 正在等待 I/O
#define BMDirty (1 << 1) // 页面被修改,需要写回
#define BM_VALID (1 << 2) // 包含有效数据
#define BM_JUST_BEEN_FREE (1 << 3) // 刚被释放
#define BM_TAG_VALID (1 << 4) // tag 有效(页面已加载)
#define BM_IO_IN_PROGRESS (1 << 5) // I/O 进行中6.4 缓冲区查找流程
BufferGetBlockNumber()
│
▼
hash_search_with_hash_value(tag)
│
├── 在 buf_table 哈希表中查找 BufferTag
│
├── 找到:
│ │
│ ├── PinBuffer() - 增加引用计数
│ │
│ └── 返回 BufferDesc 指针
│
└── 未找到:
│
├── 选择替换缓冲区 (Clock-sweep 算法)
│
├── 如果脏页 → 写回磁盘
│
├── 从磁盘读取目标页
│
└── 返回 BufferDesc 指针6.5 BufferTable 哈希表
// src/backend/storage/buffer/bufmgr.c
// 缓冲区管理器使用哈希表来快速定位缓冲区
typedef struct sbufdescinfo {
BufferDesc *bufdesc; // 缓冲区描述符指针
uint32 hashvalue; // tag 的哈希值
} sbufdescinfo;
// 哈希表大小: 必须为 2 的幂
// 位置: src/include/storage/buf_internals.h:290
#define BBUFSHIFT 14
#define BBUFSIZE (1 << BBUFSHIFT) // 默认 16384 个缓冲区7. 查询内存 (Query Memory)
7.1 work_mem 分配
work_mem 是 PostgreSQL 中最重要的内存参数之一,用于查询执行过程中的各种操作。
排序操作:
// src/backend/executor/nodeSort.c:110
TupleDesc tupDesc;
Sort *node;
// 每个 Sort 节点独立分配 work_mem
sortContext = AllocSetContextCreate(CurrentMemoryContext,
"Sort",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
work_mem * 1024); // work_mem 单位是 kB哈希连接:
// src/backend/executor/nodeHash.c:3487-3497
static uint64
get_hash_memory_limit(void)
{
double hash_mem_multiplier = 1.0;
// 使用 work_mem * hash_mem_multiplier 作为内存限制
mem_limit = (double) work_mem * hash_mem_multiplier * 1024.0;
return (size_t) mem_limit;
}
// 创建哈希表时分配
HashJoinTable
ExecHashTableCreate(HashJoinState *hjstate, List *hashOperators)
{
Size allowed = get_hash_memory_limit();
// 创建哈希表上下文
hashtable->hashCxt = AllocSetContextCreate(
CurrentMemoryContext,
"HashJoin",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
allowed); // 限制为 work_mem
}7.2 内存上下文层次
TopMemoryContext
│
└── EState (es_query_cxt)
│
├── HashJoinState (hashCxt) // 哈希表
│ ├── batchCxt (批次元数据)
│ └── spillCxt (溢出文件)
│
├── SortState (sortcontext) // 排序
│ └── tapes[] (排序磁带)
│
├── AggState (aggcontext) // 聚合
│ └── hashTable
│
└── WindowAggState (wincontext) // 窗口函数7.3 内存溢出到磁盘
排序溢出 (tuplesort.c):
// tuplesort.c 中的内存状态机
typedef enum {
TSS_INITIAL, // 初始状态
TSS_BUILDRUNS, // 构建运行(可能溢出)
TSS_SORTEDINMEM, // 全部在内存中排序完成
TSS_SORTEDONTAPE, // 溢出到磁带
TSS_FINALMERGE // 最终归并
} TupSortStatus;
// 内存不足检查
// tuplesort.c
#define LACKMEM(state) ((state)->availMem < 0)
// 溢出触发条件
if (LACKMEM(state)) {
// 将当前运行写入磁盘
currentRun = LogicalTapeWrite(state->tapeSet, ...);
// 如果运行数过多,触发全归并
if (state->numRuns > state->maxRuns)
return_above_materialized(state);
}哈希连接溢出:
// nodeHash.c:2494-2499
while (hashtable->spaceUsedSkew > hashtable->spaceAllowedSkew) {
// 减少倾斜批次
ExecHashIncreaseNumBatches(hashtable);
}
if (hashtable->spaceUsed > hashtable->spaceAllowed) {
// 溢出到磁盘
ExecHashIncreaseNumBatches(hashtable);
}
// 增加批次时将数据写入磁盘
// 数据按哈希值分散到不同的 batch 文件7.4 maintenance_work_mem
用于维护操作(VACUUM、CREATE INDEX 等):
// CREATE INDEX 时分配
// src/backend/access/nbtree/nbtsearch.c:373-432
Size sortmem = maintenance_work_mem / nparticipanttuplesorts;
tuplesort_begin_heap(tupDesc,
nKeys,
nKeys,
false, // useSortAcc?
false, // enforceUnique?
parallel,
sortmem, // 使用 maintenance_work_mem
workPlan->randomSeed,
dn,
0);// VACUUM 使用
// src/backend/commands/vacuumlazy.c
/*
* We are willing to use at most maintenance_work_mem
* for the dead tuple pool, but we have no estimate of
* how many dead tuples there are.
*/
dead_tuples = (DeadTuple *)
palloc(min(maintMemBuffer,
(Size) (ntup * sizeof(DeadTuple))));8. 临时缓冲区 (temp_buffers)
8.1 temp_buffers 特点
| 特性 | 说明 |
|---|---|
| 默认值 | 8MB (1024 个 8KB 缓冲区) |
| 作用范围 | 会话级别 |
| 影响对象 | 仅临时表 |
| 配置时机 | 必须在访问任何临时表之前设置 |
8.2 与 work_mem 的区别
| 维度 | work_mem | temp_buffers |
|---|---|---|
| 用途 | 操作内存(排序/哈希) | 临时表页缓存 |
| 作用域 | 每个操作 | 整个会话 |
| 分配方式 | 动态 palloc | 固定缓冲区池 |
| 溢出 | 基于文件的算法 | LRU 淘汰 |
| 可继承 | 否 | 否(会话级别) |
9. 关键配置参数
9.1 内存相关 GUC 参数
| 参数 | 默认值 | 说明 |
|---|---|---|
shared_buffers | 128MB | 共享缓冲区缓存大小 |
work_mem | 4MB | 查询操作内存限制 |
maintenance_work_mem | 64MB | 维护操作内存限制 |
temp_buffers | 8MB | 临时表缓冲区大小 |
effective_cache_size | 4GB | 规划器使用的缓存估计 |
huge_pages | try | 启用大页内存 |
shared_memory_type | mmap | 共享内存实现方式 |
9.2 推荐的内存配置公式
shared_buffers = (1/4) * total_memory
work_mem = (1/4) * (total_memory - shared_buffers) / max_connections
maintenance_work_mem = (1/4) * total_memory
effective_cache_size = (3/4) * total_memory9.3 查看内存使用
-- 查看缓冲区统计
SELECT * FROM pg_stat_bgwriter;
-- 查看当前缓冲区使用
SELECT * FROM pg_buffercache;
-- 查看内存上下文
SELECT * FROM pg_memory_contexts;
-- 查看共享内存大小
SELECT * FROM pg_shared_memory_detect();10. 核心源码文件索引
| 文件路径 | 关键函数 | 功能描述 |
|---|---|---|
src/include/nodes/memnodes.h | MemoryContextData | 内存上下文结构定义 |
src/backend/utils/mmgr/mcxt.c | palloc/pfree | 内存分配实现 |
src/backend/utils/mmgr/aset.c | AllocSetContext | AllocSet 实现 |
src/backend/utils/mmgr/memutils.c | AllocSetContextCreate | 上下文创建 |
src/include/storage/pg_shmem.h | PGShmemHeader | 共享内存头 |
src/backend/storage/ipc/shmem.c | ShmemIndex | 共享内存索引 |
src/include/storage/buf_internals.h | BufferDesc | 缓冲区描述符 |
src/backend/storage/buffer/bufmgr.c | BufferGet | 缓冲区获取 |
src/backend/executor/nodeHash.c | ExecHashTableCreate | 哈希表创建 |
src/backend/utils/sort/tuplesort.c | tuplesort_begin | 排序初始化 |
src/include/storage/buf_internals.h:290 | BBUFSIZE | 缓冲区数量宏 |
11. 总结
PostgreSQL 内存管理要点
内存上下文: PostgreSQL 使用树形结构的内存上下文来管理本地内存,每个上下文都有独立的块分配和空闲链表
AllocSet 策略: 小于 8KB 的分配使用空闲链表优化,大于 8KB 的直接 malloc
共享内存: PostgreSQL 使用系统 V 共享内存或 mmap,包含缓冲区缓存、WAL 缓冲区、锁表等
缓冲区缓存: 使用带 Clock-sweep 算法的 LRU 淘汰策略,通过哈希表快速查找
查询内存: work_mem 控制每个操作的内存使用,超出限制时使用基于文件的溢出算法
维护内存: maintenance_work_mem 用于 VACUUM 和索引创建等维护操作
会话内存: temp_buffers 为临时表提供独立的缓冲区缓存
设计哲学
PostgreSQL 的内存管理体现了几个关键设计原则:
- 层次化: 从共享内存到进程本地再到查询级别,层层划分
- 隔离性: 不同操作使用独立的内存上下文,互不干扰
- 可恢复性: 内存不足时通过溢出到磁盘保证查询完成
- 可观测性: 提供丰富的视图和统计信息监控内存使用
本文档基于 PostgreSQL 源码分析编写,涵盖内存上下文机制、共享内存、缓冲区管理、查询内存等核心主题。