PostgreSQL checksum

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
简介: 在计算机系统中,[checksum](https://en.wikipedia.org/wiki/Checksum) 通常用于校验数据在传输或存取过程中是否发生错误。PostgreSQL 从 9.3 开始支持 checksum,以发现数据因磁盘、 I/O 损坏等原因造成的数据异常。

在计算机系统中,checksum 通常用于校验数据在传输或存取过程中是否发生错误。PostgreSQL 从 9.3 开始支持 checksum,以发现数据因磁盘、 I/O 损坏等原因造成的数据异常。本文介绍 PostgreSQL 中 checksum 的使用及其实现原理。

概述

PostgreSQL 从 9.3 开始支持数据页的 checksum,可以在执行 initdb 时指定 -k--data-checksums 参数开启 checksum,但开启 checksum 可能会对系统性能有一定影响,官网描述如下:

Use checksums on data pages to help detect corruption by the I/O system that would otherwise be silent. Enabling checksums may incur a noticeable performance penalty. This option can only be set during initialization, and cannot be changed later. If set, checksums are calculated for all objects, in all databases.

启用 checksum 后,系统会对每个数据页计算 checksum,从存储读取数据时如果检测 checksum 失败,则会发生错误并终止当前正在执行的事务,该功能使得 PostgreSQL 自身拥有了检测 I/O 或硬件错误的能力。

Checksum 引入一个 GUC 参数 ignore_checksum_failure,该参数若设置为 true,checksum 校验失败后不会产生错误,而是给客户端发送一个警告。当然,checksum 失败意味着磁盘上的数据已经损坏,忽略此类错误可能导致数据损坏扩散甚至导致系统奔溃,此时宜尽早修复,因此,若开启 checksum,该参数建议设置为 false

实现原理

设置 checksum

数据页的 checksum 在从 Buffer pool 刷到存储时才设置,当页面再此读取至 Buffer pool 时进行检测。

PostgreSQL 中 Buffer 刷盘的逻辑集中在 FlushBuffer 中,其中设置 checksum 的逻辑如下:

/*
 * Update page checksum if desired.  Since we have only shared lock on the
 * buffer, other processes might be updating hint bits in it, so we must
 * copy the page to private storage if we do checksumming.
 */
bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);

比较有意思的是,其他进程可能会在只加 content-lock 共享锁的情况下并发修改 page 的 Hint Bits,从而导致 checksum 值发生变化,为确保 page 的内容及其 checksum 保持一致,PostgreSQL 采用了 先复制页,然后计算 checksum 的方式,如下:

/*
 * We allocate the copy space once and use it over on each subsequent
 * call.  The point of palloc'ing here, rather than having a static char
 * array, is first to ensure adequate alignment for the checksumming code
 * and second to avoid wasting space in processes that never call this.
 */
if (pageCopy == NULL)
    pageCopy = MemoryContextAlloc(TopMemoryContext, BLCKSZ);

memcpy(pageCopy, (char *) page, BLCKSZ);
((PageHeader) pageCopy)->pd_checksum = pg_checksum_page(pageCopy, blkno);

即先将数据页的内容拷贝一份,拷贝的数据自然不会被其他进程修改,然后基于该拷贝页计算并设置 checksum 值。

checksum 算法

数据页的 checksum 算法基于 FNV-1a hash 改造而来,其结果为 32 位无符号整型。由于 PageHeaderDatapd_checksum 是 16 位无符号整型,因此将其截取 16 位作为数据页的 checksum 值,如下:

/*
 * Save pd_checksum and temporarily set it to zero, so that the checksum
 * calculation isn't affected by the old checksum stored on the page.
 * Restore it after, because actually updating the checksum is NOT part of
 * the API of this function.
 */
save_checksum = cpage->phdr.pd_checksum;
cpage->phdr.pd_checksum = 0;
checksum = pg_checksum_block(cpage);
cpage->phdr.pd_checksum = save_checksum;

/* Mix in the block number to detect transposed pages */
checksum ^= blkno;

/*
 * Reduce to a uint16 (to fit in the pd_checksum field) with an offset of
 * one. That avoids checksums of zero, which seems like a good idea.
 */
return (checksum % 65535) + 1;

pg_checksum_block 函数计算数据页的 32 位 checksum 值,具体算法可以参考源码,在此不详述。

检测 checksum

PostgreSQL 会在页面从存储读入内存时检测其是否可用,调用函数为 PageIsVerified,该函数不仅会检测正常初始化过的页(non-zero page),还会检测 全零页(all-zero page)

为什么会出现 全零页 呢?
在特定场景下表中可能出现 全零页,比如有进程扩展了一个表,即在该表中添加了一个新页,但在 WAL 日志写入存储之前,进程崩溃了。此时新加的页可能已经在表文件中,下次重启时就会读取到。

对于 non-zero page,检测其 checksum 是否一致以及 page header 信息是否正确,若 checksum 失败,但 header 信息正确,此时会根据 ignore_checksum_failure 值判断验证是否通过;对于 all-zero page,如果为全零,则验证通过。

若验证失败,两种处理方式:

  • 若读取数据的模式为 RBM_ZERO_ON_ERROR 且 GUC 参数 zero_damaged_pages 为 true,则将该页全部置 0
  • 报错,invalid page

checksum 与 Hint bits

数据页写至存储时,如果写失败,可能会导致破碎的页(torn page),PostgreSQL 通过 full_page_writes 特性解决此类写失败导致数据不可用的问题。

Hint Bits 是数据页中用于标识事务状态的标记位,一般情况下,作为提示位,不是很重要。但如果使用了 checksum,Hint Bits 的变化会导致 checksum 值发生改变。设想如果一个页面发生部分写,恰好把某些 Hint Bits 写错,此页面可能并不影响正常使用,但 checksum 会抛出异常,此时应如何恢复呢?

