【PostgreSQL 内核探索笔记】FSM(Free Space Map) 空闲空间映射

前言

前些时间学了点内核知识,然后一脚踩进了PG源码里。本来是看存储的部分的,但是跟着文档又一脚踩进了FSM了,还好它不难。可能应该先去看Page Layout的,还好靠着之前学的知识没有被卡住。

FSM的结构和一些算法都挺有意思的,下面是我自己总结的一些内容。内容不是很全,也有点粗糙。写这篇文章呢首先是加深自己理解,方便以后回来复习。当然如果能帮助到各位客官就更好了。如有谬误,烦请指教~

一个目录

  • 前言
  • 介绍
    • 自己琢磨
  • 深入
    • 整体
    • category
    • FSM 页内部
      • 结构
      • 操作
        • 查找
        • 更新
      • 封装
    • FSM 的高层结构
      • FSM页组成树结构
      • 逻辑地址与磁盘布局
      • 搜索

介绍

什么是FSM?

FSM全称Free Space Map,即空闲空间映射。根据v11.2的文档上的介绍:

  • 每个堆和索引关系(除了哈希索引)都有一个FSM用来跟踪关系中的可用空间
  • FSM作为一个独立的分支存放在主要关系数据旁边,文件名格式为 filenode number 加 _fsm 后缀,如下图oid为16384的数据库里的oid为12203的表,它的FSM文件名为12203_fsm。

【PostgreSQL 内核探索笔记】FSM(Free Space Map) 空闲空间映射_第1张图片

  • FSM被组织成一课FSM页的树,底层的FSM页存储了每一个 heap (or index) page 中可用的空闲空间,每个页对应一个字节。上层FSM页面则聚集来自于下层页面的信息
  • 每个FSM页里是一个数组表示的二叉树,每个节点一个字节。每个叶节点表示一个堆页面或者一个下层FSM页面。在每一个非叶节点中存储了它孩子节点中的最大值

自己琢磨

嗯。好像懂了,又好像没懂。

自己去新建了个表,怎么没看到有fsm文件?一查:在新建表时不会产生fsm和vm文件,直到pg执行VACUUM操作时,或者是第一次要使用 fsm或vm 文件时才会生成。

pg_freespacemap模块可以用来检查存储在空闲空间映射中的信息?为什么我提示没这个函数?一查:自带的插件原来要自己装。去到源码里的contrib目录里找到要装的插件,进入到目录,make && make install。使用的时候在客户端create extension

OK,看看pg_class的freespace分布吧,每个块里有多少可用的空间都标出来了

【PostgreSQL 内核探索笔记】FSM(Free Space Map) 空闲空间映射_第2张图片

 

深入

 

整体

FSM的目的是快速定位一个有足够空间放一个元组的页,或者快速决定扩展一个新的页(所有都不够空间了)

PG8.4开始,fsm不再是global的了,消除了固定大小FSM的缺陷

记录每个数据页还有多少free space,最简单的用个数组按顺序存下每个数据页(就不分是heap page还是index page了)的free space,找的时候按顺序找不就行了?但这并不能满足【快速】这个特点

PG里对此有两个主要措施:

  1. 用一个字节表示一个页的free space

    这个FSM要小才能搜得快。一个页8KB,每个页的 free space 要用13个位来表示,这太浪费空间了。 所以PG并没有精确记录每个数据页的free space,而是用了1/256的粒度去记录 free space,就是说咱把一个页分成BLCKSZ/256块去记录 free space,就映射到了0 ~ 255,用一个字节就可以表示。比如 ,0 ~ 31大小对应0,32 ~ 63大小对应1,后面会详细说说。

  2. 把FSM组织为树结构

    按文档里说的一个FSM文件里面有好多个FSM页,它们组成一棵树。每个FSM页里都是二叉树,每个叶节点表示一个堆页面或者一个下层FSM页面。整个FSM结构大概长这样【PostgreSQL 内核探索笔记】FSM(Free Space Map) 空闲空间映射_第3张图片

    • 每个黑框框都是一个FSM页,一个FSM文件就由这些FSM页组成,左上角的红色数字是FSM页的页号,和FSM页的磁盘布局有关,后面会说
    • 所有FSM页构成一颗大树,这个树一般是三层或四层。最底层是0层
    • 这颗数是几叉树和一个FSM页里的叶子节点数量有关,看图大概能看出来
    • 不管上层还是底层的FSM页,里面都是个二叉树结构,父节点是两个孩子的最大值
    • 底层FSM页内的叶子节点记录了数据页的空闲空间大小,上层FSM页内的叶子节点记录的是它对应第几个孩子页的根节点值

有了个大概认识,再细看每个部分吧

 

category

把 free space 的值的范围分成256个级别:0~255。假设一个页是8K,它的对应关系如下

 * Range	 Category(cat)
 * 0	- 31   0
 * 32	- 63   1
 * ...    ...  ...
 * 8096 - 8127 253
 * 8128 - 8163 254 (注意这里右边界不是8159) 
 * 8164 - 8192 255 

