暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

段页式存储管理机制

原创 scmysxb 2023-02-17
1253

段页式存储

1 背景

1.1 openGauss页式存储

  • openGauss普通表在物理存储上采用页式存储管理机制,每个表的数据独立对应一个逻辑上的大文件(最大支持32TB),该逻辑文件按照固定的大小(默认1GB)划分为多个物理文件存放到对应的目录下

  • openGauss对外提供hashbucket、大分区表等特性,每张数据表会被拆分为多个子表,底层所需的文件数量成倍地增长

1.1.1 页式存储存在的问题

  • 对文件系统的依赖较大,无法进行细颗粒度的控制 提升可维护性

  • 大数据量场景下文件句柄过多,目前只能通过虚拟句柄(VFD)来解决,影响系统性能

  • 小文件数量过多会导致全量备份等场景下的随机IO问题,影响性能

page.png

页式存储

1.2 段页式存储

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

seg_page.png

段页式存储文件管理

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通过不同的物理文件进行存放

seg_page.png

段页式存储物理文件管理

 
 

2.2.2 BMT(Block Map Tree)

datarw.png

数据访问流程

主要数据结构

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 FORKsegment head在元数据文件中的物理页号作为该表的relfilenode

  • BMT向上层提供segment的概念,上层传入参数为segment号+逻辑页号,向下管理物理文件,根据上层参数找到具体的的物理文件和物理页号,进行实际的数据读写。数据访问流程如图

  • segment head中提供两个数组level0 slotlevel1 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 groupinverse pointers pages和若干个extent构成一个extent group,同一个extent group内,所有的extent类型相同

2.2.3 空闲extent管理

openGauss中,一组相同前缀的物理文件称为一个SegLogicFile,一个SegLogicFile包含的内容如图。其中file head页面存放表文件的元信息,spc head页面存放表空间的元信息,map head页面存放管理map page的元信息。前两种页面目前没有使用

SegLogicFile示意图

主要数据结构

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中的信息进行解析,因此元数据buffer会被反复的进行访问,为了减少反复的pinunpin,元数据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 pagemap pagesegment 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记录机制

  1. 主要函数接口
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();
  1. 主要数据结构
/* 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的第一部分记录本条Xlog包含的子操作的数目,每个子操作由两部分组成:第一部分是子操作的操作码,第二部分是详细的数据内容。段页式存储机制日志资源管理器是RM_SEGPAGE_IDinfoXLOG_SEG_ATOMIC_OPERATION,备机同步时,首先读取子操作数目,然后根据子操作数目逐个解析子操作的详细内容,并根据不同的子操作类型调用不同的处理函数,详见src/gausskernel/storage/smgr/segment/segxlog.cpp


3 源码解析

3.1 代码文件结构

dir.png

代码文件结构

支持段页式管理代码位于src/gausskernel/storage/smgr/segment/下,支持数据读写、buffer管理、extent管理和Xlog管理等功能。其中,data_file.cpp主要负责支持存储介质中数据的读写等功能,extent_group.cpp主要负责extent的管理,inverse_ptr.cpp主要负责inverse指针的管理,segbuffer.cpp主要负责段页式buffer管理,segxlog.cppxlog_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 forksegment_head

  • main forksegment_head最后释放

  • openGauss 3.1版本的TRUNCATE操作只是将每个SegLogicFile中的map headmap page进行更新,使后续的数据可以使用释放出来的extent,没有对物理空间进行释放,需要配合使用loacl_space_shrink()gs_space_shrink()进行物理空间的回收

3.4.2 space_shrink

openGauss提供local_space_shrinkgs_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数据库段页式机制探究

最后修改时间:2023-02-24 11:04:51
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论