Vous pouvez supprimer une base de données MySQL de plusieurs manières. Certains moyens évidents consistent à arrêter l'hôte, à débrancher le câble d'alimentation ou à arrêter le processus mysqld avec SIGKILL pour simuler un comportement d'arrêt incorrect de MySQL. Mais il existe également des moyens moins subtils de planter délibérément votre serveur MySQL, puis de voir quel type de réaction en chaîne cela déclenche. Pourquoi voudriez-vous faire cela? L'échec et la récupération peuvent avoir de nombreux cas particuliers, et les comprendre peut aider à réduire l'élément de surprise lorsque des événements se produisent en production. Idéalement, vous voudriez simuler des défaillances dans un environnement contrôlé, puis concevoir et tester des procédures de basculement de base de données.
Il existe plusieurs domaines dans MySQL auxquels nous pouvons nous attaquer, selon la manière dont vous souhaitez qu'il échoue ou se bloque. Vous pouvez corrompre l'espace de table, déborder les tampons et les caches MySQL, limiter les ressources pour affamer le serveur et également jouer avec les autorisations. Dans cet article de blog, nous allons vous montrer quelques exemples de plantage d'un serveur MySQL dans un environnement Linux. Certains d'entre eux conviendraient par ex. Instances Amazon RDS, où vous n'auriez pas accès à l'hôte sous-jacent.
Tuer, tuer, tuer, mourir, mourir, mourir
Le moyen le plus simple de faire échouer un serveur MySQL consiste simplement à tuer le processus ou l'hôte, et à ne pas donner à MySQL la possibilité de procéder à un arrêt gracieux. Pour simuler un crash mysqld, envoyez simplement le signal 4, 6, 7, 8 ou 11 au processus :
$ kill -11 $(pidof mysqld)
Lorsque vous consultez le journal des erreurs MySQL, vous pouvez voir les lignes suivantes :
11:06:09 UTC - mysqld got signal 11 ;
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.
Attempting to collect some information that could help diagnose the problem.
As this is a crash and something is definitely wrong, the information
collection process might fail.
..
Attempting backtrace. You can use the following information to find out
where mysqld died. If you see no messages after this, something went
terribly wrong...
Vous pouvez également utiliser kill -9 (SIGKILL) pour arrêter le processus immédiatement. Plus de détails sur le signal Linux peuvent être trouvés ici. Alternativement, vous pouvez utiliser une méthode plus efficace du côté matériel, comme débrancher le câble d'alimentation, appuyer sur le bouton de réinitialisation matérielle ou utiliser un dispositif d'escrime pour STONITH.
Déclencher OOM
Les offres MySQL populaires dans le cloud comme Amazon RDS et Google Cloud SQL n'ont aucun moyen simple de les planter. Premièrement parce que vous n'obtiendrez aucun accès au niveau du système d'exploitation à l'instance de base de données, et deuxièmement parce que le fournisseur utilise un serveur MySQL patché propriétaire. L'un des moyens consiste à déborder certains tampons et à laisser le gestionnaire de mémoire insuffisante (OOM) interrompre le processus MySQL.
Vous pouvez augmenter la taille du tampon de tri à quelque chose de plus grand que ce que la RAM peut gérer et lancer un certain nombre de requêtes de tri mysql sur le serveur MySQL. Créons une table de 10 millions de lignes à l'aide de sysbench sur notre instance Amazon RDS, afin de pouvoir créer un énorme tri :
$ sysbench \
--db-driver=mysql \
--oltp-table-size=10000000 \
--oltp-tables-count=1 \
--threads=1 \
--mysql-host=dbtest.cdw9q2wnb00s.ap-tokyo-1.rds.amazonaws.com \
--mysql-port=3306 \
--mysql-user=rdsroot \
--mysql-password=password \
/usr/share/sysbench/tests/include/oltp_legacy/parallel_prepare.lua \
run
Changer le sort_buffer_size à 5G (notre instance de test est db.t2.micro - 1 Go, 1vCPU) en accédant au tableau de bord Amazon RDS -> Groupes de paramètres -> Créer un groupe de paramètres -> spécifiez le nom du groupe -> Modifier les paramètres -> choisissez "sort_buffer_size" et spécifiez la valeur comme 5368709120.
Appliquez les modifications du groupe de paramètres en accédant à Instances -> Action d'instance -> Modifier -> Options de base de données -> Groupe de paramètres de base de données -> et choisissez notre groupe de paramètres nouvellement créé. Ensuite, redémarrez l'instance RDS pour appliquer les modifications.
Une fois en haut, vérifiez la nouvelle valeur de sort_buffer_size :
MySQL [(none)]> select @@sort_buffer_size;
+--------------------+
| @@sort_buffer_size |
+--------------------+
| 5368709120 |
+--------------------+
Lancez ensuite 48 requêtes simples nécessitant un tri à partir d'un client :
$ for i in {1..48}; do (mysql -urdsroot -ppassword -h dbtest.cdw9q2wnb00s.ap-tokyo-1.rds.amazonaws.com -e 'SELECT * FROM sbtest.sbtest1 ORDER BY c DESC >/dev/null &); done
Si vous exécutez ce qui précède sur un hôte standard, vous remarquerez que le serveur MySQL sera terminé et vous pourrez voir les lignes suivantes apparaître dans le syslog ou le dmesg du système d'exploitation :
[164199.868060] Out of memory: Kill process 47060 (mysqld) score 847 or sacrifice child
[164199.868109] Killed process 47060 (mysqld) total-vm:265264964kB, anon-rss:3257400kB, file-rss:0kB
Avec systemd, MySQL ou MariaDB seront redémarrés automatiquement, tout comme Amazon RDS. Vous pouvez voir que la disponibilité de notre instance RDS sera réinitialisée à 0 (sous le statut mysqladmin), et la valeur "Dernière heure de restauration" (sous le tableau de bord RDS) sera mise à jour au moment où elle s'est arrêtée.
Corrompre les données
InnoDB possède son propre espace de table système pour stocker le dictionnaire de données, les tampons et les segments d'annulation dans un fichier nommé ibdata1. Il stocke également l'espace de table partagé si vous ne configurez pas innodb_file_per_table (activé par défaut dans MySQL 5.6.6+). Nous pouvons simplement mettre ce fichier à zéro, envoyer une opération d'écriture et vider les tables pour planter mysqld :
# empty ibdata1
$ cat /dev/null > /var/lib/mysql/ibdata1
# send a write
$ mysql -uroot -p -e 'CREATE TABLE sbtest.test (id INT)'
# flush tables
$ mysql -uroot -p -e 'FLUSH TABLES WITH READ LOCK; UNLOCK TABLES'
Après avoir envoyé une écriture, dans le journal des erreurs, vous remarquerez :
2017-11-15T06:01:59.345316Z 0 [ERROR] InnoDB: Tried to read 16384 bytes at offset 98304, but was only able to read 0
2017-11-15T06:01:59.345332Z 0 [ERROR] InnoDB: File (unknown): 'read' returned OS error 0. Cannot continue operation
2017-11-15T06:01:59.345343Z 0 [ERROR] InnoDB: Cannot continue operation.
À ce stade, mysql se bloquera car il ne peut effectuer aucune opération, et après le vidage, vous obtiendrez des lignes "mysqld got signal 11" et mysqld s'arrêtera. Pour nettoyer, vous devez supprimer les ibdata1 corrompus, ainsi que ib_logfile* car les fichiers de journalisation ne peuvent pas être utilisés avec un nouveau tablespace système qui sera généré par mysqld au prochain redémarrage. Une perte de données est attendue.
Pour les tables MyISAM, nous pouvons jouer avec .MYD (fichier de données MyISAM) et .MYI (index MyISAM) sous le répertoire de données MySQL. Par exemple, la commande suivante remplace toute occurrence de la chaîne "F" par "9" dans un fichier :
$ replace F 9 -- /var/lib/mysql/sbtest/sbtest1.MYD
Ensuite, envoyez des écritures (par exemple, en utilisant sysbench) à la table cible et effectuez le vidage :
mysql> FLUSH TABLE sbtest.sbtest1;
Les éléments suivants doivent apparaître dans le journal des erreurs MySQL :
2017-11-15T06:56:15.021564Z 448 [ERROR] /usr/sbin/mysqld: Incorrect key file for table './sbtest/sbtest1.MYI'; try to repair it
2017-11-15T06:56:15.021572Z 448 [ERROR] Got an error from thread_id=448, /export/home/pb2/build/sb_0-24964902-1505318733.42/rpm/BUILD/mysql-5.7.20/mysql-5.7.20/storage/myisam/mi_update.c:227
La table MyISAM sera marquée comme plantée et l'exécution de l'instruction REPAIR TABLE est nécessaire pour la rendre à nouveau accessible.
Limiter les ressources
Nous pouvons également appliquer la limite de ressources du système d'exploitation à notre processus mysqld, par exemple le nombre de descripteurs de fichiers ouverts. L'utilisation de la variable open_file_limit (la valeur par défaut est 5000) permet à mysqld de réserver des descripteurs de fichiers à l'aide de la commande setrlimit(). Vous pouvez définir cette variable relativement petite (juste assez pour que mysqld démarre) puis envoyer plusieurs requêtes au serveur MySQL jusqu'à ce qu'il atteigne la limite.
Si mysqld s'exécute sur un serveur systemd, nous pouvons le définir dans le fichier d'unité systemd situé dans /usr/lib/systemd/system/mysqld.service, et changer la valeur suivante en une valeur inférieure (la valeur par défaut de systemd est 6000) :
# Sets open_files_limit
LimitNOFILE = 30
Appliquez les modifications à systemd et redémarrez le serveur MySQL :
$ systemctl daemon-reload
$ systemctl restart mysqld
Ensuite, commencez à envoyer de nouvelles connexions/requêtes qui comptent dans différentes bases de données et tables afin que mysqld doive ouvrir plusieurs fichiers. Vous remarquerez l'erreur suivante :
2017-11-16T04:43:26.179295Z 4 [ERROR] InnoDB: Operating system error number 24 in a file operation.
2017-11-16T04:43:26.179342Z 4 [ERROR] InnoDB: Error number 24 means 'Too many open files'
2017-11-16T04:43:26.179354Z 4 [Note] InnoDB: Some operating system error numbers are described at http://dev.mysql.com/doc/refman/5.7/en/operating-system-error-codes.html
2017-11-16T04:43:26.179363Z 4 [ERROR] InnoDB: File ./sbtest/sbtest9.ibd: 'open' returned OS error 124. Cannot continue operation
2017-11-16T04:43:26.179371Z 4 [ERROR] InnoDB: Cannot continue operation.
2017-11-16T04:43:26.372605Z 0 [Note] InnoDB: FTS optimize thread exiting.
2017-11-16T04:45:06.816056Z 4 [Warning] InnoDB: 3 threads created by InnoDB had not exited at shutdown!
À ce stade, lorsque la limite sera atteinte, MySQL se bloquera et ne pourra plus effectuer aucune opération. Lorsque vous essayez de vous connecter, vous voyez ce qui suit au bout d'un moment :
$ mysql -uroot -p
ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 104
Désordre avec les autorisations
Le processus mysqld est exécuté par l'utilisateur "mysql", ce qui signifie que tous les fichiers et répertoires auxquels il doit accéder appartiennent à l'utilisateur/groupe mysql. En bousillant les autorisations et la propriété, nous pouvons rendre le serveur MySQL inutile :
$ chown root:root /var/lib/mysql
$ chmod 600 /var/lib/mysql
Générez des charges sur le serveur, puis connectez-vous au serveur MySQL et videz toutes les tables sur le disque :
mysql> FLUSH TABLES WITH READ LOCK; UNLOCK TABLES;
En ce moment, mysqld est toujours en cours d'exécution mais c'est un peu inutile. Vous pouvez y accéder via un client mysql mais vous ne pouvez effectuer aucune opération :
mysql> SHOW DATABASES;
ERROR 1018 (HY000): Can't read dir of '.' (errno: 13 - Permission denied)
Pour nettoyer le gâchis, définissez les autorisations appropriées :
$ chown mysql:mysql /var/lib/mysql
$ chmod 750 /var/lib/mysql
$ systemctl restart mysqld
Verrouillez-le
FLUSH TABLE WITH READ LOCK (FTWRL) peut être destructeur dans un certain nombre de conditions. Comme par exemple, dans un cluster Galera où tous les nœuds sont capables de traiter les écritures, vous pouvez utiliser cette instruction pour verrouiller le cluster depuis l'un des nœuds. Cette instruction arrête simplement les autres requêtes à traiter par mysqld pendant le vidage jusqu'à ce que le verrou soit libéré, ce qui est très pratique pour les processus de sauvegarde (tables MyISAM) et les instantanés du système de fichiers.
Bien que cette action ne plante pas ou n'arrête pas votre serveur de base de données pendant le verrouillage, la conséquence peut être énorme si la session qui détient le verrou ne le libère pas. Pour essayer ceci, simplement :
mysql> FLUSH TABLES WITH READ LOCK;
mysql> exit
Envoyez ensuite un tas de nouvelles requêtes au mysqld jusqu'à ce qu'il atteigne le max_connections valeur. Évidemment, vous ne pouvez pas reprendre la même session que la précédente une fois que vous êtes sorti. Ainsi, le verrou fonctionnera indéfiniment et la seule façon de libérer le verrou est de tuer la requête, par un autre utilisateur de privilège SUPER (utilisant une autre session). Ou tuez le processus mysqld lui-même, ou effectuez un redémarrage brutal.
Avis de non-responsabilité
Ce blog est écrit pour donner des alternatives aux administrateurs système et aux DBA pour simuler des scénarios d'échec avec MySQL. Ne les essayez pas sur votre serveur de production :-)