postgreSQL源码分析——存储管理——外存管理(2)

2021SC@SDUSC

概述

由于上次源码分析发现对于postgreSQL表文件的具体形式存在疑问,所以这次分析会结合文档和源码弄清楚postgreSQL的表文件的细节。

源码分析

表文件

表的命名问题
数据库名和表文件名都是以Oid来命名。
翻阅相关头文件,最终在src/include/postgres_ext.h这里找到了Oid这个数据类型的定义。

typedef unsigned int Oid; //一个无符号整数

表文件的细节问题
经过翻阅官方文档,发现postgreSQL中存储的表文件又叫做堆文件,分为下面四种。

  1. 普通堆文件(ordinary cataloged heap),即普通表文件。
  2. 临时堆文件(temporary heap),在会话过程中临时创建,会话结束后自动删除。
  3. 序列堆文件(SEQUENCE relation),一种元组值自动增长的特殊堆。
  4. TOAST表堆文件(TOAST table)。存储变长数据。

堆文件的组成
每个堆文件由多个文件块或者页组成,类似于OS中的分页。每个文件块的大小默认为8K。文件块的组成可以在src/include/storage/bufpage.h里的注释找到。

PageHeaderData 页头数据
Linp1
Linp2
……
Freespace 空闲空间
……
Tuple2
Tuple1
Special space

一个文件块的组成为上面表格的形式,下面来逐个分析。

  • PageHeaderData,即页头数据,包含文件块的相关信息:

    • 项指针(Linp)的开始位置。
    • 空闲空间的起始和结束位置。
    • Special space的开始位置。
    • 标志信息,比如是否存在空闲项指针。
      数据类型如下,位于上面提到的头文件中:
    typedef struct PageHeaderData
    {
    	PageXLogRecPtr pd_lsn;		/*存储由本页最后一次更改所写入的XLOG记录的LSN,即当前的WAL位置。与WAL(Write-Ahead Logging)相关*/
    	uint16		pd_checksum;	/*即校验和,为每个数据页计算校验和。检测到校验和失败将导致读取数据时出错,并将中止当前正在运行的事务。因此,这为直接在数据库服务器级别检测I/O或硬件问题带来了额外的控制。*/
    	uint16		pd_flags;		/*标志位,见下面*/
    	LocationIndex pd_lower;		/*空闲空间的起始偏移量 */
    	LocationIndex pd_upper;		/*空闲空间的结束偏移量 */
    	LocationIndex pd_special;	/*特殊空间的起始偏移量*/
    	uint16		pd_pagesize_version;/*页面大小和页面版本号,低8位为版本号*/
    	TransactionId pd_prune_xid; /*最旧的可修剪(删除)的XID */
    	ItemIdData	pd_linp[FLEXIBLE_ARRAY_MEMBER]; /*项指针数组 */
    } PageHeaderData;
    /*四个标志位*/
    #define PD_HAS_FREE_LINES	0x0001	/* 是否有未使用的项指针? */
    #define PD_PAGE_FULL		0x0002	/* 无有足够的空闲空间用于新的元组? */
    #define PD_ALL_VISIBLE		0x0004	/* 本页上的所有元组对所有人都可见?*/
    
    #define PD_VALID_FLAG_BITS	0x0007	/* 所有有效位的并集,即以上三个有效位的合并*/
    
  • Linp(line pointer),即项指针。是ItemIdData类型的数组。src/include/storage/itemid.h中可以找到这个数据结构。

    typedef struct ItemIdData
    {
    	unsigned	lp_off:15,  //元组在文件块中的偏移量(从头开始计算)
    				lp_flags:2,	//元组的状态,下面四个define就是。
    				lp_len:15;  //元组的字节长度
    } ItemIdData;
    /*项指针的四种状态*/
    #define LP_UNUSED		0		//未使用
    #define LP_NORMAL		1		//正常使用
    #define LP_REDIRECT		2		//HOT重定向
    #define LP_DEAD			3		//死亡
    
  • Freespace,即空闲空间。新插入该页面块的元组以及对应的Linp都会从这里分配。由上面表格的结构,可以看出Linp从Freespace的头部开始分配,元组数据从Freespace的尾部开始分配,这种分配方法非常巧妙。

  • Tuple,即元组,或者说为表中的一行。

    headervaluevalue……

    元组信息存放的数据如上面的表格,即元组头信息和值。
    元组头信息的数据类型为HeapTupleHeaderData,存放在src/include/access/htup_details.h文件中。

    struct HeapTupleHeaderData
    {
    	union
    	{
    		HeapTupleFields t_heap;/*用于记录对元组执行插入或删除操作的事务ID和命令ID,这些信息主要用于并发控制时检查元组对事务的可见性。*/
    		DatumTupleFields t_datum;/*记录元组的长度等信息。*/
    	}			t_choice; //具有两个成员的联合类型
    
    	ItemPointerData t_ctid;		//用于记录当前元组或者新元组的物理位置(块内偏移量和元组长度),若元组被更新,则记录的是新版本元组的物理位置。
    
    	/* 下面的字段必须匹配最小的元组数据! */
    
    #define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK2 2
    	uint16		t_infomask2;	//使用其低11位表示当前元组的属性个数,其他位则用于HOT技术以及元组可见性的标志位。
    #define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK 3
    	uint16		t_infomask;		//用于标识元组当前的状态,比如元组是否具有OID、是否有空属性等,其每一位对应不同的状态,共16种状态。
    #define FIELDNO_HEAPTUPLEHEADERDATA_HOFF 4
    	uint8		t_hoff;		//表示元组头的大小。
    #define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5
    	bits8		t_bits[FLEXIBLE_ARRAY_MEMBER];	//表示元组那些字段为空。
    
    };
    

    HOT技术
        postgreSQL中对于元组采用多版本技术存储,对元组的每个更新操作都会产生一个新版本,版本之间从老到新形成一条版本链(将旧版本的t_ctid字段指向下一个版本的位置即可)。此外,更新操作不但会在表文件中产生元组的新版本,在表的每个索引中也会产生新版本的索引记录,即对一条元组的每个版本都有对应版本的索引记录。即使更新操作没有修改索引属性,也会在每个索引中都产生一个新版本。这回浪费存储空间,只有在VACUUM时才能被回收,增加数据库的负担。
        为了解决这个问题,使用一种HOT机制。HOT详解

  • Special space,即特殊空间,用于存放和索引方法相关的特定数据。由于索引文件的文件块结构和普通表文件的结构相同,因此在普通表文件块中并没有Special space的空间。

