PostgreSQL資料檔案災難恢復-解析與資料dump

德哥發表於2017-03-13

標籤

PostgreSQL , 資料檔案 , pg_filedump , 安全 , TDE


背景

俗話說常在河邊站哪有不溼鞋,作為一名戰鬥在一線的DBA或者開發者,可能有遇到過磁碟損壞,磁碟陣列損壞,如果有備份或者備庫的話,還好。

如果沒有備份,或者沒有備庫(通常有一些小型或者創業型的企業),那麼遇到磁碟損壞或者其他原因(比如掉電檔案系統損壞),導致資料庫的資料檔案並不完整時,如何從有限的資料中找出資料呢?

比如PostgreSQL,如果讀到壞塊,會報塊不可讀的錯誤,這種情況下通過設定zero_damaged_pages=on可以跳過損壞的資料塊。

如果連後設資料都損壞了,又或者壞了一些磁碟,只有某些表空間被倖免於難,這些情況下你的資料庫都已經無法啟動時,如何能從有限的資料檔案中找回資料呢?

資料檔案解析pg_filedump

pg_filedump是PostgreSQL社群託管的一個專案,類似於pg_xlogdump,不需要開啟資料庫,可以直接從資料檔案中將資料dump出來。

pg_filedump實際上可以DUMP 堆表、索引資料檔案,控制檔案的內容。(從pg_filedump引用的標頭檔案也能看出端倪)

安裝很簡單

git clone git://git.postgresql.org/git/pg_filedump.git  

cd pg_filedump  

export PATH=/home/digoal/pgsql9.6/bin:$PATH  

make ; make install  

命令幫助如下,通常來說,你只需要指定需要DUMP的檔案即可。

如果檔案的塊頭損壞了,那麼你可以手工指定一些資訊,包括塊大小,段大小,解析哪個塊,根據什麼格式解析(欄位型別列表)等。

pg_filedump [-abcdfhikxy] [-R startblock [endblock]] [-D attrlist] [-S blocksize] [-s segsize] [-n segnumber] file  

Defaults are: relative addressing, range of the entire file, block size  
              as listed on block 0 in the file  

The following options are valid for heap and index files:  
  -a  Display absolute addresses when formatting (Block header  
      information is always block relative)  
  -b  Display binary block images within a range (Option will turn  
      off all formatting options)  
  -d  Display formatted block content dump (Option will turn off  
      all other formatting options)  
  -D  Try to decode tuples using given comma separated list of types.  
      List of supported types:  
        * bigint  
        * bigserial  
        * bool  
        * char  
        * charN     -- char(n)  
        * date  
        * float  
        * float4  
        * float8  
        * int  
        * json  
        * macaddr  
        * name  
        * oid  
        * real  
        * serial  
        * smallint  
        * smallserial  
        * text  
        * time  
        * timestamp  
        * timetz  
        * uuid  
        * varchar  
        * varcharN -- varchar(n)  
        * xid  
        * xml  
        * ~        -- ignores are attributes left in a tuple  
  -f  Display formatted block content dump along with interpretation  
  -h  Display this information  
  -i  Display interpreted item details  
  -k  Verify block checksums  
  -R  Display specific block ranges within the file (Blocks are  
      indexed from 0)  
        [startblock]: block to start at  
        [endblock]: block to end at  
      A startblock without an endblock will format the single block  
  -s  Force segment size to [segsize]  
  -n  Force segment number to [segnumber]  
  -S  Force block size to [blocksize]  
  -x  Force interpreted formatting of block items as index items  
  -y  Force interpreted formatting of block items as heap items  

The following options are valid for control files:  
  -c  Interpret the file listed as a control file  
  -f  Display formatted content dump along with interpretation  
  -S  Force block size to [blocksize]  

