段页式存储
1 背景
1.1 openGauss页式存储
-
openGauss普通表在物理存储上采用页式存储管理机制,每个表的数据独立对应一个逻辑上的大文件(最大支持32TB),该逻辑文件按照固定的大小(默认1GB)划分为多个物理文件存放到对应的目录下
-
openGauss对外提供hashbucket、大分区表等特性,每张数据表会被拆分为多个子表,底层所需的文件数量成倍地增长
1.1.1 页式存储存在的问题
-
对文件系统的依赖较大,无法进行细颗粒度的控制 提升可维护性
-
大数据量场景下文件句柄过多,目前只能通过虚拟句柄(VFD)来解决,影响系统性能
-
小文件数量过多会导致全量备份等场景下的随机IO问题,影响性能

1.2 段页式存储
为了解决上述问题,openGauss引入段页式存储管理机制,在同一个database中有且仅有一个段空间(segment space),该DB下的所有表在同一个段空间上分配数据,实际物理文件个数与表的数目无关

2 段页式存储管理机制
2.1 组织形式
2.1.1 逻辑组织形式
-
在逻辑上,段页式存储以段(segment)、区(extent)、页(page)的形式进行组织、存储分配和管理
-
每个表有一个逻辑上的segment,该表的所有数据都存在该segment上
-
每个segment会挂载多个extent,每个extent是一块连续的物理页面,extent之间不一定连续,但是同一个extent中的page连续
-
在物理文件中,相同类型的extent存放在同一个物理文件中,单个物理文件最大1GB,不同的物理文件存放不同forknum的数据
2.1.2 扩容机制
-
段页式文件支持自动扩容,不需要用户手动指定,直到磁盘空间满或者达到tablespace设置的最大限制
-
openGauss提供4种不同大小的extent,每种类型的extent数目是一定的,每种类型的extent对应一个单独的物理文件
-
对于每个segment来说,每次扩展的extent大小是固定的,前16个extent大小为64KB,第17到第143个extent大小为1MB···
-
前面扩展的extent较小,以节省磁盘空间,避免空间的浪费,后面扩展的extent较大,避免频繁地进行磁盘空间的扩展
-
extent会自动扩展,加入新的extent,但是不会直接进行extent的回收,可通过对整个表做
TRUNCATE操作,以segment为单位进行存储空间的回收
| extent type | extent size | extent page count | extent count range | total page count | total size |
|---|---|---|---|---|---|
| EXT_SIZE_8 | 64KB | 8 | [1, 16] | 128 | 1MB |
| EXT_SIZE_128 | 1MB | 128 | [17, 143] | 16K | 128MB |
| EXT_SIZE_1024 | 8MB | 1024 | [144, 255] | 128K | 1GB |
| EXT_SIZE_8192 | 64MB | 8192 | [256, ···] | ··· | ··· |
2.2 管理机制
段页式管理机制向下直接对接存储介质,通过系统提供的pread()和pwrite()函数进行数据的读写,向上对接存储管理器smgr,提供类似于页式管理的relFileNode、forkNum、blockNum的概念,对上层业务透明,对元组的各种操作无影响
2.2.1 物理文件管理
openGauss段页式管理机制下仅创建5种不同的物理文件,文件命名为1-5,根据文件存储的extent类型来进行区分。其中,文件1存放数据库相关的元数据信息,文件2-5分别存放EXT_SIZE_8 - EXT_SIZE_8192四种不同类型的extent。单个物理文件大小限制为1GB,超出单个文件大小后,通过后缀.1、.2···进行区分,不同的fork通过不同的物理文件进行存放

2.2.2 BMT(Block Map Tree)

