PostgreSQL 存储之Page(页面)源码分析

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
日志服务 SLS,月写入数据量 50GB 1个月
简介:

我在另一篇文章:PostgreSQL 数据存储结构 中我们介绍了控制页和数据页的基本存储结构,那是从物理上进行说明各种页面的用途说明。

下面我们是从代码逻辑上来分析页面是如何进行操作和控制的。

页面布局示意图

image.png

PageHeader

先简单的看一下源代码中定义的Page头部信息结构体,中文是我自己的理解:
源码位置:/src/include/storage/bufpage.h

typedef struct PageHeaderData
{
    /* XXX LSN is member of *any* block, not only page-organized ones */
    /* 
       日志文件信息,保存了该页面最后一次被修改的操作对应到确切的日志文件
       位置,包括了日志文件ID和日志文件偏移量
    */
    XLogRecPtr    pd_lsn;            /* LSN: next byte after last byte of xlog
                                 * record for last change to this page */
    uint16        pd_tli;            /* least significant bits of the TimeLineID
                                        * containing the LSN */
    /* 用来表示页面状态 */
    uint16         pd_flags;        /* flag bits, see below */
    /* 空闲空间起始位置 */
    LocationIndex pd_lower;        /* offset to start of free space */
    /* 空闲空间结束位置 */
    LocationIndex pd_upper;        /* offset to end of free space */
    /* 特殊用途空间的起始位置,结束位置是page尾部,直到页面结束 */
    LocationIndex pd_special;    /* offset to start of special space */
    uint16        pd_pagesize_version;
    /* 页面类型: 有多种控制页面类型和数据页面 */
    uint8          pd_type;
    /* 物理存储与逻辑存储的关联对象的OID */
    Oid            pd_oid;            /* oid of related object,
                                 * pg_class.oid for IAM, pg_class.relfilenode for others */
    /* 数据开始位置 */
    ItemIdData    pd_linp[1];        /* beginning of line pointer array */
} PageHeaderData;

header用来保存该page相关的数据。

  • pd_lsn、pd_tli 记录日志相关的信息。
  • pd_flags 表示页面状态
    PD_ALL_VISIBLE(0x0001) - 表示所有元组可以访问。

PD_VALID_FLAG_BITS(0x0003) - 加密存储相关。
PG_PAGE_ENCRYPTED(0x0002) - 支持统一存储加密引擎。

  • pd_lower
    空闲空间起始位置,随着插入和删除操作位置发生变化。初始化时就是pd_linp的偏移位置。
  • pd_upper
    空闲空间结束位置,随着插入和删除操作位置发生变化。初始化时与pd_special相同。
  • pd_special
    相当于画了一条线,从pd_special这个位置到page的结尾,都是special的地盘,普通插入Tuple,都不许进入这个私有地盘。而且这个pd_special一旦初始化之后,这个值就不会动了。
  • pd_pagesize_version
    高位字节表示page大小。

低位字节表示版本号(PG_PAGE_LAYOUT_VERSION),代码写死。
宏PageSetPageSizeAndVersion用于设置大小和版本。
宏PageGetPageSize()获取Page大小。
宏PageGetPageLayoutVersion()获取版本号。
对应的实现:

#define PageSetPageSizeAndVersion(page, size, version) \
( \
    AssertMacro(((size) & 0xFF00) == (size)), \
    AssertMacro(((version) & 0x00FF) == (version)), \
    ((PageHeader) (page))->pd_pagesize_version = (size) | (version) \
)
#define PageGetPageSize(page) \
    ((Size) (((PageHeader) (page))->pd_pagesize_version & (uint16) 0xFF00))
#define PageGetPageLayoutVersion(page) \
    (((PageHeader) (page))->pd_pagesize_version & 0x00FF)
  • pd_type
    有这些类型:P_GAM、P_IAM、P_PFS、P_DCM、P_BCM、P_SGAM、P_VM。

对应的定义:

typedef enum PageType{
    P_TEMPDATA,
    P_HEAP,
    P_BTREE,
    P_HASH,
    P_GIN,
    P_GIST,
    P_SEQUENCE,
    P_GAM,
    P_PFS,
    P_SGAM,
    P_BCM,
    P_DCM,
    P_IAM,
    P_VM, /* vm page*/
    P_UNKNOWN,
}PageType;

Page初始化

通过如下函数初始化页面:

void
PageInit(Page page, Size pageSize, Size specialSize, PageType type)
{
    PageHeader    p = (PageHeader) page;
    uint16        pd_flags = p->pd_flags;

    specialSize = MAXALIGN_DISK(specialSize);

    Assert(pageSize == BLCKSZ);
    Assert(pageSize > specialSize + SizeOfPageHeaderData);

    /* Make sure all fields of page are zero, as well as unused space */
    MemSet(p, 0, pageSize);

    /* p->pd_flags = 0;                        done by above MemSet */
    PageSetPageSizeAndVersion(page, pageSize, PG_PAGE_LAYOUT_VERSION);
    PageSetPageType(page, type);
    PageSetOid(page, InvalidOid); /* we will set it later for heap page */
    /* Support the uniform store encryption engine. */
    p->pd_lower = SizeOfPageHeaderData;
    if ((pd_flags & PG_PAGE_ENCRYPTED) && !PageIsCtrlPage(page))
        p->pd_special = pageSize - specialSize -
                        G_EncryptData->block_key_num * (G_EncryptData->key_len + G_EncryptData->verify_len);
    else
        p->pd_special = pageSize - specialSize;
    p->pd_upper = p->pd_special;

    /*
     * Empty page is all visible. We must set PD_ALL_VISIBLE because 
     * recycled page's "visibility map bit" may be set before. (Recycled page -- 
     * means page was deallocated by Vacuum or something else, and then 
     * be allocated.)
     */
    PageSetAllVisible(p);
    if ((pd_flags & PG_PAGE_ENCRYPTED) && !PageIsCtrlPage(page))
        PageSetEncrypted(p);
}