In most cases it`s recommended to use the -i and -f options to get  
the most useful dump output.  

pg_filedump使用舉例

1. 建立測試表

postgres=# create table digoal(id int, info text, crt_time timestamp);  
CREATE TABLE  

2. 插入測試資料

postgres=# insert into digoal select generate_series(1,1000000),md5(random()::text), clock_timestamp();  
INSERT 0 1000000  

3. 找出表對應的資料檔案

postgres=# select pg_relation_filepath(`digoal`);  
 pg_relation_filepath   
----------------------  
 base/13269/173369  
(1 row)  

4. 呼叫checkpoint,把資料刷盤,便於我們接下來的觀察

checkpoint;  

5. 使用pg_filedump直接讀取資料檔案,匯出資料

-> cd $PGDATA/base/13269  
-> pg_filedump -i -f ./173369  

輸出擷取  
*******************************************************************  
* PostgreSQL File/Block Formatted Dump Utility - Version 9.6.0  
*  
* File: ./173369  
* Options used: -i -f   
*  
* Dump created on: Sun Mar 12 00:28:56 2017  
*******************************************************************  

首先是塊的頭部內容

Block    0 ********************************************************  
<Header> -----  
 Block Offset: 0x00000000         Offsets: Lower     452 (0x01c4)  
 Block: Size 8192  Version    4            Upper     488 (0x01e8)  
 LSN:  logid     61 recoff 0xe69d6490      Special  8192 (0x2000)  
 Items:  107                      Free Space:   36  
 Checksum: 0x0000  Prune XID: 0x00000000  Flags: 0x0000 ()  
 Length (including item array): 452  

...

然後是DATA部分,
<Data> ------ 
 Item   1 -- Length:   72  Offset: 8120 (0x1fb8)  Flags: NORMAL
  XMIN: 88072212  XMAX: 88072214  CID|XVAC: 0
  Block Id: 9345  linp Index: 86   Attributes: 3   Size: 24
  infomask: 0x0102 (HASVARWIDTH|XMIN_COMMITTED) 

  1fb8: 14e03f05 16e03f05 00000000 00008124  ..?...?........$
  1fc8: 56000300 02011800 01000000 43363134  V...........C614
  1fd8: 62653439 31616339 65356636 64633136  be491ac9e5f6dc16
  1fe8: 35653065 31323162 36316563 33000000  5e0e121b61ec3...
  1ff8: 791cce69 7ced0100                    y..i|...        

COPY: 1 614be491ac9e5f6dc165e0e121b61ec3        2017-03-12 00:26:23.553657
 Item   2 -- Length:   72  Offset: 8048 (0x1f70)  Flags: NORMAL
  XMIN: 88072212  XMAX: 0  CID|XVAC: 0
  Block Id: 0  linp Index: 2   Attributes: 3   Size: 24
  infomask: 0x0902 (HASVARWIDTH|XMIN_COMMITTED|XMAX_INVALID) 

  1f70: 14e03f05 00000000 00000000 00000000  ..?.............
  1f80: 02000300 02091800 02000000 43383335  ............C835
  1f90: 39653064 31623462 61323261 64336139  9e0d1b4ba22ad3a9
  1fa0: 65386634 38316231 61633336 31000000  e8f481b1ac361...
  1fb0: df1cce69 7ced0100                    ...i|...        

COPY: 2 8359e0d1b4ba22ad3a9e8f481b1ac361        2017-03-12 00:26:23.553759
......

為了得到記錄,需要提供一下欄位型別LIST,必須保證與表結構一致

-> pg_filedump -D int,text,timestamp ./173369|less  

*******************************************************************  
* PostgreSQL File/Block Formatted Dump Utility - Version 9.6.0  
*  
* File: ./173369  
* Options used: -D int,text,timestamp   
*  
* Dump created on: Sun Mar 12 00:31:25 2017  
*******************************************************************  

Block    0 ********************************************************  
<Header> -----  
 Block Offset: 0x00000000         Offsets: Lower     452 (0x01c4)  
 Block: Size 8192  Version    4            Upper     488 (0x01e8)  
 LSN:  logid     61 recoff 0xe69d6490      Special  8192 (0x2000)  
 Items:  107                      Free Space:   36  
 Checksum: 0x0000  Prune XID: 0x00000000  Flags: 0x0000 ()  
 Length (including item array): 452  

<Data> ------   
 Item   1 -- Length:   72  Offset: 8120 (0x1fb8)  Flags: NORMAL  
COPY: 1 614be491ac9e5f6dc165e0e121b61ec3        2017-03-12 00:26:23.553657  
 Item   2 -- Length:   72  Offset: 8048 (0x1f70)  Flags: NORMAL  
COPY: 2 8359e0d1b4ba22ad3a9e8f481b1ac361        2017-03-12 00:26:23.553759  
 Item   3 -- Length:   72  Offset: 7976 (0x1f28)  Flags: NORMAL  
COPY: 3 0dc8c441e91217897f994ae163510653        2017-03-12 00:26:23.553764  
..........  

COPY得到的就是使用-D提供的型別列表decode拼裝的記錄。

是不是可以從檔案中DUMP資料了呢?莫急,還要看看掩碼哦,否則你不知道這條記錄是否為你需要的,因為它可能是DEAD TUPLE。

例子

-> pg_filedump -D int,text,timestamp -i -f ./173369|less  

COPY: 1 614be491ac9e5f6dc165e0e121b61ec3        2017-03-12 00:26:23.553657  
 Item   2 -- Length:   72  Offset: 8048 (0x1f70)  Flags: NORMAL  
  XMIN: 88072212  XMAX: 0  CID|XVAC: 0  
  Block Id: 0  linp Index: 2   Attributes: 3   Size: 24  
  infomask: 0x0902 (HASVARWIDTH|XMIN_COMMITTED|XMAX_INVALID)   

記錄頭的infomask解釋

每條記錄,頭部都有infomask, infomask2掩碼,掩碼錶示的意思,可以參考標頭檔案

比如什麼是DEAD TUPLE呢?

src/include/access/htup_details.h  

/*  
 * information stored in t_infomask:  
 */  
#define HEAP_HASNULL                    0x0001  /* has null attribute(s) */  
#define HEAP_HASVARWIDTH                0x0002  /* has variable-width attribute(s) */  
#define HEAP_HASEXTERNAL                0x0004  /* has external stored attribute(s) */  
#define HEAP_HASOID                             0x0008  /* has an object-id field */  
#define HEAP_XMAX_KEYSHR_LOCK   0x0010  /* xmax is a key-shared locker */  
#define HEAP_COMBOCID                   0x0020  /* t_cid is a combo cid */  
#define HEAP_XMAX_EXCL_LOCK             0x0040  /* xmax is exclusive locker */  
#define HEAP_XMAX_LOCK_ONLY             0x0080  /* xmax, if valid, is only a locker */  

 /* xmax is a shared locker */  
#define HEAP_XMAX_SHR_LOCK      (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)  

#define HEAP_LOCK_MASK  (HEAP_XMAX_SHR_LOCK | HEAP_XMAX_EXCL_LOCK |   
                                                 HEAP_XMAX_KEYSHR_LOCK)  
#define HEAP_XMIN_COMMITTED             0x0100  /* t_xmin committed */  
#define HEAP_XMIN_INVALID               0x0200  /* t_xmin invalid/aborted */  
#define HEAP_XMIN_FROZEN                (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)  
#define HEAP_XMAX_COMMITTED             0x0400  /* t_xmax committed */  
#define HEAP_XMAX_INVALID               0x0800  /* t_xmax invalid/aborted */  
#define HEAP_XMAX_IS_MULTI              0x1000  /* t_xmax is a MultiXactId */  
#define HEAP_UPDATED                    0x2000  /* this is UPDATEd version of row */  
#define HEAP_MOVED_OFF                  0x4000  /* moved to another place by pre-9.0  
                                                                                 * VACUUM FULL; kept for binary  
                                                                                 * upgrade support */  
#define HEAP_MOVED_IN                   0x8000  /* moved from another place by pre-9.0  
                                                                                 * VACUUM FULL; kept for binary  
                                                                                 * upgrade support */  
#define HEAP_MOVED (HEAP_MOVED_OFF | HEAP_MOVED_IN)  

#define HEAP_XACT_MASK                  0xFFF0  /* visibility-related bits */  

觀察deadtuple

postgres=# update digoal set info=`new` where id=1;  
UPDATE 1  
postgres=# checkpoint;  
CHECKPOINT  

觀察

pg_filedump -D int,text,timestamp -i -f ./173369|less  

 Item   1 -- Length:   72  Offset: 8120 (0x1fb8)  Flags: NORMAL  
  包含了xmax,說明是更新過的記錄  
  XMIN: 88072212  XMAX: 88072214  CID|XVAC: 0  
  blockid表示記錄指向,即新版本記錄在哪個新資料塊,Linp指新資料塊的第幾條記錄。  
  Block Id: 9345  linp Index: 86   Attributes: 3   Size: 24  
  infomask 參考前面的標頭檔案,解讀  
  infomask: 0x0102 (HASVARWIDTH|XMIN_COMMITTED)   

  1fb8: 14e03f05 16e03f05 00000000 00008124  ..?...?........$  
  1fc8: 56000300 02011800 01000000 43363134  V...........C614  
  1fd8: 62653439 31616339 65356636 64633136  be491ac9e5f6dc16  
  1fe8: 35653065 31323162 36316563 33000000  5e0e121b61ec3...  
  1ff8: 791cce69 7ced0100                    y..i|...          

COPY: 1 614be491ac9e5f6dc165e0e121b61ec3        2017-03-12 00:26:23.553657  

檢視新版本(編號為9345資料塊,第86條記錄)

pg_filedump -D int,text,timestamp -i -f -R 9345 ./173369|less  

 Item  86 -- Length:   40  Offset: 2032 (0x07f0)  Flags: NORMAL  
  XMIN: 88072214  XMAX: 0  CID|XVAC: 0  
  Block Id: 9345  linp Index: 86   Attributes: 3   Size: 24  
  infomask: 0x2802 (HASVARWIDTH|XMAX_INVALID|UPDATED)   

  07f0: 16e03f05 00000000 00000000 00008124  ..?............$  
  0800: 56000300 02281800 01000000 096e6577  V....(.......new  
  0810: 791cce69 7ced0100                    y..i|...          

COPY: 1 new     2017-03-12 00:26:23.553657  

PostgreSQL資料塊的簡介

對於資料檔案的組織形式,可以參考標頭檔案

src/include/storage

/*  
 * BlockNumber:  
 *  
 * each data file (heap or index) is divided into postgres disk blocks  
 * (which may be thought of as the unit of i/o -- a postgres buffer  
 * contains exactly one disk block).  the blocks are numbered  
 * sequentially, 0 to 0xFFFFFFFE.  
 *  
 * InvalidBlockNumber is the same thing as P_NEW in buf.h.  
 *  
 * the access methods, the buffer manager and the storage manager are  
 * more or less the only pieces of code that should be accessing disk  
 * blocks directly.  
 */  
typedef uint32 BlockNumber;  

#define InvalidBlockNumber              ((BlockNumber) 0xFFFFFFFF)  

#define MaxBlockNumber                  ((BlockNumber) 0xFFFFFFFE)  

每個塊內的組織,與物件型別有關,比如堆表,B-TREE,HASH等索引,TOAST, FSM等。

可以參考資料layout介紹

https://www.postgresql.org/docs/9.6/static/storage.html

也可以參考對應型別的標頭檔案

閱讀pg_filedump的原始碼,也有助於你對PostgreSQL儲存構造的理解

不妨仔細閱讀以下標頭檔案

#include "access/gin_private.h"  
#include "access/gist.h"  
#include "access/hash.h"  
#include "access/htup.h"  
#include "access/htup_details.h"  
#include "access/itup.h"  
#include "access/nbtree.h"  
#include "access/spgist_private.h"  
#include "catalog/pg_control.h"  
#include "storage/bufpage.h"  

防止脫褲

我們已經看到,使用pg_filedump可直接decode資料檔案的內容,因此洩露資料檔案其實是比較危險的。

那麼如何防止脫褲呢?TDE是一個很好的手段,即資料檔案透明加密。你可以參考我末尾的文章。

另外還有加密方法,比如對敏感資料,使用加密欄位儲存。加解密交給程式完成。徹底杜絕因洩露檔案導致的資料洩露。

《PostgreSQL 資料庫安全指南》

參考

《PostgreSQL 透明加密(TDE,FDE) – 塊級加密》


相關文章