PostgreSQL学徒

数据被删了干着急?继walminer之后另一款国货之光!

在公众号菜单中点击“PDU下载”或关注公众号回复“下载”,均可获取PDU的下载链接

前言

在Postgresql数据库中,被删除的数据会以死亡元组的形式留存一段时间,我们称之为“脏数据”,通过脏数据的读取,再加以一些筛选的工作,可以实现恢复删除数据的目的。

但如果时间过了太久,或者删除的数据条数达到了一定的阈值,那么等到客户找到我们的时候,这些脏数据大概率已经被vacuum清空,是物理意义上的不存在了;那谁来看了都只能尴尬一笑。

因此包括PDU工具在内的PG数据库delete恢复工具,都会选择另外一条路径来实现删除数据的恢复,这种方式不受到任何时间空间的约束,只要删除数据期间的WAL文件依然留存,即使过了一个月,数据也可以原样恢复;本文将会针对这条恢复路径,进行详细的原理说明。

基础概念:

1、FPW(Full Page Write)-数据库在检查点(checkpoint)后对某个数据页的 初次 修改,会将该页完整写入这条修改的wal记录中。

核心实现原理

  1. 在我们使用delete删除数据,或是用update更新数据时,被更新 或 被删除 的数据页会以FPW的形态留存在wal记录中

  2. 通过FPW的读取+wal记录重做,就可以将数据块还原到 删除 或 更新 时的时间点。

  3. 利用重做后的数据块。从需要恢复的delete类型wal记录中定位到数据在数据块中的具体偏移量,最后以某种方式读取出这些定位到的数据。

获取FPW

这种恢复方式的本质逻辑,就是wal文件中天然替我们保存了一份完整的数据,就看怎么把它拿出来,因此delete恢复的起始点必须是FPW的读取。

Postgresql的表数据文件本质上就是N个8k数据页的集合,而一个FPW中就存储n个(大部分场景下是1个)相关操作的数据页。为了让大家更直观地了解FPW,我做一个简单的测试。

1、首先创建测试表xman,插入4条数据后执行一次checkpoint,然后进行3次更新,最后删除一条数据。

drop table xman;create table xman (a int,b varchar);insert into xman values(1,'sdfghgfdsadfghgfds');insert into xman values(2,'wertyuijhbhghgtgvyhjujkgfdswerfcdsw');insert into xman values(3,'qwertyuiopoiufdfghjkjhvcvbnbvcfgfcfgdxfcd');insert into xman values(4,'wsxdedcfrfgbvgyhnujmjikm,kl,lp;.llkjhghjhgfg');checkpoint;update xman set b='aaaaaaaa' where a=1;update xman set b='bbbbbbbb' where a=2;update xman set b='cccccccc' where a=3;delete from xman where a=1;

2、查询表的文件名。

[15pg@node1 ~]$ psql alldbpsql (15.10)Type "help" for help.
alldb=# select relfilenode from pg_class where relname='xman'; relfilenode-------------      602913(1 row)

3、执行pg_waldump WAL文件名 |grep 602913

pg_waldump 000000010000000000000063 |grep 602913                                 ...                                 ...                                  rmgr: Heap        len (rec/tot):     78/    78, tx:     203412, lsn: 0/63027050, prev 0/63026C08, desc: INSERT+INIT off 1 flags 0x00, blkref #0: rel 1663/352324/602913 blk 0rmgr: Heap        len (rec/tot):     95/    95, tx:     203413, lsn: 0/630270C8, prev 0/630270A0, desc: INSERT off 2 flags 0x00, blkref #0: rel 1663/352324/602913 blk 0rmgr: Heap        len (rec/tot):    101/   101, tx:     203414, lsn: 0/63027150, prev 0/63027128, desc: INSERT off 3 flags 0x00, blkref #0: rel 1663/352324/602913 blk 0rmgr: Heap        len (rec/tot):    104/   104, tx:     203415, lsn: 0/630271E0, prev 0/630271B8, desc: INSERT off 4 flags 0x00, blkref #0: rel 1663/352324/602913 blk 0rmgr: Heap        len (rec/tot):     65/   413, tx:     203416, lsn: 0/63027320, prev 0/630272A8, desc: HOT_UPDATE off 1 xmax 203416 flags 0x20 ; new off 5 xmax 0, blkref #0: rel 1663/352324/602913 blk 0 FPWrmgr: Heap        len (rec/tot):     77/    77, tx:     203417, lsn: 0/630274E8, prev 0/630274C0, desc: HOT_UPDATE off 2 xmax 203417 flags 0x20 ; new off 6 xmax 0, blkref #0: rel 1663/352324/602913 blk 0rmgr: Heap        len (rec/tot):     77/    77, tx:     203418, lsn: 0/63027560, prev 0/63027538, desc: HOT_UPDATE off 3 xmax 203418 flags 0x20 ; new off 7 xmax 0, blkref #0: rel 1663/352324/602913 blk 0rmgr: Heap        len (rec/tot):     54/    54, tx:     203419, lsn: 0/630275D8, prev 0/630275B0, desc: DELETE off 5 flags 0x00 KEYS_UPDATED , blkref #0: rel 1663/352324/602913 blk 0

