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

2021SC@SDUSC

概述

这次来分析一下磁盘管理的FSM(free space map)机制,也就是空闲空间映射。前面我有提到过,它是以表文件的附属文件的形式存在的。这个机制类似于在学习操作系统的文件系统时,关于空闲空间的查找和管理这一部分的知识。简而言之,如果通过遍历所有文件块来查找空闲空间会付出很高的时间代价,FSM机制就是为了优化空闲空间的管理,降低时间复杂度。

源码分析

关于空闲空间管理的相关源码都放在这个文件夹中src/backend/storage/freespace。老规矩,先从读文件夹中的README开始,初步了解一下这个机制。

FSM机制的目的

空闲空间映射表的目的为:当有一个元组需要储存时,快速定位到有足够(能够容纳这个元组)空闲空间的表文件块;或者能够快速判断是否存在有足够空间的表文件块,从而决定是直接插入还是新增表文件块。

FSM文件块的结构

为了保持搜索的快速,FSM文件也需要保持小体积,因此也采用类似于表的页面块的机制。但不同的是,FSM通过一个字节来记录一个表文件页面块的空闲空间大小(范围)。一个字节即2^8=256,也就是将页面块的大小等分成256份,BLCKSZ/256,每份对应一个范围。设字节取值为A(0<=A<=255),每个字节对应的取值范围就是 [A*(BLCKSZ/256),(A+1)*(BLCKSZ/256)) (只能取整数),左闭右开的一个范围。

FSM中字节的取值表文件文件块空闲空间大小范围(字节数)
00-31
132-63
…………
2548128-8163
2558164-8191

二叉树结构
为了实现快速的查找,VFM文件没有选择传统的线性数据结构,而是采用了以数组形式存储的树形结构
根据readme文件介绍,二叉树的结构分为三个层级,如下图所示:
结构
图片省略了内部的二叉树结构,只保留了根节点和叶子节点。
每一块的内部都是一个局部的最大堆的二叉树形式,这些块合起来就构成了一个全局的最大堆二叉树。

  • 第0层,只存放0号FSM块。叶子节点按照左→右的顺序,对应第1层的FSM块的根节点。
  • 第1层,可以有多个FSM块。叶子节点按照左→右的顺序,对应第2层的FSM块的根节点。
  • 第2层,可以有多个FSM块。每个叶子节点按照左→右的顺序对应表文件的文件块的空闲空间。比如图中的12指的就是对应表文件的第一个文件块的空闲空间范围为384-415字节。

也就是说,前两层的FSM块的作用只是来快速定位到第2层的FSM块,只有第2层的FSM块的叶子节点才保存有表文件块的空闲空间值。

块使用最大堆的原因
前面提到过FSM机制的目的,就是为了快速判断是否有符合条件的空闲空间或者快速找到符合条件的空闲空间。而块内部使用最大堆能够将该块保存的最大空闲空间放在根节点,便于比较;所有块采用最大堆能够将最大的空闲空间放在根节点,这样只需和根节点的值比较一下,就可以知道空闲空间是否足够,如果足够,再沿着树下去寻找即可。

一个FSM文件能够储存一个表的所有表文件块的空闲空间信息吗?
根据结构,每个FSM块内可以存储 (BLCKSZ - headers) / 2个叶子节点,当BLCKSZ为默认值时,结果约等于4000(>4000)。headers为文件块头部信息。我们假设每个FSM块中二叉树最多有4000个叶子节点,这样的话,一个FSM文件中共可以存储40003 个叶子节点,由于postgreSQL中的块号长度为32位,这意味着单个表至多有232个块。我们可以来比较一下这两个数的大小,40003可以拆分成26×109,我们可以放缩一下,109>89=227,因此40003大于233,也就意味着这个三层的结构可以储存表文件中所有文件块的空闲空间值

编号问题

FSM文件的创建

FSM文件并非跟着表文件一同创建,当第一次需要使用FSM文件时,才会创建FSM文件。

typedef struct
{
	int			fp_next_slot;//指向下一次开始查询二叉树的叶子节点位置
	uint8		fp_nodes[FLEXIBLE_ARRAY_MEMBER];//以数组形式存储的二叉树
} FSMPageData;

typedef FSMPageData *FSMPage;//即上面数据结构的指针

