这种数据恢复方式,我敢打赌,99%的人没有见过!
前言
关于数据恢复,笔者也写过很多期了,大致思路便是
1.如果数据还没有被清理,可以用 pg_dirtyread/pg_recovery/pg_resetwal/pageinspect 这类工具进行抢救2.如果数据已经被清理,可以用 walminer/PITR/延迟备库(删除的数据还在延迟窗口内) 等方式进行抢救3.某些国产库,实现了类似 Oracle 的闪回查询,基于闪回也可以进行抢救4.审计日志类5....
今天和各位再分享一个 16 版本中一个鲜为人知的技巧:pg_waldump: Add --save-fullpage=PATH to save full page images from WAL records,即使你的数据被 VACUUM 清理了,依然有可能抢救回来!
This is a tool aimed mostly for very experienced users, useful for fixing page-level corruption or just analyzing the past state of a page, and there were no easy way to do that with the in-core tools up to now when looking at WAL.
没错,基于全页写。
小试牛刀
由于是基于全页写的原理来实现的,因此需要打开 full_page_writes 参数。让我们构建一个测试用例,同时利用 fillfactor,使得一个数据块 (默认 8KB) 内只可以容纳 2 条数据:
postgres=# createtable test_fpi(id int,info char(2000)) with (fillfactor =50);CREATETABLEpostgres=# insertinto test_fpi values(1,'A'),(2,'B'),(3,'C');INSERT03postgres=# analyze test_fpi;ANALYZEpostgres=# select relpages,reltuples,relname from pg_class where relname ='test_fpi';relpages | reltuples | relname----------+-----------+----------2 | 3 | test_fpi(1 row)postgres=# select pg_relation_filepath('test_fpi');pg_relation_filepath----------------------base/5/16521(1 row)
可以看到,现在 3 条数据占了 2 个数据块,为了测试目的,手动执行一下检查点。
postgres=# checkpoint ;CHECKPOINTpostgres=# deletefrom test_fpi where id =2;DELETE1postgres=# vacuum ;VACUUM
这样的话,更新语句变为记录 FPW 到 WAL 日志中。现在,让我们提取一下 FPI
[postgres@mypg pg_wal]$ pg_waldump --save-fullpage=FPI ./00000001000000000000000A...[postgres@mypg pg_wal]$ ls -lrth FPI/ | grep 16521-rw-rw-r-- 1 postgres postgres 8.0K Jan 5 17:58 00000001-00000000-0A00A3F0.1663.5.16521.0_main-rw-rw-r-- 1 postgres postgres 8.0K Jan 5 17:58 00000001-00000000-0A003AE0.1663.5.16521.2_fsm-rw-rw-r-- 1 postgres postgres 8.0K Jan 5 17:58 00000001-00000000-0A012760.1663.5.16521.0_fsm-rw-rw-r-- 1 postgres postgres 8.0K Jan 5 17:58 00000001-00000000-0A010710.1663.5.16521.1_fsm-rw-rw-r-- 1 postgres postgres 8.0K Jan 5 17:58 00000001-00000000-0A00FEA8.1663.5.16521.1_main-rw-rw-r-- 1 postgres postgres 8.0K Jan 5 17:58 00000001-00000000-0A00DE58.1663.5.16521.2_fsm-rw-rw-r-- 1 postgres postgres 8.0K Jan 5 17:58 00000001-00000000-0A00BE00.1663.5.16521.0_vm
这些文件的命名方式是:TIMELINE-LSN + RELTABLESPACE + DATOID + RELNODE + BLKNO_FORK,因此我们只需要根据表的 OID 来过滤即可。
现在,我们有了该表的 FPI 记录,接着创建一个镜像表,用于恢复
postgres=# createtable test_restore(like test_fpi including all);CREATETABLEpostgres=# select pg_relation_filepath('test_restore');pg_relation_filepath----------------------base/5/16526(1 row)
恢复方式很简单,直接 cat 即可,此处只恢复第一个数据块
[postgres@mypg pg_wal]$ cat FPI/00000001-00000000-0A00A3F0.1663.5.16521.0_main > ../base/5/16526postgres=# selectcount(*) from test_restore ;count-------1(1 row)postgres=# select id,left(info,10) from test_restore ;id | left----+------1 | A(1 row)
由于 FPI 是记录的全页数据,即使被 vacuum 了,WAL 中也包含着数据
postgres=# SELECT lp,t_xmin,t_ctid,infomask(t_infomask, 1) as infomask,left(t_data::text,32) FROM heap_page_items(get_raw_page('test_restore', 0));lp | t_xmin | t_ctid | infomask |left----+--------+--------+-------------------------------------------+----------------------------------1 | 836 | (0,1) | XMAX_INVALID|XMIN_COMMITTED|HASVARWIDTH | \x01000000501f0000412020202020202 | 836 | (0,2) | XMAX_COMMITTED|XMIN_COMMITTED|HASVARWIDTH | \x02000000501f000042202020202020(2 rows)
只不过其 xmax 是 committed 的状态,正常查询是不可见的,这个时候,我们就可以利用 pg_dirtyread 等工具将其读取出来即可 👇🏻
postgres=# select id,left(info,10) from pg_dirtyread('test_restore') as ab(id int,info char(2000));id |left----+------1 | A2 | B(2 rows)postgres=# select id,left(info,10) from pg_dirtyread('test_fpi') as ab(id int,info char(2000));id | left----+------1 | A3 | C(2 rows)
如果后续还有其他需要,可以使用 cat >> 追加的方式继续恢复其他数据块,如果不小心覆盖了,也没关系,毕竟是幂等的。
postgres=# select id,left(info,10) from pg_dirtyread('test_restore') as ab(id int,info char(2000));id |left----+------1 | A2 | B3 | C(3 rows)postgres=# select id,left(info,10) from pg_dirtyread('test_fpi') as ab(id int,info char(2000));id | left----+------1 | A3 | C(2 rows)
小结
这种恢复方式需要对 PostgreSQL 有足够的了解,并且恢复方式也比较 tricky,为了演示目的,我也使用了一个比较 toy 的方式,实际情况中,可能还有 FPI FOR Hint (wal_log_hints 或者 checksum),页剪枝等等的影响,再加上 TOAST,因此恢复并不是一件简单的事情,做好备份是重中之重。
但是这种方式,可以给我们一个新的选择,当数据被清理了,后知后觉想要抢救的时候,或许也能救你一命,毕竟能抢救一些是一些。
参考
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=d497093cbecccf6df26365e06a5f8f8614b591c8[1]
References
[1]: https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=d497093cbecccf6df26365e06a5f8f8614b591c8