暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PolarDB PostgreSQL存储I/O路径分析

内核开发者 2023-11-14
540

关于 PolarDB PostgreSQL 版

PolarDB PostgreSQL 版是一款阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 、Ganos全空间数据处理能力和高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB PostgreSQL 版具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载。

背景

PostgreSQL中不同类型的数据持久化在不同的路径下,由于这些数据之间存在不同的特征,因此其各自的I/O操作路径也是不同的。

按照I/O路径的不同,可以将PostgreSQL的持久化文件分为以下五类:

  1. 数据文件,主要保存在路径base/、global/下;

  2. WAL日志文件,主要保存在路径pg_wal/下;

  3. 事务文件,主要保存在路径pg_commit_ts/、pg_csnlog/、pg_multixact/、pg_xact/下;

  4. 特殊文件,主要保存在路径global/pg_control、pg_replslot/、pg_twophase/下;

  5. 配置及错误日志文件,主要保存在log/、pg_hba.conf、postgresql.conf中。

PostgreSQL存储I/O路径

  1. 数据文件,即存储表、索引等数据的文件,是PostgreSQL中用户数据的主要持久化形式。

    1. 由于这部分数据量较大,于是PostgreSQL提供了Buffer Pool缓存池技术来加速这部分数据的读写速度,针对Buffer Pool缓存池的介绍可以参见以往文章《Persistent Buffer Pool》

    2. 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);
  1. smgr模块缓存了上层模块打开的表信息,结构为SMgrRelationData,主要保存的是已打开的fd句柄信息、缓存的文件大小nblocks信息、表文件节点信息RelFileNode。这些信息都缓存到了进程私有内存中,使用哈希表SMgrRelationHash执行快速的查找。

  2. 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;

static const 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 */
	}
};
  1. md_*接口包装了PostgreSQL实现的虚拟文件描述符Vfd模块,向上层smgr模块提供具体的I/O操作实现。md_*接口主要对Vfd模块接口的返回结果做了错误处理、优化了I/O操作执行的时机、抽象了上层逻辑的表对象和底层存储文件之间的映射关系。

  2. Vfd(Virtual File Descriptor)模块包装了底层glibc库实现的open、read、write等读写接口,兼容了这些接口返回的错误码,并提供了VfdCache缓存当前已经打开的文件句柄信息。

  3. WAL日志文件,即存储PostgreSQL产生的数据修改记录,主要用于崩溃恢复、备份、备库数据同步等。

  4. PostgreSQL提供了WAL Buffer机制来加速WAL日志文件的读写,具体可以参见文章《PostgreSQL WALBuffer并发机制》

  5. WAL日志文件的I/O操作,是直接通过调用Vfd模块实现的接口执行的,没有复杂的缓存机制,因为WAL日志文件的I/O操作比较简单:Append-only的追加写、同一时刻只能有一个进程写文件、多进程并发写入机制由WAL Buffer保证。

  6. 事务文件,存储了CLOG信息、事务提交时间、组合事务信息、CSN事务信息等内容。

  7. 事务文件存在这样的特征:事务是递增分配的,而事务状态在持久化文件层面是连续存储的、一一对应的关系,可以通过简单的线性公式将事务ID与持久化存储的文件位置一一关联起来。另外,事务文件与事务状态是一对多的映射关系。因此,事务文件的读写操作一般情况下出现这样的规律:读写操作大多数发生在同一个文件block上,于是PostgreSQL提供了SLRU(Simple LRU buffering)机制来加速该类文件的读写操作。

  8. SLRU模块将事务文件的读写频率最小化,其真正执行I/O操作是通过调用Vfd模块的接口实现的。

  9. 特殊文件,包含controlfile文件读写、replslot复制槽、两阶段事务文件pg_twophase,这些文件的读写操作都是直接调用Vfd模块提供的接口实现的。

  10. 配置及错误日志文件,包含postgresql.conf配置文件、pg_hba.conf配置文件、错误日志文件log等都是通过直接调用glibc提供的接口实现的。

PolarDB PostgreSQL存储I/O路径

  1. PolarDB PostgreSQL实现了polar_vfs模块,让Vfd模块调用polar_vfs模块实现的polar_open、polar_read、polar_write等接口,polar_vfs模块自动判断文件是否位于共享存储设备上,如果是的话,polar_vfs模块将I/O操作调用PolarFS库提供的接口,将I/O操作转发给外部共享存储设备,比如:PolarStore存储设备。

  2. 关于polar_vfs模块的详情内容介绍,见以往文章《PolarDB PostgreSQL版 VFS 模块介绍》

总结

本文介绍了PostgreSQL中各类文件的I/O操作路径实现原理,涉及了多个模块的实现。同时,介绍了PolarDB PostgreSQL在I/O操作路径上做的演进—VFS模块的实现理念,从I/O路径的模块层次关系上看,VFS模块的实现是一种比较好的设计方案。