可以看到:

  1. cat 0~253都是32一份的 (32 = 8192 / 256)

  2. cat最高是255,它的 free space 范围是大于等于 MaxFSMRequestSize,这个 MaxFSMRequestSize 取决于页的大小和结构,在这是8164字节,如同这个变量名,在fsm里想找一个比这个值大时是不能满足的。

    // 需要的大小大于MaxFSMRequestSize
    static uint8 fsm_space_needed_to_cat(Size needed)
    {
    	int cat;
    
    	/* Can't ask for more space than the highest category represents */
    	if (needed > MaxFSMRequestSize)
    		elog(ERROR, "invalid FSM request size %zu", needed);
    ......
    

    这个MaxFSMRequestSizeMaxHeapTupleSize(页大小 - 页头 - 1个ItemIdData)

  3. cat第二高是254,它代表的 free space 范围是 [254 * 32, MaxFSMRequestSize),这里MaxFSMRequestSize = 8164 > 32 * 255 = 8160

freespace.c中提供了几个cat和 free space 转换的函数:

// 返回avail大小对应的cat
static uint8 fsm_space_avail_to_cat(Size avail);
// 返回装下avail大小需要的最小cat
static uint8 fsm_space_needed_to_cat(Size needed);
// 返回cat对应范围的左边界Size
static Size fsm_space_cat_to_avail(uint8 cat);

 

FSM 页内部

 

结构

FSM页里面是一个数组存的二叉树结构,从src\include\storage\fsm_internals.hFSMPage中可以看出,FSM页里有两个东西,fp_next_slotfp_nodes节点数组,fp_next_slot是查找的时候用的,fp_nodes就是这颗二叉树了

typedef struct
{
	int fp_next_slot;
	uint8 fp_nodes[FLEXIBLE_ARRAY_MEMBER];
} FSMPageData;

typedef FSMPageData *FSMPage;

在Page里面的结构就是这样的了,除了页头,剩下的都是 FSMPage

【PostgreSQL 内核探索笔记】FSM(Free Space Map) 空闲空间映射_第4张图片

节点数组 fp_nodes 里包含了 NodesPerPage 个节点,正好是留给fp_nodes的空间的字节大小。非叶子节点数是 NonLeafNodesPerPage,为 (BLCKSZ / 2 - 1),剩下的就是叶子节点数 LeafNodesPerPage

#define NodesPerPage (BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - \
					  offsetof(FSMPageData, fp_nodes))

#define NonLeafNodesPerPage (BLCKSZ / 2 - 1)
#define LeafNodesPerPage (NodesPerPage - NonLeafNodesPerPage)

它就是一个满二叉树少了一些叶子节点,如下图所示,底层最右边会缺少一些叶子节点,还有在右边的一些非叶子(其实是非底层节点)是没用的

【PostgreSQL 内核探索笔记】FSM(Free Space Map) 空闲空间映射_第5张图片

这里我纳闷了一下,非叶子节点数是(BLCKSZ / 2 - 1),为什么不可以是(NodePerPages/2-1) ?
 
这么想只考虑了满二叉树中,底层节点数比上层节点数多1的特点,但是还有一点是,上层节点数是2^n-1,NodePerPages并不能保证是2的幂次。
 
8192 - 头 - slot - 4095 ≈ 4000 个叶子节点;如果强行让NodePerPages变成2的幂次,即NodePerPages=4096,那么叶子节点数=4096/2=2048,少得多

 

操作

在一个FSM页内,有两个基操:查找和更新

 

查找
  • 要在一个FSM页里找一个有 X 空闲空间的页,也就是n >= X的叶子节点,只需要从根节点开始,选一个大于n >= X的孩子作为下一步,一直遍历到叶子节点即可。

  • 但从 fsm_set_avail() 看不完全是这样的。在一个FSM页里找一个有 X 空闲空间的页,它会先看根节点是否 >= X,如果有再从 fp_next_slot 开始找:每一步向右移再爬上父节点,在有足够free space的节点停下,停下后在用开始说得方法向下走。(这个过程是logN的)

  • 举个例子:假设这棵树长下面这样,要找一个空闲空间满足7的页。看根节点为7,有满足的,就以fp_next_slot为起点开始找。先看深绿色,第一步指向5,不满足,右移上移。第二步指向5,不满足右移上移。第三步指向7满足了,OK(我把如果还不满足的第四步页画出来了)。然后跟着蓝色路径向下走到底就可以找到符合的叶子节点。

    【PostgreSQL 内核探索笔记】FSM(Free Space Map) 空闲空间映射_第6张图片

  • 这个fp_next_slot有两个作用,在找底层FSM页时,每次找到后会指向找到的slot+1,以分散FSM搜索返回的页面。在找上层FSM页时,找到后指向找到的slot,那下次也从这里开始找,可以利用OS的预取和批量写入的优化

  • 还有就是,这样子找可以发现,每一步找的三角形区域会大一倍,找出来的slot是在fp_next_slot右边第一个满足的(向右是当前层环绕的,也就是最右侧节点的右侧节点是当前层最左侧节点)

