Recuperare un database MySQL corrotto

Gianluigi Tiesi

Recuperare un database MySQL/MariaDB corrotto utilizzando i data files (InnoDB)

❓ Cause

Un database MySQL corrotto può dipendere da molteplici cause, la maggior parte delle volte è possibile risolvere con gli strumenti integrati di repair.

Mi è capitato però di ritrovarmi dei database corrotti a causa di immagini Docker MySQL o MariaDB con versioni non pinnate.

Vorrei ricordare che utilizzare :latest in un'immagine docker è un'attività terroristica, specialmente per i database.

Le immagini Docker ufficiali spesso offrono un percorso di aggiornamento ma non garantiscono il funzionamento saltando più di una versione.

✔️ Prerequisiti

  • Linux con Docker funzionante
  • Una minima conoscenza di docker
  • Una directory di lavoro
  • La directory /var/lib/mysql corrotta del database che ci interessa recuperare (Io ho provato solo InnoDB), in questo caso è il database di BookStack
  • I comandi MySQL con -p  richiedono di inserire ogni volta la password ma se è vuota basta premere invio, in alternativa omettere il -p

🏁  Si parte

Utilizzeremo delle utilities di MySQL (archiviate) per ottenere il DDL del database, questo passo abbastanza rognoso è possibile saltarlo se si ha un vecchio dump da cui estrarre lo schema.

Purtroppo questi tool non funzionano correttamente con MariaDB e le versioni di MySQL maggiori della 5, quindi utilizzeremo l'ultima immagine MySQL 5 disponibile (al momento 5.7).

Copiamo la directory mysql  danneggiata nella vostra directory di lavoro.

I files devono avere uid e gid dell'utente mysql dell'immagine docker (999), quindi:

sudo chown -R 999:999 mysql

Facciamo partire un container (effimero va bene):

docker run --name mysql5 --rm -it \
    --mount type=bind,src=$(pwd)/mysql,dst=/var/lib/mysql \
    mysql:5.7
2020-12-31 01:54:03+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.32-1debian10 started. 2020-12-31 01:54:03+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql' 2020-12-31 01:54:03+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.32-1debian10 started. 2020-12-31T01:54:03.392643Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details). 2020-12-31T01:54:03.393619Z 0 [Note] mysqld (mysqld 5.7.32) starting as process 1 ... 2020-12-31T01:54:03.395877Z 0 [Note] InnoDB: PUNCH HOLE support available 2020-12-31T01:54:03.395887Z 0 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins 2020-12-31T01:54:03.395890Z 0 [Note] InnoDB: Uses event mutexes 2020-12-31T01:54:03.395892Z 0 [Note] InnoDB: GCC builtin __atomic_thread_fence() is used for memory barrier 2020-12-31T01:54:03.395894Z 0 [Note] InnoDB: Compressed tables use zlib 1.2.11 2020-12-31T01:54:03.395896Z 0 [Note] InnoDB: Using Linux native AIO 2020-12-31T01:54:03.396148Z 0 [Note] InnoDB: Number of pools: 1 2020-12-31T01:54:03.396293Z 0 [Note] InnoDB: Using CPU crc32 instructions 2020-12-31T01:54:03.397472Z 0 [Note] InnoDB: Initializing buffer pool, total size = 128M, instances = 1, chunk size = 128M 2020-12-31T01:54:03.404588Z 0 [Note] InnoDB: Completed initialization of buffer pool 2020-12-31T01:54:03.406334Z 0 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority(). 2020-12-31T01:54:03.418103Z 0 [Note] InnoDB: Highest supported file format is Barracuda. 2020-12-31T01:54:03.419267Z 0 [Note] InnoDB: Log scan progressed past the checkpoint lsn 35671581 2020-12-31T01:54:03.419276Z 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 35674967 2020-12-31T01:54:03.419305Z 0 [ERROR] InnoDB: Plugin initialization aborted with error Generic error 2020-12-31T01:54:04.021325Z 0 [ERROR] Plugin 'InnoDB' init function returned error. 2020-12-31T01:54:04.021375Z 0 [ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed. 2020-12-31T01:54:04.021390Z 0 [ERROR] Failed to initialize builtin plugins. 2020-12-31T01:54:04.021399Z 0 [ERROR] Aborting 2020-12-31T01:54:04.021432Z 0 [Note] Binlog end 2020-12-31T01:54:04.021519Z 0 [Note] Shutting down plugin 'CSV' 2020-12-31T01:54:04.022648Z 0 [Note] mysqld: Shutdown complete

💥 Boooooom!! 💥


beh se funzionasse non sareste qui vero?
Ritentiamo con una shell:

docker run --name mysql5 --rm -it \
    --entrypoint=/bin/bash \
    --mount type=bind,src=$(pwd)/mysql,dst=/var/lib/mysql \
    mysql:5.7

Per far partire manualmente MySQL utilizziamo il comando:

gosu mysql mysqld

