【PGCCC】Postgresql 文件存储层

前言

在 postgresql 数据库里,数据都会以表的形式组织起来。表的数据会被持久化到底层的磁盘里。负责与底层的磁盘交互,就是 postgresql 的文件存储层。本篇博客会依次介绍表的文件构成、分片机制和存储接口。

表的文件类型

postgresql 使用 RelFileNode 来标识一张表

typedef struct RelFileNode
{
	Oid			spcNode;		/* tablespace */
	Oid			dbNode;			/* database */
	Oid			relNode;		/* relation */
} RelFileNode;

数据表都有下面三种文件

  1. main 文件存储着实际的数据,
  2. fsm 文件记录着 main 文件块的空闲大小,当有数据插入时,会优先从空闲位置写入。
  3. vm 文件记录了过期的数据,这在 vaccum 清洗过程中有用到。

ForkNumber 枚举定义文件类型

typedef enum ForkNumber
{
	InvalidForkNumber = -1,
	MAIN_FORKNUM = 0,  // 存储着实际的数据,文件名没有后缀
	FSM_FORKNUM,       // 存储着空闲位置,文件名的后缀为fsm
	VISIBILITYMAP_FORKNUM,  // 文件名的后缀为vm
	INIT_FORKNUM  // 用于数据库初始化,文件名的后缀为init
} ForkNumber;

#define MAX_FORKNUM		INIT_FORKNUM // 等于3

文件分片

因为有些文件系统对单个文件的数目大小有限制,postgresql 数据库为了良好的移植性,将上述文件进行切片,每个切片对应着一个文件,通过在文件名添加数字后缀来区分(注意第一个分片没有数字后缀)。每个文件切片由MdfdVec表示

typedef struct _MdfdVec
{
	File		mdfd_vfd;		// 文件描述符
	BlockNumber mdfd_segno;		// 分片索引
} MdfdVec;

块存储

postgresql 存储数据并不是一条一条的存储,而是将数据合并成一个小块,每个块的大小都相同,默认为8KB。之所以这样设计,是因为磁盘的读写速度太慢了,尤其是随机读写,通过增加单次的吞吐量,来提高读写性能。

每个块都有一个唯一的标识,叫做 block number。它们依次递增,连续的存储在文件里。每个文件分片包含的块数目是相同的。下面展示了根据 block number 找到对应的文件切片,

// blkno是块的唯一标识,RELSEG_SIZE是切片包含的块数目
targetseg = blkno / ((BlockNumber) RELSEG_SIZE);

动态哈希表

postgresql 使用 SMgrRelationData来表示单个表的文件结构,下面展示了主要成员

typedef struct SMgrRelationData
{
	RelFileNodeBackend smgr_rnode;	// 表的标识

	BlockNumber smgr_targblock; // 准备写的blocknum
	BlockNumber smgr_fsm_nblocks;	// 最大的fsm文件的fsm block number,用于快速判断用户传递的blocknum是否超过最大值,需要扩充
	BlockNumber smgr_vm_nblocks;	/* last known size of vm fork */

	int			smgr_which;		// 选择哪个存储接口,目前只有一种实现,默认为0
    
	int			md_num_open_segs[MAX_FORKNUM + 1];  // 每种类型文件的分片数目
	struct _MdfdVec *md_seg_fds[MAX_FORKNUM + 1];  // 每种类型对应的分片数组
} SMgrRelationData;

注意到和分片相关的两个数组md_num_open_segs和md_seg_fds,它们的数组长度都是文件类型的数目。前者是一个一维数组,表示打开的分片文件的数目。后者是一个二维数组,存储着每种类型文件的的分片 。

这里还额外说下RelFileNodeBackend的定义,它与RelFileNode的区别仅仅是多了一个BackendId

typedef struct RelFileNodeBackend
{
	RelFileNode node;  // 表标识
	BackendId	backend; // 属于后台哪个进程,值为-1表示普通表,否则表示临时表
} RelFileNodeBackend;

postgresql 为了方便的找到表对应的 SMgrRelationData,使用了动态哈希表存储。动态哈希表和普通的哈希表区别,在于可以动态的扩容而尽可能的减少数据在槽之间的移动。这个动态哈希表的 key 类型为 RelFileNodeBackend,value 类型为 SMgrRelationData。

存储接口

postgresql 使用一组函数指针组成的结构体,定义了存储接口。目前只有一种实现。如果用户想扩展,只需要实现这个接口就行。下面只取几个重要的接口:

