openGauss内核求索 ---- 缓冲区管理器

openGauss内核求索 ---- 缓冲区管理器

1.基本原理

缓冲管理器:主要是管理 共享内存持久存储 之间的数据传输,并可能对 DBMS 的性能产生重大影响。

缓冲区管理器持久存储后端进程之间的关系如下图所示:
在这里插入图片描述

2.缓冲区管理器结构

缓冲区管理器包括一个 缓冲区表缓冲区描述符缓冲池

缓冲区表

缓冲区描述符:保存着页面的元数据,对应的页面则保存在缓冲池的槽位中。

缓冲池:缓冲池存储数据文件的页,如表的页面。缓冲池是一个数组,每个插槽存储数据文件的一页。缓冲池数组的索引称为buffer_id。

相关函数:BufferDesc *BufferAlloc

2.1 缓冲区标签

数据库为所有数据文件的每个页面分配一个唯一的标记,即缓冲区标签。

缓冲区标签由 关系文件节点关系分支编号页面块号

typedef struct buftag {
    RelFileNode rnode; /* physical relation identifier */
    ForkNumber forkNum;
    BlockNumber blockNum; /* blknum relative to begin of reln */
} BufferTag;

​ (1)关系文件节点用于定位页面所属的关系,包含了表空间、数据库和表的oid。例如:(表空间,数据库, 表) -> (1663, 16384, 37721)

​ (2)关系分支编号用于定位关系文件的具体分支文件,一个关系可能有三种分支,分别是关系主体(main分支,编号为0)、空闲空间映射( fsm分支,编号为1)及可见性映射(vm分支,编号为2)

​ (3)页面块号则在具体分支文件中指明相应页面的偏移量,即页面号

例如,{(1663, 16384, 37721), 0, 7} 标签表示,在某个表空间(oid=1663)中,某个数据库(oid=16384)的某张表(oid=37721)的 0 号分支( 0代表关系表本体)的第 7 号页面。再比如,缓冲区标签 {(1663, 16384, 37721), 1, 3} 表示该表空闲空间映射文件的三号页面。关系本体 main 分支编号为 0,空闲空间映射 fsm 分支编号为1。

2.2 缓冲区表

缓冲区表:一个散列表(hash 表),存储着页面的 buffer_tag缓冲区描述符的 buffer_id 之间的映射关系。

缓冲表在逻辑上可分为三部分: 散列函数散列桶槽数据项

散列函数:hashquickany

散列桶槽: key值

**数据项: **(页面的 buffer_tag,包含页面元数据的描述符的 buffer_id)

Notes: 为了避免哈希函数的冲突,缓冲表采用了使用链表的分离链接方法来解决冲突。当数据项被映射至同一个桶槽时,该方法会将数据项(包括两个值,即页面的 buffer_tag 和包含页面元数据的描述符的 buffer_id)保存在一个链表中。例如,数据项 (Tag_A,id=1)表示在 buffer_id=1 对应的缓冲区描述符中,存储着页面 Tag_A 的元数据
在这里插入图片描述

页面简单访问流程:根据后端进程发送的请求,创建目标页面的 buffer_tag,然后将 buffer_tag 通过内置的散列函数映射到哈希桶槽,并分配 buffer_id, 即目标页面在缓冲池数组中存储的槽位的序号。

2.3缓冲区描述符

缓冲区描述符:保存着页面的元数据,对应的页面则保存在缓冲池的槽位中。缓冲区描述符的结构由BufferDesc结构定义。

2.3.1 缓冲区描述符结构体

结构体BufferDesc定义如下:

typedef struct BufferDesc {  
   BufferTag tag; /* ID of page contained in buffer */ 
​ 
  /* state of the tag, containing flags, refcount and usagecount */ 
  pg_atomic_uint64 state; 
​ 
  int buf_id;   /* buffer's index number (from 0) */ 
​ 
  ThreadId wait_backend_pid; /* backend PID of pin-count waiter */ 
​ 
  LWLock* io_in_progress_lock; /* to wait for I/O to complete */ 
  LWLock* content_lock;       /* to lock access to buffer contents */  
​  
   BufferDescExtra *extra;  
​  
#ifdef USE_ASSERT_CHECKING  
   volatile uint64 lsn_dirty;  
#endif  
} BufferDesc;