关于 PolarDB PostgreSQL 版

PolarDB PostgreSQL 版是一款阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 、Ganos全空间数据处理能力和高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB PostgreSQL 版具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载。

背景

PostgreSQL中不同类型的数据持久化在不同的路径下,由于这些数据之间存在不同的特征,因此其各自的I/O操作路径也是不同的。

按照I/O路径的不同,可以将PostgreSQL的持久化文件分为以下五类:

  1. 数据文件,主要保存在路径base/、global/下;
  2. WAL日志文件,主要保存在路径pg_wal/下;
  3. 事务文件,主要保存在路径pg_commit_ts/、pg_csnlog/、pg_multixact/、pg_xact/下;
  4. 特殊文件,主要保存在路径global/pg_control、pg_replslot/、pg_twophase/下;
  5. 配置及错误日志文件,主要保存在log/、pg_hba.conf、postgresql.conf中。

PostgreSQL存储I/O路径

  1. 数据文件,即存储表、索引等数据的文件,是PostgreSQL中用户数据的主要持久化形式。
    1. 由于这部分数据量较大,于是PostgreSQL提供了Buffer Pool缓存池技术来加速这部分数据的读写速度,针对Buffer Pool缓存池的介绍可以参见以往文章《Persistent Buffer Pool》
    2. 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);
  1. smgr模块缓存了上层模块打开的表信息,结构为SMgrRelationData,主要保存的是已打开的fd句柄信息、缓存的文件大小nblocks信息、表文件节点信息RelFileNode。这些信息都缓存到了进程私有内存中,使用哈希表SMgrRelationHash执行快速的查找。
  2. 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;

static const 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 */
	}
};
  1. md_*接口包装了PostgreSQL实现的虚拟文件描述符Vfd模块,向上层smgr模块提供具体的I/O操作实现。md_*接口主要对Vfd模块接口的返回结果做了错误处理、优化了I/O操作执行的时机、抽象了上层逻辑的表对象和底层存储文件之间的映射关系。
  2. Vfd(Virtual File Descriptor)模块包装了底层glibc库实现的open、read、write等读写接口,兼容了这些接口返回的错误码,并提供了VfdCache缓存当前已经打开的文件句柄信息。
  3. WAL日志文件,即存储PostgreSQL产生的数据修改记录,主要用于崩溃恢复、备份、备库数据同步等。
  4. PostgreSQL提供了WAL Buffer机制来加速WAL日志文件的读写,具体可以参见文章《PostgreSQL WALBuffer并发机制》
  5. WAL日志文件的I/O操作,是直接通过调用Vfd模块实现的接口执行的,没有复杂的缓存机制,因为WAL日志文件的I/O操作比较简单:Append-only的追加写、同一时刻只能有一个进程写文件、多进程并发写入机制由WAL Buffer保证。
  6. 事务文件,存储了CLOG信息、事务提交时间、组合事务信息、CSN事务信息等内容。
  7. 事务文件存在这样的特征:事务是递增分配的,而事务状态在持久化文件层面是连续存储的、一一对应的关系,可以通过简单的线性公式将事务ID与持久化存储的文件位置一一关联起来。另外,事务文件与事务状态是一对多的映射关系。因此,事务文件的读写操作一般情况下出现这样的规律:读写操作大多数发生在同一个文件block上,于是PostgreSQL提供了SLRU(Simple LRU buffering)机制来加速该类文件的读写操作。
  8. SLRU模块将事务文件的读写频率最小化,其真正执行I/O操作是通过调用Vfd模块的接口实现的。
  9. 特殊文件,包含controlfile文件读写、replslot复制槽、两阶段事务文件pg_twophase,这些文件的读写操作都是直接调用Vfd模块提供的接口实现的。
  10. 配置及错误日志文件,包含postgresql.conf配置文件、pg_hba.conf配置文件、错误日志文件log等都是通过直接调用glibc提供的接口实现的。

PolarDB PostgreSQL存储I/O路径

  1. PolarDB PostgreSQL实现了polar_vfs模块,让Vfd模块调用polar_vfs模块实现的polar_open、polar_read、polar_write等接口,polar_vfs模块自动判断文件是否位于共享存储设备上,如果是的话,polar_vfs模块将I/O操作调用PolarFS库提供的接口,将I/O操作转发给外部共享存储设备,比如:PolarStore存储设备。
  2. 关于polar_vfs模块的详情内容介绍,见以往文章《PolarDB PostgreSQL版 VFS 模块介绍》

总结

本文介绍了PostgreSQL中各类文件的I/O操作路径实现原理,涉及了多个模块的实现。同时,介绍了PolarDB PostgreSQL在I/O操作路径上做的演进—VFS模块的实现理念,从I/O路径的模块层次关系上看,VFS模块的实现是一种比较好的设计方案。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论