PostgreSQL checksum与Data Corruption

存储介质故障指的是读取或写入数据的存储介质出现的物理上的故障。这是DBA不得不重点关注的问题之一。

一个典型的介质故障就是磁盘的磁盘头损坏,这会导致磁盘中存储的所有文件丢失。与数据库关联的文件例如数据文件、wal日志文件和控制文件都会因为磁盘崩溃而损坏。

这篇文章我们将重点介绍在PostgreSQL中遇到磁盘故障后,我们该如何在故障后找回数据(并不是还原备份)。

1、PostgreSQL checksum概述

  • 第1章:什么是checksum?
  • 第2章:PostgreSQL checksum:实际实现
  • 第3章:如何解决PostgreSQL损坏的页面问题?

在PostgreSQL9.3中引入了checksum,此后发生了许多变化。现在,我们在PostgreSQL12中有了一个完善的视图,可以通过pg_checksums的来获取关于checksum的详细信息。

1.1、什么是checksum?

启用checksum后,会将一个小的整数校验和写入Postgres存储在硬盘驱动器上的每个“page”的数据中。读取该块后,将重新计算校验和值并将其与存储的值进行比较。

这可以检测到数据损坏,而这些数据损坏(没有checksum)可能长时间潜伏在数据库中。

因此我们可以知道:使用checksum可以检验数据块的损坏。

1.2、checksum如何工作?

PostgreSQL主要在进出缓冲区缓存的过程中维护页面有效性。

从这里我们可以理解,PostgreSQL页面在离开或进入共享缓冲区之前必须经过OS Cache。因此,页面有效性发生在离开共享缓冲区之前和进入共享缓冲区之前。

当PostgreSQL尝试将页面复制到其缓冲区高速缓存中时,它将(如果可能)检测到错误,并且将不允许页面使用无效的8k页面进入共享缓冲区,并弹出报错需要该页面的所有查询用于处理错误消息:

ERROR: invalid page in block 0 of relation base/13455/16395

如果您已经有一个块,而该块在磁盘中具有无效数据,而其page在缓冲区中,则在下一个检查点期间,当其page被调出时,它将更新无效的校验和详细信息,但是在实时环境中很少这样做。

如果无效数据是PostgreSQL数据库缓冲区高速缓存的一部分,则PostgreSQL将会假设没有错误,并尝试处理页面上的数据。结果不可预测;有时您会得到一个错误,有时您可能会得到错误的数据。

1.3、PostgreSQL如何检查页面有效性?

在典型的页面中,如果启用了数据校验和,则信息将存储在2个字节的字段中,该字段包含页面标头之后的标志位。

随后是三个2字节整数字段(pd_lower,pd_upper和pd_special)。它们包含从页面开始到未分配空间的开头,到未分配空间的结尾以及到特殊空间的开头的字节偏移。

checksum的值通常以零开始,并且每次读取该块时,都会重新计算校验和值并将其与存储的值进行比较。这将检测到数据损坏。

当块位于共享缓冲区中时,不会为它们维护checksum。因此,如果使用pageinspect查看PostgreSQL页面缓存中的缓冲区,并且看到校验和值,请注意,当您在已存在于缓冲区中的页面上进行检查时,您可能无法获得实际的checksum值。当页面从缓冲区高速缓存中写出到操作系统页面高速缓存中时,将计算checksum值并将其标记在页面上。

2、checksum使用实例

这里创建一张名为check_corruption的表,然后进行以下操作。

表的大小是8 kB。
有5条记录。
使用的版本是PostgreSQL v12。

postgres=# select * from check_corruption;
aid | bid | abalance | filler
-----+-----+----------+--------------------------------------------------------------------------------------
1 | 1 | 0 | This is checksum example, checksum is for computing block corruption
2 | 1 | 0 | This is checksum example, checksum is for computing block corruption
3 | 1 | 0 | This is checksum example, checksum is for computing block corruption
4 | 1 | 0 | This is checksum example, checksum is for computing block corruption
5 | 1 | 0 | This is checksum example, checksum is for computing block corruption
(5 rows)
 
