當備份失效、Binlog 不全、延遲從庫也被污染時,**Percona Data Recovery Tool (innodb-tools)** 是唯一能從原始 `.ibd` / `ibdata1` 頁面層面提取行記錄的開源工具。本文記錄實戰全流程:編譯、頁面切分、Index ID 定位、表結構映射、行提取、資料導入,附避坑指南。
⚠️ 核心前提 (必讀):
mysql> TRUNCATE TABLE customer;
# 依賴:gcc, make, cmake (舊版用 configure)
wget https://launchpad.net/percona-data-recovery-tool-for-innodb/trunk/release-0.5/+download/percona-data-recovery-tool-for-innodb-0.5.tar.gz
tar -zxvf percona-data-recovery-tool-for-innodb-0.5.tar.gz
cd percona-data-recovery-tool-for-innodb-0.5
# 配置 (引用 MySQL 源碼內建 headers)
cd mysql-source
./configure
cd ..
# 編譯產出 page_parser、constraints_parser
make
產出:`page_parser` (頁面切分)、`constraints_parser` (行提取,需二次編譯注入表結構)。
InnoDB 預設頁面 16KB。依 `innodb_file_per_table` 分兩種情況:
通常直接用 `.ibd` 即可,但若表有多個索引 (主鍵+二級索引),仍建議切分以隔離主鍵頁面。
# MySQL 5.0+ (COMPACT 格式)
./page_parser -5 -f /path/to/table.ibd
# MySQL 5.0 前 (REDUNDANT 格式)
./page_parser -4 -f /path/to/table.ibd
**必須切分**,從巨大 `ibdata1` 中按 Index ID 拆出目標表頁面。
./page_parser -5 -f /var/lib/mysql/ibdata1
產出目錄結構:`pages-
主鍵 (PRIMARY) 包含完整行記錄,優先恢復主鍵 Index ID。
CREATE TABLE innodb_table_monitor (id INT) ENGINE=InnoDB;
-- 觀察 error log 輸出
-- 範例輸出:
-- TABLE: name test/customer, id 0 142, ...
-- INDEX: name PRIMARY, id 0 286, fields 1/11, type 3
-- root page 50, ...
-- 主鍵 Index ID = 0-286
DROP TABLE innodb_table_monitor;
直接處理 `page_parser` 產出的**所有 Index ID 目錄**,後續用 `constraints_parser` 配合表結構自動過濾。實測可行,省去啟動 MySQL 步驟。
最簡單方式:`create_defs.pl` 連線 MySQL 讀取 `SHOW CREATE TABLE` 自動生成。
# 需有一個可連線的 MySQL (結構相同即可,資料可空)
./create_defs.pl --host=localhost --user=root --password=xxx --db=test --table=customer > include/table_defs.h
# 根據生成的 table_defs.h 重新編譯 constraints_parser
make
table_def_t table_definitions[] = {
{
name: "customer",
columns: {
{ name: "customer_id", type: FT_UINT, fixed_length: 2, has_limits: TRUE, limits: { uint_min_val: 0, uint_max_val: 65535 }, can_be_null: FALSE },
{ name: "DB_TRX_ID", type: FT_INTERNAL, fixed_length: 6, can_be_null: FALSE }, -- InnoDB 隱藏列
{ name: "DB_ROLL_PTR", type: FT_INTERNAL, fixed_length: 7, can_be_null: FALSE }, -- InnoDB 隱藏列
{ name: "store_id", type: FT_UINT, fixed_length: 1, ... },
{ name: "first_name", type: FT_CHAR, min_length: 0, max_length: 45, ... },
...
{ type: FT_NONE } -- 結束標記
}
}
};
**隱藏列 `DB_TRX_ID`、`DB_ROLL_PTR` 必須保留**,位置順序需與 InnoDB 行格式一致 (緊接主鍵之後)。若手工編寫,極易出錯,強烈建議用 `create_defs.pl` 自動生成。
# 將目標 Index ID 目錄下所有 .page 依頁號排序合併
find pages-1246363747/0-286/ -name '*.page' -type f | sort -n | xargs cat > pages-1246363747/0-286/customer_pages_concatenated
# 單頁測試
./constraints_parser -5 -f pages-1246363747/0-286/50-00000050.page
# 全量合併檔案
./constraints_parser -5 -f pages-1246363747/0-286/customer_pages_concatenated > /tmp/customer_recovered.tsv
customer 1 1 "PATRICIA" "JOHNSON" "PATRICIA.JOHNSON@..." 6 1 "2006-02-14 22:04:36" 1140008240
customer 2 1 "LINDA" "WILLIAMS" "LINDA.WILLIAMS@..." 7 1 "2006-02-14 22:04:36" 1140008240
... (夾雜大量全 0/空值/亂碼的髒行) ...
輸出包含:正確行、舊版本行 (MVCC)、未提交事務行、已刪除標記行、頁面碎片亂碼。需根據業務特徵過濾:
LOAD DATA INFILE '/tmp/customer_clean.tsv'
REPLACE INTO TABLE customer
FIELDS TERMINATED BY '\t'
OPTIONALLY ENCLOSED BY '\"'
LINES STARTING BY 'customer\t'
(customer_id, store_id, first_name, last_name, email,
address_id, active, create_date, @last_update)
SET last_update = FROM_UNIXTIME(@last_update);
| 問題 | 原因 | 對策 |
|---|---|---|
| 編譯失敗缺 headers | MySQL 源碼版本不匹配 | 用相同大版本源碼 (如 5.7.33 恢復 5.7.x);或安裝 `libmysqlclient-dev` |
| page_parser 產出空目錄 | 格式參數錯誤 (-4/-5)、檔案非 InnoDB | 確認 `innodb_file_format` (Barracuda=COMPACT/COMPRESSED 用 -5;Antelope=REDUNDANT 用 -4) |
| constraints_parser 全是髒資料 | 表結構定義錯誤 (欄位順序/類型/長度不符) | 嚴格核對 `SHOW CREATE TABLE`、隱藏列順序、CHARSET (utf8mb4 影響 FT_CHAR 長度) |
| 恢復行數遠少於預期 | 頁面已被覆寫、碎片化嚴重、非主鍵索引頁 | 嘗試恢復二級索引頁 (需手工拼接主鍵);接受部分遺失 |
| 大表合併頁面極慢 | 海量 .page 檔案 | 分批處理、並行 `constraints_parser`、SSD 存儲 |
InnoDB 災難恢復是**與時間賽跑、與覆寫博弈**的過程。核心鐵律:
平時多演練、備份驗證、延遲從庫保護,才是真正的「免疫系統」。