在 checksum 的实现中,checkpoint 后,如果页面因更新 Hint Bits 第一次被标记为 dirty,需要记录一个 Full Page Image 至 WAL 日志中,以应对以上提到的因 Hint Bits 更新丢失导致 checksum 失败的问题,具体实现可参考 MarkBufferDirtyHint。对于已经是 dirty 的页,更新 Hint Bits 则不需要记录 WAL 日志,因为在 checkpoint 后,第一次将该页标记为 dirty 时已经写入了对应的 Full Page Image

可见,在启用 checksum 的情况下,checkpoint 后页面的第一次修改如果是更新 Hint Bits, 会写 Full Page Image 至 WAL 日志,这会导致 WAL 日志占用更多的存储空间。

关于 PostgreSQL checksum 和 Full Page Image 的关系,可以参考 stackoverflow 上这个问题

查看 checksum

PostgreSQL 10 在 pageinspect 插件中添加了函数 page_checksum() 用来查看 page 的 checksum,当然使用 page_header() 也可以查看 page 的 checksum,如下:

postgres=# SELECT page_checksum(get_raw_page('pg_class', 0), 0);
 page_checksum
---------------
         17448
(1 row)

postgres=# SELECT * FROM page_header(get_raw_page('pg_class', 0));
    lsn     | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
 0/78A1E918 |    17448 |     0 |   200 |   368 |    8192 |     8192 |       4 |         0
(1 row)

总结

Checksum 使 PostgreSQL 具备检测因硬件故障或传输导致数据不一致的能力,一旦发生异常,通常会报错并终止当前事务,用户可以尽早察觉数据异常并予以恢复。当然,开启 checksum 也会引入一些开销,体现在两个方面:

  • 计算数据页的 checksum 会引入一些 CPU 开销,具体开销取决于 checksum 算法的效率
  • checkpoint 后,若因更新 Hint Bits 将页面第一次置为 dirty 会写一条记录 Full Page Image 的 WAL 日志,以用于恢复因更新 Hint Bits 产生的破碎页。

对于数据可用性要求较高的场景,通常建议将 full_page_writes 和 checksum 都打开,前者用于避免写失败导致的数据缺失,后者用于尽早发现因硬件或传输导致数据不一致的场景,一旦发现,可以利用 full_page_writes 和 checksum 记录在 WAL 日志中的 Full Page Image 进行数据恢复。

References

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
5月前
|
存储 关系型数据库 数据库
PostgreSQL孤儿文件
与所有其他关系数据库系统一样,PostgreSQL需要通过写入wal日志或在Checkpoint时同步数据到数据文件来持久化数据到磁盘上。对于数据文件,一旦Relation达到SEGMENT_SIZE(默认1GB),PostgreSQL就会创建一个新的数据文件。因此如果Relation持续增长,则该Relation可能会由多个文件组成。在这篇文章中想要考虑的问题是,是否可能存在孤儿文件。
1108 2
|
关系型数据库 数据库 PostgreSQL
PostgreSQL 11 新特性解读 : Initdb/Pg_resetwal支持修改WAL文件大小
PostgreSQL 11 版本的一个重要调整是支持 initdb 和 pg_resetwal 修改 WAL 文件大小,而 11 版本之前只能在编译安装 PostgreSQL 时设置 WAL 文件大小。
8710 0
|
9月前
|
关系型数据库 PostgreSQL
PostgreSQL pg_resetwal处理机制
PostgreSQL pg_resetwal处理机制
83 0
|
9月前
|
关系型数据库 数据库 PostgreSQL
PostgreSQL pg_rewind报错分析
PostgreSQL pg_rewind报错分析
98 0
LXJ
|
安全 关系型数据库 Linux
PostgreSQL通过pg_upgrade进行大版本升级
PostgreSQL通过pg_upgrade进行大版本升级
LXJ
2471 0
|
关系型数据库 PostgreSQL
postgresql 的WAL日志解析工具 pg_waldump
postgresql 的WAL日志解析工具 pg_waldump
1271 0
postgresql 的WAL日志解析工具 pg_waldump
|
SQL 关系型数据库 数据库
PostgreSQL 服务器日志 pg_log
10.0版本PostgreSQL,存在三种日志 WAL日志,即重做日志,一般不可读 日志对应目录为 $PGDATA/pg_xlog 事务提交日志,记录的是事务的元数据 日志对应目录为 $PGDATA/pg_clog 数据库运行日志 日志对应目录为$PGDATA/pg_log 前两种日志,虽然仍然非常重要,但却是不可读的,我们日常使用不多。
6921 0
|
关系型数据库 数据库 PostgreSQL
PostgreSQL pg_basebackup
本文探讨 pg_basebackup工具,本地备份、远程备份、单一表空间本地数据库的备份、表空间重定向
1828 0
PostgreSQL pg_basebackup
|
SQL 存储 Oracle
PostgreSQL WAL解析与闪回的一些想法
最近在walminer基础做了不少修改,以支持我们的使用场景。详细参考 如何在PostgreSQL故障切换后找回丢失的数据 修改也花了不少精力和时间,这个过程中有些东西想记录下来,方便以后查阅。
1488 0
|
关系型数据库 数据库 PostgreSQL
PostgreSQL 10.0 的三种日志
当前使用版本为PostgreSQL [postgres@localhost ~]$ psql psql (10.7) 网络上还存在大量的帖子,关于pg_log,xlog,clog刚刚接触PG的我一直没有找到这些目录,查资料发现,从PG 10.
3593 0