移动云大云海山数据库(He3DB)存算分离架构下Page页存储正确性校验框架介绍

一、概述

1.1目的

He3PG采用数据共享存储架构,当用户请求计算节点进行数据查询获取,但是本地数据被淘汰时,没有缓存指定数据时会去共享存储获取数据,在此存算分离场景下,获取的数据可能无法判断正确性,可能存在从共享存储读到的是未来页或者过去页,与计算节点本身应有的数据是不一致的,因此需要一种框架,来发现存在不一致数据页的问题,从而帮助定位问题,提升数据库的正确性。

基于以上一些考虑点,所以设计此框架,帮助发现及定位数据正确性问题。

1.2范围

He3DB--PG内核存储模块Page页正确性校验。

二、整体设计

2.1整体设计

数据库共享存储架构下的Page页正确性校验框架是利用对于PG数据页的内容进行Checksum计算,计算层在数据淘汰到FC本地缓存时,记录数据Page页的checksum值到Redis中,每一个FC节点对应Redis的一个DB库,至多15个库。每次计算节点读取数据时,首先根据对应的Page页的属性,拼凑成key值,从Redis中获取对应的value即为checksum值,再根据读取的数据页计算数据页的checksum值,进行比对,如果不一致,则说明page页不正确,此时进程崩溃。

主体架构图如下图所示:

2.2流程设计

**步骤一:**计算节点启动时,在Redis库中初始化对应的启动时间starttimestamp,验证Redis是否可用。对应的计算节点编号即为Redis中的db号。对应函数:

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #ifdef CHECKSUM_DS //not push standby should flush all every database start up if(is_redis_checksum && !is_push_standby){ redisContext * c = redisConnect(redis_connect, 32571); if (c->err) { elog(WARNING,"Connect Redis Error: %sn", c->errstr); redisFree(c); }else{ redisCommand(c, "auth %s","****"); if(*cluster_name != '0'){ elog(INFO,"Flush db %s",cluster_name); redisCommand(c, "select %s",cluster_name); redisCommand(c, "flushdb"); } time_t timestamp; time(&timestamp); char *strTime = ctime(&timestamp); redisReply *reply = redisCommand(c, "set starttime %s",strTime); if (reply!=NULL&&reply->type == REDIS_REPLY_ERROR) { elog(WARNING,"Command Error is: %sn", reply->str); freeReplyObject(reply); redisFree(c); } redisFree(c); } } #endif |

**步骤二:**在计算节点将内存中的数据淘汰至磁盘时,进行checksum的计算及将其存储到Redis中,对应的key值为基于Page页拼凑的key值。Page对应的表Oid+BlockNumber构成,checksum计算函数如下:

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| static uint32 pg_checksum_block(const PGChecksummablePage *page) { uint32 sums[N_SUMS]; uint32 result = 0; uint32 i, j; Assert(sizeof(PGChecksummablePage) == BLCKSZ); memcpy(sums, checksumBaseOffsets, sizeof(checksumBaseOffsets)); for (i = 0; i < (uint32) (BLCKSZ / (sizeof(uint32) * N_SUMS)); i++) for (j = 0; j < N_SUMS; j++) CHECKSUM_COMP(sums[j], page->data[i][j]); for (i = 0; i < 2; i++) for (j = 0; j < N_SUMS; j++) CHECKSUM_COMP(sums[j], 0); for (i = 0; i < N_SUMS; i++) result ^= sums[i]; return result; } |

**步骤三:**当从FC或DS读取Page页数据时,首先根据Page页构造相应的key值,再根据key值去Redis中获取对应的checksum。接着,获取到checksum之后针对新获取到的page页进行checksum的计算,计算完了再比对校验,如果checksum一致则验证通过,否则panic异常进程。

三、功能模块设计

3.1基于数据页的正确性校验

Postgresql 保存数据的基本单位是 page,一个 page 里包含多条数据。postgresql 同磁盘的读写单位也是 page,一个 page 对应于磁盘的一个 block。block 的格式和 page 是相同的,本篇文章详细得介绍了 page 的数据存储格式和相关的增删改查操作。

内存结构,page 可以简单划分为四块区域:

  1. Page 头部区域,描述整个 page 的情况,比如空闲空间,校检值等;
  2. 数据指针区域,数据指针用来描述实际数据的存储信息;
  3. 数据区域,用来存储实际数据;
  4. 特殊区域,用来存储一些特殊数据;

Page 头部,page 头部由结构体PageHeaderData来表示:

对于数据页的Special空间就是0;

因此对于数据页直接进行clog的mask函数,去除对应的信息即可。

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Page page = (Page) pagedata; OffsetNumber off; mask_page_lsn_and_checksum(page); mask_page_hint_bits(page); mask_unused_space(page); for (off = 1; off <= PageGetMaxOffsetNumber(page); off++) { ItemId iid = PageGetItemId(page, off); char *page_item; page_item = (char *) (page + ItemIdGetOffset(iid)); if (ItemIdIsNormal(iid)) { HeapTupleHeader page_htup = (HeapTupleHeader) page_item; if (!HeapTupleHeaderXminFrozen(page_htup)) page_htup->t_infomask &= ~HEAP_XACT_MASK; else { page_htup->t_infomask &= ~HEAP_XMAX_INVALID; page_htup->t_infomask &= ~HEAP_XMAX_COMMITTED; } page_htup->t_choice.t_heap.t_field3.t_cid = MASK_MARKER; if (HeapTupleHeaderIsSpeculative(page_htup)) ItemPointerSet(&page_htup->t_ctid, blkno, off); } if (ItemIdHasStorage(iid)) { int len = ItemIdGetLength(iid); int padlen = MAXALIGN(len) - len; if (padlen > 0) memset(page_item + len, MASK_MARKER, padlen); } } |