Page有效性检查

通过如下函数实现:

bool
PageHeaderIsValid(PageHeader page)
{
    char       *pagebytes;
    int            i;

    /* Check normal case */
    /* Support the uniform store encryption engine. */
    if (PageGetPageSize(page) == BLCKSZ &&
        PageGetPageLayoutVersion(page) == PG_PAGE_LAYOUT_VERSION &&
        (page->pd_flags & ~PD_VALID_FLAG_BITS) == 0 &&
        page->pd_lower >= SizeOfPageHeaderData &&
        page->pd_lower <= page->pd_upper &&
        page->pd_upper <= page->pd_special &&
        (PageIsEncrypted(page) ?
            page->pd_special <= BLCKSZ - G_EncryptData->block_key_num * (G_EncryptData->key_len + G_EncryptData->verify_len) :
            page->pd_special <= BLCKSZ) &&
        page->pd_special == MAXALIGN_DISK(page->pd_special))
        return true;

    /* Check all-zeroes case */
    pagebytes = (char *) page;
    for (i = 0; i < BLCKSZ; i++)
    {
        /* Support the uniform store encryption engine. */
        if (pagebytes[i] != 0 && pagebytes[i] != 2)
            return false;
    }
    
    return true;
}

Insert操作分析

当初始化的时候,pd_lower设置为SizeOfPageHeaderData,pd_upper设置为和pd_special一样。但是注意,这个lower和upper不是固定的,随着Tuple的不断插入,lower变大,而upper不断变小。当我们每插入一条Tuple,需要在当前的lower位置再分配一个Item,记录Tuple的长度,Tuple的起始位置offset,还有flag信息。这个Page Header中的pd_lower就是记录分配下一个Item的起始位置。所以如果不断插入,lower不断增加,每增加一条Tuple,就要分配一个Item(4个字节)

对应实现函数:

  • PageAddItem
OffsetNumber
PageAddItem(Page page,
            Item item,
            Size size,
            OffsetNumber offsetNumber,
            Size tupleSize,
            ItemIdFlags flags)

Delete操作分析

对应实现函数:

  • PageIndexMultiDelete
void
PageIndexMultiDelete(Page page, OffsetNumber *itemnos, int nitems)

offnum指示第几个记录,offnum是从1开始计数的,查找对应item 是offnum-1.
我们找到Item,就可以找到Tuple对应的offset和size。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
关系型数据库 数据库 索引
AnalyticDB for PostgreSQL 黑科技解析 - 列存储 Meta Scan 性能加速
本文介绍阿里云 AnalyticDB for PostgreSQL(原HybridDB for PostgreSQL) 产品,即 MPP 数据仓库服务,其列存储 meta scan机制,及其对 分析场景的性能提升。
2540 0
|
存储 关系型数据库 PostgreSQL
Postgresql内核源码分析-heapam分析
Postgresql内核源码分析-heapam分析
163 1
|
存储 缓存 NoSQL
[译]解锁TOAST的秘密:如何优化PostgreSQL的大型列存储以最佳性能和可扩展性
[译]解锁TOAST的秘密:如何优化PostgreSQL的大型列存储以最佳性能和可扩展性
190 0
|
存储 关系型数据库 对象存储
PolarDB-PG | PostgreSQL + 阿里云OSS 实现高效低价的海量数据冷热存储分离
数据库里的历史数据越来越多, 占用空间大, 备份慢, 恢复慢, 查询少但是很费钱, 迁移慢 怎么办? 冷热分离方案: - 使用PostgreSQL 或者 PolarDB-PG 存成parquet文件格式, 放到aliyun OSS存储里面. 使用duckdb_fdw对parquet文件进行查询. - duckdb 存储元数据(parquet 映射) 方案特点: - 内网oss不收取网络费用, 只收取存储费用, 非常便宜 - oss分几个档, 可以根据性能需求选择 - parquet为列存储, 一般历史数据的分析需求多,性能不错 - duckdb 支持 parquet下推过滤, 数据过滤性能不错
6877 6
PolarDB-PG | PostgreSQL + 阿里云OSS 实现高效低价的海量数据冷热存储分离
|
存储 对象存储 块存储
|
存储 关系型数据库 API
|
存储 关系型数据库 Linux
|
存储 SQL 缓存
PostgreSQL 内核解读系列 - 第4节 存储管理(下)
本文整理自阿里云数据库开源社区 Maintainer 于巍(花名漠雪),在PostgreSQL数据库内核解读系列的分享。本篇内容主要分为四个部分: 1. 磁盘管理 2. 空闲空间映射表(FSM) 3. 可见性映射表(VM) 4. 内存管理。
PostgreSQL 内核解读系列 - 第4节 存储管理(下)
|
存储 关系型数据库 分布式数据库
直播预告 | PolarDB for PostgreSQL - 共享存储在线扩容
随着业务的发展,当数据库的存储容量不能满足数据规模的增长时,需要对存储空间进行扩容,此过程中会因数据迁移而导致服务中断。本期开源学堂将演示对部署在共享存储上的 PolarDB-PG 集群进行不中断业务的在线扩容,同时简析部署流程背后的技术原理。
 直播预告 | PolarDB for PostgreSQL - 共享存储在线扩容
|
存储 SQL 关系型数据库
关于PostgreSQL数据的存储,你有必要有所了解
关于PostgreSQL数据的存储,你有必要有所了解
479 0
关于PostgreSQL数据的存储,你有必要有所了解