MySQL: Retter in der Not.

MySQL: Retter in der Not.

Manchmal kommt man als DBA in „schwere See". Neulich hatte ein Kunde das Problem, dass nach einem Hardware-Crash eine seiner InnoDB-Tabellen unlesbar war. Diagnose: Eine korrupte Tabelle.

Wir haben dieses Problem noch einmal intern anhand der MySQL-Demodatenbank „sakila" nachgestellt.

Kein Rettungsring…

Ein Backup wurde von diesem System seit geraumer Zeit leider nicht mehr angefertigt. Gibt es trotzdem einen Weg an die geliebten Daten zu kommen? Ja, den gibt es…(vielleicht)…

Für die folgende Rettungsaktion wird das TwinDB data recovery toolkit benutzt:

https://twindb.com/data-recovery-toolkit/
https://github.com/twindb/undrop-for-innodb

Search & Rescue…

Im ersten Schritt musste das Problem erst einmal verifiziert werden:

innochecksum /mysql/db01/sakila/actor.ibd
Fail: page 3 invalid
Exceeded the maximum allowed checksum mismatch count::0

Ok, diese Tabelle befindet sich tatsächlich in einer extremen „Schieflage". Ein Zugriff auf diese Tabelle war zu diesem Zeitpunkt über den Server ebenfalls nicht mehr möglich und führte sogar zum Absturz des mysqld-Prozesses.

Auch der Versuch die Tabelle mit „mysqlcheck" zu analysieren verlieft erfolglos und mit einem Absturz des Systems.

bash> mysqlcheck sakila actor
mysqlcheck: Got error: 2013: Lost connection to MySQL server during query when executing 'CHECK TABLE ... '

Die Rettung beginnt…

Im ersten Schritt wird die korrupte Datei gelesen und der Inhalt derselbigen Page für Page kopiert:

bash> /stream_parser -f /mysql/db01/sakila/actor.ibd
Opening file: /mysql/db01/sakila/actor.ibd
File information:

ID of device containing file: 64774
inode number: 161849
protection: 100640 (regular file)
number of hard links: 1
user ID of owner: 27
group ID of owner: 27
device ID (if special file): 0
blocksize for filesystem I/O: 4096
number of blocks allocated: 264
time of last access: 1554279976 Wed Apr 3 10:26:16 2019
time of last modification: 1554279935 Wed Apr 3 10:25:35 2019
time of last status change: 1554279935 Wed Apr 3 10:25:35 2019
total size, in bytes: 131102 (128.029 kiB)
Size to process: 131102 (128.029 kiB)
All workers finished in 0 sec

Die „geretteten" Daten werden in einem gesonderten Verzeichnis abgelegt:

ls -lR pages-actor.ibd/
pages-actor.ibd/:
insgesamt 0
drwxr-xr-x. 2 root root 64 3. Apr 10:59 FIL_PAGE_INDEX
drwxr-xr-x. 2 root root 35 3. Apr 10:59 FIL_PAGE_TYPE_BLOB

Die Rettungsaktion …

Um aus den physikalischen Informationen die Daten extrahieren zu können, benötigen wir noch das DDL-Kommando für den Aufbau / die Struktur der Tabelle (CREATE TABLE actor…). In unserem Fall wurde diese Information aus dem Skript der Datenbank (sakila-schema.sql) extrahiert. Natürlich würden sich aus alte Dumps der Datenbank dafür anbieten:

bash> cat actor.sql

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;

Mit diesen beiden Informationen (Pages der Daten und Indices und SQL / DDL Kommando) kann nun versucht werden die Rettung zu starten.

./c_parser -6f pages-actor.ibd/FIL_PAGE_INDEX/0000000000000166.page -t actor.sql

-- Page id: 4, Format: COMPACT, Records list: Valid, Expected records: (200 200)
0000000013EA 82000000E90110 actor 1 "PENELOPE""GUINESS" "2006-02-15 04:34:33"
0000000013EA 82000000E9011B actor 2 "NICK" "WAHLBERG""2006-02-15 04:34:33"
0000000013EA 82000000E90126 actor 3 "ED" "CHASE""2006-02-15 04:34:33"
0000000013EA 82000000E90131 actor 4 "JENNIFER""DAVIS""2006-02-15 04:34:33"

0000000013EA 82000000E909D8 actor 199 "JULIA""FAWCETT" "2006-02-15 04:34:33"
0000000013EA 82000000E909E4 actor 200 "THORA""TEMPLE" "2006-02-15 04:34:33"
SET FOREIGN_KEY_CHECKS=0;
LOAD DATA LOCAL INFILE '/opt/mysql/undrop-for-innodb-master/ dumps/default/actor' REPLACE INTO TABLE `actor` CHARACTER SET UTF8 FIELDS TERMINATED BY '\t' OPTIONALLY ENCLOSED BY '"' LINES STARTING BY 'actor\t' (`actor_id`, `first_name`, `last_name`, `last_update`);
-- STATUS {"records_expected": 200, "records_dumped": 200, "records_lost": false} STATUS END
-- Page id: 4, Found records: 200, Lost records: NO, Leaf
page: YES

Wie unschwer zu erkennen ist, enthält die Ausgabe exakt die gewünschten Tabellen-Informationen.

mysql> desc actor,
+-----------+--------------------+----+---+-----------------+---------------------------+
|Field      |Type                |Null|Key|Default          |Extra                      |
+-----------+--------------------+----+---+-----------------+---------------------------+
|actor_id   |smallint(5) unsigned|NO  |PRI|NULL             |auto_increment             |
|first_name |varchar(45)         |NO  |   |NULL             |                           |
|last_name  |varchar(45)         |NO  |MUL|NULL             |                           |
|last_update|timestamp           |NO  |   |CURRENT_TIMESTAMP|on update CURRENT_TIMESTAMP|
​+-----------+--------------------+----+---+-----------------+---------------------------+

Altes Seemansgarn…

Das ist gerade noch einmal gut gegangen. In diesem Fall (!) konnten die Daten tatsächlich vollständig gerettet werden ("records_lost": false). So schön es ist, dass es solche Tools auf dem Markt gibt, sie ersetzen jedoch keine sinnvolle Backup-Strategie.

Darüber hinaus erkennt man auch die Gefahr die mit solchen Tools einhergeht. Daten können auch ohne gültigen DB-Account gelesen werden. Und wer weiß denn immer ganz genau wann und wo Filesystem-Backups erstellt und verwahrt werden. Um einen Zugriff auf seine sensiblen Daten besser zu schützen, sollte man in diesem Zusammenhang ggfs. auch über eine Verschlüsselung von Tabellen nachdenken. Das ist aber ein ganz anderes Thema.

 

Kommentare

Derzeit gibt es keine Kommentare. Schreibe den ersten Kommentar!
Gäste
Samstag, 20. Juli 2019

Sicherheitscode (Captcha)