typedef struct f_smgr
{
	void		(*smgr_init) (void);	// 存储系统初始化
	void		(*smgr_shutdown) (void);  // 存储系统关闭
	void		(*smgr_open) (SMgrRelation reln); // 打开表
	void		(*smgr_close) (SMgrRelation reln, ForkNumber forknum);  // 关闭指定类型的文件 
	void		(*smgr_create) (SMgrRelation reln, ForkNumber forknum, bool isRedo); // 创建指定类型的文件
	void		(*smgr_extend) (SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, char *buffer, bool skipFsync);  // 新建数据块

	void		(*smgr_read) (SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, char *buffer);   // 读取块数据
	void		(*smgr_write) (SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, char *buffer, bool skipFsync); // 写入块数据
	BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);  // 返回指定类型文件的块数目
	void		(*smgr_immedsync) (SMgrRelation reln, ForkNumber forknum); // 立即刷新数据到存储层
} f_smgr;

下面展示了用户如何使用这些接口

  1. 调用 smgropen 创建 SMgrRelation 实例,这里并没有任何文件操作
  2. 调用 smgrcreate 方法创建底层文件,如果底层文件之前创建过,那么此步可以跳过。
  3. 调用 smgrread 方法读取数据
  4. 调用 smgrwrite 方法写入数据

存储接口实现

下面的声明了实现接口的函数,这些函数的实现定义在src/backend/storage/smgr/md.c文件。

static const f_smgr smgrsw[] = {
	/* magnetic disk */
	{
		.smgr_init = mdinit,
		.smgr_shutdown = NULL,
		.smgr_open = mdopen,
         // ......
	}
};

下面挑选出比较重要的函数来讲解,代码都是经过简化过的。

创建文件

mdcreate函数负责创建文件

void mdcreate(SMgrRelation reln, ForkNumber forkNum, bool isRedo) {
    MdfdVec    *mdfd;
    char	   *path;
    File		fd;
    // 找到文件目录
    path = relpath(reln->smgr_rnode, forkNum);
    // 获取文件描述符
    fd = PathNameOpenFile(path, O_RDWR | O_CREAT | O_EXCL | PG_BINARY);
    // 设置此类型文件的分片数目为1,并且截断分片数组
    _fdvec_resize(reln, forkNum, 1);
    // 设置分片数组的第一个分片
    mdfd = &reln->md_seg_fds[forkNum][0];
    mdfd->mdfd_vfd = fd;
    mdfd->mdfd_segno = 0;
}

查找块位置

因为数据存储最终都是存储在文件分片中,所以对于数据块的读写,都必须先打开分片文件。_mdfd_getseg函数会根据 block number,找到对应的分片。如果分片已经打开了,则直接返回。如果没有,则需要先打开前面的分片文件,最后才打开指定的分片文件。

static MdfdVec* _mdfd_getseg(SMgrRelation reln, ForkNumber forknum, BlockNumber blkno, bool skipFsync, int behavior);

一般来说,数据块都是按照顺序写入分片文件的,只有在这个分片写满后,才会创建新的分片。但是有时会出现异常,比如中间有块segment 并没有写满,那么这个时候就需要用户来抉择如何处理。用户可以通过指定 behavior 参数:

  1. EXTENSION_CREATE 表示没有写满的部分,直接填充空的block。
  2. EXTENSION_CREATE_RECOVERY 表示只有系统处于恢复模式下,回填充空的block。
  3. EXTENSION_RETURN_NULL 表示遇到这种情况,直接返回 null。
  4. EXTENSION_FAIL 表示直接报错。

块读取

然后再来看看数据块的读取操作,mdread函数会调用_mdfd_getseg函数打开文件分片,注意到这里的 behavior 参数设置为 EXTENSION_FAIL 和 EXTENSION_CREATE_RECOVERY。

void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, char *buffer)
{
    // 打开文件分片
    v = _mdfd_getseg(reln, forknum, blocknum, false, EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY);
    // BLCKSZ表示数据块的长度,这里返回切片文件的偏移位置
    seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE));
    // 从文件中读取数据到参数 buffer 指定的位置
    nbytes = FileRead(v->mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_READ);
    if (nbytes != BLCKSZ)
    {
        // 如果读取的块长度不等于标准值,需要额外处理
    }
}

块写入

再看看数据块的写入操作,mdwrite函数会调用_mdfd_getseg函数打开文件分片,注意到这里的 behavior 参数设置为 EXTENSION_FAIL 和 EXTENSION_CREATE_RECOVERY。

void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
		char *buffer, bool skipFsync)
{
    
	v = _mdfd_getseg(reln, forknum, blocknum, skipFsync, EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY);
    seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE));
    nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_WRITE);
    if (nbytes != BLCKSZ) {
        // 如果成功写入的长度不等于标准值,需要额外处理
    }
    
    // 如果指定skipFsync为false,并且不是临时表,那么需要进行fsync操作,要立即刷新到磁盘
    if (!skipFsync && !SmgrIsTemp(reln))
        register_dirty_segment(reln, forknum, v);
}

块新增