其中在最后带有FPW字样的update记录,便是在数据恢复过程中必不可少的 全页写 记录。

desc: HOT_UPDATE off 1 xmax 203416 flags 0x20 ; new off 5 xmax 0, blkref #0: rel 1663/352324/602913 blk 0 FPW

相信大家也注意到了,这些FPW数据页并非100%存在于delete的wal记录中,某个数据页的FPW只会存在于checkpoint后对该数据页的第一次修改,而这个修改动作,可能是增删改,也可能是各类数据库内部的流程。

在上文的测试中,我们创建的数据并不多,所以只用到了一个数据页,该数据页在数据库中被记作0号数据页(blk 0)。

我们手动执行了一次checkpoint,因此,在检查点(checkpoint)后对0号数据页的 初次 修改就是下面这条update

update xman set b='aaaaaaaa' where a=1;

那么你说如果我不懂C语言编程,但是也想把这些FPW拿出来观察一下,是否有其他办法呢?

在PG 16版本中,pg_waldump命令新增了--save-fullpage命令,让用户可以导出对应的FPW到指定目录中。

 --save-fullpage=DIR    save full page images to DIR

但是请注意,用 --save-fullpage 获取出来的FPW仅仅可以算做是一个"基础备份",如果我们想利用FPW进行删除数据的恢复,我们必须要将其往前重做到删除时刻才行,重做的手段就是通俗意义上的"增量恢复"。

重做FPW

仅仅获取FPW远不足以实现我们恢复数据的目的,这些导出的FPW如果距离我们想恢复的delete记录或update记录有一定的时间距离,则必须对FPW进行重做。

举个简单的例子

  • 我获取了18:00时刻针对0号数据页的FPW

  • 但是我要恢复的删除记录在18:02时刻,且18:00-18:02之间只有18:00时刻的这一个FPW

  • 18:00和18:02之间发生了大量针对0号数据页的数据库操作,如update、vacuum、insert等

  • 那么在18:00时刻获取的FPW必须要通过wal记录的重做之后,才能够用于18:02删除数据的恢复

  • 换言之,这两分钟内数据库对0号数据页做的所有操作,我们都必须对0号数据页的FPW重新做一遍,才能让我们获取的FPW达到被删除时刻的正确数据分布状态。

如果简单将"基础备份版本"的FPW用于数据的恢复,要么恢复出的数据货不对板,要么需要恢复的数据偏移位置根本不存在,对于这两点问题,笔者在PDU研发的过程中有亲身体会。

具体的重做逻辑总结如下:

  • 对于insert、update、multi-insert、vacuum、prune这些会对数据页中的数据内容和数据定位产生实际影响的操作,必须对其相应的FPW进行重做操作,让FPW达到真实的数据存储状态。

  • 对于lock、visible、freeze_page、confirm等处理数据可见性的操作,对数据页内的数据并不产生实际影响,可酌情进行重做。

而对于数据页的重做,Postgresql本身就具备一套完整的函数接口,有兴趣的同学可以自己复用这套接口,就可以用数据库的逻辑将FPW恢复至目标时间点的状态。

后续我也会开启源码解读的文章,手把手讲解如何进行FPW的读取,重做,和恢复删除数据,敬请期待。

定位delete数据

即使将FPW重做到了我们的目标时间点,我们也仅仅是获取了一个数据页,距离我们的目标:恢复删除的数据,还有一段距离,下一步就要从FPW中获取被我们删除的数据。

在delete操作的wal记录中,会存储以下信息

  • xl_heap_delete结构体存储删除记录的信息,包括删除操作的事务号、删除数据在数据页中的偏移量等

typedef struct xl_heap_delete{    TransactionId xmax;         /* xmax of the deleted tuple */    OffsetNumber offnum;        /* deleted tuple's offset */    uint8       infobits_set;   /* infomask bits */    uint8       flags;} xl_heap_delete;
  • DecodeBkpBlock结构体存储这个delete操作对应的数据块号

typedef struct{    /* Is this block ref in use? */    bool        in_use;    /* Identify the block this refers to */    RelFileNode rnode;    ForkNumber  forknum;    BlockNumber blkno;            ...} DecodedBkpBlock;

结合以上两种信息,我们就可以从这条delete操作的wal记录中获取到这个操作删除的是哪个数据页里的第几条数据。

并且我们在此前的重做FPW环节中已经将数据页还原到了删除时刻的状态,因此只要从对应的FPW中定位到具体的偏移量,我们就可以获取到心心念念的 被删除数据 了。

至于如何解析这些数据,那就不是本文的讨论范围了,我们后面再聊。

以上就是PDU工具中的delete恢复思路了。大家有任何疑问或者发现我有说的不对的地方,欢迎留言Image
在公众号菜单中点击“PDU下载”或关注公众号回复“下载”,均可获取PDU的下载链接