总结
RM_SMGR_ID处理的是PG的文件层修改,调用smgr提供的接口干活。只做创建 和 truncate,不做文件删除。不做删除的原因是:只有在事务成功提交才能物理删除文件;如果事务回滚,文件必须保留。 redo机制需要旧的基础页面+新的日志,如果是页面比日志新,就玩不转了。
| WAL 类型 | 宏 | 值 | redo 核心动作 | 幂等性保证 |
|---|---|---|---|---|
| CREATE | XLOG_SMGR_CREATE | 0x10 | smgrcreate(reln, forkNum, true) |
第三参数=true: 已存在则忽略 |
| TRUNCATE | XLOG_SMGR_TRUNCATE | 0x20 | smgrtruncate2() + FSM/VM 处理 |
先 XLogFlush 确保不可逆操作安全 |
WAL Record 类型与结构体
1 数据结构
c
/* src/include/catalog/storage_xlog.h */
/*
* xl_smgr_create: 记录新建关系文件所需的最小信息
* 包含 RelFileLocator(表空间+数据库+关系文件编号) 和 fork 编号
*/
typedef struct xl_smgr_create
{
RelFileLocator rlocator; /* 关系文件定位器: spcOid/dbOid/relNumber */
ForkNumber forkNum; /* fork 编号: MAIN_FORKNUM=0, FSM=1, VM=2, INIT=3 */
} xl_smgr_create;
/*
* xl_smgr_truncate: 记录 truncate 操作所需的信息
* 包含 truncate 到哪个块号、要 truncate 哪些 fork
*/
typedef struct xl_smgr_truncate
{
BlockNumber blkno; /* truncate 目标: 保留前 blkno 个块 */
RelFileLocator rlocator; /* 关系文件定位器 */
int flags; /* truncate 标志位: 哪些 fork 需要被 truncate */
} xl_smgr_truncate;
/* truncate 标志位 */
#define SMGR_TRUNCATE_HEAP 0x0001 /* truncate 主数据 fork (MAIN_FORKNUM) */
#define SMGR_TRUNCATE_VM 0x0002 /* truncate 可见性映射 fork (VM) */
#define SMGR_TRUNCATE_FSM 0x0004 /* truncate 空闲空间映射 fork (FSM) */
#define SMGR_TRUNCATE_ALL \
(SMGR_TRUNCATE_HEAP|SMGR_TRUNCATE_VM|SMGR_TRUNCATE_FSM)
/*
* RelFileLocator: 物理文件的三级定位器
*/
typedef struct RelFileLocator
{
Oid spcOid; /* 表空间 OID (如 1663 = pg_default) */
Oid dbOid; /* 数据库 OID */
RelFileNumber relNumber; /* 关系文件编号(即 pg_class.relfilenode) */
} RelFileLocator;
2 触发 SQL 示例
XLOG_SMGR_CREATE (0x10) -- 创建表时产生
sql
-- 创建表时, heap_create() -> RelationCreateStorage() -> log_smgrcreate()
-- 会写入 XLOG_SMGR_CREATE 记录
CREATE TABLE test_smgr (id int, name text);
-- 创建索引时也会产生 SMGR_CREATE
CREATE INDEX idx_smgr ON test_smgr(id);
XLOG_SMGR_TRUNCATE (0x20) -- VACUUM FULL / TRUNCATE 时产生
sql
-- TRUNCATE 命令会 truncate 关系文件
TRUNCATE test_smgr;
-- VACUUM FULL 在重写表时, 也可能产生 truncate 记录
VACUUM FULL test_smgr;
-- CLUSTER 同样可能触发
CLUSTER test_smgr USING idx_smgr;
3 Redo 核心代码分析
c
/* src/backend/catalog/storage.c:982-1097 */
void
smgr_redo(XLogReaderState *record)
{
XLogRecPtr lsn = record->EndRecPtr;
/* 从 WAL 记录中提取 info 字段, 去掉高位标志后得到子类型 */
uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
/* smgr 记录不使用 backup block(全页写), 所以这里做断言检查 */
Assert(!XLogRecHasAnyBlockRefs(record));
if (info == XLOG_SMGR_CREATE)
{
/*
* ===== 处理 CREATE: 创建物理文件 =====
* 从 WAL 记录的 data 区域解析出 xl_smgr_create 结构
*/
xl_smgr_create *xlrec = (xl_smgr_create *) XLogRecGetData(record);
SMgrRelation reln;
/* 打开(或创建)SMgrRelation 对象, InvalidBackendId 表示非临时表 */
reln = smgropen(xlrec->rlocator, InvalidBackendId);
/* 创建物理文件; 第三个参数 true 表示"若已存在则忽略" */
smgrcreate(reln, xlrec->forkNum, true);
}
else if (info == XLOG_SMGR_TRUNCATE)
{
/*
* ===== 处理 TRUNCATE: truncate 物理文件 =====
*/
xl_smgr_truncate *xlrec = (xl_smgr_truncate *) XLogRecGetData(record);
SMgrRelation reln;
Relation rel;
ForkNumber forks[MAX_FORKNUM]; /* 记录要 truncate 的 fork 列表 */
BlockNumber blocks[MAX_FORKNUM]; /* 每个 fork truncate 到的目标块号 */
BlockNumber old_blocks[MAX_FORKNUM]; /* truncate 前各 fork 的块数 */
int nforks = 0;
bool need_fsm_vacuum = false;
reln = smgropen(xlrec->rlocator, InvalidBackendId);
/*
* 强制创建关系文件(如果不存在的话)。
* 不存在说明该关系在 WAL 序列后面被 DROP 了,
* 但为了正确回放, 需要先重建再执行 truncate。
*/
smgrcreate(reln, MAIN_FORKNUM, true);
关键: 在执行 truncate 之前必须先 XLogFlush(lsn)
原因:
- truncate 操作是【不可逆】的 ------ 文件尾部的数据块一旦被截掉就永远找不回来了。这与普通的页面修改不同(页面修改可以通过重放WAL 来重做)。
- WAL-first 规则要求: 描述某项数据变更的 WAL 记录,必须在该变更实际落盘之前先持久化。对于普通的 buffer 写操作,缓冲管理器在 FlushBuffer() 时会自动调用 XLogFlush() 来保证这一点。但 truncate 不经过缓冲管理器,而是直接操作文件系统(ftruncate),所以需要在这里【手动】调用 XLogFlush(lsn)。
如果不先刷 WAL 会怎样? 设想以下崩溃场景:
- 备库回放到这条 TRUNCATE WAL 记录
- 执行了 ftruncate(), 文件被截短, 数据块被丢弃
- 但对应的 WAL 记录还只在内存中, 尚未刷到磁盘
- 此时系统崩溃!
- 重启后, minRecoveryPoint 还停留在 truncate 之前
- 恢复从 minRecoveryPoint 开始, 但 truncate 之前引用
那些被截掉块的 WAL 记录(如 INSERT/UPDATE)需要读取
那些块来做 redo ------ 但块已经不存在了。 - 结果: 恢复失败, 数据损坏
XLogFlush(lsn) 确保这条 TRUNCATE 记录先落盘, 从而更新minRecoveryPoint 到 truncate 之后。这样即使崩溃, 恢复时也不会尝试去读已经被截掉的块。
【总结】redo机制需要旧的基础页面+新的日志,如果是页面比日志新,就玩不转了。
源码注释 (storage.c:1020-1034):
Before we perform the truncation, update minimum recovery point to cover this WAL record. Once the relation is truncated, there's no going back. Doing this before the truncation means that if the truncation fails for some reason, you cannot start up the system even after restart, until you fix the underlying situation so that the truncation will succeed.
c
XLogFlush(lsn);
/* 准备 truncate MAIN fork */
if ((xlrec->flags & SMGR_TRUNCATE_HEAP) != 0)
{
forks[nforks] = MAIN_FORKNUM;
old_blocks[nforks] = smgrnblocks(reln, MAIN_FORKNUM);
blocks[nforks] = xlrec->blkno;
nforks++;
/* 同时通知 xlogutils.c 该关系已被 truncate */
XLogTruncateRelation(xlrec->rlocator, MAIN_FORKNUM, xlrec->blkno);
}
/* 准备 truncate FSM 和 VM fork */
rel = CreateFakeRelcacheEntry(xlrec->rlocator);
if ((xlrec->flags & SMGR_TRUNCATE_FSM) != 0 &&
smgrexists(reln, FSM_FORKNUM))
{
blocks[nforks] = FreeSpaceMapPrepareTruncateRel(rel, xlrec->blkno);
if (BlockNumberIsValid(blocks[nforks]))
{
forks[nforks] = FSM_FORKNUM;
old_blocks[nforks] = smgrnblocks(reln, FSM_FORKNUM);
nforks++;
need_fsm_vacuum = true;
}
}
if ((xlrec->flags & SMGR_TRUNCATE_VM) != 0 &&
smgrexists(reln, VISIBILITYMAP_FORKNUM))
{
blocks[nforks] = visibilitymap_prepare_truncate(rel, xlrec->blkno);
if (BlockNumberIsValid(blocks[nforks]))
{
forks[nforks] = VISIBILITYMAP_FORKNUM;
old_blocks[nforks] = smgrnblocks(reln, VISIBILITYMAP_FORKNUM);
nforks++;
}
}
/* 执行真正的 truncate 操作 */
if (nforks > 0)
{
START_CRIT_SECTION();
smgrtruncate2(reln, forks, nforks, old_blocks, blocks);
END_CRIT_SECTION();
}
/*
* 更新上层 FSM 页面以反映 truncate。
* 被 truncate 的页面之前可能被标记为 all-free, 会被优先选择分配,
* 所以必须及时更新 FSM 索引。
*/
if (need_fsm_vacuum)
FreeSpaceMapVacuumRange(rel, xlrec->blkno, InvalidBlockNumber);
FreeFakeRelcacheEntry(rel);
}
else
elog(PANIC, "smgr_redo: unknown op code %u", info);
}
WAL 写入端代码 (log_smgrcreate)
c
/* src/backend/catalog/storage.c:184-196 */
/*
* log_smgrcreate: 向 WAL 写入一条 XLOG_SMGR_CREATE 记录
* 在 RelationCreateStorage() 中, 创建完物理文件后调用
*/
void
log_smgrcreate(const RelFileLocator *rlocator, ForkNumber forkNum)
{
xl_smgr_create xlrec;
/* 填充 WAL 记录数据 */
xlrec.rlocator = *rlocator;
xlrec.forkNum = forkNum;
XLogBeginInsert();
XLogRegisterData((char *) &xlrec, sizeof(xlrec));
XLogInsert(RM_SMGR_ID, XLOG_SMGR_CREATE); /* 资源管理器=SMGR, 子类型=CREATE */
}
4 GDB 调试数据
XLOG_SMGR_CREATE 断点数据
===== smgr_redo (info=0x10) =====
CREATE: spcOid=1663 dbOid=5 relNumber=74340 forkNum=0
ReadRecPtr=0x15a6cd80 EndRecPtr=0x15a6cdb0
#0 smgr_redo() at storage.c:985
#1 ApplyWalRecord() at xlogrecovery.c:1941
字段解读:
| 字段 | 值 | 含义 |
|---|---|---|
| info | 0x10 | XLOG_SMGR_CREATE 类型 |
| spcOid | 1663 | pg_default 表空间 |
| dbOid | 5 | 目标数据库 OID |
| relNumber | 74340 | 新建关系文件的文件编号(relfilenode) |
| forkNum | 0 | MAIN_FORKNUM, 主数据 fork |
| ReadRecPtr | 0x15a6cd80 | WAL 记录起始位置 |
| EndRecPtr | 0x15a6cdb0 | WAL 记录结束位置(差值=0x30=48字节) |
调用栈分析:
ApplyWalRecord()(xlogrecovery.c:1941) -- WAL 回放调度器,根据 rmid 分发到对应 redo 函数smgr_redo()(storage.c:985) -- SMGR 资源管理器入口,解析 info=0x10 后走 CREATE 分支
redo 动作 : 调用 smgropen() 打开 SMgrRelation 对象,然后调用 smgrcreate() 在磁盘上创建 base/5/74340 物理文件。
5 官方文档
src/include/catalog/storage_xlog.h
Note: we log file creation and truncation here, but logging of deletion
actions is handled by xact.c, because it is part of transaction commit.
SMGR 只负责 CREATE 和 TRUNCATE 的 WAL 记录,DELETE 由事务管理器(xact.c)在提交时处理。这是因为文件删除必须与事务提交原子绑定。
src/backend/access/transam/README
Database writes that skip WAL for new relfilenumbers are also safe. In these
cases it's entirely possible for the data to reach disk before T1's commit,
because T1 will fsync it down to disk without any sort of interlock. However,
all these paths are designed to write data that no other transaction can see
until after T1 commits.
新建的 relfilenode 的数据写入可以跳过 WAL(因为其他事务在 T1 提交前看不到),但文件创建本身(SMGR_CREATE)仍然需要记 WAL,确保 crash recovery 时能重建文件。
TRUNCATE 的 WAL-first 规则
A basic assumption of a write AHEAD log is that log entries must reach stable
storage before the data-page changes they describe.
在 smgr_redo() 的 TRUNCATE 分支中,代码显式调用了 XLogFlush(lsn) 来确保 WAL 记录先于 truncate 操作持久化。这是因为 truncate 是不可逆的,必须严格遵守 WAL-first 规则。