Sélectionner une page

Chez Octopuce, on gère des MariaDB (une version de MySQL non possédée par Oracle ;) ) en masse. (sans fouiller on en trouve 206 à ce jour) Et qui dit service hébergé en masse, dit problème plus fréquents. Et si MariaDB est un logiciel très fiable et très stable, il peut arriver que des données se perdent ou des structures se corrompent, typiquement à cause d’une mémoire défaillante non détectée malgré l’ECC ou de divers rayons cosmiques sur les SSD ou disques durs … (ou encore d’un bug dans MariaDB, avouons-le)

Récemment, nous avons eu un cas problématique : un MariaDB hébergeant peu de données (72G) mais beaucoup de petites tables (~240 000 !). Ce MariaDB nous a planté en pleine journée, problème rarissime, mais surtout il nous a planté salement :

2021-04-08 17:29:55 139989363230464 [ERROR] InnoDB: Clustered record field for column 0 not found table n_user_defined 1 index n_user_defined 39 InnoDB table databasename/pre_formule field name id MySQL table pre_formule field name id n_fields 39 query SELECT * FROM pre_formule LIMIT 1
2021-04-08 17:29:55 7f51d0448700 InnoDB: Assertion failure in thread 139989363230464 in file ha_innodb.cc line 8172
InnoDB: Failing assertion: templ->clust_rec_field_no != ULINT_UNDEFINED
InnoDB: We intentionally generate a memory trap.
InnoDB: Submit a detailed bug report to https://jira.mariadb.org/
InnoDB: If you get repeated assertion failures or crashes, even
InnoDB: immediately after the mysqld startup, there may be
InnoDB: corruption in the InnoDB tablespace. Please refer to
InnoDB: http://dev.mysql.com/doc/refman/5.6/en/forcing-innodb-recovery.html
InnoDB: about forcing recovery.
210408 17:29:55 [ERROR] mysqld got signal 6 ;
This could be because you hit a bug. It is also possible that this binary
or one of the libraries it was linked against is corrupt, improperly built,
or misconfigured. This error can also be caused by malfunctioning hardware.

Quand on a ce genre de message d’erreur, on sait qu’on va en avoir d’autres si on ne fait rien et que le serveur risque de planter régulièrement… Le MySQL a planté et nous avons mis près de 15 minutes à le relancer, le temps qu’il rejoue ses journaux de transaction… (Ce client n’ayant pas de slave MySQL sur ce serveur, la coupure fut franche et douloureuse…)

Pour se sortir de ce genre de situation, nous avons trouvé il y a quelques années une solution simple, un peu lourde, mais très efficace : refaire totalement les structures de données binaires de MariaDB.

Recréer les structures de données de MariaDB de zéro

1. Mettre en place un futur serveur secondaire / slave

Pour cela, on procède par l’installation d’un serveur secondaire de réplication MySQL (appelé aussi « slave ») sur la base de la dernière sauvegarde perconabackup que l’on réalise sur nos serveurs toutes les nuits, destination : un serveur MariaDB temporaire de même version. On recopie données et configuration, et on prépare un compte de réplication :

client:~# rsync /var/backups/percona/ root@sqldev:/var/lib/mysql/ -aPHSA 
client:~# rsync /etc/mysql/ root@sqldev:/etc/mysql/ -aPHSA --delete
client:~# mysql> GRANT REPLICATION SLAVE ON *.* TO 'sqldev'@'%' IDENTIFIED BY 'motdepassedur';

Ensuite, on démarre ce MariaDB là, (les sauvegarde innobackup doivent être « préparées » pour être restaurées, à l’aide de l’outil innobackupex) et on effectue un dump de toutes ses bases. On n’oublie pas de noter à quel emplacement le serveur primaire était dans son fichier « master » au moment de la sauvegarde :

sqldev:~# innobackupex --apply-log /var/lib/mysql
sqldev:~# service mysql start 
sqldev:~# cat /var/lib/mysql/xtrabackup_binlog_info
mysql-bin.001922    84437488    0-1-646142986

2. Dump de toutes les bases MySQL

Ensuite, on crée un dump de toutes les bases de ce serveur, à l’aide de notre script de dump quotidien (aussi), qui crée un mysqldump de toutes les bases (y compris la base « mysql » mais pas les bases virtuelles comme « information_schema« ) Ce dump est une version texte du contenu des bases, pas binaire. Si on le restaure ensuite sur un MariaDB « tout neuf », on sera certain que les données seront les mêmes, mais avec des structures de fichier binaires .ibd / .MYD / .MYI non corrompues.

cd /var/backups/mysql/$date
for database in $(mysql --defaults-file=/etc/mysql/debian.cnf -Bs \
-e "show databases"|grep -v "^performance_schema$"|grep -v "^information_schema$")
do
mysqldump --defaults-file=/etc/mysql/debian.cnf --default-character-set=utf8mb4 --skip-opt \
--force --routines --add-drop-table --create-options --disable-keys \
--extended-insert --quick --set-charset $database \
--events --skip-lock-tables | nice pbzip2 > $database.sql.bz2
done

3. Restauration des dumps sur un MariaDB « nu »