3.2基于索引页的正确性校验

Postgresql中主要支持6种类型的索引:BTREE、HASH、GiST、SP-GiST、GIN、BRIN。

针对不同的索引,需要对索引页中的clog等信息进行mask,因此,对于不同的索引,需要区分索引的不同结构,同时针对其中变化的clog数据进行mask后再计算checksum,否则极可能因为clog信息不一致导致checksum校验不通过。

如何区分不同的索引:

首先,BTREE,HASH,GiST页的Special空间是16个字节,而sp,gin,brin的special空间是8个字节,因此可以简单区分出BTREE,HASH,GiST索引的Page页。

其次,针对同类索引内部,在16个字节中,同样有唯一标识出索引的字段,进行解析即可。

|----------|-------------|--------------------------------|
| 索引Page类型 | Special空间大小 | Special标识字段 |
| BTREE | 16 | -- |
| HASH | 16 | HASHO_PAGE_ID (0xFF80) |
| GiST | 16 | GIST_PAGE_ID (0xFF81) |
| SP-GiST | 8 | SPGIST_PAGE_ID (0xFF82) |
| GIN | 8 | BRIN_PAGETYPE_REGULAR (0xF093) |
| BRIN | 8 | -- |

针对不同的索引,分别过滤对应的clog信息即可,完成checksum的计算。

GinMask函数:

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| void gin_mask(char *pagedata, BlockNumber blkno) { Page page = (Page) pagedata; PageHeader pagehdr = (PageHeader) page; GinPageOpaque opaque; mask_page_lsn_and_checksum(page); opaque = GinPageGetOpaque(page); mask_page_hint_bits(page); if (opaque->flags & GIN_DELETED) mask_page_content(page); else if (pagehdr->pd_lower > SizeOfPageHeaderData) mask_unused_space(page); } |

3.3Page页CheckSum存储

在内存中计算好了对应Page页的CheckSum,将其转储到Redis的数据库中。对应代码及流程如下:

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #ifdef CHECKSUM_DS //set checksum to redis if(is_redis_checksum&&!is_push_standby&&!c->err){ // while (!acquire_lock(c, redis_lock_key, redis_lock_value, lock_ttl)) { // printf("Write Lock not acquired, waiting...n"); // sleep(1); // } // uint16 newCheckSum=pg_checksum_page((char *)bufBlock, buf->tag.blockNum); char *pageKey=GetRelationPage(reln->smgr_rnode.node.dbNode,reln->smgr_rnode.node.spcNode,reln->smgr_rnode.node.relNode,forknum,blocknum); //except global if(pageKey[0]!='g'){ uint16 checksum = pgx_checksum_page_with_mask(buffer,blocknum,reln->smgr_rnode.node); if(checksum==0){ return; } ((PageHeader) buffer)->pd_checksum=checksum; elog(DEBUG5,"begin set %s with check sum %u",pageKey,checksum); redisReply *reply = redisCommand(c, "SET %s %u",pageKey,checksum); if (reply->type == REDIS_REPLY_ERROR) { elog(WARNING,"Set error"); freeReplyObject(reply); redisFree(c); // release_lock(c, redis_lock_key, redis_lock_value); } elog(DEBUG5,"done set %s with check sum %u",pageKey,checksum); pfree(pageKey); freeReplyObject(reply); } // release_lock(c, redis_lock_key, redis_lock_value); } #endif |

相关推荐
·云扬·2 小时前
MySQL Undo Log 深度解析:事务回滚与 MVCC 的底层支柱
android·数据库·mysql
SQL必知必会2 小时前
SQL 数据分析终极指南
数据库·sql·数据分析
AC赳赳老秦2 小时前
云原生AI趋势:DeepSeek与云3.0架构协同,提升AI部署性能与可移植性
大数据·前端·人工智能·算法·云原生·架构·deepseek
Tadas-Gao2 小时前
大模型实战装备全解析:从本地微调到移动算力的笔记本电脑选择指南
架构·系统架构·大模型·llm
SQL必知必会2 小时前
SQL 优化技术精要:让查询飞起来
数据库·sql
少云清2 小时前
【安全测试】5_应用服务器安全性测试 _SQL注入和文件上传漏洞
数据库·sql·安全性测试
H Journey2 小时前
Django 教程
数据库·django·sqlite
eWidget2 小时前
核心业务系统国产化:如何破解 Oracle 迁移中的“重构代价”与“性能瓶颈”?
数据库·oracle·重构·kingbase·数据库平替用金仓·金仓数据库
lhxsir2 小时前
oracle常用命令(DBA)
数据库·oracle·dba