postgres=# SELECT * FROM page_header(get_raw_page('check_corruption',0));
    lsn    | checksum | flags | lower | upper | special | pagesize | version | prune_xid
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
 0/17EFCA0 |        0 |     0 |    44 |  7552 |    8192 |     8192 |       4 |         0
(1 row)
 
postgres=# \dt+ check_corruption
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------------------+-------+----------+------------+-------------
public | check_corruption | table | postgres | 8192 bytes |
(1 row)
 
postgres=# select pg_relation_filepath('check_corruption');
 pg_relation_filepath
----------------------
 base/13455/16490
(1 row)

2.1、检查是否启用checksum

[postgres@stagdb ~]$ pg_controldata -D /u01/pgsql/data | grep checksum
Data page checksum version: 0

可以看到checksum被禁用了。

2.1.1、让我们在启用页面checksum
语法:

pg_checksums -D /u01/pgsql/dataenable –progress –verbose

[postgres@stagdb ~]$ pg_checksums -D /u01/pgsql/data --enable --progress --verbose
pg_checksums: checksums enabled in file "/u01/pgsql/data/global/2847"
pg_checksums: checksums enabled in file "/u01/pgsql/data/global/1260_fsm"
pg_checksums: checksums enabled in file "/u01/pgsql/data/global/4175"
 
..
..
 
23/23 MB (100%) computed
Checksum operation completed
Files scanned: 969
Blocks scanned: 3006
pg_checksums: syncing data directory
 
pg_checksums: updating control file
Data checksum version: 1
Checksums enabled in cluster

再检查是否启用checksum:
[postgres@stagdb ~]$ pg_controldata -D /u01/pgsql/data | grep checksum
Data page checksum version: 1

我们可以使用–disable选项禁用校验和:
[postgres@stagdb ~]$
[postgres@stagdb ~]$ pg_checksums -D /u01/pgsql/data --disable
pg_checksums: syncing data directory
pg_checksums: updating control file
Checksums disabled in cluster

我们首先检查当前数据目录中是否有错误,然后再处理数据。

2.1.2、要检查PostgreSQL页面错误,我们使用以下命令
pg_checksums -D /u01/pgsql/data –check

[postgres@stagdb ~]$ pg_checksums -D /u01/pgsql/data –check
Checksum operation completed
Files scanned: 969
Blocks scanned: 3006
Bad checksums: 0
Data checksum version: 1
[postgres@stagdb ~]$

警告:不要在生产环境进行以下的操作去研究!

由于表check_corruption数据文件为16490,因此我将使用操作系统的dd命令来破坏该文件。

dd bs=8192 count=1 seek=1 of=16490 if=16490

[postgres@stagdb 13455]$ dd bs=8192 count=1 seek=1 of=16490 if=16490

再次查看该表数据:

postgres=# select * from check_corruption;
aid | bid | abalance | filler
-----+-----+----------+--------------------------------------------------------------------------------------
1 | 1 | 0 | This is checksum example, checksum is for computing block corruption
2 | 1 | 0 | This is checksum example, checksum is for computing block corruption
3 | 1 | 0 | This is checksum example, checksum is for computing block corruption
4 | 1 | 0 | This is checksum example, checksum is for computing block corruption
5 | 1 | 0 | This is checksum example, checksum is for computing block corruption
(5 rows)

我们得到了上面的结果,这是为什么呢?

我从共享缓冲区得到了结果。让我重新启动集群并获取相同的集群。

/usr/local/pgsql/bin/pg_ctl restart -D /u01/pgsql/data
postgres=# select * from check_corruption;
aid | bid | abalance | filler
-----+-----+----------+--------------------------------------------------------------------------------------
1 | 1 | 0 | This is checksum example, checksum is for computing block corruption
2 | 1 | 0 | This is checksum example, checksum is for computing block corruption
3 | 1 | 0 | This is checksum example, checksum is for computing block corruption
4 | 1 | 0 | This is checksum example, checksum is for computing block corruption
5 | 1 | 0 | This is checksum example, checksum is for computing block corruption
(5 rows)

又是一样的值,为什么呢?