参数说明:

tag: 保存着目标页面的buffer_tag(2.1 缓冲区标签),缓冲区标签中对应的页面存储在相应的缓冲池槽中。

buffer_id: 标识了缓冲区描述符,相当于对应缓冲池槽的buffer_id。

context_lock: 轻量级锁,用于控制对相关页面的访问。

io_in_progress_lock: 轻量级锁,用于控制对相关页面的访问。

state: 用于保存相应页面的状态,主要状态如下:

/*
 * Flags for buffer descriptors
 *
 * Note: TAG_VALID essentially means that there is a buffer hashtable
 * entry associated with the buffer's tag.
 */
#define BM_IN_MIGRATE (1U << 16 << 32)        /* buffer is migrating */
#define BM_IS_TMP_BUF (1U << 21 << 32)         /* temp buf, can not write to disk */
#define BM_IS_META (1LU << 17 << 32)
#define BM_LOCKED (1LU << 22 << 32)            /* buffer header is locked */
#define BM_DIRTY (1LU << 23 << 32)             /* data needs writing */
#define BM_VALID (1LU << 24 << 32)             /* data is valid */
#define BM_TAG_VALID (1LU << 25 << 32)         /* tag is assigned */
#define BM_IO_IN_PROGRESS (1LU << 26 << 32)    /* read or write in progress */
#define BM_IO_ERROR (1LU << 27 << 32)          /* previous I/O failed */
#define BM_JUST_DIRTIED (1LU << 28 << 32)      /* dirtied since write started */
#define BM_PIN_COUNT_WAITER (1LU << 29 << 32)  /* have waiter for sole pin */
#define BM_CHECKPOINT_NEEDED (1LU << 30 << 32) /* must write for checkpoint */
#define BM_PERMANENT (1LU << 31 << 32)         /* permanent relation (notunlogged, or init fork) */

BM_DIRTY: 脏位指明相应页面是否为脏页。

BM_VALID: 有效位指明相应页面是否可以被读写(有效)。例如,如果该位被设置为 “valid”,那就意味着对应的缓冲池槽中存储着一个页面,而该描述符中保存着该页面的元数据,因而可以对该页面进行读写。反之如果有效位被设置为 “invalid”,那就意味着该描述符中并没有保存任何元数据,即对应的页面无法读写,缓冲区管理器可能正在将该页面换出。

buf_state |= BM_VALID;   /* TerminateBufferIO */
buf_state &= ~BM_VALID;  

BM_IO_IN_PROGRESS: IO进行标记位。指明缓冲区管理器是否正在从存储中读/写相应页面。换句话说,该位指示是否有一个进程正持有此描述符上的 io_in_pregress_lock。(StartBufferIO)

refcount: 保存当前访问相应页面的线程数(业务后端数量),也被称为钉数。当openGauss访问相应页面时,其引用计数必须自增1(refcount ++)。访问结束后其引用计数必须减1(refcount––)。当 refcount为零,即页面当前并未被访问时,页面将取钉,否则它会被钉住。

usage_count: 保存着相应页面加载至相应缓冲池槽后的访问次数。usage_count会在页面置换算法中被用到。

为了简化后续的描述,这里定义三种描述符状态。

空: 当相应的缓冲池槽不存储页面时,即 refcount 与 usage_count 都是0,该描述符的状态为空。

钉住: 当相应缓冲池槽中存储着页面,且有 PostgreSQL 进程正在访问的相应页面时, 即 refcount和 usage_count 都大于等于1,该缓冲区描述符的状态为钉住。

