前言 关于数据恢复,笔者也写过很多期了,大致思路便是
如果数据还没有被清理,可以用 pg_dirtyread /pg_recovery /pg_resetwal /pageinspect 这类工具进行抢救
如果数据已经被清理,可以用 walminer /PITR/延迟备库(删除的数据还在延迟窗口内) 等方式进行抢救
某些国产库,实现了类似 Oracle 的闪回查询,基于闪回也可以进行抢救
审计日志类
…
今天和各位再分享一个 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 条数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 postgres= # create table test_fpi(id int ,info char (2000 )) with (fillfactor = 50 ); CREATE TABLE postgres= # insert into test_fpi values (1 ,'A' ),(2 ,'B' ),(3 ,'C' ); INSERT 0 3 postgres= # analyze test_fpi; ANALYZE postgres= # 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 个数据块,为了测试目的,手动执行一下检查点。
1 2 3 4 5 6 postgres= # checkpoint ; CHECKPOINT postgres= # delete from test_fpi where id = 2 ; DELETE 1 postgres= # vacuum ; VACUUM
这样的话,更新语句变为记录 FPW 到 WAL 日志中。现在,让我们提取一下 FPI
1 2 3 4 5 6 7 8 9 10 11 [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 记录,接着创建一个镜像表,用于恢复
1 2 3 4 5 6 7 postgres= # create table test_restore(like test_fpi including all ); CREATE TABLE postgres= # select pg_relation_filepath('test_restore' ); pg_relation_filepath base/ 5 / 16526 (1 row )
恢复方式很简单,直接 cat 即可,此处只恢复第一个数据块
1 2 3 4 5 6 7 8 9 10 11 12 13 [postgres@mypg pg_wal]$ cat FPI/00000001-00000000-0A00A3F0.1663.5.16521.0_main > ../base/5/16526 postgres=# select count(*) 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 中也包含着数据
1 2 3 4 5 6 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 | \x01000000501f000041202020202020 2 | 836 | (0 ,2 ) | XMAX_COMMITTED| XMIN_COMMITTED| HASVARWIDTH | \x02000000501f000042202020202020 (2 rows )
只不过其 xmax 是 committed 的状态,正常查询是不可见的,这个时候,我们就可以利用 pg_dirtyread 等工具将其读取出来即可 👇🏻
1 2 3 4 5 6 7 8 9 10 11 12 13 postgres= # select id,left (info,10 ) from pg_dirtyread('test_restore' ) as ab(id int ,info char (2000 )); id | left 1 | A 2 | B (2 rows ) postgres= # select id,left (info,10 ) from pg_dirtyread('test_fpi' ) as ab(id int ,info char (2000 )); id | left 1 | A 3 | C (2 rows )
如果后续还有其他需要,可以使用 cat >> 追加的方式继续恢复其他数据块,如果不小心覆盖了,也没关系,毕竟是幂等的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 postgres=# select id ,left(info,10) from pg_dirtyread('test_restore' ) as ab(id int,info char(2000)); id | left ----+------ 1 | A 2 | B 3 | C (3 rows) postgres=# select id ,left(info,10) from pg_dirtyread('test_fpi' ) as ab(id int,info char(2000)); id | left ----+------ 1 | A 3 | 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