ce script nous a généré des fichiers « .sql.bz2 » dans le dossier /var/backups/mysql/2021-04-09. Afin de nous simplifier le travail ensuite on va sortir le fichier « mysql.sql.bz2 » et enlever cette extension :

sqldev:~/var/backups/mysql/# cd 2021-04-09
sqldev:~/var/backups/mysql/2021-04-09# mv mysql.sql.bz2 ../ && rename 's/.sql.bz2$//' *

Ensuite, on termine le MariaDB qui tourne, on lui purge ses fichiers, et on le relance « à nu » :

sqldev:~# service mysql stop
sqldev:~# rm -rf /var/lib/mysql/*  # ne faites pas ça chez vous :) 
sqldev:~# mysqld --skip-grant

La dernière commande lance un MariaDB sans aucune gestion de droits, donc il veut bien démarrer même sans base nommée « mysql » comprenant les permissions et comptes. On restaure la base MySQL, on recrée les autres bases à vide, et on redémarre le MariaDB en mode normal :

sqldev:~# mysql -e "create database mysql" 
sqldev:~# bzcat /var/backups/mysql/mysql.sql.bz2|mysql -f mysql
sqldev:~# cd /var/backups/mysql/2021-04-09/ ; for i in * ; do mysql -e "CREATE DATABASE $i" ; done 
sqldev:~# service mysql stop
sqldev:~# service mysql start

Maintenant, on restaure tous les dumps. Pour cela, on peut le faire avec une boucle for, base par base, mais notre machine disposant de plusieurs cœurs de processeur, de pas mal de RAM et de disques SSD, autant paralléliser au maximum.

3Bis. Où l’on découvre que InnoDB ne sait pas créer beaucoup de tables (rapidement)

En restaurant les bases de ce client, avec GNU-parallel comme on le verra plus bas et comme on le fait d’habitude dans cette situation, on a découvert que pour restaurer 240 000 tables, il nous faudrait 44H rien que pour les CREATE TABLE !! Les professionnels de Percona semblent d’accord avec nous : MySQL passe beaucoup de temps à s’assurer que tout est bien écrit sur disque dans ce genre de situation, multipliant les sync() fsync() ou O_DIRECT (des commandes demandant au noyau Linux de s’assurer que les données ou métadonnées sont bien écrites sur disque, pas juste dans un cache en écriture temporaire).

Aussi, dans cette situation, j’ai tenté une idée iconoclaste pour les professionnels : désactiver totalement les appels à fsync() sync() & cie. ! Je connais un outil formidable pour cela : EatMyData, qui, comme son nom l’indique (traduction: « dévore mes données !« ) , vous met dans une situation à risque où le moindre plantage vous assurera de perdre des données. N’utilisez donc pas cet outil sans savoir ce que vous faites ! (ici sur un secondaire MySQL, donc aucun risque : on peut recommencer si besoin). Pour cela je lance mysql avec eatmydata en préfixe :

sqldev:/etc/init.d emacs mysql


et on redémarre mysql avec service mysql restart

3Ter. On restaure en parallèle

Pour restaurer les bases en parallèle, je produis d’abord un script shell demandant la restauration « une base à la fois » :

sqldev:~# cd /var/backups/mysql/2021-04-09
sqldev:~/var/backups/mysql/2021-04-09# for i in * ; do echo "bzcat '$i'|mysql -f '$i'" ;done >/root/restaure.sh
sqldev:~/var/backups/mysql/2021-04-09# chmod a+x /root/restaure.sh

puis on le lance avec gnu-parallel, en précisant le nombre de core que l’on souhaite utiliser (ici 4 fois le nombre de core de la machine, histoire d’être sûr que cela sature bien les diverses ressources, IO & CPU principalement)

sqldev:~/var/backups/mysql/2021-04-09# parallel -j 32 </root/restaure.sh

et là on attend :) Une première estimation en le faisant en parallèle mais sans eatmydata me donnait 44H de temps de calcul. Avec eatmydata, cela a pris … 3H et 12 minutes (y compris le « service mysql stop » et le « sync » final !)

Ne pas oublier d’enlever le « eatmydata » du script de démarrage de mysql ensuite !

4. On configure le secondaire / slave et on rattrape le retard

À partir de là, on peut redémarrer le MariaDB et le configurer pour de bon comme un secondaire du serveur principal de production :

sqldev~# service mysql start
sqldev~# mysql
mysql> CHANGE MASTER TO MASTER_HOST='client', MASTER_USER='sqldev', MASTER_PASSWORD='motdepassedur', MASTER_LOG_FILE='mysql-bin.001922', MASTER_LOG_POS=84437488;
mysql> START SLAVE;
mysql> SHOW SLAVE STATUS;

Et là, on attend que le secondaire rattrape son retard…

Et voila ! On a, sans interruption du serveur de production, recréé un MariaDB « neuf » avec des structures binaires « neuves / propres » contenant exactement les mêmes données que celui de production.

Idéalement, on utilisera l’outil de comparaison fourni par Percona, qui permet de s’assurer que le serveur primaire et le secondaire ont véritablement les mêmes données, avant de basculer la production sur ce serveur là !