上篇图文openGauss数据库源码解析系列文章——存储引擎源码解析(二)中,从astore和行存储索引机制两点对磁盘引擎进行了分享,本篇接着从磁盘引擎的行存储缓存机制、cstore、日志系统三方面展开介绍。
(五)行存储缓存机制
|图1 行存储缓存和淘汰机制示意图|
行存储堆表和索引表页面的缓存和淘汰机制主要包含以下几个部分:
1. 共享缓冲区内存页面数组下标哈希表
用于将远大于内存容量的物理页面与内存中有限个数的内存页面建立映射关系。该映射关系通过一个分段、分区的全局共享哈希表结构实现。哈希表的键值为buftag(页面标签)结构体。该结构体由“rnode”、“forkNum”、“blockNum”三个成员组成。其中“rnode”对应行存储表物理文件名的主体命名;“forkNum”对应主体命名之后的后缀命名,通过主体命名和后缀命名,可以找到唯一的物理文件;而“blockNum”对应该物理文件中的页面号。因此,该三元组可以唯一确定任意一个行存储表物理文件中的物理页面位置。哈希表的内容值为与该物理页面对应的内存页面的“buffer id”(共享内存页面数组的下标)。
typedef struct buftag {
RelFileNode rnode; /* 表的物理文件位置结构体 */
ForkNumber forkNum; /* 表的物理文件后缀信息 */
BlockNumber blockNum; /* 页面号 */
} BufferTag;
2. 共享buffer desc数组
该数组有“g_instance.attr.attr_storage.NBuffers”个成员,与实际存储页面内容的共享buffer数组成员一一对应,用来存储相同“buffer id”(即这两个全局数组的下标)的数据页面的属性信息。该数组成员为BufferDesc结构体,具体定义代码如下:
typedef struct BufferDesc {
BufferTag tag; /* Buffer页面标签 */
pg_atomic_uint32 state; /* 状态位、引用计数、使用历史计数 */
int buf_id; /* Buffer下标 */
ThreadId wait_backend_pid;
LWLock* io_in_progress_lock;
LWLock* content_lock;
pg_atomic_uint64 rec_lsn;
volatile uint64 dirty_queue_loc;
} BufferDesc;
(1)tag成员是该页面的(relfilenode,forknum,blocknum)三元组。
(2)state成员是该内存状态的标志位,主要包含BM_LOCKED(该buffer desc结构体内容的排他锁标志)、BM_DIRTY(脏页标志)、BM_VALID(有效页面标志)、BM_TAG_VALID(有效tag标志)、BM_IO_IN_PROGRESS(页面I/O状态标志)等。
(3)buf_id成员,是该成员在数组中的下标。
(4)wait_backend_pid成员,是等待页面unpin(取消引用)的线程的线程号。
(5)io_in_progress_lock成员,是用于管理页面并发I/O操作(从磁盘加载和写入磁盘)的轻量级锁。
(6)content_lock成员,是用于管理页面内容并发读写操作的轻量级锁。
(7)rec_lsn成员,是上次写入磁盘之后该页面第一次修改操作的日志lsn值。
(8)dirty_queue_loc成员,是该页面在全局脏页队列数组中的(取模)下标。
3. 共享buffer数组
4. bgwriter线程组
该数组有“g_instance.attr.attr_storage.bgwriter_thread_num”个线程。每个“bgwriter”线程负责一定范围内(目前为均分)的共享内存页面的写入磁盘操作,如图4-11中所示。如果全局共享buffer数组的长度为12,一共有3个“bgwriter”线程,那么第1个“bgwriter”线程负责“buffer id 0 - buffer id 3”的内存页面的维护和写入磁盘;第2个“bgwriter”线程负责“buffer id 4 - buffer id 7”的内存页面的维护和写入磁盘;第3个“bgwriter”线程负责buffer id 8 - buffer id 11的内存页面的维护和写入磁盘。每个“bgwriter”进程在后台循环扫描自己负责的那些共享内存页面和它们的buffer desc状态,将被业务修改过的脏页收集起来,批量写入双写文件,然后写入表文件系统。对于刷完的内存页,将其状态变为非脏,并追加到空闲buffer id队列的尾部,用于后续业务加载其他当前不在共享缓冲区的物理页面。每个“bgwriter”线程的信息记录在BgWriterProc结构体中,该结构体的定义代码如下:
typedef struct BgWriterProc {
PGPROC *proc;
CkptSortItem *dirty_buf_list;
uint32 dirty_list_size;
int *cand_buf_list;
volatile int cand_list_size;
volatile int buf_id_start;
pg_atomic_uint64 head;
pg_atomic_uint64 tail;
bool need_flush;
volatile bool is_hibernating;
ThrdDwCxt thrd_dw_cxt;
volatile uint32 thread_last_flush;
int32 next_scan_loc;
BgWriterProc;
(1)dirty_buf_list为存储每批收集到的脏页面buffer id的数组。dirty_list_size为该数组的长度。
(2)cand_buf_list为存储写入磁盘之后非脏页面buffer id的队列数组(空闲buffer id数组)。cand_list_size为该数组的长度。
(3)buf_id_start为该bgwriter负责的共享内存区域的起始buffer id,该区域长度通过“g_instance.attr.attr_storage.NBuffers / g_instance.attr.attr_storage.bgwriter_thread_num”得到。
(4)head为当前空闲buffer id队列的队头数组下标,tail为当前空闲buffer id队列的队尾数组下标。
(5)next_scan_loc为上次bgwriter循环扫描时停止处的buffer id,下次收集脏页从该位置开始。
5. pagewriter线程组
typedef struct PageWriterProc {
PGPROC* proc;
volatile uint32 start_loc;
volatile uint32 end_loc;
volatile bool need_flush;
volatile uint32 actual_flush_num;
} PageWriterProc;
(1)proc成员为“pagewriter”线程属性信息。
(2)start_loc为分配给本线程待写入磁盘的脏页在全量脏页队列中的起始位置。
(3)end_loc为分配给本线程待写入磁盘的脏页在全量脏页队列中的结尾位置。
(4)need_flush为是否有脏页被分配给本“pagewriter”的标志。
(5)actual_flush_num为本批实际写入磁盘的脏页个数(有些脏页在分配给本“pagewriter”线程之后,可能被“bgwriter”线程写入磁盘,或者被DROP(删除)类操作失效)。
6. 双写文件
(1)计算“buffer tag”对应的hash值和分区值。
(2)对“buffer id”哈希表加分区共享锁,并查找“buffer tag”键值是否存在。
(3)如果“buffer tag”键值存在,确认对应的磁盘页面是否已经加载上来。如果是,则直接返回对应的“buffer id + 1”;如果不是,则尝试加载到该“buffer id”对应的buffer内存中,然后返回“buffer id + 1”。
(4)如果“buffer tag”键值不存在,则寻找一个“buffer id”来进行替换。首先尝试从各个“bgwriter”线程的空闲“buffer id”队列中获取可以用来替换的“buffer id”;如果所有“bgwriter” 线程的空闲buffer id队列都为空队列,那么采用clock-sweep算法,对整个buffer缓冲区进行遍历,并且每次遍历过程中将各个buffer的使用计数减一,直到找到一个使用计数为0的非脏页面,就将其作为用来替换的buffer。
(5)找到替换的“buffer id”之后,按照分区号从小到大的顺序,对两个“buffer tag”对应的分区同时加上排他锁,插入新“buffer tag”对应的元素,删除原来“buffer tag”对应的元素。然后再按照分区号从小到大的顺序释放上述两个分区排他锁。
(6)最后确认对应的磁盘页面是否已经加载上来。如果是,则直接返回上述被替换的“buffer id + 1”;如果不是,则尝试加载到该“buffer id”对应的buffer内存中,然后返回“buffer id + 1”。
行存储共享缓冲区访问的主要接口和含义如表1所示。
函数名 |
操作含义 |
ReadBufferExtended |
读、写业务线程从共享缓冲区获取页面用于读、写查询 |
ReadBufferWithoutRelcache |
恢复线程从共享缓冲区获取页面用于回放日志 |
ReadBufferForRemote |
备机页面修复线程从共享缓冲区获取页面用于修复主机损坏页面 |
(六)cstore
1. cstore整体框架
2. cstore存储单元结构
(1) CU的CRC值,为CU结构中除CRC成员之外,其他所有字节计算出的32位CRC值。
(2) CU的magic值,为插入CU的事务号。
(3) CU的属性值,为16位标志值,包括CU是否包含NULL行、CU使用的压缩算法等CU粒度属性信息。
(4) 压缩后NULL值位图长度,如果属性值中标识该CU包含NULL行,则本CU在实际数据内容开始处包含NULL值位图,此处储存该位图的字节长度,如果该CU不包含NULL行,则无该成员。
(5) 压缩前数据长度,即CU数据内容在压缩前的字节长度,用于读取CU时进行内存申请和校验。
(6) 压缩后数据长度,即CU数据内容在压缩后的字节长度,用于插入CU时进行内存申请和校验。
(7) 压缩后NULL值位图内容,如果属性值中标识该CU包含NULL行,则该成员即为每行的NULL值位图,否则无该成员。
(8) 压缩后数据内容,即实际写入磁盘的CU主体数据内容。
每个CU最多保存对应字段的MAX_BATCH_ROWS行(默认60000行)数据。相邻CU之间按8KB对齐。
函数名称 |
接口含义 |
AppendCuData |
向组装的CU中增加一行(仅对应字段) |
Compress |
压缩(若需)和组装CU |
FillCompressBufHeader |
填充CU头部 |
CompressNullBitmapIfNeed |
压缩NULL值位图 |
CompressData |
压缩CU数据 |
CUDataEncrypt |
加密CU数据 |
ToVector |
将CU数据解构为向量数组结构 |
UnCompress |
解压(若需)和解析CU |
UnCompressHeader |
解析CU头部内容 |
UnCompressNullBitmapIfNeed |
解压NULL值位图 |
UnCompressData |
解压CU数据 |
CUDataDecrypt |
解密CU数据 |
3. cstore多版本机制
(1)CU间的可见性:每个CU对应CUDESC表(astore行存储表)中的一行记录(一对一),该CU的可见性完全取决于该行记录的可见性。
(2)同一个CU内不同行的可见性:每个CU的内部可见性对应CUDESC表中的一行(多对一),该行的bitmap字段为最长MAX_BATCH_ROWS个bit的删除位图(bit 1表示删除,bit 0表示未删除),通过该位图记录的可见性和多版本,来支持CU内不同行的可见性。同时由于DML操作都是行粒度操作的,因此对于行号范围相同的、不同字段的多个CU均对应同一行位图记录。
(3)CU文件读写并发控制:CU文件自身为APPEND-ONLY,只在追加时对文件大小扩展进行加锁互斥,无须其他并发控制机制。
(4)同一个字段的不同CU,对应严格单调递增的cu_id编号,存储在对应的CUDESC表记录中,该cu_id的获取也通过上述文件扩展锁来进行并发控制。
(5)对于cstore表的单条插入以及更新操作,同时也提供与每个cstore表对应的delta表(astore行存储表),来接受这些新插入的或更新后的元组,以降低CU文件的碎片化。
表3 CUDESC表的结构
字段名 |
类型 |
含义 |
col_id |
integer |
字段序号,即该cstore列存储表的第几个字段;特殊的,对于CU位图记录,该字段恒为-10 |
cu_id |
oid |
CU序号,即该列的第几个CU |
min |
text |
该CU中该字段的最小值 |
max |
text |
该CU中该字段的最大值 |
row_count |
integer |
该CU中的行数 |
cu_mode |
integer |
CU模式 |
size |
bigint |
该CU大小 |
cu_pointer |
text |
该CU偏移(8K对齐);特殊的,对于CU位图记录,该字段为删除位图的二进制内容 |
magic |
integer |
该CU magic号,与CU头部的magic相同,校验用 |
extra |
text |
预留字段 |
|图5 cstore表并发插入示意图|
1) 并发插入操作:
