数据被删了干着急?继walminer之后另一款国货之光!
在公众号菜单中点击“PDU下载”或关注公众号回复“下载”,均可获取PDU的下载链接
前言
在Postgresql数据库中,被删除的数据会以死亡元组的形式留存一段时间,我们称之为“脏数据”,通过脏数据的读取,再加以一些筛选的工作,可以实现恢复删除数据的目的。
但如果时间过了太久,或者删除的数据条数达到了一定的阈值,那么等到客户找到我们的时候,这些脏数据大概率已经被vacuum清空,是物理意义上的不存在了;那谁来看了都只能尴尬一笑。
因此包括PDU工具在内的PG数据库delete恢复工具,都会选择另外一条路径来实现删除数据的恢复,这种方式不受到任何时间空间的约束,只要删除数据期间的WAL文件依然留存,即使过了一个月,数据也可以原样恢复;本文将会针对这条恢复路径,进行详细的原理说明。
基础概念:
1、FPW(Full Page Write)-数据库在检查点(checkpoint)后对某个数据页的 初次 修改,会将该页完整写入这条修改的wal记录中。
核心实现原理
在我们使用delete删除数据,或是用update更新数据时,被更新 或 被删除 的数据页会以FPW的形态留存在wal记录中
通过FPW的读取+wal记录重做,就可以将数据块还原到 删除 或 更新 时的时间点。
利用重做后的数据块。从需要恢复的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中定位到具体的偏移量,我们就可以获取到心心念念的 被删除数据 了。
至于如何解析这些数据,那就不是本文的讨论范围了,我们后面再聊。
