前些时间学了点内核知识,然后一脚踩进了PG源码里。本来是看存储的部分的,但是跟着文档又一脚踩进了FSM了,还好它不难。可能应该先去看Page Layout的,还好靠着之前学的知识没有被卡住。
FSM的结构和一些算法都挺有意思的,下面是我自己总结的一些内容。内容不是很全,也有点粗糙。写这篇文章呢首先是加深自己理解,方便以后回来复习。当然如果能帮助到各位客官就更好了。如有谬误,烦请指教~
什么是FSM?
FSM全称Free Space Map,即空闲空间映射。根据v11.2的文档上的介绍:
_fsm
后缀,如下图oid为16384的数据库里的oid为12203的表,它的FSM文件名为12203_fsm。嗯。好像懂了,又好像没懂。
自己去新建了个表,怎么没看到有fsm文件?一查:在新建表时不会产生fsm和vm文件,直到pg执行VACUUM操作时,或者是第一次要使用 fsm或vm 文件时才会生成。
pg_freespacemap模块可以用来检查存储在空闲空间映射中的信息?为什么我提示没这个函数?一查:自带的插件原来要自己装。去到源码里的contrib
目录里找到要装的插件,进入到目录,make && make install
。使用的时候在客户端create extension
OK,看看pg_class的freespace分布吧,每个块里有多少可用的空间都标出来了
FSM的目的是快速定位一个有足够空间放一个元组的页,或者快速决定扩展一个新的页(所有都不够空间了)
PG8.4开始,fsm不再是global的了,消除了固定大小FSM的缺陷
记录每个数据页还有多少free space,最简单的用个数组按顺序存下每个数据页(就不分是heap page还是index page了)的free space,找的时候按顺序找不就行了?但这并不能满足【快速】这个特点
PG里对此有两个主要措施:
用一个字节表示一个页的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,后面会详细说说。
把FSM组织为树结构
按文档里说的一个FSM文件里面有好多个FSM页,它们组成一棵树。每个FSM页里都是二叉树,每个叶节点表示一个堆页面或者一个下层FSM页面。整个FSM结构大概长这样
有了个大概认识,再细看每个部分吧
把 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
可以看到:
cat 0~253都是32一份的 (32 = 8192 / 256)
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);
......
这个MaxFSMRequestSize
是MaxHeapTupleSize
(页大小 - 页头 - 1个ItemIdData)
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页里面是一个数组存的二叉树结构,从src\include\storage\fsm_internals.h
中FSMPage
中可以看出,FSM页里有两个东西,fp_next_slot
和fp_nodes
节点数组,fp_next_slot
是查找的时候用的,fp_nodes
就是这颗二叉树了
typedef struct
{
int fp_next_slot;
uint8 fp_nodes[FLEXIBLE_ARRAY_MEMBER];
} FSMPageData;
typedef FSMPageData *FSMPage;
在Page里面的结构就是这样的了,除了页头,剩下的都是 FSMPage
节点数组 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)
它就是一个满二叉树少了一些叶子节点,如下图所示,底层最右边会缺少一些叶子节点,还有在右边的一些非叶子(其实是非底层节点)是没用的
这里我纳闷了一下,非叶子节点数是(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(我把如果还不满足的第四步页画出来了)。然后跟着蓝色路径向下走到底就可以找到符合的叶子节点。
这个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页组成一颗大树,这颗树一般是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文件中的块号是这样的:
可以看出来,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)
,大致过程:
从大树的根节点的FSM页开始找
对一个FSM页用fsm_search_avail
找
fsm_get_child
计算出FSMAddress继续第2步。如果是底层的FSM页,则用fsm_get_heap_blk
返回数据页的块号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相关的我还没弄懂,日后再回来完善吧 ^_^