首先来看一下FSM块的数据结构,路径为src/include/storage/fsm_internals.h,这相当于内存中的FSM块,和存储在磁盘上的形式是一样的,都是以数组形式存储的二叉树。
关于fp_next_slot这个变量,当从FSM块中找到一个合适的叶子节点位置时(满足请求的叶子节点的序号,设为slot),如果这个FSM块位于第2层,则会将该变量的值设为(slot+1)(表示下一个表文件块),如果该FSM不位于第2层,则将该变量的值设置为slot(表示当前表文件块)。这样做的目的如下:

  1. 当多个进程同时向同一个表中插入时,借助该变量使用寻路算法,可以向请求的进程返回不同的表文件块,避免了同时向同一个表块中插入的情况。
  2. OS本身有预读取和批量写入的技术,这两个技术的前提是按照线性顺序往下读写,而按照表块顺序符合这个前提条件,因此多进程同时进行时能够加快读和写的速度。

下面来看一下FSM文件是如何创建的。

FSM文件的查找

相关源码位于src/backend/storage/freespace/freespace.c中。

static BlockNumber
fsm_search(Relation rel, uint8 min_cat)
{
	int			restarts = 0;//for的循环次数,防止无限循环。
	FSMAddress	addr = FSM_ROOT_ADDRESS;//从第0个FSM块开始查找,结构体见下面

	for (;;)//循环步骤,直到找到对应的块。
	{
		int			slot;//当前块号
		Buffer		buf;//缓存数组
		uint8		max_avail = 0;

		buf = fsm_readbuf(rel, addr, false);//将addr转换为“物理地址”,读取FSM块内容到buf中。

		if (BufferIsValid(buf))//判断块内容是否符合条件
		{
			LockBuffer(buf, BUFFER_LOCK_SHARE);//给当前的块上共享锁
			slot = fsm_search_avail(buf, min_cat,
									(addr.level == FSM_BOTTOM_LEVEL),
									false);//块内查找,源码见下面。
			if (slot == -1)
				max_avail = fsm_get_max_avail(BufferGetPage(buf));
			UnlockReleaseBuffer(buf);//解锁
		}
		else//不符合条件则认为不存在。
			slot = -1;

		if (slot != -1)//如果找到了符合条件的块号
		{
			if (addr.level == FSM_BOTTOM_LEVEL)//如果到第2层则直接返回表的块号
				return fsm_get_heap_blk(addr, slot);

			addr = fsm_get_child(addr, slot);//未到第二层则进入下一层,继续循环
		}
		else if (addr.level == FSM_ROOT_LEVEL)//如果没有找到并且仍位于第0块
		{
			//这说明和第0块根节点比较之后发现不符合条件,意味着FSM中存储的所有空闲空间都比条件要求的要小。
			return InvalidBlockNumber;//返回无效块号
		}
		else //如果没有找到并且不在第0层(level=2)
		{
			uint16		parentslot;//父节点的块号
			FSMAddress	parent;//父节点的地址

			/*这种情况的出现可能是因为更低层的FSM块的最大空闲空间的值发生变化以后没有更新到高层
			 *的FSM块中,这就需要先更新相关联的FSM块的最大空闲空间值。*/
			 
			parent = fsm_get_parent(addr, &parentslot);//根据父节点的块号获取父节点的地址
			fsm_set_and_search(rel, parent, parentslot, max_avail, 0);//查找并更新父节点的最大空闲空间值

			/* 如果块内的最大空闲空间值太久没变,这会导致循环发生非常多的次数。
			 * 为了防止无限循环的产生,设置了一个危险阈值,一旦循环次数超过了这个值,就会终止。*/
			if (restarts++ > 10000)//循环次数超过阈值
				return InvalidBlockNumber;//返回无效块号

			addr = FSM_ROOT_ADDRESS;//更新完后要从头开始搜索!
		}
	}
}
//FSMAddress结构体
typedef struct
{
	int			level;			/* 该FSM块位于的层级,但不同于前面的层级,level=2对应第0层,level=1对应第1层 */
	int			logpageno;		/* 层级内的块号 */
} FSMAddress;