主要数据结构
typedef struct SegmentHead {
uint64 magic;
XLogRecPtr lsn;
uint32 nblocks; // block number reported to upper layer
uint32 nextents; // extent number allocated to this segment
uint32 nslots : 16; // not used yet
int bucketid : 16; // not used yet
uint32 total_blocks; // total blocks can be allocated to table (exclude metadata pages)
uint64 reserved;
BlockNumber level0_slots[BMT_HEADER_LEVEL0_SLOTS]; // 指向extent的起始页位置
BlockNumber level1_slots[BMT_HEADER_LEVEL1_SLOTS]; // 指向level1 page
BlockNumber fork_head[SEGMENT_MAX_FORKNUM + 1]; // non-main fork的segment head的存放位置
} SegmentHead;
/* An Extent Group is a logical segment file which allocates extents in the same size. */
typedef struct SegExtentGroup {
SegLogicFile *segfile;
RelFileNode rnode; // rnode.relNode is equal to the EXTENT_TYPE of this extent group
ForkNumber forknum;
int extent_size;
SegSpace *space;
pthread_mutex_t lock;
BlockNumber map_head_entry; // default is DF_MAP_HEAD_PAGE
df_map_head_t *map_head;
Buffer map_head_buffer;
} SegExtentGroup;
/* inverse pointer page存放的内容 */
typedef struct ExtentInversePointer {
uint32 flag;
BlockNumber owner; // owner oid (or block number)
} ExtentInversePointer;
-
每个表有自己的逻辑段空间,保留fork的概念,一个fork使用一个segment
-
MAIN FORK的segment head在元数据文件中的物理页号作为该表的relfilenode -
BMT向上层提供segment的概念,上层传入参数为segment号+逻辑页号,向下管理物理文件,根据上层参数找到具体的的物理文件和物理页号,进行实际的数据读写。数据访问流程如图
-
segment head中提供两个数组level0 slot和level1 slot,大小分别是1255和256。其中,level0 slot直接存放每个extent起始页的物理页号,level1 slot指向level0 page,在openGauss 3.1.0版本代码中,level0 page共有2000个level0 slot,可寻址8*16 + 128*127 + 1024*112 + 8192*1000 + 8192*2000*256个page,约32TB的数据!其中segment head中的level0 slot可寻址8128K个数据页,总计63.5GB的数据 -
由
map group、inverse pointers pages和若干个extent构成一个extent group,同一个extent group内,所有的extent类型相同
2.2.3 空闲extent管理
openGauss中,一组相同前缀的物理文件称为一个SegLogicFile,一个SegLogicFile包含的内容如图。其中file head页面存放表文件的元信息,spc head页面存放表空间的元信息,map head页面存放管理map page的元信息。前两种页面目前没有使用
主要数据结构
typedef struct st_df_map_head {
uint16 bit_unit; // page count that managed by one bit
uint16 group_count; // count of bitmap group that already exists
uint32 free_group; // first free group
uint32 reserved;
uint32 allocated_extents;
uint32 high_water_mark;
df_map_group_t groups[DF_MAX_MAP_GROUP_CNT]; // 33
} df_map_head_t;
typedef struct st_df_map_page {
BlockNumber first_page; // 当前map_page管理的extent group的第一个page的起始位置
uint16 free_begin; // 当前map_page管理的extent group的第一个空闲extent的起始位置
uint16 dirty_last; // last dirty bit
uint16 free_bits; // free bits
uint16 reserved;
uint8 bitmap[0]; // following is the bitmap
} df_map_page_t;
-
若干连续的
map page称为一个map group,在openGauss 3.1.0的代码中,最多可以有33个map group,每个map group有64个map page -
map page中的每一个bit位标识一个空闲的extent,每个map page可标识65248个extent,即使是EXT_SIZE_8类型的extent,一个map group页可以管理约2TB的数据量 -
SegLogicFile数目的计算方式为:extent种类 * forkNum,openGauss 3.1.0的代码中有4种不同的fork,5种不同的extent分类,即系统中最多有20个SegLogicFile
2.2.4 buffer管理
段页式管理机制需要缓存表数据的buffer和表的元信息的buffer,如果直接使用页式存储的buffer pool,可能会出现循环依赖的问题:假如需要读取segment head,这时会发生buffer置换,把旧的buffer刷盘,如果旧的buffer刚好属于这个segment,则刷盘的时候需要用到segment head,此时发生循环依赖的问题
-
在段页式管理机制中,为了解决上述循环依赖的问题,在实现时,数据的buffer和元数据buffer分别管理,共用一个buffer数组,分别位于数组的前后两部分,共享一个脏页队列,采用相同的置换策略
-
数据buffer和元数据buffer通过不同的接口进行访问
-
数据buffer的访问需要元数据buffer中的信息进行解析,因此元数据buffer会被反复的进行访问,为了减少反复的
pin和unpin,元数据buffer设计了常驻内存模式,在整个SMgrRelation的生命周期内只需要pin一次就能一直使用
2.2.5 Xlog管理
页式存储的最小原子操作是通过Xlog实现的,在对buffer中的内容进行修改时,首先会对buffer上互斥锁,然后修改数据,将修改的内容记录到Xlog,把Xlog的lsn记录到数据页,再放锁,以此保证在对buffer的内容进行修改时该buffer不会被其它线程看到。一条Xlog能够保证一次修改的多条数据在恢复之后的原子性,lsn保证了数据页的刷盘在Xlog刷盘之后。这种机制对跨函数、多block的原子操作不友好
/* 页式存储Xlog记录机制 */
void func()
{
xl_head_struct xlrec;
XLogBeginInsert();
XLogRegisterData((char*)&xlrec, sizeof(xlrec));
XLogRegisterBuffer(0, buffer, flag);
XLogInsert(rmid, info);
}
段页式存储管理机制中存在许多跨函数、多block的原子操作,为了解决传统页式存储Xlog机制的局限性,openGauss在段页式存储中,基于原有的Xlog机制新设计了一套支持Xlog跨函数的记录机制
典型场景
在扩展一个extent的时候,首先需要申请一个extent,然后修改map_head page、map page和segment head page总共三个页面,可能还会初始化一个level0 page,即在一个原子操作中同时修改多个block
void seg_extend_segment(SegSpace *spc, ForkNumber forknum, Buffer seg_head_buffer, BlockNumber seg_head_blocknum)
{
XLogAtomicStart(); // 开始一个原子操作
spc_alloc_extent()->eg_allocc_extent()-->eg_alloc_extent_from_map_internal()
{
XLogAtomicOpRegisterBuffer(map_buffer ···); // map page buffer
XLogAtomicOpRegisterBufData();
XLogAtomicOpRegisterBuffer(map_head_buffer ···); // map head buffer
XLogAtomicOpRegisterBufData();
}
if (new_extent_id < BMT_HEADER_LEVEL0_SLOTS) { // 不需要初始化level0_page
seg_head_update_xlog()
{
XLogAtomicOpRegisterBuffer(seg_head_buffer ···); // seg head buffer
XLogAtomicOpRegisterBufData();
}
} else {
seg_init_new_level0_page(···)
{
XLogAtomicOpRegisterBuffer(level0_page_buffer ···); // level0 page buffer
XLogAtomicOpRegisterBufData();
}
seg_head_update_xlog()
{
XLogAtomicOpRegisterBuffer(seg_head_buffer ···); // seg head buffer
XLogAtomicOpRegisterBufData();
}
}
XLogAtomicOpCommit(); // 结束原子操作
}
实现机制
为了实现上述Xlog跨函数、多block的记录,openGauss基于原有的Xlog机制设计了一套支持跨函数、多block的Xlog记录机制
- 主要函数接口
void XLogAtomicOpStart();
/* op标识当前操作的类型,clean_flag标识该条Xlog提交后是否需要释放dirty buffer */
void XLogAtomicOpRegisterBuffer(Buffer buffer, uint8 flags, uint8 op, uint32 clean_flag);
void XLogAtomicOpRegisterBufData(char *data, int len);
void XLogAtomicOpCommit();
- 主要数据结构
/* XLogAtomicOperation: atomic operation controller */
struct XLogAtomicOperation {
BufferStack stack; // 保存buffer
XLogDataCache dataCache; // 保存buffer操作和实际的日志信息
···
void XLogStart();
void RegisterBuffer(Buffer buffer, uint8 flags, uint8 op, uint32 clean_flag);
void RegisterBufData(char *data, int len, bool is_opcode);
void XLogCommit(RmgrId rmid, uint8 info, int bucket_id);
int CurrentBlockId();
};
struct BufferStack {
BufferStack() :mcnxt(NULL), buffers(NULL), depth(0), capacity(0) { }
MemoryContext mcnxt;
XLogBuffer *buffers;
int depth;
int capacity;
void Init(MemoryContext mc);
int Insert(Buffer buf, uint8 flags, uint32 clean_flag);
void Reset();
int contains(Buffer buf);
void RegisterOpData(int block_id, char *opData, int len, bool is_opcode);
};
struct XLogBuffer { // 记录的是单个buffer单次修改相关的信息
Buffer buffer;
uint8 flags;
uint8 opnum; // buffer在单条Xlog中的修改次数
XLogOperation operations[BufMaxOperations];
uint32 clean_flag; // what to do when xlog commit
uint32 rdata_len;
};
struct XLogOperation {
char *data;
uint32 data_len;
};
struct XLogDataCache {
XLogDataCache(): mcnxt(NULL), cache(NULL), used(0), capacity(0) { }
MemoryContext mcnxt;
char *cache;
int used;
int capacity;
void Init(MemoryContext mc);
char *RegisterData(char *data, int len);
void Reset();
};
说明
-
为了支持跨函数的数据记录,openGauss通过一个线程级的变量
(XLogAtomicOperation *)t_thrd.xlog_cxt.xlog_atomic_op来记录不同函数中记录的Xlog数据 -
每次调用
XLogAtomicOpRegisterBuffer()时,内部会调用XLogAtomicOperation::RegisterBuffer()记录当前buffer的相关信息,并将当前buffer压栈,最大支持20个不同的buffer,然后通过XLogAtomicOperation::RegisterBufData()记录与当前buffer相关的标识信息。假如同一个buffer多次被修改,则会记录修改的次数,并分别记录每次修改,一条Xlog中单个buffer最多支持16次不同的修改
主要函数源码解析
void XLogAtomicOperation::RegisterBuffer(Buffer buffer, uint8 flags, uint8 op, uint32 clean_flag)
{
int buf_id = stack.contains(buffer); // 如果当前buffer在栈中已经存在,返回index,不存在,返回-1
if (buf_id >= 0) {
curr_blockid = buf_id;
} else {
curr_blockid = stack.Insert(buffer, flags, clean_flags); // 将当前buffer压栈
}
stack.buffers[curr_blockid].opnum++;
RegisterBufData((char*)&op, sizeof(op), true); // 保存当前buffer的相关标识信息
}
void XLogAtomicOperation::RegisterBufData(char *data, int len, bool is_opcode)
{
int blockid = CurrentBlockId(); // 返回最近一次调用RegisterBuffer()时对应的buffer的id
char* opData = dataCache.RegisterData(data, len);
stack.RegisterOpData(blockid, opData, len, is_opcode); // is_opcode仅在通过RegisterBuffer()调用时为真
}
void XLogAtomicOperation::XLogCommit(RmgrId rmid, uint8 info, int bucket_id)
{
/* 1. 开始插入 */
XlogBeginInsert();
/* 2. 注册子操作的数目 */
XLogRegisterData((char *)&stack.depth, sizeof(stack.depth));
/* 3. 注册每个buffer和对应的操作 */
for (int i = 0; i < stack.depth; i++ ) {
XLogBuffer *buf = *stack.buffers[i];
SegMarkBufferDirty(buf->buffer);
XLogRegisterBuffer(i, buf->buffer, buf->flags); // 注册buffer
XLogRegisterBufData(i, (char *)&buf->opnum, sizeof(uint8)); // 注册该buffer修改的次数
for (int j = 0; j < buf->opnum; j++) {
XLogOperation *xlog_op = &buf->operations[j];
XLogRegisterBufData(i, (char *)&xlog_op->data_len, sizeof(uint32)); // 注册子操作的数据长度
XLogRegisterBufData(i, xlog_op->data, xlog_op->data_len); // 注册子操作详细信息
}
}
/* Xlog insert */
XLogRecPtr recptr = XLogInsert(rmid, info, bucket_id);
/* 标脏、设置lsn、放锁 */
for (int i = 0; i < stack.depth; i++) {
Buffer buf = stack.buffers[i].buffer;
PageSetLSN(BufferGetPage(buf), recptr); //设置LSN
uint32 clean_flag = stack.buffers[i].clean_flag;
if (BUF_NEED_UNLOCK_RELEASE(clean_flag)) { // 放锁
SegUnlockReleaseBuffer(buf);
} else {
if (BUF_NEED_RELEASE(clean_flag)) {
SegReleaseBuffer(buf);
}
if (BUF_NEED_UNLOCK(clean_flag)) {
LockBuffer(buf, BUFFER_LOCK_UNLOCK);
}
}
}
Reset();
}
单条Xlog内容解析
单条Xlog内容如图,单条Xlog中可能包含若干个子操作,因此Xlog的第一部分记录本条Xlog包含的子操作的数目,每个子操作由两部分组成:第一部分是子操作的操作码,第二部分是详细的数据内容。段页式存储机制日志资源管理器是RM_SEGPAGE_ID,info为XLOG_SEG_ATOMIC_OPERATION,备机同步时,首先读取子操作数目,然后根据子操作数目逐个解析子操作的详细内容,并根据不同的子操作类型调用不同的处理函数,详见src/gausskernel/storage/smgr/segment/segxlog.cpp
3 源码解析
3.1 代码文件结构