如前所述,在重新启动期间,PostgreSQL用共享缓冲区的值替换了错误checksum值。

我们如何触发checksum警告?

我们需要从共享缓冲区中删除该行。在此测试方案中,最快的方法是重新启动数据库,然后确保在进行磁盘上修改之前,我们甚至没有查看(如SELECT)表。完成后,checksum值将失败,并且我们将按预期接收checksum错误:

停止数据库服务,损坏磁盘数据,然后重启:
/usr/local/pgsql/bin/pg_ctl stop -D /u01/pgsql/data

dd bs=8192 count=1 seek=1 of=16490 if=16490

/usr/local/pgsql/bin/pg_ctl start -D /u01/pgsql/data

下一次查看时,我得到了下列报错:

postgres=# select * from check_corruption;
2020-02-06 19:06:17.433 IST [25218] WARNING: page verification failed, calculated checksum 39428 but expected 39427
WARNING: page verification failed, calculated checksum 39428 but expected 39427
2020-02-06 19:06:17.434 IST [25218] ERROR: invalid page in block 1 of relation base/13455/16490
2020-02-06 19:06:17.434 IST [25218] STATEMENT: select * from check_corruption;
ERROR: invalid page in block 1 of relation base/13455/16490

2.1.3、让我们更深入地研究问题并确认该数据块已损坏
您可以通过两种方法来查找问题,其中包括Linux命令,例如
dd
od
hexdump

使用dd命令:

dd if=16490 bs=8192 count=1 skip=1 | od -A d -t x1z -w16 | head -1

[postgres@stagdb 13455]$ dd if=16490 bs=8192 count=1 skip=1 | od -A d -t x1z -w16 | head -2
1+0 records in
1+0 records out
8192 bytes (8.2 kB) copied, 4.5e-05 seconds, 182 MB/s
0000000 00 00 00 00 a0 fc 7e 01 03 9a 00 00 2c 00 80 1d >……~……,…<

00 00 00 00 a0 fc 7e 01前8个字节为pd_lsn,后两个字节为03 9a为checksum值。

Using hexdump : hexdump -C 16490 | head -1

[postgres@stagdb 13455]$ hexdump -C 16490 | head -1
00000000 00 00 00 00 a0 fc 7e 01 03 9a 00 00 2c 00 80 1d |……~……,…|

hexdump和dd返回相同的结果。

让我们了解一下我们的PostgreSQL自己的pg_checksums怎么说?

pg_checksums -D /u01/pgsql/data –check

[postgres@stagdb 13455]$ pg_checksums -D /u01/pgsql/data –check
pg_checksums: error: checksum verification failed in file “/u01/pgsql/data/base/13455/16490”, block 1: calculated checksum 9A04 but block contains 9A03
Checksum operation completed
Files scanned: 968
Blocks scanned: 3013
Bad checksums: 1
Data checksum version: 1

在这里,根据pg_checksums,校验和9A03与hexdump的校验和9A03相匹配。

将十六进制9A03转换为十进制,我得到了39427。

这刚好和前面的报错相匹配:

2020-02-06 19:06:17.433 IST [25218] WARNING: page verification failed, calculated checksum 39428 but expected 39427

3、如何解决PostgreSQL损坏页面的问题?

使用以下函数查找页面损坏的确切位置。

CREATE OR REPLACE FUNCTION
find_bad_row(tableName TEXT)
RETURNS tid
as $find_bad_row$
DECLARE
result tid;
curs REFCURSOR;
row1 RECORD;
row2 RECORD;
tabName TEXT;
count BIGINT := 0;
BEGIN
SELECT reverse(split_part(reverse($1), '.', 1)) INTO tabName;
OPEN curs FOR EXECUTE 'SELECT ctid FROM ' || tableName;
count := 1;
FETCH curs INTO row1;
WHILE row1.ctid IS NOT NULL LOOP
result = row1.ctid;
count := count + 1;
FETCH curs INTO row1;
EXECUTE 'SELECT (each(hstore(' || tabName || '))).* FROM '
|| tableName || ' WHERE ctid = $1' INTO row2
USING row1.ctid;
IF count % 100000 = 0 THEN
RAISE NOTICE 'rows processed: %', count;
END IF;
END LOOP;
CLOSE curs;
RETURN row1.ctid;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'LAST CTID: %', result;
RAISE NOTICE '%: %', SQLSTATE, SQLERRM;
RETURN result;
END
$find_bad_row$
LANGUAGE plpgsql;

