PolarDB PostgreSQL存储I/O路径分析
关于 PolarDB PostgreSQL 版
PolarDB PostgreSQL 版是一款阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 、Ganos全空间数据处理能力和高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB PostgreSQL 版具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载。
背景
PostgreSQL中不同类型的数据持久化在不同的路径下,由于这些数据之间存在不同的特征,因此其各自的I/O操作路径也是不同的。
按照I/O路径的不同,可以将PostgreSQL的持久化文件分为以下五类:
数据文件,主要保存在路径base/、global/下;
WAL日志文件,主要保存在路径pg_wal/下;
事务文件,主要保存在路径pg_commit_ts/、pg_csnlog/、pg_multixact/、pg_xact/下;
特殊文件,主要保存在路径global/pg_control、pg_replslot/、pg_twophase/下;
配置及错误日志文件,主要保存在log/、pg_hba.conf、postgresql.conf中。
PostgreSQL存储I/O路径

数据文件,即存储表、索引等数据的文件,是PostgreSQL中用户数据的主要持久化形式。
由于这部分数据量较大,于是PostgreSQL提供了Buffer Pool缓存池技术来加速这部分数据的读写速度,针对Buffer Pool缓存池的介绍可以参见以往文章《Persistent Buffer Pool》。
PostgreSQL为Buffer Pool提供了专门的I/O接口—sgmr(Storage Manager)模块,该模块实现了以下接口函数来执行具体的I/O操作:
extern SMgrRelation smgropen(RelFileNode rnode, BackendId backend);
extern bool smgrexists(SMgrRelation reln, ForkNumber forknum);
extern void smgrsetowner(SMgrRelation *owner, SMgrRelation reln);
extern void smgrclearowner(SMgrRelation *owner, SMgrRelation reln);
extern void smgrclose(SMgrRelation reln);
extern void smgrcloseall(void);
extern void smgrclosenode(RelFileNodeBackend rnode);
extern void smgrcreate(SMgrRelation reln, ForkNumber forknum, bool isRedo);
extern void smgrdosyncall(SMgrRelation *rels, int nrels);
extern void smgrdounlinkall(SMgrRelation *rels, int nrels, bool isRedo);
extern void smgrextend(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer, bool skipFsync);
extern bool smgrprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
struct PgAioInProgress;
extern void smgrstartread(struct PgAioInProgress *io,
SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrstartwrite(struct PgAioInProgress *io,
SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer, bool skipFsync);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer, bool skipFsync);
extern void smgrwriteback(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, BlockNumber nblocks);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern BlockNumber smgrnblocks_cached(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber *forknum,
int nforks, BlockNumber *nblocks);
extern void smgrimmedsync(SMgrRelation reln, ForkNumber forknum);复制
smgr模块缓存了上层模块打开的表信息,结构为SMgrRelationData,主要保存的是已打开的fd句柄信息、缓存的文件大小nblocks信息、表文件节点信息RelFileNode。这些信息都缓存到了进程私有内存中,使用哈希表SMgrRelationHash执行快速的查找。
smgr模块提供了一层表信息缓存机制,以减少一些重复性的I/O操作,比如:open、lseek、close等。smgr模块本身并不关心底层存储设备的类型,因此,提供了一个结构体f_smgr来提供具体的I/O操作函数句柄(如下所示),目前PostgreSQL中实现了一套md_*接口用于执行具体的I/O操作。
typedef struct f_smgr
{
void (*smgr_init) (void); /* may be NULL */
void (*smgr_shutdown) (void); /* may be NULL */
void (*smgr_open) (SMgrRelation reln);
void (*smgr_close) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_create) (SMgrRelation reln, ForkNumber forknum,
bool isRedo);
bool (*smgr_exists) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_unlink) (RelFileNodeBackend rnode, ForkNumber forknum,
bool isRedo);
void (*smgr_extend) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer, bool skipFsync);
bool (*smgr_prefetch) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_startread) (struct PgAioInProgress *io,
SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_startwrite) (struct PgAioInProgress *io,
SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer,
bool skipFsync);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer, bool skipFsync);
void (*smgr_writeback) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, BlockNumber nblocks);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
void (*smgr_immedsync) (SMgrRelation reln, ForkNumber forknum);
int (*smgr_fd) (SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, uint32 *off);
/* POLAR: bulk read */
void (*polar_smgr_bulkread) (SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
int blockCount, char *buffer);
/* POLAR: bulk write */
void (*polar_smgr_bulkwrite) (SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
int blockCount, char *buffer, bool skipFsync);
/* POLAR: bulk extend */
void (*polar_smgr_bulkextend) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, int blockCount, char *buffer, bool skipFsync,
ExtendBatchType extendtype, bool created_in_current_xact);
/* POLAR end */
} f_smgr;
staticconst f_smgr smgrsw[] = {
/* magnetic disk */
{
.smgr_init = mdinit,
.smgr_shutdown = NULL,
.smgr_open = mdopen,
.smgr_close = mdclose,
.smgr_create = mdcreate,
.smgr_exists = mdexists,
.smgr_unlink = mdunlink,
.smgr_extend = mdextend,
.smgr_prefetch = mdprefetch,
.smgr_read = mdread,
.smgr_startread = mdstartread,
.smgr_write = mdwrite,
.smgr_startwrite = mdstartwrite,
.smgr_writeback = mdwriteback,
.smgr_nblocks = mdnblocks,
.smgr_truncate = mdtruncate,
.smgr_immedsync = mdimmedsync,
.smgr_fd = mdfd,
/* POLAR: bulk read */
.polar_smgr_bulkread = polar_mdbulkread,
/* POLAR: bulk write */
.polar_smgr_bulkwrite = polar_mdbulkwrite,
/* POLAR: extend batch */
.polar_smgr_bulkextend = polar_mdbulkextend,
/* POLAR end */
}
};复制
md_*接口包装了PostgreSQL实现的虚拟文件描述符Vfd模块,向上层smgr模块提供具体的I/O操作实现。md_*接口主要对Vfd模块接口的返回结果做了错误处理、优化了I/O操作执行的时机、抽象了上层逻辑的表对象和底层存储文件之间的映射关系。
Vfd(Virtual File Descriptor)模块包装了底层glibc库实现的open、read、write等读写接口,兼容了这些接口返回的错误码,并提供了VfdCache缓存当前已经打开的文件句柄信息。
WAL日志文件,即存储PostgreSQL产生的数据修改记录,主要用于崩溃恢复、备份、备库数据同步等。
PostgreSQL提供了WAL Buffer机制来加速WAL日志文件的读写,具体可以参见文章《PostgreSQL WALBuffer并发机制》。
WAL日志文件的I/O操作,是直接通过调用Vfd模块实现的接口执行的,没有复杂的缓存机制,因为WAL日志文件的I/O操作比较简单:Append-only的追加写、同一时刻只能有一个进程写文件、多进程并发写入机制由WAL Buffer保证。
事务文件,存储了CLOG信息、事务提交时间、组合事务信息、CSN事务信息等内容。
事务文件存在这样的特征:事务是递增分配的,而事务状态在持久化文件层面是连续存储的、一一对应的关系,可以通过简单的线性公式将事务ID与持久化存储的文件位置一一关联起来。另外,事务文件与事务状态是一对多的映射关系。因此,事务文件的读写操作一般情况下出现这样的规律:读写操作大多数发生在同一个文件block上,于是PostgreSQL提供了SLRU(Simple LRU buffering)机制来加速该类文件的读写操作。
SLRU模块将事务文件的读写频率最小化,其真正执行I/O操作是通过调用Vfd模块的接口实现的。
特殊文件,包含controlfile文件读写、replslot复制槽、两阶段事务文件pg_twophase,这些文件的读写操作都是直接调用Vfd模块提供的接口实现的。
配置及错误日志文件,包含postgresql.conf配置文件、pg_hba.conf配置文件、错误日志文件log等都是通过直接调用glibc提供的接口实现的。
PolarDB PostgreSQL存储I/O路径

PolarDB PostgreSQL实现了polar_vfs模块,让Vfd模块调用polar_vfs模块实现的polar_open、polar_read、polar_write等接口,polar_vfs模块自动判断文件是否位于共享存储设备上,如果是的话,polar_vfs模块将I/O操作调用PolarFS库提供的接口,将I/O操作转发给外部共享存储设备,比如:PolarStore存储设备。
关于polar_vfs模块的详情内容介绍,见以往文章《PolarDB PostgreSQL版 VFS 模块介绍》。
总结
本文介绍了PostgreSQL中各类文件的I/O操作路径实现原理,涉及了多个模块的实现。同时,介绍了PolarDB PostgreSQL在I/O操作路径上做的演进—VFS模块的实现理念,从I/O路径的模块层次关系上看,VFS模块的实现是一种比较好的设计方案。