存储管理器SMGR
简要介绍一下 PostgreSQL 中的 SMGR 和 FD 层。
SMGR 层(Storage Manager)为上层提供面向 relation 的磁盘存储接口;而 FD 层(更准确地说是 VFD, Virtual File Descriptor)则封装了具体的文件打开、关闭、缓存和复用管理,并直接与 OS 系统调用交互。
buffer pool──►[smgr]───┐
│
slru──────────────────┼────►[fd]───►syscall
│
WAL ──────────────────┘
代码主要包括:
- src/backend/storage/smgr/smgr.c
- src/backend/storage/file/fd.c
SMGR接口
SMGR 的接口定义见 smgrsw,它本质上是一个 f_smgr 结构数组(若干函数指针)。目前 PG 主线代码内部只有 md(magnetic disk,src/backend/storage/smgr/md.c)这一套实现:
static const f_smgr smgrsw[] = {
/* magnetic disk */
{
.smgr_init = mdinit,
.smgr_shutdown = NULL,
...
.smgr_read = mdread,
.smgr_write = mdwrite,
...
.smgr_immedsync = mdimmedsync,
}
};
这些接口函数的名字都比较直白。需要留意的是,它们的操作对象都是
SMgrRelation,也就是把“关系”对应的底层文件状态聚合起来管理。
以写操作为例:
void smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, char *buffer, bool skipFsync)
-> mdwrite(...); //函数签名和smgrwrite一致
-> FileWrite(v->mdfd_vfd, buffer, BLCKSZ, ...);
它的功能非常直接:把 buffer 中的数据写入 reln 的第 forknum 个 fork 的第 blocknum 个块。内部实现会继续转发到 FD 层的 FileWrite(),再由后者通过系统调用完成真正的写入。
FD接口
FD 层更准确地说是 VFD(virtual file descriptor)机制。PG 自己实现了一套文件描述符缓存,用来管理和复用有限的 OS file descriptor。SMgrRelation 会持有对底层 File/VFD 的引用,但 VFD 缓存本身是由 FD 层统一维护的。
同样以写操作为例:
int FileWrite(File file, char *buffer, int amount, off_t offset, uint32 wait_event_info)
-> pg_pwrite();
-> pwrite(); // syscall
将buffer中的内容通过pwrite()系统调用写入到文件的相应偏移上。
如下是FD层几个常用函数和系统调用的对应关系:
PathNameOpenFile() → open()
FileRead() → pread()
FileWrite() → pwrite()
FileSync() → fsync()
系统调用通常通过 libc 封装后暴露给上层,因此在名称上可能略有差异。比如 64 位系统中常见的是
pwrite64();如果 gdb 下断点打不住,可以先确认一下实际符号名。
调用栈示例
通过下面两个例子看一下整体调用栈的脉络。
★读操作 SELECT
#0 pread (__offset=0, __nbytes=8192, __buf=0xed65230f8f80, __fd=27) at /usr/include/aarch64-linux-gnu/bits/unistd.h:38
#1 FileRead (file=<optimized out>, buffer=buffer@entry=0xed65230f8f80 "", amount=amount@entry=8192, offset=offset@entry=0, wait_event_info=167772175) at fd.c:2053
#2 mdread (reln=<optimized out>, forknum=<optimized out>, blocknum=0, buffer=0xed65230f8f80 "") at md.c:656
#3 smgrread (reln=reln@entry=0xada30a82fd80, forknum=forknum@entry=MAIN_FORKNUM, blocknum=blocknum@entry=0, buffer=buffer@entry=0xed65230f8f80 "") at smgr.c:504
#4 ReadBuffer_common (smgr=0xada30a82fd80, relpersistence=<optimized out>, forkNum=forkNum@entry=MAIN_FORKNUM, blockNum=blockNum@entry=0, mode=mode@entry=RBM_NORMAL, strategy=strategy@entry=0x0, hit=hit@entry=0xffffd2e655d7) at bufmgr.c:1013
#6 heapgetpage (sscan=sscan@entry=0xada30a867b50, page=page@entry=0) at heapam.c:413
#9 table_scan_getnextslot (sscan=<optimized out>, direction=direction@entry=ForwardScanDirection, slot=slot@entry=0xada30a8674b0) at ../../../src/include/access/tableam.h:1045
#13 ExecSeqScan (pstate=<optimized out>) at nodeSeqscan.c:112
整体路径非常清晰:
Scan(Executor) → heap(tableAM) → ReadBuffer(buffer pool) → smgrread(SMGR) → FileRead(FD) → pread(syscall)
★写操作 INSERT
#0 __libc_pwrite64 (fd=7, buf=buf@entry=0xf10990b22f80, count=count@entry=8192, offset=offset@entry=655360) at ../sysdeps/unix/sysv/linux/pwrite64.c:24
#1 FileWrite (file=<optimized out>, buffer=buffer@entry=0xf10990b22f80 "", amount=amount@entry=8192, offset=offset@entry=655360, wait_event_info=167772171) at fd.c:2135
#2 mdextend (reln=0xab1ec04c0230, forknum=MAIN_FORKNUM, blocknum=80, buffer=0xf10990b22f80 "", skipFsync=false) at md.c:448
#3 smgrextend (reln=reln@entry=0xab1ec04c0230, forknum=forknum@entry=MAIN_FORKNUM, blocknum=blocknum@entry=80, buffer=buffer@entry=0xf10990b22f80 "", skipFsync=skipFsync@entry=false) at smgr.c:465
#4 ReadBuffer_common (smgr=0xab1ec04c0230, relpersistence=<optimized out>, forkNum=forkNum@entry=MAIN_FORKNUM, blockNum=80, blockNum@entry=4294967295, mode=mode@entry=RBM_ZERO_AND_LOCK, strategy=strategy@entry=0x0, hit=hit@entry=0xffffe15fa987) at bufmgr.c:988
#7 RelationGetBufferForTuple (relation=relation@entry=0xf109993af760, len=32, otherBuffer=otherBuffer@entry=0, options=options@entry=0, bistate=bistate@entry=0x0, vmbuffer=vmbuffer@entry=0xffffe15faa8c, vmbuffer_other=vmbuffer_other@entry=0x0) at hio.c:624
#8 heap_insert (relation=relation@entry=0xf109993af760, tup=tup@entry=0xab1ec04e5140, cid=cid@entry=0, options=options@entry=0, bistate=bistate@entry=0x0) at heapam.c:2124
#10 table_tuple_insert (bistate=0x0, options=0, cid=<optimized out>, slot=0xab1ec04e5028, rel=0xf109993af760) at ../../../src/include/access/tableam.h:1375
#11 ExecInsert (mtstate=mtstate@entry=0xab1ec04e57a8, resultRelInfo=resultRelInfo@entry=0xab1ec04e59c0, slot=0xab1ec04e5028, planSlot=planSlot@entry=0xab1ec04e6b60, estate=estate@entry=0xab1ec04e5530, canSetTag=<optimized out>) at nodeModifyTable.c:1031
#12 ExecModifyTable (pstate=0xab1ec04e57a8) at nodeModifyTable.c:2719
这条脉络和 read 类似,但有一个容易误解的点:这里立即发生的是同步的 extend 操作,也就是给关系文件新增一个 block;而插入后的实际页内容首先体现在 shared buffers 中,后续脏页既可能由 bgwriter/checkpointer 刷盘,也可能在某些场景下由后端自身写出,因此不应简单理解为“只能等待后台进程异步写盘”。
Discussion
PG 的 SMGR 和 FD 层一直比较朴实,在较长一段时间里也较少使用 OS 提供的高级 I/O 特性,因此这里一直有很大的优化空间:
- pg17 中引入了 vectored I/O:可以在一次系统调用里处理多个 I/O 请求。
- pg18 中继续引入了 AIO 和 DIO:进一步增强吞吐和延迟表现。
- The path to using AIO in postgres (PGConf.EU 2023)
For a few years we (Andres Freund, Thomas Munro, Melanie Plageman, David Rowley) have been working towards using asynchronous IO (AIO) and direct IO in Postgres. The goal of using AIO and DIO in postgres is to improve throughput, decrease latency, reduce jitter, reduce double buffering and more.
- 目前的主要使用场景还是数据预读(prefetch),社区也还在持续优化中,可以多关注。
- The path to using AIO in postgres (PGConf.EU 2023)
另外,SMGR 层也支持通过扩展 hook 改写最终的读写行为。比如 NeonDB 就在这里接管了部分存储路径:
static const struct f_smgr neon_smgr =
{
...
#if PG_MAJORVERSION_NUM >= 17
.smgr_prefetch = neon_prefetch,
.smgr_readv = neon_readv, // 先读本地缓存,未命中再去拉取存储服务
.smgr_writev = neon_writev, // 不写本地数据
#else
.smgr_prefetch = neon_prefetch,
.smgr_read = neon_read,
.smgr_write = neon_write,
#endif
...
Questions:
- 前面提到 buffer pool 中的数据写盘通常是异步的,具体可以再细分为哪些场景?
提示:比如数据页换出,可以通过gdb来看看
- MySQL InnoDB 也有 buffer pool,而且一般建议配得越大越好;那么 PG 也是这样吗?
如果2者有差异,造成这种区别的原因是什么?