PostgreSQL-04-Storage-存储引擎
模块概览
Storage 模块负责 PostgreSQL 的底层数据存储与管理,包括缓冲区管理(Buffer Manager)、磁盘 I/O、页面组织、共享内存管理等核心功能。
核心能力
- 缓冲区管理(Buffer Manager):管理共享内存中的数据页缓存
- 磁盘 I/O:异步/同步读写磁盘页面
- 页面组织:8KB 页面格式、空闲空间管理
- 共享内存:进程间数据共享、锁管理
- 文件管理:虚拟文件描述符(VFD)管理
架构图
flowchart TB
subgraph Storage["Storage 存储引擎"]
BufMgr[Buffer Manager<br/>缓冲区管理] --> BufPool[Buffer Pool<br/>共享缓冲区]
BufMgr --> BufTable[Buffer Mapping Table<br/>页面查找哈希表]
BufMgr --> FreeList[Free List<br/>空闲缓冲区链表]
BufMgr --> DiskIO[磁盘 I/O]
DiskIO --> SMGR[SMGR<br/>存储管理器接口]
SMGR --> MD[MD<br/>磁盘文件管理]
BufMgr --> LockMgr[Lock Manager<br/>Buffer锁管理]
subgraph Page["页面结构"]
PageHeader[Page Header<br/>页头]
ItemIds[Item Pointers<br/>行指针数组]
Tuples[Tuples<br/>元组数据]
Special[Special Space<br/>索引特殊空间]
end
end
subgraph Access["访问层"]
HeapAM[Heap AM<br/>堆表访问]
IndexAM[Index AM<br/>索引访问]
end
Access --> BufMgr
MD --> Disk[(磁盘文件)]
核心数据结构
1. BufferDesc(缓冲区描述符)
/* src/include/storage/buf_internals.h */
typedef struct BufferDesc
{
BufferTag tag; /* 页面标识(关系OID + 块号) */
int buf_id; /* 缓冲区编号(0-NBuffers-1) */
pg_atomic_uint32 state; /* 状态标志位(脏页、有效、锁等) */
int wait_backend_pgprocno; /* 等待此缓冲区的进程 */
int freeNext; /* 空闲链表下一个 */
LWLock content_lock; /* 内容锁(读写锁) */
LWLock io_in_progress_lock; /* I/O 进行中锁 */
} BufferDesc;
typedef struct BufferTag
{
Oid spcOid; /* 表空间 OID */
Oid dbOid; /* 数据库 OID */
RelFileNumber relNumber; /* 关系文件号 */
ForkNumber forkNum; /* Fork 类型(主数据/FSM/VM) */
BlockNumber blockNum; /* 块号 */
} BufferTag;
状态标志位
#define BM_DIRTY (1U << 0) /* 脏页标志 */
#define BM_VALID (1U << 1) /* 有效页标志 */
#define BM_TAG_VALID (1U << 2) /* Tag 有效 */
#define BM_IO_IN_PROGRESS (1U << 3) /* I/O 进行中 */
#define BM_LOCKED (1U << 4) /* 内容锁定 */
#define BM_PERMANENT (1U << 5) /* 永久缓冲区(系统表) */
2. 页面结构(Page Layout)
/* 8KB 页面布局 */
typedef struct PageHeaderData
{
PageXLogRecPtr pd_lsn; /* WAL 日志序列号(8 字节) */
uint16 pd_checksum; /* 页面校验和 */
uint16 pd_flags; /* 标志位 */
LocationIndex pd_lower; /* 空闲空间起始位置 */
LocationIndex pd_upper; /* 空闲空间结束位置 */
LocationIndex pd_special; /* 特殊空间起始位置 */
uint16 pd_pagesize_version; /* 页面大小和版本 */
TransactionId pd_prune_xid; /* 最早需要清理的事务ID */
ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* 行指针数组 */
} PageHeaderData;
/* 行指针(4 字节) */
typedef struct ItemIdData
{
unsigned lp_off:15; /* 元组偏移(字节) */
unsigned lp_flags:2; /* 标志(未使用/正常/重定向/死亡) */
unsigned lp_len:15; /* 元组长度(字节) */
} ItemIdData;
页面布局示意
+------------------+ 0
| Page Header | 24 字节
+------------------+
| Item Pointers | pd_lower
| (growing down) |
+------------------+
| Free Space |
+------------------+
| Tuples | pd_upper
| (growing up) |
+------------------+
| Special Space | pd_special
+------------------+ 8192
核心功能实现
功能 1:缓冲区查找与分配
ReadBuffer() - 读取页面
/* src/backend/storage/buffer/bufmgr.c */
Buffer
ReadBuffer(Relation reln, BlockNumber blockNum)
{
return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum,
RBM_NORMAL, NULL);
}
Buffer
ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
ReadBufferMode mode, BufferAccessStrategy strategy)
{
BufferDesc *bufHdr;
Block bufBlock;
bool found;
/* 1. 构造 BufferTag */
BufferTag newTag;
INIT_BUFFERTAG(newTag, reln->rd_smgr->smgr_rlocator.locator,
forkNum, blockNum);
/* 2. 在缓冲池中查找页面 */
bufHdr = BufferAlloc(reln->rd_smgr, reln->rd_rel->relpersistence,
forkNum, blockNum, strategy, &found, NULL);
if (found)
{
/* 缓存命中,直接返回 */
return BufferDescriptorGetBuffer(bufHdr);
}
/* 3. 缓存未命中,从磁盘读取 */
bufBlock = BufHdrGetBlock(bufHdr);
smgrread(reln->rd_smgr, forkNum, blockNum, bufBlock);
/* 4. 标记为有效 */
pg_atomic_fetch_or_u32(&bufHdr->state, BM_VALID);
return BufferDescriptorGetBuffer(bufHdr);
}
BufferAlloc() - 分配缓冲区
static BufferDesc *
BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
BlockNumber blockNum, BufferAccessStrategy strategy,
bool *foundPtr, IOContext io_context)
{
BufferTag newTag;
uint32 newHash;
int buf_id;
BufferDesc *buf;
bool found;
/* 1. 计算哈希值 */
INIT_BUFFERTAG(newTag, smgr->smgr_rlocator.locator, forkNum, blockNum);
newHash = BufTableHashCode(&newTag);
/* 2. 在哈希表中查找 */
LWLockAcquire(BufMappingPartitionLock(newHash), LW_EXCLUSIVE);
buf_id = BufTableLookup(&newTag, newHash);
if (buf_id >= 0)
{
/* 找到,增加引用计数 */
buf = GetBufferDescriptor(buf_id);
PinBuffer(buf);
*foundPtr = true;
LWLockRelease(BufMappingPartitionLock(newHash));
return buf;
}
/* 3. 未找到,从空闲链表获取缓冲区 */
buf = GetFreeBuffer(strategy);
/* 4. 如果缓冲区是脏页,先写回磁盘 */
if (pg_atomic_read_u32(&buf->state) & BM_DIRTY)
{
FlushBuffer(buf, NULL, IOOBJECT_RELATION, io_context);
}
/* 5. 插入哈希表 */
buf->tag = newTag;
BufTableInsert(&newTag, newHash, buf->buf_id);
*foundPtr = false;
LWLockRelease(BufMappingPartitionLock(newHash));
return buf;
}
时钟扫描算法(Clock-Sweep)
PostgreSQL 使用时钟扫描算法选择牺牲页:
static BufferDesc *
StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state,
bool *from_ring)
{
BufferDesc *buf;
int trycounter;
uint32 local_buf_state;
/* 时钟指针,全局共享 */
static pg_atomic_uint32 StrategyControl_nextVictimBuffer;
for (trycounter = NBuffers; trycounter > 0; trycounter--)
{
/* 获取下一个缓冲区(时钟前进) */
buf = GetBufferDescriptor(pg_atomic_fetch_add_u32(
&StrategyControl_nextVictimBuffer, 1) % NBuffers);
local_buf_state = LockBufHdr(buf);
/* 检查是否可替换 */
if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0)
{
if (BUF_STATE_GET_USAGECOUNT(local_buf_state) > 0)
{
/* 使用计数 > 0,递减后继续 */
local_buf_state -= BUF_USAGECOUNT_ONE;
UnlockBufHdr(buf, local_buf_state);
}
else
{
/* 找到牺牲页 */
*buf_state = local_buf_state;
return buf;
}
}
else
{
/* 被 Pin,不能替换 */
UnlockBufHdr(buf, local_buf_state);
}
}
/* 未找到可用缓冲区 */
elog(ERROR, "no unpinned buffers available");
return NULL;
}
时钟算法原理
- 使用计数(Usage Count):每次访问页面时增加(最大 5)
- 时钟扫描:从当前位置顺序扫描缓冲区
- 使用计数 > 0:递减,继续扫描
- 使用计数 = 0 且未被 Pin:选为牺牲页
- 优势:近似 LRU,开销低(O(1)),无需维护链表
功能 2:脏页管理
MarkBufferDirty() - 标记脏页
void
MarkBufferDirty(Buffer buffer)
{
BufferDesc *bufHdr;
uint32 old_buf_state;
if (!BufferIsValid(buffer))
elog(ERROR, "bad buffer ID");
if (BufferIsLocal(buffer))
{
/* 本地缓冲区(临时表) */
MarkLocalBufferDirty(buffer);
return;
}
bufHdr = GetBufferDescriptor(buffer - 1);
/* 设置脏页标志 */
old_buf_state = pg_atomic_fetch_or_u32(&bufHdr->state, BM_DIRTY);
/* 如果是第一次标记为脏,更新统计 */
if (!(old_buf_state & BM_DIRTY))
pgstat_count_buffer_write(bufHdr);
}
FlushBuffer() - 刷脏页
static void
FlushBuffer(BufferDesc *buf, SMgrRelation reln,
IOObject io_object, IOContext io_context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
Block bufBlock;
/* 1. 获取页面 LSN */
bufBlock = BufHdrGetBlock(buf);
recptr = BufferGetLSN(buf);
/* 2. 确保 WAL 已刷盘(Write-Ahead Logging) */
XLogFlush(recptr);
/* 3. 写入磁盘 */
smgrwrite(reln,
BufTagGetForkNum(&buf->tag),
buf->tag.blockNum,
bufBlock,
false);
/* 4. 清除脏页标志 */
pg_atomic_fetch_and_u32(&buf->state, ~BM_DIRTY);
/* 5. 更新统计 */
pgstat_count_io_op(IOOBJECT_RELATION, IOCONTEXT_NORMAL, IOOP_WRITE);
}
WAL-Logging 规则
- 规则:页面的 LSN ≤ WAL 刷盘位置
- 保证:崩溃恢复时,WAL 包含所有已落盘页面的修改
- 实现:刷脏页前调用
XLogFlush(page_lsn)
功能 3:页面组织与管理
PageAddItem() - 插入行指针
OffsetNumber
PageAddItem(Page page, Item item, Size size, OffsetNumber offsetNumber,
bool overwrite, bool is_heap)
{
PageHeader phdr = (PageHeader) page;
Size alignedSize;
int lower;
int upper;
ItemId itemId;
OffsetNumber limit;
/* 1. 对齐大小(8 字节对齐) */
alignedSize = MAXALIGN(size);
/* 2. 检查空闲空间 */
lower = phdr->pd_lower;
upper = phdr->pd_upper;
if (offsetNumber == InvalidOffsetNumber)
{
/* 追加模式:分配新行指针 */
limit = OffsetNumberNext(PageGetMaxOffsetNumber(page));
if (limit > MaxHeapTuplesPerPage)
return InvalidOffsetNumber;
offsetNumber = limit;
}
/* 计算需要的空间 */
Size needSpace = alignedSize + sizeof(ItemIdData);
if (upper < lower + needSpace)
return InvalidOffsetNumber; /* 空间不足 */
/* 3. 写入元组数据(从上往下) */
upper -= alignedSize;
memcpy((char *) page + upper, item, size);
/* 4. 设置行指针(从下往上) */
itemId = PageGetItemId(page, offsetNumber);
itemId->lp_off = upper;
itemId->lp_len = size;
itemId->lp_flags = LP_NORMAL;
/* 5. 更新页头 */
phdr->pd_lower = lower + sizeof(ItemIdData);
phdr->pd_upper = upper;
return offsetNumber;
}
性能优化
1. shared_buffers 调优
# postgresql.conf
shared_buffers = 16GB # 推荐系统内存的 25%
监控缓存命中率
SELECT
sum(heap_blks_hit) / nullif(sum(heap_blks_hit + heap_blks_read), 0) AS cache_hit_ratio
FROM pg_statio_user_tables;
-- 结果应 > 0.99(99% 命中率)
2. effective_io_concurrency
# SSD 环境
effective_io_concurrency = 200 # 并发 I/O 能力
3. 检查点调优
checkpoint_timeout = 15min # 检查点间隔
max_wal_size = 10GB # 触发检查点的 WAL 量
checkpoint_completion_target = 0.9 # 平滑 I/O
总结
Storage 模块是 PostgreSQL 的基础设施,通过高效的缓冲区管理、页面组织和 I/O 调度,支撑上层的事务处理与查询执行。关键设计包括:
- 时钟扫描算法:高效的页面替换策略
- WAL-Logging:保证数据持久性
- 页面结构:灵活的行指针数组,支持就地更新
- 共享内存:进程间高效数据共享