存储接口中对文件的操作

在上篇博客中介绍了存储接口的一些结构体和函数,下面看一下这些接口是如何调用和实现的。
调用顺序大概如下:

  1. 调用 smgropen 创建 SMgrRelation 实例,这里并没有任何文件操作
  2. 调用 smgrcreate 方法创建底层文件,如果底层文件之前创建过,那么此步可以跳过。
  3. 调用 smgrread 方法读取数据
  4. 调用 smgrwrite 方法写入数据
    一些关键的函数
    这里可能会用到一些没分析过的东西,比如vfd机制,会在下篇博客详细分析。
    文件创建 mdcreate
void
mdcreate(SMgrRelation reln, ForkNumber forkNum, bool isRedo)//创建底层文件
{
	MdfdVec    *mdfd;
	char	   *path;
	File		fd;

	if (isRedo && reln->md_num_open_segs[forkNum] > 0)
		return;					/* 检查是否早已创建和打开 */

	Assert(reln->md_num_open_segs[forkNum] == 0);

	path = relpath(reln->smgr_rnode, forkNum);//获取文件目录

	fd = PathNameOpenFile(path, O_RDWR | O_CREAT | O_EXCL | PG_BINARY);//获取虚拟文件描述符

	if (fd < 0)//获取描述符失败,即文件打开失败。
	{
		int			save_errno = errno;

		if (isRedo)//如果是已经创建过
			fd = PathNameOpenFile(path, O_RDWR | PG_BINARY);//重新获取虚拟文件描述符
		if (fd < 0)//仍旧获取失败
		{
			errno = save_errno;
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not create file \"%s\": %m", path)));
		}
	}

	pfree(path);

	_fdvec_resize(reln, forkNum, 1);//设置此类型文件的分段数目为1,并且截断分段数组
	mdfd = &reln->md_seg_fds[forkNum][0];//设置分段数组的第一个分段
	mdfd->mdfd_vfd = fd;//添加虚拟文件描述符
	mdfd->mdfd_segno = 0;//段号设置为0
}

大致流程就是:
检测重复->获取文件目录->获取虚拟文件描述符->初始化
块读取 mdread

void
mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
	   char *buffer)
{
	off_t		seekpos;//要查找的位置的偏移量
	int			nbytes;//读取的字节数
	MdfdVec    *v;//mdfd向量指针


	v = _mdfd_getseg(reln, forknum, blocknum, false,
					 EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY);//打开文件的分段

	seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE));//获取分段文件的偏移位置

	Assert(seekpos < (off_t) BLCKSZ * RELSEG_SIZE);//偏移量错误则报错

	nbytes = FileRead(v->mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_READ);//获取字节数,并将读取的数据放入buffer中。


	if (nbytes != BLCKSZ)//如果读取的字节数不为标准的块长度
	{
		if (nbytes < 0)//字节数为负数则报错
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not read block %u in file \"%s\": %m",
							blocknum, FilePathName(v->mdfd_vfd))));

		if (zero_damaged_pages || InRecovery)//如果页面损坏或者处于恢复中
			MemSet(buffer, 0, BLCKSZ);//将读取的数据置0
		else//读取块失败
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("could not read block %u in file \"%s\": read only %d of %d bytes",
							blocknum, FilePathName(v->mdfd_vfd),
							nbytes, BLCKSZ)));
	}
}