//在块内查找,返回一个符合条件的块号。
int
fsm_search_avail(Buffer buf, uint8 minvalue, bool advancenext,
				 bool exclusive_lock_held)
{
	Page		page = BufferGetPage(buf);
	FSMPage		fsmpage = (FSMPage) PageGetContents(page);//转换成上面分析的结构体的形式,便于查找
	int			nodeno;
	int			target;
	uint16		slot;

restart:

	//对0号块的根节点进行检查,如果不符合条件则直接返回找不到。
	if (fsmpage->fp_nodes[0] < minvalue)
		return -1;

	target = fsmpage->fp_next_slot;//利用前面提到过的fp_next_slot作为目标节点开始搜索
	if (target < 0 || target >= LeafNodesPerPage)//目标节点错误
		target = 0;//将目标节点清零
	target += NonLeafNodesPerPage;//目标节点加上非叶子节点的数量,指向第一个叶子节点(数组存储因此有这样的特性)

	
	//查找方法
	nodeno = target;//将当前节点指针指向目标节点
	while (nodeno > 0)//循环,直到回到根节点或者找到满足需求的节点
	{
		if (fsmpage->fp_nodes[nodeno] >= minvalue)//找到了满足需求的节点,退出循环
			break;

		nodeno = parentof(rightneighbor(nodeno));//未找到,则将当前节点指针移动到它的右边的节点(如果当前节点已经是最右边的节点,则回到最左边)的父节点。至于为什么这样做,将在后面解释。
	}

	 //经过上面的循环,nodeno指向的节点一定是拥有足够的空闲空间,但它可能在树根,也可能在树中间。
	 //接下来的循环,将会逐渐下降,准确定位到一个满足条件的叶子节点。
	while (nodeno < NonLeafNodesPerPage)
	{
		int			childnodeno = leftchild(nodeno);//优先去寻找左孩子

		if (childnodeno < NodesPerPage &&
			fsmpage->fp_nodes[childnodeno] >= minvalue)//如果左孩子满足要求
		{
			nodeno = childnodeno;//将当前节点设置为左孩子
			continue;//继续循环
		}
		childnodeno++;//如果左孩子不满足要求,则利用指针+1从而指向右孩子(数组二叉树特性)。
		if (childnodeno < NodesPerPage &&
			fsmpage->fp_nodes[childnodeno] >= minvalue)//如果右孩子满足要求
		{
			nodeno = childnodeno;//当前节点指向右孩子
		}
		else//如果右孩子也不满足要求
		{
			//这就是意想不到的情况了,因为父节点中的空闲空间值保证了其左孩子或右孩子中至少有一个满足条件。
			//但是现在都不满足条件,可能的原因是写入时出现了问题,导致该页成为了torn page。
			//需要进行修复然后从根节点重新开始。
			
			RelFileNode rnode;
			ForkNumber	forknum;
			BlockNumber blknum;

			BufferGetTag(buf, &rnode, &forknum, &blknum);
			elog(DEBUG1, "fixing corrupt FSM block %u, relation %u/%u/%u",
				 blknum, rnode.spcNode, rnode.dbNode, rnode.relNode);//记录修复信息

			if (!exclusive_lock_held)//如果没有获取排他锁
			{
				LockBuffer(buf, BUFFER_LOCK_UNLOCK);
				LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);//获取排他锁
				exclusive_lock_held = true;//修改状态
			}
			fsm_rebuild_page(page);//修复当前的FSM块
			MarkBufferDirtyHint(buf, false);//将当前块变为脏块,写入磁盘。
			goto restart;//回到restart处重新开始循环。
		}
	}

	//跳出循环说明我们已经找到了符合条件的第2层的叶子节点。
	slot = nodeno - NonLeafNodesPerPage;//减去非叶子节点个数(直接获取在叶子层中的序号)

	 //根据函数的参数advancenext,更新fp_next_slot,如果为true则为slot+1,否则为slot。
	fsmpage->fp_next_slot = slot + (advancenext ? 1 : 0);

	return slot;//返回找到的节点号
}

大致流程如下:(我在注释中写的很详细,可以跟着走一遍)

  1. fsm_search 初始化块指针,使其指向0号块,然后进入循环体。

  2. 由块指针表示的逻辑地址计算出物理地址,并将该FSM块装入buf缓冲区中,调用fsm_search_avail函数,查找一个符合条件的叶子节点。

    1. 首先检测根节点进行判断,如果不满足min_cat,则直接返回找不到(-1)。
    2. 否则,就根据fp_next_slot,设置nodeno=当前非叶子节点数+fp_next_slot从而指向对应叶子节点。进入下面的循环:
    3. 判断节点是否满足需求,如满足则跳出循环,否则设置当前节点为当前节点的右节点(如果已经为最右的节点,可以调整为最左边的节点)的父节点,继续下一次循环。
    4. 跳出循环后,更新fp_next_slot。
    5. 返回slot(叶子节点编号)。

    根据返回值进行判断是返回无效块号还是继续循环还是跳出循环,判断情况都在上面源码的注释中,这里就不详细说了。

  3. 如果对应的FSM块不存在,则返回一个无效块号,跳出循环终止。

排他锁

排他锁(Exclusive Locks,简称X锁),又称为写锁、独占锁,在数据库管理上,是锁的基本类型之一。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。

共享锁

共享锁(Share Locks,简称S锁),又称为读锁,若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

FSM文件的更新