未钉住: 当相应的缓冲池槽存储页面,但没有PostgreSQL进程正在访问相应页面时,即 usage_count 大于或等于1,但 refcount 为0,该缓冲区描述符的状态为未钉住。每个描述符都处于上述状态之一,描述符的状态会根据特定条件而改变。

2.3.2 缓冲区描述符初始状态

缓冲区描述符的集合构成了一个数组,称该数组为缓冲区描述符层。当openGauss服务器启动时,所有缓冲区描述符的状态都为空。

2.4 缓冲池

2.4.1 缓冲池结构

缓冲池只是一个用于存储关系数据文件(例如表或索引)页面的简单数组。缓冲池数组的序号索引也就是 buffer_id。缓冲池槽的大小为 8KB,等于页面大小,因而每个槽都能存储整个页面。

数据类型:数组 key: buffer_id value:block

#define BufferGetBlock(buffer)                                                           \
        (BufferIsLocal(buffer) ? u_sess->storage_cxt.LocalBufferBlockPointers[-(buffer)-1] \
                              : IsNvmBufferID((buffer-1)) ? (Block)(t_thrd.storage_cxt.NvmBufferBlocks + ((Size)((uint)(buffer)-1-NvmBufferStartID)) * BLCKSZ) \
                              : IsSegmentBufferID(buffer-1) ? (Block)(t_thrd.storage_cxt.BufferBlocks + ((Size)((uint)(buffer)-NVM_BUFFER_NUM-1)) * BLCKSZ) \
                              : (Block)(t_thrd.storage_cxt.BufferBlocks + ((Size)((uint)(buffer)-1)) * BLCKSZ))
2.4.2 buffer候选队列

buffer候选队列:即candidate_buffer,内核中与normal_list、candidate_free_map联合一起管理空闲buffer的使用。

candidate_buffer: 数据类型:数组 key: 0~(N-1) value:buf_id

normal_list: 数据类型:CandidateList类型链表 size: 自增++/自减-- head: pop(buf_id) tail: push(buf_id) value:buf_id

candidate_free_map: 数据类型:数组 key: buf_id value:true/false

candidate_buffer候选队列和缓冲区、缓冲区描述的关系如下:
在这里插入图片描述

  • 1.初始化

    BufferBlocks 、BufferDescriptors、candidate_buffers 、candidate_free_map 在函数InitBufferPool中初始化

    normal_list 在函数init_candidate_list中初始化,将candidate_buffers分为pagewriter_thread_num份,将candidate_buffers均分后的每一份起始地址分别赋值给n个normal_list

  • 2.pagewriter 线程

    在incre_ckpt_pgwr_scan_candidate_list函数中,每一个pagewriter线程遍历自己的normal_list,判断是否为脏(!(local_buf_state & BM_DIRTY)),非脏则将buf_id逐次添加至链表尾部,并将对应candidate_free_map[buf_id]置为true。数据库启动第一次执行到这里的时候,buf_id对应的candidate_free_map会全部是true。后续数据库正常运行状态时,pagewriter 线程会重复上述逻辑。

  • 3.业务线程读取页面,获取一个free buffer

    在缓冲区中没有找到对应的页面后,会进入StrategyGetBuffer流程,先寻找一个空闲的buffer,如果没有空闲buffer,则会通过ClockSweepTick找到一个受害者缓冲池槽。

    (3.1) 寻找一个空闲的buffer:在StrategyGetBuffer->get_buf_from_candidate_list函数中,获取一个随机的thread_id(最大不超过pagewriter_thread_num),然后找到第thread_id的pagewriter线程的normal_list,从头部pop出第一个buf_id,检查candidate_free_map是否是空闲,refcount是否为0,是否是脏页等,检验全部通过后返回buf_id,即从候选队列中找到一个空闲可用的buffer

    (3.2) 寻找一个受害者缓冲池槽:ClockSweepTick 基于时钟扫描的页面置换算法

2.5 缓冲区相关的锁

Notes: 本节描述的锁,指的是缓冲区管理器同步机制的一部分。它们与SQL语句和SQL操作中的锁没有任何关系。