块写入 mdwrite

void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
		char *buffer, bool skipFsync)
{
	off_t		seekpos;//要写入的块的偏移量
	int			nbytes;//写入的字节数
	MdfdVec    *v;//mdfd向量指针

	
#ifdef CHECK_WRITE_VS_EXTEND  //宏定义判断,判断是写入还是新增页面块
	Assert(blocknum < mdnblocks(reln, forknum));//若是新增块,不能超出一个段中块的最大数量。
#endif

	v = _mdfd_getseg(reln, forknum, blocknum, skipFsync,
					 EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY);//获取文件的分段

	seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE));//获取段的偏移量

	Assert(seekpos < (off_t) BLCKSZ * RELSEG_SIZE);//偏移量异常

	nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_WRITE);//获得成功写入的字节数,并将数据从buffer中写入到文件中。


	if (nbytes != BLCKSZ)//如果写入的字节数不等于标准的块长度
	{
		if (nbytes < 0)//写入字节为负数,写入失败
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not write block %u in file \"%s\": %m",
							blocknum, FilePathName(v->mdfd_vfd))));
		
		ereport(ERROR,
				(errcode(ERRCODE_DISK_FULL),
				 errmsg("could not write block %u in file \"%s\": wrote only %d of %d bytes",
						blocknum,
						FilePathName(v->mdfd_vfd),
						nbytes, BLCKSZ),
				 errhint("Check free disk space.")));
	}

	if (!skipFsync && !SmgrIsTemp(reln))//如果对应的skipFsync(跳过同步)为false,并且不是临时表,那么需要进行fsync操作,要立即刷新到磁盘。
		register_dirty_segment(reln, forknum, v);//标记为脏段,要刷新到磁盘
}

块读取和块写入的大致流程是很相似的,区别就是块写入多了 同步(维护一致性)的问题。

总结

  1. 了解了postgreSQL的表是如何存储的,以及页面块的组成。
  2. 初步了解了WAL、HOT两种机制。
  3. 熟悉了标志位的使用规范,未定义的标志位初始化为0,并且留下空间为以后的扩展做准备,如果要表示OR可以使用相关标志位的求和来表示。
  4. 掌握了一种内存(外存)页的设计方法,这种空闲空间机制非常巧妙,能够把插入的时间复杂度降为O(1),极大提升插入的速度。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PostgreSQL是以加州大学伯克利分校计算机系开发的POSTGRES,现在已经更名为PostgreSQL. PostgreSQL支持大部分SQL标准并且提供了许多其它现代特性:复杂查询、外键、触发器、视图、事务完整性等。PostgreSQL 是一个免费的对象-关系数据库服务器(数据库管理系统),它在灵活的 BSD-风格许可证下发行。它提供了相对其他开放源代码数据库系统(比如 MySQL 和 Firebird),和专有系统(比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server)之外的另一种选择。事实上, PostgreSQL 的特性覆盖了 SQL-2/SQL-92 和 SQL-3/SQL-99,首先,它包括了可以说是目前世界上最丰富的数据类型的支持,其中有些数据类型可以说连商业数据库都不具备, 比如 IP 类型和几何类型等;其次,PostgreSQL 是全功能的自由软件数据库,很长时间以来,PostgreSQL 是唯一支持事务、子查询、多版本并行控制系统(MVCC)、数据完整性检查等特性的唯一的一种自由软件的数据库管理系统。 Inprise 的 InterBase 以及SAP等厂商将其原先专有软件开放为自由软件之后才打破了这个唯一。最后,PostgreSQL拥有一支非常活跃的开发队伍,而且在许多黑客的努力下,PostgreSQL 的质量日益提高。从技术角度来讲,PostgreSQL 采用的是比较经典的C/S(client/server)结构,也就是一个客户端对应一个服务器端守护进程的模式,这个守护进程分析客户端来的查询请求,生成规划树,进行数据检索并最终把结果格式化输出后返回给客户端。为了便于客户端的程序的编写,由数据库服务器提供了统一的客户端 C 接口。而不同的客户端接口都是源自这个 C 接口,比如ODBC,JDBC,Python,Perl,Tcl,C/C++,ESQL等, 同时也要指出的是,PostgreSQL 对接口的支持也是非常丰富的,几乎支持所有类型的数据库客户端接口。这一点也可以说是 PostgreSQL 一大优点。本课程作为PostgreSQL数据库管理一,主要讲解以下内容: 1.     PostgreSQL 存储过程基本知识2.     PostgreSQL 用户自定义函数3.     PostgreSQL 控制结构4.     PostgreSQL 游标和存储过程5.     PostgreSQL 索引6.     PostgreSQL 视图7.     PostgreSQL 触发器8.     PostgreSQL 角色、备份和还原9.     PostgreSQL 表空间管理

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值