一、FSM的基本原理
FSM文件是Free Space Map(空闲空间映射)的缩写,用于跟踪和记录数据块(Page)中的空闲空间。随着数据表不断进行插入、更新和删除元组等操作,数据页内必然会存在空页空间。在插入新的元组时,有两种方式可以选择:直接选择新的页来存放、存放到已有页的空闲空间中。
如果采用第一种存放方式,会明显造成空间利用率的浪费;而采用第二种存放方式,可以明显地节约存储成本,但是这种方式会存在一个问题:如何快速知道哪个页有足够的空闲空间可以存放元组数据,如果每次插入都要遍历所有数据页、直到找到有满足条件的数据页,但是这种方式的查询效率非常低。因此,海山PG采用FSM来记录每个页的空闲空间信息。
通过FSM,在插入新的元组时可以快速地定位到足够空间的空闲页面进行存放,或者发现没有满足条件的页面,需要扩展出一个新页。
FSM机制采用一个字节表示空闲空间的大小范围,并将这个字节叫做FSM级别(category)。通过这种方式,可以减少FSM的存储空间,加快搜索查询效率。FSM级别和真实的FSM范围之间的映射关系如下表所示。
| 字节 | 空闲空间范围(字节) |
|---|---|
| 0 | 0~31 |
| 1 | 32~63 |
| … | … |
| 255 | 8164~8191 |
为了实现快速查找,FSM文件并不是简单使用数组顺序存储每个表块的空闲空间,而是使用了树结构。在FSM块之间使用了一种三层树结构,其中第0层和第1层是辅助层,第二层FSM块实际存储各表块的FSM级别。第0层FSM块只有一个,作为树根;第1层FSM块可以有多个,作为第0层FSM块的子节点;第2层FSM块同理。每个FSM块内又构成一棵局部的最大堆二叉树,但每一层FSM块内的最大堆二叉树又略有不同。
(1)第2层FSM块内大根堆二叉树的每一个叶子节点对应一个表块的空闲空间级别。按照从左到右的顺序,最左边FSM块的最左边叶子节点代表表文件的第一块,左边第二个叶子节点代表表文件的第二块。
(2)第1层FSM块内大根堆二叉树的叶子节点从左到右对应第2层FSM块的根节点。
(3)第0层FSM块内大根堆二叉树的叶子节点从左到右对应第1层FSM块的根节点。
从上面的介绍可知,三层树结构形成了一个逻辑上的大根堆结构,其叶子节点从左到右依次对应表文件中的文件块。
在单个FSM块内,使用大根堆能保证所有叶子节点的最大值被上升到根节点处。因此只要看单个FSM块内的根节点值就可指导此FSM块内空闲空间的上限值。
每个FSM块大小为8KB,除去必要的页头信息,FSM块剩下的空间都用来存储块内的二叉树结构,每个叶子节点都用一个字节表示,因此FSM块内大约可以保存4000个叶子节点(这样,一个FSM文件如果采用三层树结构,大约可以记录4000^3 个叶子节点,对应于4000^3 个表块,远大于2^32 (单个表的最大块数,PG块号数据结构BlockIdData中定义块号长度为32,故单个表最多只能有(2^32-1)个表块)。因此,这样的三层树结构足以记录表文件所有文件块的空闲空间值。
一个FSM文件内所包含的FSM块和表块的映射关系如下图所示。

二、FSM文件的管理
1、FSM结构体
typedef struct
{
/*
* fsm_search_avail() 函数尝试通过以循环方式返回不同页面给不同的后端来分散多个后端的负载
*/
int fp_next_slot; // 用于提示下一次开始查询二叉树时的叶子节点位置
/*
* fp_nodes 包含以数组形式存储的二叉树。前NonLeafNodesPerPage个元素是上层节点
* 后LeafNodesPerPage个元素是叶节点,未使用的节点为零
*/
uint8 fp_nodes[FLEXIBLE_ARRAY_MEMBER];
} FSMPageData;
/* 一个FSM块中可以保存的节点总数目(8192 - 24 - 4 = 8164 ) */
#define NodesPerPage (BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - \
offsetof(FSMPageData, fp_nodes))
/* FSM块中保存的非叶子节点数目(8192/2 - 1 = 4095) */
#define NonLeafNodesPerPage (BLCKSZ / 2 - 1)
/* FSM块中保存的叶子节点数目 (8164 - 4095 = 4069) */
#define LeafNodesPerPage (NodesPerPage - NonLeafNodesPerPage)
2、FSM文件的创建
FSM文件并不是在创建表文件时立即创建的,而是在需要时才会创建,比如执行VACUUM操作时,或者为了插入行而第一次查询FSM文件时。在创建FSM文件时,会预先创建三个FSM块:第0号为根节点即第0层的FSM块,第1号为第1层的第一个节点,第2号为第2层的第一个结点。在第二号FSM块内叶子节点中一次存储从第0号开始的各表块的空闲空间值,若没有空闲空间或空闲空间太小,则记录为0。当第2号FSM块满了之后,将会扩展新的FSM块。该工作主要调用fsm_extend进行实现。
static void
fsm_extend(Relation rel, BlockNumber fsm_nblocks)
{
BlockNumber fsm_nblocks_now; // 当前FSM文件块数
PGAlignedBlock pg; // 结构体,用于存储页面数据
SMgrRelation reln; // 获取的存储管理器关系
PageInit((Page) pg.data, BLCKSZ, 0); // 初始化一个页面
LockRelationForExtension(rel, ExclusiveLock); // 加锁,防止其他后端同时扩展FSM文件或主文件
/* 如果发生了缓存刷新,可能需要重新获取关系 */
reln = RelationGetSmgr(rel);
if ((reln->smgr_cached_nblocks[FSM_FORKNUM] == 0 ||
reln->smgr_cached_nblocks[FSM_FORKNUM] == InvalidBlockNumber) &&
!smgrexists(reln, FSM_FORKNUM)) // 检查FSM文件是否存在;如果
smgrcreate(reln, FSM_FORKNUM, false); // 不存在,则创建;smgr_cached_nblocks是正数,则必定存在
/* Invalidate cache so that smgrnblocks() asks the kernel. */
reln->smgr_cached_nblocks[FSM_FORKNUM] = InvalidBlockNumber;
fsm_nblocks_now = smgrnblocks(reln, FSM_FORKNUM); // 获取当前FSM分支最新的块数
/* Extend as needed. */
while (fsm_nblocks_now < fsm_nblocks) // 扩展条件
{
PageSetChecksumInplace((Page) pg.data, fsm_nblocks_now); // 在新块上设置页面的校验和
smgrextend

最低0.47元/天 解锁文章
1067

被折叠的 条评论
为什么被折叠?