更新

要更新一个页的空闲空间为 X,首先更新对应的叶子节点,然后不断向上走,维护父节点为两个孩子的最大值,直到一个父节点的值不变即可停下。#define SlotsPerFSMPage LeafNodesPerPage

这个没有复杂的东西,倒是会因为FSM页不是明确的WAL日志管理,更新时如果发现崩溃,会有个rebuild的过程。(这部分我还没理解)

封装

其实一个FSM页里面二叉树结构在下面的高层结构中被隐藏起来了,Higher-level routines把一个FSM当成用确定数量个slot来存free space信息(slot数量就是叶子节点数量)

Higher-level routines使用 fsm_set_avail()fsm_search_avail() 来使用FSM页

 

FSM 的高层结构

 

FSM页组成树结构

首先,FSM文件的众多FSM页组成一颗大树,这颗树一般是3层或4层,在 freespace.c里有说:最大文件是1GB,所以要保证这颗大的FSM能寻址2^32-1个块。根据 X^3 >= 2^32-1可以得到1626是能使用3层树结构最小的SlotsPerFSMPage。实际上能使用3层树结构的最小的BLCKSZ是4KB;216是能使用4层树结构最小的SlotsPerFSMPage,实际上我们支持的BLCKSZ最小是512

总结就是所以如果BLCKSZ>=4KB,就有SlotsPerFSMPage >= 1626,就可让FSM_TREE_DEPTH=3。小的BLCKSZ就要用4层结构

#define FSM_TREE_DEPTH ((SlotsPerFSMPage >= 1626) ? 3 : 4)

#define FSM_ROOT_LEVEL (FSM_TREE_DEPTH - 1)
#define FSM_BOTTOM_LEVEL 0

 

在上面有写,8KB的页大小有大约4000个节点,那这棵FSM页组成树是棵4000叉树咯(别忘了它内部是用二叉树找合适的孩子的,不用遍历4000个孩子)
 

逻辑地址与磁盘布局

这颗树用一种逻辑地址来寻址这一大堆FSM页,以0位底层的层号,每个FSM页作为这颗大树的节点,可用所在的层号和在当前层的逻辑页号来做唯一映射

typedef struct
{
	int level;	   /* level */
	int logpageno; /* page number within the level */
} FSMAddress;

/* Address of the root page. */
static const FSMAddress FSM_ROOT_ADDRESS = {FSM_ROOT_LEVEL, 0};

假设这颗FSM树有3层,每个FSM页能存4个页的信息(即内部是4个叶子节点),那么每个FSM页的逻辑地址FSMAddress和块号和它在FSM文件中的块号是这样的:

【PostgreSQL 内核探索笔记】FSM(Free Space Map) 空闲空间映射_第7张图片

可以看出来,FSM页就是按这颗树深度优先遍历的顺序在磁盘上存放的

在freespace.c里面有几个函数可以加深理解

static FSMAddress fsm_get_child(FSMAddress parent, uint16 slot);  // 获得给定FSM页的某个孩子的FSMAddress
static FSMAddress fsm_get_parent(FSMAddress child, uint16 *slot);  // 获得给定FSM页所在父页的FSMAddress和slot号
static FSMAddress fsm_get_location(BlockNumber heapblk, uint16 *slot);  // 通过数据页的块号获取所在FSM页的FSMAddress和slot号
static BlockNumber fsm_get_heap_blk(FSMAddress addr, uint16 slot);  // 通过FSM页和slot号计算出来数据页的块号
static BlockNumber fsm_logical_to_physical(FSMAddress addr);  // FSM页的FSMAddress转换成在FSM文件中的块号

 

搜索

主要是看freespace.c中的函数 fsm_search(Relation rel, uint8 min_cat),大致过程:

  1. 从大树的根节点的FSM页开始找

  2. 对一个FSM页用fsm_search_avail

    • 有如果找到OK的slot:而且还不是底层的FSM页,则用fsm_get_child计算出FSMAddress继续第2步。如果是底层的FSM页,则用fsm_get_heap_blk返回数据页的块号
    • 如果没找到OK的slot:如果不是根页,那可能是上层页面没有更新(因为修改一个页不会马上修改它的父页),就会调用fsm_set_and_search更新它的父页,然后回到第一步;如果根页都没有OK的了,说明所有数据页的free space都不能满足,需要扩展新的页了。

     


参考资料

 
文档:

https://www.postgresql.org/docs/11/storage-fsm.html

源码:

src\backend\storage\freespace\README
-src\include\storage\fsm_internals.h
src\backend\storage\freespace\freespace.c
src\backend\storage\freespace\fsmpage.c

 

大概就这些了,还有VACCUM、EXTEND、WAL相关的我还没弄懂,日后再回来完善吧 ^_^

你可能感兴趣的:(postgresql,数据库)