暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

关键时候救你命: 简要介绍PostgreSQL 插件(2) - walminer - 抢救误删数据

数据库杂记 2023-03-28
11

1、前言

对于从事DBA相关的朋友而言,最大的悲哀莫过于没有好好的做好数据备份,最后导致数据丢失,正所谓,出事了,被迫或有意提桶跑路,甚至有如股市里的“关灯吃面”,自己慢慢承受带来的恶果。

这里就大致介绍一下自己国人开发的walminer,用于PG数据库中的历史数据恢复。本篇先暂时介绍完全开源的3.0版。以后有机会再介绍4.0收费版。它们各有特色。

什么是WalMiner

WalMiner是从PostgreSQL的WAL(write ahead logs)日志的解析工具,旨在挖掘wal日志所有的有用信息,从而提供PG的数据恢复支持。目前主要有如下功能:

  • 从waL日志中解析出SQL,包括DML和少量DDL

    解析出执行的SQL语句的工具,并能生成对应的undo SQL语句。与传统的logical decode插件相比,walminer不要求logical日志级别且解析方式较为灵活。

  • 数据页挽回

    当数据库被执行了TRUNCATE等不被wal记录的数据清除操作,或者发生磁盘页损坏,可以使用此功能从wal日志中搜索数据,以期尽量挽回数据。

这里借一张简图(简图来自参考文章里的原图,一并致谢),可以概括它的基本工作原理:

2、编译安装

基本上有两种方法,一般使用第2种方法比较常见。

取代码:它现在换名字了,叫XLogMiner,由国内相关人员开发并发布。源码:https://gitee.com/movead/XLogMiner
walminer3.0支持PostgreSQL 10,11,12,13,14。(此版本放弃对9.x的支持)