2.5.1 缓冲表锁

缓冲表锁 BufMappingLock 保护整个缓冲表的数据完整性。它是一种轻量级的锁,有共享模式与独占模式。在缓冲表中查询条目时,后端进程会持有共享的 BufMappingLock。插入或删除条目时,后端进程会持有独占的BufMappingLock。BufMappingLock 会被分为多个分区,以减少缓冲表中的争用(默认为128个分区)。每个BufMappingLock 分区都保护着一部分相应的散列桶槽。

下图给出了一个 BufMappingLock 分区的典型示例。两个后端进程可以同时持有各自分区的 BufMappingLock 独占锁,以插入新的数据项。如果 BufMappingLock 是系统级的锁,那么其中一个进程就需要等待另一个进程完成处理。
在这里插入图片描述

缓冲表也需要许多其他锁。例如,在缓冲表内部会使用自旋锁(spin lock)来删除数据项。不过本章不需要这些锁的其他相关知识,因此这里省略了对其他锁的介绍。

2.5.2 缓冲区描述符相关的锁

每个缓冲区描述符都会用到内容锁(content_lock)与IO进行锁(io_in_progress_lock)这两个轻量级锁,以控制对相应缓冲池槽页面的访问。当检查或更改描述符本身字段的值时,就会用到自旋锁。

  • 内容锁(content_lock): 是一个典型的强制限制访问的锁,它有共享与独占两种模式。

    获取共享模式的content_lock的场景:当读取页面时,后端进程以共享模式获取页面相应缓冲区描述符中的 content_lock。执行下列操作之一时,则会获取独占模式的content_lock。

    获取独占模式的content_lock的场景:(1)将行(即元组)插入页面,或更改页面中元组的 t_xmin/t_xmax 字段时 (2)物理移除元组,或压紧页面上的空闲空间(由清理过程和HOT执行) (3) 冻结页面中的元组。

    LWLockAcquire(bufHdr->content_lock, LW_EXCLUSIVE);
    LWLockRelease(buf->content_lock);
    
  • IO 进行锁(io_in_progress_lock): 用于等待缓冲区上的I/O完成。当PostgreSQL进程加载/写入页面数据时,该进程在访问页面期间,持有对应描述符上独占的io_in_progres_lock。

    LWLockAcquire(buf->io_in_progress_lock, LW_EXCLUSIVE);
    
  • 自旋锁: 当检查或更改标记字段与其他字段时,例如 refcount和 usage_count,会用到自旋锁。

    钉住缓冲区描述符的场景: 获取缓冲区描述符上的自旋锁,将其refcount和usage_count的值增加1,释放自旋锁。

    将脏位设置为"1"的场景: 获取缓冲区描述符上的自旋锁,使用位操作将脏位置位为"1",释放自旋锁。

    Notes: 总结来说就是涉及到缓冲区描述符的state字段的检查和修改,需要获取自旋锁

    /*
     * Lock buffer header - set BM_LOCKED in buffer state.
     */
    {
    uint64 LockBufHdr(BufferDesc *desc)
    #ifndef ENABLE_THREAD_CHECK
        SpinDelayStatus delayStatus = init_spin_delay(desc);
    #endif
        uint64 old_buf_state;
    
        while (true) {
            /* set BM_LOCKED flag */
            old_buf_state = pg_atomic_fetch_or_u64(&desc->state, BM_LOCKED);
            /* if it wasn't set before we're OK */
            if (!(old_buf_state & BM_LOCKED))
                break;
    #ifndef ENABLE_THREAD_CHECK
            perform_spin_delay(&delayStatus);
    #endif
        }
    #ifndef ENABLE_THREAD_CHECK
        finish_spin_delay(&delayStatus);
    #endif
    
        /* ENABLE_THREAD_CHECK only, acquire semantic */
        TsAnnotateHappensAfter(&desc->state);
        return old_buf_state | BM_LOCKED;
    }
    

    自旋锁技术博客: https://zhuanlan.zhihu.com/p/659527782

