一、概述
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(×tamp); char *strTime = ctime(×tamp); 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 可以简单划分为四块区域:

- Page 头部区域,描述整个 page 的情况,比如空闲空间,校检值等;
- 数据指针区域,数据指针用来描述实际数据的存储信息;
- 数据区域,用来存储实际数据;
- 特殊区域,用来存储一些特殊数据;
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 |