当前位置: 首页 > news >正文

PostgreSQL checksum

在计算机系统中,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

  • https://paquier.xyz/postgresql-2/postgres-9-3-feature-highlight-data-checksums/
  • https://en.wikipedia.org/wiki/Checksum
  • https://www.postgresql.org/docs/11/pgverifychecksums.html

相关文章:

  • 关于在vim中的查找和替换
  • Vuex的初探与实战小结
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 表格单元格td设置宽度无效的解决办法
  • python基础-数据类型
  • 【FPGA】Xilinx-7系的时钟资源与DDR3配置
  • 谈项目中如何选择框架和库(FEDAY主题分享总结)
  • 如何做线段绕着点旋转一定角度的动画
  • python写商品管理练习
  • React组件设计模式(一)
  • 【技能意志矩阵-skill will matrix】工作中究竟是个人能力更重要,还是我们的积极性更能提高我们的业绩?...
  • Kubernetes-架构路线图
  • libevent的入门学习-库的安装【转】
  • swift - UIWebView 和 WKWebView(iOS12 之后替换UIWebView)
  • jmeter聚合报告详解
  • “Material Design”设计规范在 ComponentOne For WinForm 的全新尝试!
  • eclipse的离线汉化
  • E-HPC支持多队列管理和自动伸缩
  • el-input获取焦点 input输入框为空时高亮 el-input值非法时
  • es6
  • exports和module.exports
  • interface和setter,getter
  • JS实现简单的MVC模式开发小游戏
  • Markdown 语法简单说明
  • React Native移动开发实战-3-实现页面间的数据传递
  • Three.js 再探 - 写一个跳一跳极简版游戏
  • 从0搭建SpringBoot的HelloWorld -- Java版本
  • 对象引论
  • 高程读书笔记 第六章 面向对象程序设计
  • 聊聊redis的数据结构的应用
  • 想使用 MongoDB ,你应该了解这8个方面!
  • 在Unity中实现一个简单的消息管理器
  • ​secrets --- 生成管理密码的安全随机数​
  • #我与Java虚拟机的故事#连载10: 如何在阿里、腾讯、百度、及字节跳动等公司面试中脱颖而出...
  • (04)odoo视图操作
  • (06)Hive——正则表达式
  • (3)STL算法之搜索
  • (附源码)ssm旅游企业财务管理系统 毕业设计 102100
  • (一)【Jmeter】JDK及Jmeter的安装部署及简单配置
  • (转)ABI是什么
  • (转)mysql使用Navicat 导出和导入数据库
  • .gitignore文件_Git:.gitignore
  • .Net Remoting常用部署结构
  • .net Signalr 使用笔记
  • .NET(C#、VB)APP开发——Smobiler平台控件介绍:Bluetooth组件
  • .NET/C# 使用 ConditionalWeakTable 附加字段(CLR 版本的附加属性,也可用用来当作弱引用字典 WeakDictionary)
  • /etc/fstab 只读无法修改的解决办法
  • @ 代码随想录算法训练营第8周(C语言)|Day57(动态规划)
  • @AutoConfigurationPackage的使用
  • [AIGC] SQL中的数据添加和操作:数据类型介绍
  • [BZOJ1010] [HNOI2008] 玩具装箱toy (斜率优化)
  • [C++][基础]1_变量、常量和基本类型
  • [c语言]小课堂 day2
  • [EFI]Atermiter X99 Turbo D4 E5-2630v3电脑 Hackintosh 黑苹果efi引导文件
  • [ESP32 IDF]web server