技術分享 | MySQL 刪庫不跑路(建議收藏)

做者:洪斌

每一個 DBA 是否是都有過刪庫的經歷?刪庫了沒有備份怎麼辦?備份恢復後沒法啓動服務什麼狀況?表定義損壞數據沒法讀取怎麼辦?
我曾遇到某初創互聯網企業,因維護人員不規範的備份恢復操做,致使系統表空間文件被初始化,上萬張表沒法讀取,花了數小時才搶救回來。
當你發現數據沒法讀取時,也許並不是數據丟失了,多是 DBMS 找不到描述數據的信息。html

背景

先來了解下幾張關鍵的 InnoDB 數據字典表,它們保存了部分表定義信息,在咱們恢復表結構時須要用到。mysql

SYS_TABLES 描述InnoDB表信息git

CREATE TABLE `SYS_TABLES` (
`NAME` varchar(255) NOT NULL DEFAULT '',  表名
`ID` bigint(20) unsigned NOT NULL DEFAULT '0',  表id
`N_COLS` int(10) DEFAULT NULL,
`TYPE` int(10) unsigned DEFAULT NULL,
`MIX_ID` bigint(20) unsigned DEFAULT NULL,
`MIX_LEN` int(10) unsigned DEFAULT NULL,
`CLUSTER_NAME` varchar(255) DEFAULT NULL,
`SPACE` int(10) unsigned DEFAULT NULL,   表空間id
PRIMARY KEY (`NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

SYS_INDEXES 描述InnoDB索引信息github

CREATE TABLE `SYS_INDEXES` (
  `TABLE_ID` bigint(20) unsigned NOT NULL DEFAULT '0', 與sys_tables的id對應
  `ID` bigint(20) unsigned NOT NULL DEFAULT '0',  索引id
  `NAME` varchar(120) DEFAULT NULL,         索引名稱
  `N_FIELDS` int(10) unsigned DEFAULT NULL, 索引包含字段的個數
  `TYPE` int(10) unsigned DEFAULT NULL,
  `SPACE` int(10) unsigned DEFAULT NULL,  存儲索引的表空間id
  `PAGE_NO` int(10) unsigned DEFAULT NULL,  索引的root page id
  PRIMARY KEY (`TABLE_ID`,`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

SYS_COLUMNS 描述InnoDB表的字段信息sql

CREATE TABLE `SYS_COLUMNS` (
  `TABLE_ID` bigint(20) unsigned NOT NULL, 與sys_tables的id對應
  `POS` int(10) unsigned NOT NULL,     字段相對位置
  `NAME` varchar(255) DEFAULT NULL,    字段名稱
  `MTYPE` int(10) unsigned DEFAULT NULL,  字段編碼
  `PRTYPE` int(10) unsigned DEFAULT NULL, 字段校驗類型
  `LEN` int(10) unsigned DEFAULT NULL,  字段字節長度
  `PREC` int(10) unsigned DEFAULT NULL, 字段精度
  PRIMARY KEY (`TABLE_ID`,`POS`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

SYS_FIELDS 描述所有索引的字段列工具

CREATE TABLE `SYS_FIELDS` (
  `INDEX_ID` bigint(20) unsigned NOT NULL, 
  `POS` int(10) unsigned NOT NULL,
  `COL_NAME` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`INDEX_ID`,`POS`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

./storage/innobase/include/dict0boot.h 文件定義了每一個字典表的index id,對應id的page中存儲着字典表的數據。
圖片描述測試

這裏咱們須要藉助undrop-for-innodb工具恢復數據,它能讀取表空間信息獲得page,將數據從page中提取出來。flex

# wget https://github.com/chhabhaiya/undrop-for-innodb/archive/master.zip
# yum install -y gcc flex bison
# make
# make sys_parser

./sys_parser 讀取表結構信息
sys_parser [-h <host>] [-u <user>] [-p <passowrd>] [-d <db>] databases/tablethis

stream_parser 讀取InnoDB page 從ibdata1或ibd 或分區表編碼

# ./stream_parser
You must specify file with -f option
Usage: ./stream_parser -f <innodb_datafile> [-T N:M] [-s size] [-t size] [-V|-g]
  Where:
    -h         - Print this help
    -V or -g   - Print debug information
    -s size    - Amount of memory used for disk cache (allowed examples 1G 10M). Default 100M
    -T         - retrieves only pages with index id = NM (N - high word, M - low word of id)
    -t size    - Size of InnoDB tablespace to scan. Use it only if the parser can't determine it by himself.

c_parser 從innodb page中讀取記錄保存到文件

# ./c_parser
Error: Usage: ./c_parser -4|-5|-6 [-dDV] -f <InnoDB page or dir> -t table.sql [-T N:M] [-b <external pages directory>]
  Where
    -f <InnoDB page(s)> -- InnoDB page or directory with pages(all pages should have same index_id)
    -t <table.sql> -- CREATE statement of a table
    -o <file> -- Save dump in this file. Otherwise print to stdout
    -l <file> -- Save SQL statements in this file. Otherwise print to stderr
    -h  -- Print this help
    -d  -- Process only those pages which potentially could have deleted records (default = NO)
    -D  -- Recover deleted rows only (default = NO)
    -U  -- Recover UNdeleted rows only (default = YES)
    -V  -- Verbose mode (lots of debug information)
    -4  -- innodb_datafile is in REDUNDANT format
    -5  -- innodb_datafile is in COMPACT format
    -6  -- innodb_datafile is in MySQL 5.6 format
    -T  -- retrieves only pages with index id = NM (N - high word, M - low word of id)
    -b <dir> -- Directory where external pages can be found. Usually it is pages-XXX/FIL_PAGE_TYPE_BLOB/
    -i <file> -- Read external pages at their offsets from <file>.
    -p prefix -- Use prefix for a directory name in LOAD DATA INFILE command

接下來,咱們演示場景的幾種數據恢復場景。

場景1:drop table

是否啓用了innodb_file_per_table其恢復方法有所差別,當發生誤刪表時,應儘快中止MySQL服務,不要啓動。若innodb_file_per_table=ON,最好只讀方式從新掛載文件系統,防止其餘進程寫入數據覆蓋以前塊設備的數據。

若是評估記錄是否被覆蓋,能夠表中某些記錄的做爲關鍵字看是否能從ibdata1中篩選出。
grep WOODYHOFFMAN ibdata1
Binary file ibdata1 matches
也可使用bvi(適用於較小文件)或hexdump -C(適用於較大文件)工具

以表sakila.actor爲例

CREATE TABLE `actor` (
`actor_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`first_name` varchar(45) NOT NULL,
`last_name` varchar(45) NOT NULL,
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`actor_id`),
KEY `idx_actor_last_name` (`last_name`)
) ENGINE=InnoDB AUTO_INCREMENT=201 DEFAULT CHARSET=utf8

首先恢復表結構信息
1.解析系統表空間獲取page信息

./stream_parser -f /var/lib/mysql/ibdata1

2.新建一個schema,把系統字典表的DDL導入

cat dictionary/SYS_* | mysql recovered

3.建立恢復目錄

mkdir -p dumps/default

4.解析系統表空間包含的字典表信息,

./c_parser -4f pages-ibdata1/FIL_PAGE_INDEX/0000000000000001.page -t dictionary/SYS_TABLES.sql > dumps/default/SYS_TABLES 2> dumps/default/SYS_TABLES.sql
./c_parser -4f pages-ibdata1/FIL_PAGE_INDEX/0000000000000002.page -t dictionary/SYS_COLUMNS.sql > dumps/default/SYS_COLUMNS 2> dumps/default/SYS_COLUMNS.sql
./c_parser -4f pages-ibdata1/FIL_PAGE_INDEX/0000000000000003.page -t dictionary/SYS_INDEXES.sql > dumps/default/SYS_INDEXES 2> dumps/default/SYS_INDEXES.sql
./c_parser -4f pages-ibdata1/FIL_PAGE_INDEX/0000000000000004.page -t dictionary/SYS_FIELDS.sql > dumps/default/SYS_FIELDS 2> dumps/default/SYS_FIELDS.sql

5.導入恢復的數據字典

cat dumps/default/*.sql | mysql recovered

6.讀取恢復後的表結構信息

./sys_parser -pmsandbox -d recovered sakila/actor

因爲5.x 版本 innodb引擎並不是完整記錄表結構信息,會丟失AUTO_INCREMENT屬性、二級索引和外鍵約束,DECIMAL精度等信息。

如果mysql 5.5版本 frm文件被從系統刪除,在原目錄下touch與原表名相同的frm文件,還能讀取表結構信息和數據。若只有frm文件,想要得到表結構信息,可以使用mysqlfrm --diagnostic /path/to/xxx.frm,鏈接mysql會顯示字符集信息。

  • innodb_file_per_table=OFF

由於是共享表空間模式,數據頁都存儲在ibdata1,能夠從ibdata1文件中提取數據。
1.獲取表的table id,sys_table存有表的table id,sys_table表index id是1,因此從0000000000000001.page獲取表id

./c_parser -4Df pages-ibdata1/FIL_PAGE_INDEX/0000000000000001.page -t dictionary/SYS_TABLES.sql | grep sakila/actor
000000000B28  2A000001430D4D  SYS_TABLES  "sakila/actor"  158  4  1 0   0   ""  0
000000000B28  2A000001430D4D  SYS_TABLES  "sakila/actor"  158  4  1 0   0   ""  0

2.利用table id獲取表的主鍵id,sys_indexes存有表索引信息,innodb索引組織表,找到主鍵id即找到數據,sys_indexes的index id是3,因此從0000000000000003.page獲取主鍵 id

./c_parser -4Df pages-ibdata1/FIL_PAGE_INDEX/0000000000000003.page -t dictionary/SYS_INDEXES.sql | grep 158
000000000B28    2A000001430BCA  SYS_INDEXES     158     376     "PRIMARY"       1       3       0       4294967295
000000000B28    2A000001430C3C  SYS_INDEXES     158     377     "idx\_actor\_last\_name"        1       0       0       4294967295
000000000B28    2A000001430BCA  SYS_INDEXES     158     376     "PRIMARY"       1       3       0       4294967295
000000000B28    2A000001430C3C  SYS_INDEXES     158     377     "idx\_actor\_last\_name"        1       0       0       4294967295

3.知道了主鍵id,就能夠從對應page中提取表數據,並生成sql文件。

./c_parser -4f pages-ibdata1/FIL_PAGE_INDEX/0000000000000376.page -t sakila/actor.sql > dumps/default/actor 2> dumps/default/actor_load.sql

4.最後導入恢復的數據

cat dumps/default/*.sql | mysql sakila
  • innodb_file_per_table=ON

這種狀況恢復步驟與上述基本一致,但因爲是獨立表空間模式,數據頁存儲在各自的ibd文件,ibd文件刪除了,沒法經過ibdata1提取數據頁,因此pages-ibdata1目錄找不到數據頁,stream_parser要從塊設備中讀取數據頁信息。掃描完成後,在pages-sda1目錄下提取數據。

./stream_parser -f /dev/sda1 -t 1000000k

場景2:Corrupted InnoDB table

在InnoDB表發生損壞,即便innodb_force_recovery=6也沒法啓動MySQL
日誌中可能會出現相似報錯

InnoDB: Database page corruption on disk or a failed
InnoDB: file read of page 4.

此時的恢復策略須要將數據頁從獨立表空間中提取出,再刪除表空間,從新建立表導入數據。
1.先得到故障表的主鍵index id
2.經過index id page獲取到數據記錄

select t.name, t.table_id, i.index_id, i.page_no from INNODB_SYS_TABLES t join INNODB_SYS_INDEXES i on t.table_id=i.table_id and t.name='test/sbtest1';

3.因爲數據頁可能有部分記錄損壞,須要過濾掉「壞」的數據,保留好的數據
例如:前兩行記錄實際是「壞」數據,須要過濾掉。

root@test:~/recovery/undrop-for-innodb# ./c_parser -6f pages-actor.ibd/FIL_PAGE_INDEX/0000000000000015.page -t sakila/actor.sql > dumps/default/actor 2> dumps/default/actor_load.sql
root@test:~/recovery/undrop-for-innodb# cat dumps/default/actor
-- Page id: 3, Format: COMPACT, Records list: Invalid, Expected records: (0 200)
72656D756D07    08000010002900  actor   30064   "\0\0\0\0"      ""      "1972-09-20 23:07:44"
1050454E454C    4F50454755494E  actor   19713   "ESSC▒" ""      "2100-08-09 07:52:36"
00000000051E    9F0000014D011A  actor   2       "NICK"  "WAHLBERG"      "2006-02-15 04:34:33"
00000000051E    9F0000014D0124  actor   3       "ED"    "CHASE" "2006-02-15 04:34:33"
00000000051E    9F0000014D012E  actor   4       "JENNIFER"      "DAVIS" "2006-02-15 04:34:33"
00000000051E    9F0000014D0138  actor   5       "JOHNNY"        "LOLLOBRIGIDA"  "2006-02-15 04:34:33"
00000000051E    9F000001414141  actor   6       "AAAAA" "AAAAAAAAA"     "2004-09-10 01:53:05"
00000000051E    9F0000014D016A  actor   10      "CHRISTIAN"     "GABLE" "2006-02-15 04:34:33"
...

能夠在sql文件中加上篩選條件,好比:經過actor_id作範圍篩選,再用新的sql文件讀數據頁。

CREATE TABLE `actor` (
  `actor_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT
    /*!FILTER
     int_min_val: 1
     int_max_val: 300 */,
  `first_name` varchar(45) NOT NULL,
  `last_name` varchar(45) NOT NULL,
  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`actor_id`),
  KEY `idx_actor_last_name` (`last_name`)
) ENGINE=InnoDB AUTO_INCREMENT=201 DEFAULT CHARSET=utf8;

4.刪除故障表文件,innodb_force_recovery=6啓動MySQL,啓動後刪除元數據
5.建立新表導入恢復好的數據

疑問:如何知道丟失了多少記錄?
讀取數據頁時開頭會顯示指望的記錄數,最後會顯示實際恢復的記錄數,差值即是丟失記錄數

-- Page id: 3, Format: COMPACT, Records list: Invalid, Expected records: (0 200)
-- Page id: 3, Found records: 197, Lost records: YES, Leaf page: YES

場景3:磁盤或文件系統損壞如何恢復數據

這種狀況下儘快保護損壞的塊設備不要再寫入,並用 dd 工具讀取鏡像數據用做恢復
本地方式

dd if=/dev/sdb of=/path/to/faulty_disk.img  conv=noerror

遠程方式

remote server> nc -l 1234 > faulty_disk.img
local server> dd if=/dev/sdb of=/dev/stdout  conv=noerror | nc a.b.c.d 1234

保存好磁盤鏡像後,後續恢復操做參考場景2。

總結

1.千萬不要在服務運行時把copy數據文件做爲備份方式,看似備份了數據,但實際數據是不一致的。
2.正確的使用物理備份工具xtrabackup/meb或邏輯備份方式。
3.對備份數據要按期進行恢復驗證測試。

但願你永遠不會用到這些方法,作好備份,勤驗證!

參考
https://twindb.com/how-to-rec...
https://twindb.com/recover-co...
https://twindb.com/take-image...
https://twindb.com/data-loss-...
https://twindb.com/repair-cor...
https://twindb.com/resolving-...
https://dev.mysql.com/doc/ref...
相關文章
相關標籤/搜索