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
💥
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:
ma oggi mi sentivo fortunato 🍀 ... quasi affogando nello spam ho trovato un'informazione interessante:
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
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
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!
Recuperare un database MySQL corrotto