3.缓冲区管理器的工作原理

本节介绍缓冲区管理器的工作原理。当后端进程想要访问所需页面时,它会调用ReadBufferExtended函数。

ReadBufferExtended —> buffer_read_extended_internal —> ReadBuffer_common —> BufferAlloc

3.1 访问存储在缓冲池中的页面

当从缓冲池槽中的页面里读取行时,会获取相应缓冲区描述符的共享content_lock,因而缓冲池槽可以同时被多个进程读取。当向页面插入(及更新、删除)行时,后端进程会获取相应缓冲区描述符的独占content_lock。访问完页面后,相应缓冲区描述符的引用计数值减1。
在这里插入图片描述

介绍最简单的情况,即所需页面已经存储在缓冲池中。在这种情况下,postgresql-16.2最新版本的缓冲区管理器会执行以下步骤:
(1)创建所需页面的 buffer_tag(在本例中 buffer_tag 是’Tag_C’),并使用散列函数计算与描述符相对应的散列桶槽。

(2)获取相应散列桶槽分区上的 BufMappingLock 共享锁。

(3)查找标签为 ‘Tag_C’ 的条目,并从条目中获取 buf_id。本例中 buf_id 为2。

(4)将 buffer_id=2 的缓冲区描述符钉住,即将描述符的 refcount 和 usage_count 增加1。

(5)释放BufMappingLock。

(6)访问buffer_id=2的缓冲池槽。

openGauss优化代码:半无锁dynamic hash逻辑bufferalloc半无锁hash · Pull Request !3935 · openGauss/openGauss-server - Gitee.com

− − 姐姐我的小脑瓜子不太理解为啥叫半无锁,所以参加革命的同志们自行脑补 ! ! ! − − --姐姐我的小脑瓜子不太理解为啥叫半无锁,所以参加革命的同志们自行脑补!!!-- 姐姐我的小脑瓜子不太理解为啥叫半无锁,所以参加革命的同志们自行脑补!!!
在这里插入图片描述

(1)创建所需页面的 buffer_tag(在本例中 buffer_tag 是’Tag_C’),并使用散列函数计算与描述符相对应的散列桶槽。

(2)查找标签为 ‘Tag_C’ 的条目,并从条目中获取 buf_id。本例中 buf_id 为2。

(3)将 buffer_id=2 的缓冲区描述符钉住,即将描述符的 refcount 和 usage_count 增加1。

(4)判断buf->tag 与 new_tag(即‘Tag_C)是否相等,如果不相等说明buf_id对应的缓冲区描述符已经被替换,则unpinbuffer,回到步骤(1)

(5)访问buffer_id=2的缓冲池槽。

3.2 将页面从存储加载到空槽

假设所需页面不在缓冲池中,且 candidate_buffer(2.4.2节介绍) 中有空闲元素(空描述符)。这时,openGauss缓冲区管理器将执行以下步骤:
在这里插入图片描述

(1)创建所需页面的 buf_tag(本例中 buffer_tag 为 ‘Tag_E’ )并计算其散列桶槽。 查找缓冲区表(假设页面不存在,在缓冲表中找不到对应页面)。 (2)从 normal_list(2.4.2节介绍)中获取空闲缓冲区描述符,返回一个持有spin lock的buf_id(在本例中所获的描述符:buf_id=4)。

(3)释放spin lock,pin住buf_id对应的缓冲区描述符。

(4)由于是从normal_list获取到的buf_id,不会是脏页,脏页情况在3.3中讲解。

(5)以独占模式获取相应分区的 BufMappingLock。

(6)将buf_tag插入缓冲表数据项:可能存在someone else allocated another buffer for the same block we want to read in 的情况,因此这里有两种场景:

  • (6.1) 没有其他线程在我们之前读取同样block,则创建一条新的缓冲表数据项buf_tag=‘Tag_E’, buf_id=4,并将其插入缓冲区表中。

  • (6.2) buf_tag已存在于缓冲表中,则接下来参考3.1节的(3)~(5)步骤