gosu  è una sorta di su  scritto in go che ho trovato all'interno dell'immagine docker di MySQL, è comodo perché non occorre quotare gli argomenti e usare -c , d'altra parte l'entrypoint dell'immagine lo usa per lanciare il server, chi sono io per non essere d'accordo (cit.)?

Ovviamente l'output è lo stesso...avevate dubbi?

🏃‍♂️ Soluzione veloce 

Se siamo fortunati questa soluzione può andar bene, ma potrebbe non avere tutti i record aggiornati.

Per prima cosa bisogna eliminare i files  ib_logfile0  e ib_logfile1 , nella cartella  /var/lib/mysql  (tanto avete una copia no?), ora facciamo partire MySQL (gosu mysql mysqld ) e, per la serie moriremo di log, un miliardo di questi:

2020-12-31T01:28:34.500262Z 5 [ERROR] InnoDB: Page [page id: space=111, page number=169] log sequence number 33548601 is in the future! Current system log sequence number 32382614. 2020-12-31T01:28:34.500269Z 5 [ERROR] InnoDB: Your database may be corrupt or you may have copied the InnoDB tablespace but not the InnoDB log files. Please refer to http://dev.mysql.com/doc/refman/5.7/en/forcing-innodb-recovery.html for information about forcing recovery.

ma oggi mi sentivo fortunato 🍀 ... quasi affogando nello spam ho trovato un'informazione interessante:

2020-12-31T01:27:59.989337Z 0 [Warning] InnoDB: Table mysql/innodb_table_stats has length mismatch in the column name table_name. Please run mysql_upgrade 2020-12-31T01:27:59.989374Z 0 [Warning] InnoDB: Table mysql/innodb_index_stats has length mismatch in the column name table_name. Please run mysql_upgrade

Questo significa che la versione dei data files è inferiore a quella della nostra immagine (5.7), l'avevo detto che oggi mi sentivo fortunato...

apriamo un'altra shell ed entriamo nel container


                
  • docker exec -it mysql5 /bin/bash

                
  • mysql_upgrade -p
Checking if update is needed. Checking server version. Running queries to upgrade MySQL server. Checking system database. mysql.columns_priv OK mysql.db OK ... bookstack.activities OK bookstack.api_tokens OK bookstack.attachments OK bookstack.books OK bookstack.bookshelves OK bookstack.bookshelves_books OK bookstack.cache OK bookstack.chapters OK bookstack.comments OK bookstack.email_confirmations OK bookstack.entity_permissions OK bookstack.images OK bookstack.joint_permissions OK bookstack.migrations OK bookstack.page_revisions OK bookstack.pages OK bookstack.password_resets OK bookstack.permission_role OK bookstack.role_permissions OK bookstack.role_user OK bookstack.roles OK bookstack.search_terms OK bookstack.sessions OK bookstack.settings OK bookstack.social_accounts OK bookstack.tags OK bookstack.user_invites OK bookstack.users OK bookstack.views OK sys.sys_config OK

Continuiamo a sentirci fortunati? Facciamo un dump

mysqldump -p bookstack > /tmp/bookstack.sql

controlliamo il file con more /tmp/bookstack.sql  (potete installare less o vim con il comando apt), se è di nostro gradimento possiamo recuperare il file dall'host con:

docker cp mysql5:/tmp/bookstack.sql 

🤔 Ok, ma io ho dimenticato la password di MySQL

Oppure non l'ho mai saputa ed essendo le 3 di notte non posso telefonare al proprietario del database per farmela dare (ad essere sinceri probabilmente non la conosce nemmeno).