void
RecordPageWithFreeSpace(Relation rel, BlockNumber heapBlk, Size spaceAvail)
{
	int			new_cat = fsm_space_avail_to_cat(spaceAvail);//获取新的空闲空间值
	FSMAddress	addr;//FSM块的逻辑地址
	uint16		slot;//要修改的节点号

	addr = fsm_get_location(heapBlk, &slot);//得到具体的FSM块号和FSM块中节点的编号。

	fsm_set_and_search(rel, addr, slot, new_cat, 0);//搜索并更新节点的空闲空间值。源码见下面
}

//fsm_set_and_search源码
static int
fsm_set_and_search(Relation rel, FSMAddress addr, uint16 slot,
				   uint8 newValue, uint8 minValue)
{
	Buffer		buf;//缓冲区
	Page		page;//块
	int			newslot = -1;//符合条件的节点号

	buf = fsm_readbuf(rel, addr, true);//读取块内容到缓冲区
	LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);//获取排他锁

	page = BufferGetPage(buf);//将缓冲区的内容转为块结构

	if (fsm_set_avail(page, slot, newValue))//判断是否成功修改,函数源码见下面
		MarkBufferDirtyHint(buf, false);//修改成功则设置为脏块,写入磁盘。

	if (minValue != 0)//搜索条件不为0,启用搜索,返回更新后符合条件的节点号
	{
		newslot = fsm_search_avail(buf, minValue,
								   addr.level == FSM_BOTTOM_LEVEL,
								   true);//搜索函数,前面分析过。
	}

	UnlockReleaseBuffer(buf);//解锁

	return newslot;//返回修改的节点号
}

//fsm_set_avail源码
bool
fsm_set_avail(Page page, int slot, uint8 value)
{
	int			nodeno = NonLeafNodesPerPage + slot;//指向对应的叶子节点
	FSMPage		fsmpage = (FSMPage) PageGetContents(page);
	uint8		oldvalue;//旧的空闲空间值

	Assert(slot < LeafNodesPerPage);//如果slot不是叶子节点,则报错

	oldvalue = fsmpage->fp_nodes[nodeno];//获取旧的空闲空间值

	if (oldvalue == value && value <= fsmpage->fp_nodes[0])//如果旧的空闲空间值和新的相等,则不用做任何操作
		return false;//返回未改变

	fsmpage->fp_nodes[nodeno] = value;//不相等,则赋新值

	
	do//调整除二叉树根节点外的其他节点,使父节点的值总为两个字节点的最大值(维护大根堆)
	{
		uint8		newvalue = 0;//新的空闲空间值,用于比较大小和修改
		int			lchild;//左孩子
		int			rchild;//右孩子

		nodeno = parentof(nodeno);//当前节点变为当前节点的父节点
		lchild = leftchild(nodeno);//获取当前节点的左孩子
		rchild = lchild + 1;//获取当前节点的右孩子

		newvalue = fsmpage->fp_nodes[lchild];//将newvalue设置为左孩子的空闲空间值
		if (rchild < NodesPerPage)//如果右孩子不是叶子节点
			//newvalue=左孩子的空闲空间值和右孩子的空闲空间值中更大的那一个。
			newvalue = Max(newvalue,
						   fsmpage->fp_nodes[rchild]);

		oldvalue = fsmpage->fp_nodes[nodeno];//记录当前节点的空闲空间值
		if (oldvalue == newvalue)//新旧相同则无需修改,说明修改已经完成,可以跳出循环。
			break;

		fsmpage->fp_nodes[nodeno] = newvalue;//否则修改当前节点的空闲空间值,继续循环
	} while (nodeno > 0);//遇到根节点停止

	 //如果要要修改的空闲空间值比根节点的空闲空间值还要大,则需要重新建树。
	if (value > fsmpage->fp_nodes[0])
		fsm_rebuild_page(page);//重建树函数

	return true;//返回成功修改
}

过程大致如下:

  1. RecordPageWithFreeSpace 函数根据表块号读取块内容到缓冲区中并且上锁。
  2. 调用fsm_set_avail函数对FSM块的内容进行修改
    1. 如果新的空闲空间值和旧的空闲空间值相等,则无需修改。
    2. 如果不相等,则赋新值给对应的叶子节点。
    3. 调整根节点外的其他节点,维持大根堆的特性。
    4. 如果新的空闲空间值比根节点的空闲空间值还大,则需要重新建树。
  3. 如果发生了修改,则设置当前块为脏,写入到磁盘中。

总结

  1. 掌握了FSM机制的知识,类似于OS中对空闲空间的管理,这是postgreSQL对表空闲空间的管理,用空间换时间,从而达到加快Insert操作的目的。
  2. FSM采用了最大堆的二叉树形式,二叉树是以数组的形式存储的。
  3. 掌握了排他锁和共享锁的相关知识。
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、付费专栏及课程。

余额充值