(7)给缓冲区描述符赋值,((BufferDesc *)buf)->tag = new_tag,并给相应描述符的BM_TAG_VALID标记位置为"1"。

(8)释放相应分区的 BufMappingLock。

(9)此时buf_tag是有效的,但是buf_tag对应的缓冲池槽(即页面)还是无效的。

(10)将页面数据从存储加载至 buffer_id=4 的缓冲池槽中,如下所示:buf_id

  • (10.1)以排他模式获取相应描述符的 io_in_progress_lock。

  • (10.2)将相应描述符的 IO_IN_PROGRESS 标记位设置为1,以防其他进程访问。

  • (10.3)将所需的页面数据从存储加载到缓冲池插槽中。

  • (10.4)更改相应描述符的状态,将 IO_IN_PROGRESS 标记位设置为"0",且相应描述符的VALID标记位设置为"1"。

  • (10.5)释放 io_in_progress_lock。

(11)访问 buffer_id=4 的缓冲池槽。

3.3 将页面从存储加载到受害者缓冲池槽

后期补充

4. 相关函数解析

4.1 函数BufferAlloc

BufferAlloc

4.1 函数StrategyGetBuffer

BufferAlloc -> StrategyGetBuffer

4.2 函数PageCheckIfCanEliminate

BufferAlloc -> PageCheckIfCanEliminate: 当前选中的buf是否是BM_TAG_VALID,并且磁盘中的lsn_on_disk是否和内存buf的lsn一样,并且是否非脏页,是否处于回放状态中。如果满足则提示check lsn is not matched on disk,并且置脏。

此种情况出现的场景:淘汰一个buf,需要检查该buf是否为脏页,主机运行状态不会出现没有置脏的情况,代码中额外加了限制条件RecoveryInProgress,表明只有回放状态时,可能存在内存buf的lsn和磁盘页面的lsn不一致,并且内存buf没有置脏的情况,因此在这里做了一次置脏。

if ((*oldFlags & BM_TAG_VALID) && !XLByteEQ(buf->extra->lsn_on_disk, PageGetLSN(tmpBlock)) &&
        !(*oldFlags & BM_DIRTY) && RecoveryInProgress()) {
        int mode = DEBUG1;
#ifdef USE_ASSERT_CHECKING
        mode = PANIC;
#endif
        const uint32 shiftSize = 32;
        ereport(mode, (errmodule(MOD_INCRE_BG),
                errmsg("check lsn is not matched on disk:%X/%X on page %X/%X, relnode info:%u/%u/%u %u %u",
                       (uint32)(buf->extra->lsn_on_disk >> shiftSize), (uint32)(buf->extra->lsn_on_disk),
                       (uint32)(PageGetLSN(tmpBlock) >> shiftSize), (uint32)(PageGetLSN(tmpBlock)),
                       buf->tag.rnode.spcNode, buf->tag.rnode.dbNode, buf->tag.rnode.relNode,
                       buf->tag.blockNum, buf->tag.forkNum)));
        SimpleMarkBufDirty(buf);
        *oldFlags |= BM_DIRTY;
        *needGetLock = true;
    }

5.相关系统对象

5.1 参数

shared_buffers

5.2 函数或视图

  1. 系统函数pg_buffercache_pages
select count(*) from pg_buffercache_pages();

观测参数: bufferid, relfilenode, bucketid, storage_type, reltablespace, reldatabase, relforknumber, relblocknumber, isdirty, isvalid, usage_count, pinning_backends

  1. 系统函数local_candidate_stat
select count(*) from local_candidate_stat;

观测参数:node_name, candidate_slots, get_buf_from_list, get_buf_clock_sweep, seg_candidate_slots, seg_get_buf_from_list, seg_get_buf_clock_sweep

本文借鉴博客:https://blog.csdn.net/hmxz2nn/article/details/118526453

author: 丑丑的老太婆(unique woman)

  • 25
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值