代码位置:(https://gitee.com/movead/XLogMiner/releases)可以找到source code下载https://gitee.com/movead/XLogMiner/repository/archive/walminer_3.0_stable.zip 
或者clone下来,取master branch,当然这个时候你看到的可能是2.0版本,不影响。

编译一:PG源码编译
如果你从编译pg数据库开始

  1. 将walminer目录放置到编译通过的PG工程的"../contrib/"目录下

  2. 进入walminer目录

  3. 执行命令

    make && make install

    复制

编译二:依据PG安装编译
如果你使用yum或者pg安装包安装了pg

  1. 配置pg的bin路径至环境变量

    export PATH=/usr/pgsql-14/bin:$PATH

    复制
  2. 进入walminer代码路径

  3. 执行编译安装

    USE_PGXS=1 MAJORVERSION=14 make
    #MAJORVERSION支持‘10’,‘11’,‘12’,‘13’,'14'
    USE_PGXS=1 MAJORVERSION=14 make install

    复制

注:原文档中,可能上边的变量:MAJORVERSION只有10到13,没关系,我们也可以把它设为14,甚至15,跟实际的PG版本相匹配就行。

3、简单使用实例

先记启用插件,要让walminer能用,还得启用归档日志,防止wal log被覆盖重用。因为它的基础就是建立在wal log还在的基础上进行逆向分析。

如,有一张表,我插入了几条数据,中间误删除了一条记录:id=2。现在想把它恢复回去。

mydb=# create extension walminer;
CREATE EXTENSION
mydb=#
mydb=# create table t(id int primary key, col2 varchar(32));
CREATE TABLE
mydb=# insert into t values(1, 'wang');
INSERT 0 1
mydb=# insert into t values(2, 'wang2');
INSERT 0 1
mydb=# insert into t values(3, 'wang3');
INSERT 0 1
mydb=# insert into t values(4, 't4'), (5, 't5');
INSERT 0 2
mydb=# delete from t where id = 2;
DELETE 1
mydb=# checkpoint;
CHECKPOINT

复制

我们这里也不想通过pg_waldump之类的,挖出xid相关信息进行PITR恢复了,那个不是挺费时间的吗。看看walminer怎么做。

在上边建完插件之后,添加wal log 目录或列表,然后就可以查询了。如:

mydb=# select walminer_wal_list();
                    walminer_wal_list
----------------------------------------------------------
 (/var/lib/pgsql/14/data/pg_wal/000000010000000000000001)
 (/var/lib/pgsql/14/data/pg_wal/000000010000000000000002)
 (/var/lib/pgsql/14/data/pg_wal/000000010000000000000003)
 (/var/lib/pgsql/14/data/pg_wal/000000010000000000000004)
(4 rows)

mydb=# select wal2sql();
NOTICE:  Switch wal to 000000010000000000000001 on time 2023-03-28 05:10:47.622928+08
NOTICE:  Switch wal to 000000010000000000000002 on time 2023-03-28 05:10:47.790919+08
NOTICE:  Switch wal to 000000010000000000000003 on time 2023-03-28 05:10:48.052498+08
NOTICE:  Switch wal to 000000010000000000000004 on time 2023-03-28 05:10:48.391953+08
       wal2sql
---------------------
 pg_minerwal success
(1 row)

mydb=# \d
               List of relations
 Schema |       Name        | Type  |  Owner
--------+-------------------+-------+----------
 public | t                 | table | postgres
 public | walminer_contents | table | postgres
(2 rows)

mydb=# select * from walminer_contents;
 sqlno | xid | topxid | sqlkind | minerd |           timestamp           |                      op_text                      |
           undo_text                     | complete | schema | relation | start_lsn | commit_lsn
-------+-----+--------+---------+--------+-------------------------------+---------------------------------------------------+----------
-----------------------------------------+----------+--------+----------+-----------+------------
     1 | 789 |      0 |       1 | t      | 2023-03-28 05:01:48.924149+08 | INSERT INTO public.t(id ,col2) VALUES(1 ,'wang')  | DELETE FR
OM public.t WHERE id=1                   | t        | public | t        | 0/42D4EF8 | 0/42D5000
     1 | 790 |      0 |       1 | t      | 2023-03-28 05:01:53.97429+08  | INSERT INTO public.t(id ,col2) VALUES(2 ,'wang2') | DELETE FR
OM public.t WHERE id=2                   | t        | public | t        | 0/42D5000 | 0/42D50B0
     1 | 791 |      0 |       1 | t      | 2023-03-28 05:02:01.33367+08  | INSERT INTO public.t(id ,col2) VALUES(3 ,'wang3') | DELETE FR
OM public.t WHERE id=3                   | t        | public | t        | 0/42D50E8 | 0/42D5198
     1 | 792 |      0 |       1 | t      | 2023-03-28 05:02:18.997766+08 | INSERT INTO public.t(id ,col2) VALUES(4 ,'t4')    | DELETE FR
OM public.t WHERE id=4                   | t        | public | t        | 0/42D5198 | 0/42D52C0
     2 | 792 |      0 |       1 | t      | 2023-03-28 05:02:18.997766+08 | INSERT INTO public.t(id ,col2) VALUES(5 ,'t5')    | DELETE FR
OM public.t WHERE id=5                   | t        | public | t        | 0/42D5218 | 0/42D52C0
     1 | 793 |      0 |       3 | t      | 2023-03-28 05:02:29.346528+08 | DELETE FROM public.t WHERE id=2                   | INSERT IN
TO public.t(id ,col2) VALUES(2 ,'wang2') | t        | public | t        | 0/42D52F8 | 0/42D5358
(6 rows)

复制

最后我们从表walminer_contents里可以清楚看到id=2时的undo SQL。将其执行一下,就能得到恢复。

当然这上边是把所有的wal log全部解析。你也可以只针对某张表进行过滤解析。先得到表的oid,然后再过滤。

-- 可以捋一下,walminer_contents是一个unlogged表,便于清除,性能也不错。
mydb=# \d+ walminer_contents
                                          Unlogged table "public.walminer_contents"
   Column   |           Type           | Collation | Nullable | Default | Storage  | Compression | Stats target | Description
------------+--------------------------+-----------+----------+---------+----------+-------------+--------------+-------------
 sqlno      | integer                  |           |          |         | plain    |             |              |
 xid        | bigint                   |           |          |         | plain    |             |              |
 topxid     | bigint                   |           |          |         | plain    |             |              |
 sqlkind    | integer                  |           |          |         | plain    |             |              |
 minerd     | boolean                  |           |          |         | plain    |             |              |
 timestamp  | timestamp with time zone |           |          |         | plain    |             |              |
 op_text    | text                     |           |          |         | extended |             |              |
 undo_text  | text                     |           |          |         | extended |             |              |
 complete   | boolean                  |           |          |         | plain    |             |              |
 schema     | text                     |           |          |         | extended |             |              |
 relation   | text                     |           |          |         | extended |             |              |
 start_lsn  | pg_lsn                   |           |          |         | plain    |             |              |
 commit_lsn | pg_lsn                   |           |          |         | plain    |             |              |
Access method: heap

-- 得以目标表的oid: 24605
mydb=# select oid, pg_relation_filepath(oid) from pg_class where relname='t';
  oid  | pg_relation_filepath
-------+----------------------
 24605 |
 base/16384/24605
(1 row)

复制

再重新生成content:

mydb=# select wal2sql('2023-03-27 00:00:00', '2023-03-28 08:08:08', true, 24605);
NOTICE:  Add wal from current pg_wal directory, do not suggest use this way in produce
NOTICE:  Switch wal to 000000010000000000000004 on time 2023-03-28 05:20:52.612745+08
       wal2sql
---------------------
 pg_minerwal success
(1 row)

mydb=# select sqlno, xid, op_text, undo_text, timestamp, commit_lsn from walminer_contents;
 sqlno | xid |                      op_text                      |                     undo_text                     |           timesta
mp           | commit_lsn
-------+-----+---------------------------------------------------+---------------------------------------------------+------------------
-------------+------------
     1 | 789 | INSERT INTO public.t(id ,col2) VALUES(1 ,'wang')  | DELETE FROM public.t WHERE id=1                   | 2023-03-28 05:01:
48.924149+08 | 0/42D5000
     1 | 790 | INSERT INTO public.t(id ,col2) VALUES(2 ,'wang2') | DELETE FROM public.t WHERE id=2                   | 2023-03-28 05:01:
53.97429+08  | 0/42D50B0
     1 | 791 | INSERT INTO public.t(id ,col2) VALUES(3 ,'wang3') | DELETE FROM public.t WHERE id=3                   | 2023-03-28 05:02:
01.33367+08  | 0/42D5198
     1 | 792 | INSERT INTO public.t(id ,col2) VALUES(4 ,'t4')    | DELETE FROM public.t WHERE id=4                   | 2023-03-28 05:02:
18.997766+08 | 0/42D52C0
     2 | 792 | INSERT INTO public.t(id ,col2) VALUES(5 ,'t5')    | DELETE FROM public.t WHERE id=5                   | 2023-03-28 05:02:
18.997766+08 | 0/42D52C0
     1 | 793 | DELETE FROM public.t WHERE id=2                   | INSERT INTO public.t(id ,col2) VALUES(2 ,'wang2') | 2023-03-28 05:02:
29.346528+08 | 0/42D5358
(6 rows)

复制

这样把范围一下子缩小了很多, 同样能找出结果。上边的例子主要是用于单表解析,用的是oid。不是filenodeid。

每次wal2sql的时候,系统先是直接truncate那张content表。所以不用担心记录会重复叠加。找到对应的undo_text (SQL),执行一下,就可以立即恢复回去。

-- 简单恢复一下数据
mydb=# INSERT INTO public.t(id ,col2) VALUES(2 ,'wang2');
INSERT 0 1
mydb=# select * from t;
 id | col2
----+-------
  1 | wang
  3 | wang3
  4 | t4
  5 | t5
  2 | wang2
(5 rows)

复制

当然,walminer还有很多其它骚操作。你可以直接上它的源码网站上仔细跟读并实验验证。

4、提一提pg_resetwal

我们知道PostgreSQL的多版本原理是旧数据并不删除:

1. 对于删除数据的操作,只是把行上的xmax改成当前的事务id
2. 对于更新操作,只是把原先行上xmax改成当前的事务id,并插入一个新行,而新行上的xmin置为当前的事务id

所以,假如数据还没有被vacuum进程清理掉的话,那么就可以找回来。

说到这里,不得不提一下pg_resetwal,在目标表没有vacuum(如果vacuum的话,数据可能被清理)的情况下,它也可以通过重置系统的xid值,恢复到那个删除操作之前的状态。但是这要的代价还是比较大的。

网上有不少相关文章介绍这种暴力骚操作。不赘述。

参考:

  1.  https://gitee.com/movead/XLogMiner

  2. 如何抢救被删的数据 (https://mp.weixin.qq.com/s/lWgZskzX_75-K81MFZh2LQ)

  ---END---


复制

文章转载自数据库杂记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论