最后看看数据块的新增函数,由mdextend函数负责。它的代码和mdwrite是一样的,只不过调用mdwrite函数传递的behavior 不一样,它传递的是EXTENSION_CREATE,表示如果该对应的segment不存在,会自动创建该文件。

函数简介

下面简单的列表出一些常见的函数

// 获取指定segment文件包含的block数目,这里MdfdVec代表着segment文件
static BlockNumber _mdnblocks(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg);

// 打开segment文件,segno参数代表着segment的编号,
// oflags可以指定打开标志,支持o_create,o_rdwr,o_binary等
// 如果文件不存在并且没有指定o_create,那么返回 null
static MdfdVec* _mdfd_openseg(SMgrRelation reln, ForkNumber forknum, BlockNumber segno, int oflags);

// 根据segment编号,获取对应的文件路径
static char* _mdfd_segpath(SMgrRelation reln, ForkNumber forknum, BlockNumber segno);

// 获取指定类型的所有文件,包含的block数目总和,
// 因为每个segment文件都是按照顺序存储,并且最大block数目都是固定的。
// 前面的segment存储满后才会创建新的segment,所以计算方法为:最后一个segment的block数目 + 前面的segment文件数目 * segment文件包含的最大block数目
BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);


// 向指定的block写入数据,注意到这个block必须事先存在
// 参数buffer指向数据,参数skipFsync表示是否不需要sync操作,也就是立即持久化到磁盘
void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, char *buffer, bool skipFsync);

// 标识segment文件的更新数据还没被持久化,这里首先会发出通知给checkpointer进程,如果发送失败,则会自己调用fsync操作完成
static void register_dirty_segment(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg);


// 打开指定类型的第一个segment文件,如果文件不存在,behavior指定了EXTENSION_RETURN_NULL则返回null
// 其余情况的behavior灰报错
static MdfdVec* mdopenfork(SMgrRelation reln, ForkNumber forknum, int behavior);

作者:zhmin
链接:https://zhmin.github.io/posts/postgresql-storage-interface/
#PG证书#PG考试#postgresql初级#postgresql中级#postgresql高级

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/914675.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

已解决:spark代码中sqlContext.createDataframe空指针异常

这段代码是使用local模式运行spark代码。但是在获取了spark.sqlContext之后,用sqlContext将rdd算子转换为Dataframe的时候报错空指针异常 Exception in thread "main" org.apache.spark.sql.AnalysisException: java.lang.RuntimeException: java.lang.Nu…

物联网低功耗广域网LoRa开发(一):LoRa物联网行业解决方案

一、LoRa的优势以及与其他无线通信技术对比 (一)LoRa的优势 1、164dB链路预算 、距离>15km 2、快速、灵活的基础设施易组网且投资成本较少 3、LoRa节点模块仅用于通讯电池寿命长达10年 4、免牌照的频段 网关/路由器建设和运营 、节点/终端成本低…

【2024最新】渗透测试工具大全(超详细),收藏这一篇就够了!

【2024最新】渗透测试工具大全(超详细),收藏这一篇就够了! 黑客/网安大礼包:👉基于入门网络安全/黑客打造的:👉黑客&网络安全入门&进阶学习资源包 所有工具仅能在取得足够合…

Redis - 哨兵(Sentinel)

Redis 的主从复制模式下,⼀旦主节点由于故障不能提供服务,需要⼈⼯进⾏主从切换,同时⼤量 的客⼾端需要被通知切换到新的主节点上,对于上了⼀定规模的应⽤来说,这种⽅案是⽆法接受的, 于是Redis从2.8开始提…

双十二入手什么比较划算?双十二母婴好物推荐

随着双十二购物狂欢节的临近,双十二入手什么比较划算?许多准父母和有小孩的家庭都在寻找最佳的母婴产品优惠。在这个特别的日子里,各大电商平台都会推出一系列针对母婴用品的折扣和促销活动,使得这个时期成为囤货和更新宝宝生活必…

[运维][Nginx]Nginx学习(1/5)--Nginx基础

Nginx简介 背景介绍 Nginx一个具有高性能的【HTTP】和【反向代理】的【WEB服务器】,同时也是一个【POP3/SMTP/IMAP代理服务器】,是由伊戈尔赛索耶夫(俄罗斯人)使用C语言编写的,Nginx的第一个版本是2004年10月4号发布的0.1.0版本。另外值得一…

手动安装Ubuntu系统中的network-manager包(其它包同理)

自己手闲把系统中的network-manager包给删了,导致的结果就是Ubuntu系统彻底没有网络。结果再装network-manager时,没有网络根本装不了,网上的方法都试了也没用,然后就自己源码装,这篇文章就是记录一下怎么手动下载包然…

【 ElementUI 组件Steps 步骤条使用新手详细教程】