支持段页式管理代码位于src/gausskernel/storage/smgr/segment/下,支持数据读写、buffer管理、extent管理和Xlog管理等功能。其中,data_file.cpp主要负责支持存储介质中数据的读写等功能,extent_group.cpp主要负责extent的管理,inverse_ptr.cpp主要负责inverse指针的管理,segbuffer.cpp主要负责段页式buffer管理,segxlog.cpp和xlog_atomic_op.cpp主要负责段页式Xlog的管理,space.cpp主要负责段空间管理,segstore.cpp主要实现段页式相关的接口
3.2 函数集
段页式管理机制向上对接存储管理器smgr,向下通过系统提供的pread()和pwrite()函数直接对存储介质进行数据读写
3.3 主要操作流程源码解析
3.3.1 CREATE TABLE
heap_create(const char* relname, Oid relnamespace, Oid reltablespace, Oid relid, Oid relfilenode ···)
{
···
if (storage_type == SEGMENT_PAGE) {
relfilenode = seg_alloc_segment(); // 创建第一个物理文件1,保存表的元信息segment head
}
rel = RelationBuildLocalRelation(); // build the relcache entry
RelationCreateStorage(); // 为SMgrRelation创建fork_desc
···
}
说明
-
执行
CREATE TABLE tb() with(segment=on)语句时,通过heap_create创建一个heap relation,首先通过seg_alloc_segment()创建第一个物理文件(仅在第一次创建段页式存储表时),并将表的元信息segment_head写到文件中,segment_head在物理文件中的页号作为该表的relfilenode。物理文件的创建通过eg_init_data_files()实现 -
RelationCreateStorage()并不会创建实际的物理文件,只是创建该表的fork_desc -
物理文件2的创建在
index_build()中,会创建第一个extent group,并且占用第一个extent的位置,即第4157-4164这些block,插入的第一条数据从第4165个block开始存储
3.3.2 INSERT INTO
heap_insert(Relation relation, HeapTuple tup, CommandId cid, int options, BulkInsertState bistate, bool istoast)
{
···
buffer = RelationGetBufferForTuple();
RelationPutHeapTuple(relation, buffer, heaptup, xid);
···
}
说明
-
在插入第一条数据时,由于buffer pool中没有可用的buffer,会调用
ReadBufferExtended()扩展一个block,并加入到buffer pool中。在ReadBufferExtended()中,最终通过调用seg_extend()从SegLogicFile中获取一个空闲的extent,并将extent中的第一个block加载到buffer pool -
每次插入数据需要切换block的时候都需要通过
seg_extend()加载一个新的block到buffer pool中
3.3.3 扩展extent
/* Add one extent for a given segment. */
void seg_extend_segment(SegSpace *spc, ForkNumber forknum, Buffer seg_head_buffer, BlockNumber seg_head_blocknum)
{
SegmentHead *seg_head = (SegmentHead *)PageGetContents(BufferGetPage(seg_head_buffer));
uint32 extent_size = ExtentSizeByCount(seg_head->nextents);
ExtentInversePointer iptr = {.flag = SPC_INVRSPTR_ASSEMBLE_FLAG(DATA_EXTENT, seg_head->nextents),.owner = seg_head_blocknum};
XLogAtomicOpStart();
BlockNumber new_extent = spc_alloc_extent(spc, extent_size, forknum, InvalidBlockNumber, iptr); // 扩展extent并初始化map head、map page
seg_head->nextents++;
seg_head->total_blocks += extent_size;
if (new_extent_id < BMT_HEADER_LEVEL0_SLOTS) { // 不需要level0 page
seg_head->level0_slots[new_extent_id] = new_extent;
seg_head_update_xlog(seg_head_buffer, seg_head, new_extent_id, -1);
} else {
if (RequireNewLevel0Page(new_extent_id)) { // 初始化一个level0 page
BlockNumber level0_page =
spc_alloc_extent(spc, SEGMENT_HEAD_EXTENT_SIZE, MAIN_FORKNUM, InvalidBlockNumber, iptr);
seg_init_new_level0_page(spc, new_extent_id, seg_head_buffer, level0_page, new_extent);
} else { // 在已有的level0 page上记录新增的extent的起始位置
seg_record_new_extent_on_level0_page(spc, seg_head_buffer, new_extent_id, new_extent);
seg_head_update_xlog(seg_head_buffer, seg_head, -1, -1);
}
}
XLogAtomicOpCommit();
}
-
该函数用于在segment中扩展一个extent,并根据需求初始化一个level0 page,将新扩展的extent记录到适当的level0 slot
-
该函数中使用到了段页式存储管理中实现的跨函数、多block的Xlog机制,一个原子操作包含多个block,在多个函数中注册Xlog数据
3.4. 物理空间回收
段页式表不支持VACUUM FULL命令对空闲的磁盘空间进行回收,提供了local_space_shrink(tablespacename TEXT, databasename TEXT)和gs_space_shrink(int4 tablespace, int4 database, int4 extent_type, int4 forknum)两个函数对物理空间进行收缩
3.4.1 TRUNCATE
TRUNCATE命令用于快速地从表中删除所有行,和不带WHERE条件的DELETE语句效果相同,但是TRUNCATE的速度快很多。DELETE不会回收物理空间,但是TRUNCATE会释放被占用的物理空间
主要函数
/* 段页式存储和smgr层对接的接口 */
void seg_unlink(const RelFileNodeBackend &rnode, ForkNumber forknum, bool isRedo, BlockNumber blockNum);
void normal_unlink_segment(SegSpace *spc, const RelFileNode &rnode, Buffer main_head_buffer)
{
SegmentHead *main_head = (SegmentHead *)PageGetContents(BufferGetBlock(main_head_buffer));
LockBuffer(main_head_buffer, BUFFER_LOCK_EXCLUSIVE);
/* 回收non-main fork的数据占用的extent */
for (int i = SEGMENT_MAX_FORKNUM; i > MAIN_FORKNUM; i--) {
BlockNumber fork_head = main_head->fork_head[i];
if (BlockNumberIsValid(fork_head)) {
Buffer buffer = ReadSegmentBuffer(spc, fork_head);
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
off_t offset = offsetof(SegmentHead, fork_head) + sizeof(BlockNumber) * i;
free_one_segment(spc, i, buffer, fork_head, main_head_buffer, offset);
SegUnlockReleaseBuffer(buffer);
}
}
/* 回收main fork的数据占用的空间 */
free_segment_extents(spc, MAIN_FORKNUM, main_head_buffer);
}
void free_one_segment(···)
{
/* 释放所有的extent */
free_segment_extents(spc, forknum, segment_buffer);
/* 在文件1中释放non-main fork 的segment_head占用的空间 */
spc_free_extent(spc, SEGMENT_HEAD_EXTENT_SIZE, MAIN_FORKNUM, head_blocknum);
···
}
/* remove segment中的最小原子操作 */
void free_last_extent(SegSpace *spc, ForkNumber forknum, Buffer head_buffer)
{
/* 1. 获取该Segment_head管理的最后一个extent的位置 */
SegmentHead *head = (SegmentHead *)(PageGetContents(BufferGetPage(head_buffer)));
int last_ext_id = head->nextents - 1;
BlockNumber ext_blk = seg_extent_location(spc, head, last_ext_id);
ExtentSize ext_size = ExtentSizeByCount(last_ext_id);
/* 2. 开启Xlog记录,释放extent */
XLogAtomicOpStart();
spc_free_extent(spc, ext_size, forknum, ext_blk);
/* 3.设置Segment_head中的相关信息 */
head->nextents--;
head->total_blocks -= ext_size;
XLogAtomicOpCommit();
/* 如果所有的extent都释放了,将Segment_head刷盘 */
if (head->total_blocks == 0) {
SegmentCheck(head->nextents == 0);
FlushOneSegmentBuffer(head_buffer);
}
}
说明
-
释放extent的流程:把
map page中对应的比特位设置为UNUSE-> 更新map head中已声请的extent数(map_head->alloc_extents--) -> 更新segment_head中的信息 ->segment_head刷盘(所有的extent的释放了) -> 释放non-main fork的segment_head -
main fork的segment_head最后释放 -
openGauss 3.1版本的
TRUNCATE操作只是将每个SegLogicFile中的map head和map page进行更新,使后续的数据可以使用释放出来的extent,没有对物理空间进行释放,需要配合使用loacl_space_shrink()或gs_space_shrink()进行物理空间的回收
3.4.2 space_shrink
openGauss提供local_space_shrink和gs_space_shrink两个函数来回收段页式存储中空闲的extent,前一个对指定DB下的所有空闲extent进行回收,后一个仅回收指定物理文件中的空闲空间
主要函数
void spc_shrink(Oid spcNode, Oid dbNode, int extent_type, ForkNumber forknum)
{
SegSpace *spc = spc_open(spcNode, dbNode, false);
SegExtentGroup *seg = &spc->extent_group[EXTENT_TYPE_TO_GROUPID(extent_type)][forknum];
/* 计算空间收缩的目标大小 */
BlockNumber target_size = 0;
int new_group_count, old_group_count;
get_space_shrink_target(seg, &target_size, &old_group_count, &new_group_count);
/* 对extent进行移动 */
move_extents(seg, target_size);
BlockNumber new_hwm = shrink_hwm(seg, target_size);
if (BlockNumberIsValid(new_hwm)) {
spc_shrink_files(seg, new_hwm, false);
}
}
说明
-
spc_shrink为空间回收的核心调用函数,该函数实现对指定物理文件中的空闲空间进行回收。extent_type + forknum唯一确定一个物理文件 -
目标大小的计算方式:已申请的extent数目 * 单个extent包含的block数目 + map_group包含的block数目,按照128MB对齐
当前存在的问题
- openGauss 3.1.0的代码中记录了已申请的extent数目和高水平线,高水平线对应已使用的extent的位置,但是在
DELETE数据时没有高水平线更新的机制,只有执行TRUNCATE时会更新高水平线的位置。即普通的DELETE操作后通过select loacl_space_shrink()和select gs_space_shrink()无法对空闲的extent进行回收
参考资料
Gauss数据库段页式机制探究