现在,使用find_bad_row()函数,您可以找到损坏位置的ctid。

使用该函数前需要安装hstore扩展。

postgres=# CREATE EXTENSION hstore;
CREATE EXTENSION
postgres=#
postgres=# select find_bad_row(‘check_corruption’);
2020-02-06 19:44:24.227 IST [25929] WARNING: page verification failed, calculated checksum 39428 but expected 39427
2020-02-06 19:44:24.227 IST [25929] CONTEXT: PL/pgSQL function find_bad_row(text) line 21 at FETCH
WARNING: page verification failed, calculated checksum 39428 but expected 39427
NOTICE: LAST CTID: (0,5)
NOTICE: XX001: invalid page in block 1 of relation base/13455/16490
find_bad_row
————–
(0,5)
(1 row)

删除特定的CTID将解决问题。

postgres=# delete from check_corruption where ctid='(0,6)’;
DELETE 1
postgres=#

如果删除ctid不适用于您,则您可以使用替代解决方案,该解决方案是设置zero_damaged_pa​​ges参数。

例如:

postgres=# select * from master;
WARNING: page verification failed, calculated checksum 8770 but expected 8769
ERROR: invalid page in block 1 of relation base/13455/16770
postgres=#

由于块已损坏,我无法从表主访问数据。

解决方法:

postgres=# SET zero_damaged_pages = on;
SET
postgres=# vacuum full master;

postgres=# select * from master;
WARNING: page verification failed, calculated checksum 8770 but expected 8769
WARNING: invalid page in block 1 of relation base/13455/16770; zeroing out page
id | name | city
----+---------+-----------
1 | Orson | hyderabad
2 | Colin | chennai
3 | Leonard | newyork

在这里,它清除了损坏的页面并给出了其余结果。

zero_damaged_pages (boolean):
检测到损坏的页眉通常会导致PostgreSQL报告错误,从而中止当前事务。将zero_damaged_pa​​ges设置为on会导致系统改为报告警告,将内存中的损坏页面清零,然后继续处理。此行为将破坏数据,即损坏页面上的所有行。但是,它确实允许您克服错误并从表中可能存在的任何未损坏页面中检索行。如果由于硬件或软件错误而发生损坏,它对于恢复数据很有用。通常,除非您已放弃从表的损坏页面中恢复数据的希望,否则不应该将其设置为on。调零的页面不会强制插入磁盘,因此建议在重新关闭此参数之前重新创建表或索引。默认设置为关闭,并且只能由超级用户更改。

但是,使用此功能时需要注意两点。首先,使用checksum会降低性能,因为它会为每个数据页引入额外的计算(默认为8kB),因此请注意在使用数据时在数据安全性和性能之间进行权衡。

启用checksum时,有许多因素会影响速度变慢,其中包括:

从shared_buffers读取数据的可能性有多高,这取决于设置了shared_buffers的大小以及内部有多少活动数据库。
您的服务器通常运行多快,以及它(以及您的编译器)如何优化校验和计算。
您有多少个数据页(可能受数据类型影响)。
您编写新页面的频率(通过COPY,INSERT或UPDATE)。
您多久读取一次值(通过SELECT)

共享缓冲区使用得越多(有效地使用它们是一个好的总体目标),checksum就越少,并且checksum对数据库性能的影响就越小。平均而言,如果启用checksum,则性能成本将超过2%,而对于插入,则平均差异为6%。对于选择,该值跳升到19%。完整的计算基准测试可在此处找到。

您可以在测试前后使用pg_filedump转储文件的内容,并且可以使用diff命令来分析数据损坏:

  1. pg_filedump -if 16770 > before_corrupt.txt
  2. corrupt the disk block
  3. pg_filedump -if 16770 > before_corrupt.txt
  4. diff or beyond compare both the files.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值