本文介绍如何使用 ElementUI 组件库中的步骤条组件完成分步表单设计。 效果图: 基础用法​ 简单的步骤条。 设置 active 属性,接受一个 Number,表明步骤的 index,从 0 开始。 需要定宽的步骤条时,设置 space 属性即…

Java项目实战II基于微信小程序的童装商城(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 基于微信小…

基于Cocos Creator开发的打砖块游戏

一、简介 Cocos简而言之就是一个开发工具,详见官方网站TypeScript简而言之就是开发语言,是JavaScript的一个超集详解官网 今天我们就来学习如何写一个打砖块的游戏,很简单的一个入门级小游戏。 二、实现过程 2.1 布局部分 首先来一个整体…

BigDecimal 详解

《阿里巴巴 Java 开发手册》中提到:“为了避免精度丢失,可以使用 BigDecimal 来进行浮点数的运算”。 浮点数的运算竟然还会有精度丢失的风险吗?确实会! 示例代码: float a 2.0f - 1.9f; float b 1.8f - 1.7f; Sys…

使用git命令实现对gitee仓库的下载、更新、上传、上传更新操作。

博客内容为使用git命令实现对gitee仓库的下载、更新、上传、上传更新操作。 1、下载(检出) 使用 git clone 命令 项目仓库地址 eg: git clone https://gitee.com/zzzzzed/ChinessChess.git 如果本地已经下载了该项目则跳过该步骤。 注意使用 git clone 首次检出需要输入用户名…

攻防世界38-FlatScience-CTFWeb

攻防世界38-FlatScience-Web 点开这个here看到一堆pdf,感觉没用&#xff0c;扫描一下 试试弱口令先 源码里有&#xff1a; 好吧0.0 试试存不存在sql注入 根本没回显&#xff0c;转战login.php先 输入1’,发现sql注入 看到提示 访问后得源码 <?php ob_start(); ?>…

JavaWeb后端开发案例——苍穹外卖day01

day1遇到问题&#xff1a; 1.前端界面打不开&#xff0c;把nginx.conf文件中localhost:80改成81即可 2.前后端联调时&#xff0c;前端登录没反应&#xff0c;application.yml中默认用的8080端口被占用&#xff0c;就改用了8081端口&#xff0c;修改的时候需要改两个地方&…

常用中间件介绍

1. RabbitMQ RabbitMQ是一个基于AMQP&#xff08;Advanced Message Queuing Protocol&#xff0c;高级消息队列协议&#xff09;的开源消息代理软件&#xff0c;实现了面向消息的中间件。它支持消息持久化、队列、交换机&#xff08;Exchange&#xff09;和绑定&#xff08;Bin…

【HAProxy06】企业级反向代理HAProxy调度算法之其他算法

HAProxy 调度算法 HAProxy通过固定参数 balance 指明对后端服务器的调度算法&#xff0c;该参数可以配置在listen或backend选项中。 HAProxy的调度算法分为静态和动态调度算法&#xff0c;但是有些算法可以根据不同的参数实现静态和动态算法 相互转换。 官方文档&#xff1…

什么是 eCPRI,它对 5G 和 Open RAN 有何贡献?

这里写目录标题 eCPRI 协议平面&#xff1a;功能分解eCPRI与CPRI的区别CPRI具有以下特点&#xff1a;eCPRI具有以下特点&#xff1a;eCPRI 的优势 所需带宽减少 10 倍适用于 5G 和 Open RAN 的 eCPRI&#xff1a; 通用公共无线接口&#xff08;CPRI&#xff09;是一种行业合作&…

【网页设计】HTML5 和 CSS3 提高

目标 能够说出 3~5 个 HTML5 新增布局和表单标签能够说出 CSS3 的新增特性有哪些 1. HTML5 的新特性 注&#xff1a;该部分所有内容可参考菜鸟教程菜鸟教程 - 学的不仅是技术&#xff0c;更是梦想&#xff01; (runoob.com) HTML5 的新增特性主要是针对于以前的不足&#xf…

Dinky控制台:利用SSE技术实现实时日志监控与操作

1、前置知识 1.1 Dinky介绍 实时即未来,Dinky 为 Apache Flink 而生,让 Flink SQL 纵享丝滑。 Dinky 是一个开箱即用、易扩展,以 Apache Flink 为基础,连接 OLAP 和数据湖等众多框架的一站式实时计算平台,致力于流批一体和湖仓一体的探索与实践。 致力于简化Flink任务开…

如何判断 Hive 表是内部表还是外部表

在使用 Apache Hive 进行大数据处理时&#xff0c;理解表的类型&#xff08;内部表或外部表&#xff09;对于数据管理和维护至关重要。本篇文章将详细介绍如何判断 Hive 表是内部表还是外部表&#xff0c;并提供具体的操作示例。 目录 Hive 表的类型简介判断表类型的方法 方法…