Cosa si fa? Spegniamo il container (o dall'altra shell killall mysqld )

Torniamo alla shell principale e creiamo un init file per MySQL:

echo "ALTER USER 'root'@'localhost' IDENTIFIED BY '';" > /tmp/init-file.txt

Facciamo partire MySQL con il comando:

gosu mysql mysqld --init-file=/tmp/init-file.txt

Torniamo alla seconda shell e questa volta non abbiamo bisogno di password.

🥱 Se oggi non mi sento fortunato 

Mi è capitato un altro database MySQL che non riconosceva nessuna tabella del database in questione, quindi anche riparando le tabelle avevano i data files sul filesystem ma MySQL asseriva di non trovarne nessuna.

Se avete già provato la soluzione veloce e state leggendo questo paragrafo, spegnete tutti i container, cancellate la directory mysql e ricopiate quella originale, non dimenticate chown .

Ovviamente si inizia con una shell:

docker run --name mysql5 --rm -it \
    --entrypoint=/bin/bash \
    --mount type=bind,src=$(pwd)/mysql,dst=/var/lib/mysql \
    mysql:5.7

Copiamo dall'host l'archivio con le utilities:

docker cp mysql-utilities-1.6.5.tar.gz mysql5:/tmp/

Torniamo al container e scompattiamo l'archivio:

  • cd /tmp/
  • tar xvf mysql-utilities-1.6.5.tar.gz

Ora ci serve Python , quindi:

apt update && apt install -y python

proseguiamo:


                
  • cd /tmp/mysql-utilities-1.6.5

                
  • python setup.py install

vediamo cosa possiamo fare:

esiste una modalità diagnostica dello script che tira fuori qualcosa, ma spesso salta tabelle e non estrae informazioni essenziali, quindi bisogna che il server sia su, cosa al momento non possibile finché non elimino i Binlog.

Nell'esame fatto in precedenza sembra che i Binlog siano vuoti quindi procediamo alla potatura (a MySQL spento, comunque non partiva, ma è sempre meglio essere attenti).

Rechiamoci nella cartella /var/lib/mysql  ed eliminiamo i files ib_logfile0  e ib_logfile1

Ora facciamo ripartire MySQL:
gosu mysql mysqld

Apriamo una seconda shell sul container:

docker exec -it mysql5 /bin/bash

per prima cosa dobbiamo abilitare l'accesso root a 127.0.0.1 , non so per quale motivo le utilities usino il numerico:

avviamo MySQL cl (mysql mysql )

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '';
FLUSH PRIVILEGES

Verifichiamo che funzioni con:

mysql -h 127.0.0.1 mysql

Ora ritentiamo "forte":

mysqlfrm \
    --server=root@localhost --port 3307 \
    --user mysql \
    /var/lib/mysql/bookstack | grep -Ev '^(#|ERROR:)' > /tmp/bookstack-ddl.sql

La porta 3307 è per la seconda istanza di MySQL che tira su.

Ora sembra ci sia tutto (controllate con grep -F 'CREATE TABLE' bookstack-ddl.sql)

Torniamo all'host e recuperiamo il file:

docker cp mysql5:/tmp/bookstack-ddl.sql .

Il nostro amato tool non inserisce i  ;  alla fine degli statement CREATE TABLE , ora potreste farvelo a mano con un editor, ma se guardiamo com'è fatta l'ultima linea della CREATE TABLE  possiamo usare sed:

CREATE TABLE `bookstack`.`activities` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
...

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
sed -i -e 's/COLLATE=utf8mb4_unicode_ci$/COLLATE=utf8mb4_unicode_ci;/g' bookstack-ddl.sql

Spegniamo i due container e facciamone un altro, questa volta senza passargli il database, il container ne creerà uno vuoto:

docker run --name mysql5 --rm -it \
    -e MYSQL_ALLOW_EMPTY_PASSWORD=yes \
    mysql:5.7

Copiamolo il DDL nel nuovo container:

docker cp bookstack-ddl.sql mysql5:/tmp/

apriamo una seconda shell nel container:

docker exec -it mysql5 /bin/bash

creiamo e importiamo lo schema del database:


                
  • mysqladmin create bookstack

                
  • mysql bookstack < /tmp/bookstack-ddl.sql
se qualcosa non va bene nel DDL, modificatelo, droppate il database il database ( mysqladmin drop bookstack) e riprovate, non posso mica fare tutto io.

🤹 I giocolieri

Questa è la parte più "maranza" del recupero:

🪄 [ Magia 1

(
    for table in $(mysql -BN -e 'show tables from bookstack;'); do \
        echo "ALTER TABLE $table DISCARD TABLESPACE;"; \
    done
) | mysql bookstack

Torniamo all'host entriamo nella cartella mysql corrotta, ora nella sottocartella bookstack e copiamo i files *.frm nel container:

for idb in *.ibd; do \
    docker cp $idb mysql5:/var/lib/mysql/bookstack/; \
done

Torniamo alla shell secondaria nel container e sistemiamo l'ownership dei files:

chown -R mysql:mysql /var/lib/mysql/bookstack


🪄 [ Magia 2 ]

(
    for table in $(mysql -BN -e 'show tables from bookstack;'); do \
        echo "ALTER TABLE $table IMPORT TABLESPACE;"; \
    done
) | mysql bookstack

Ora possiamo effettuare il dump:

mysqldump -p bookstack > /tmp/bookstack.sql

controlliamo il file con more /tmp/bookstack.sql  (potete installare less o vim con il comando apt), se è di nostro gradimento possiamo recuperare il file dall'host con:

docker cp mysql5:/tmp/bookstack.sql .

🔚 Conclusioni

Confrontando i due dump, con il secondo metodo ho perso delle CONSTRAINT  sulle FOREIGN KEY  ed una tabella, quindi occorre sempre ricontrollare lo schema, in questo caso il metodo veloce ha funzionato, in un altro recupero invece il database non vedeva proprio le tabelle, quindi è stato il meglio che si poteva fare.

Probabilmente si perdono anche gli indici, in BookStack non ne ho trovati e tipicamente le applicazioni che usano PHP/MySQL non ne usano.

Pensavo mysql_upgrade  non servisse in realtà mi sono accorto che ha "resuscitato" la tabella joint_permissions  che invece non risulta senza fare l'upgrade e nella modalità "disperato".

Insomma fate varie prove e tenete sempre da parte la copia originale della cartella  /var/lib/mysql  corrotta.

PS : Non sono amico dei database e quando proprio devo uso PostgreSQL!