Contexte
Un problème a été détecté en production. Par chance, nous disposons d’un dump de la base de données contenant un cas pour lequel nous savons que le bug se produit. Cependant…
-
Le dump ne contient qu’un seul cas pour lequel le bug se produit.
-
La cause du bug n’étant pas connue, on ne sait pas créer un nouveau jeu de données permettant de le reproduire.
-
Le scénario de reproduction du bug modifie les données. Il ne peut donc pas être reproduit deux fois de suite.
Et c’est là que nous avons un problème : le dump en provenance de l’environnement de production pèse quelques dizaines de Go, et met donc beaucoup de temps à être restauré (entre un quart d’heure et une heure). Or, pour bien comprendre ce qui se passe et corriger le problème, il va probablement falloir recharger le jeu de données plusieurs fois.
Idée
Pour résoudre ce problème, nous allons créer une image Docker, contenant la base de données que nous voulons tester. Le scénario sera alors le suivant :
-
On démarre un conteneur basé sur cette image, et on lance l’application, utilisant ce conteneur comme base de données.
-
On joue le scénario que l’on veut tester.
-
Si le test n’est pas conclant, on arrête et et on détruit le conteneur, et on en démarre un nouveau.
Les données modifiées dans le conteneur sont normalement perdues à la suppression de celui-ci. Le nouveau conteneur démarré disposera donc du jeu de données qui nous intéresse prêt à l’emploi (cf. la documentation officielle). |
Mise en place
Nous avons l’idée, il ne reste plus qu’à la mettre en place. On récupère et on démarre l’image MySQL.
# Démarrage du conteneur.
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=monpwdroot -e MYSQL_DATABASE=mabdd -e MYSQL_USER=monuser -e MYSQL_PASSWORD=monpwd -d mysql:5.7
# Chargement du dump.
mysql -h 127.0.0.1 -p 3306 -u monuser -Pmonpwd mabdd < mondump.sql
# Création de l'image contenant la base permettant de reproduire le bug.
docker commit mysql monappli/mabdd:bug
La ligne de lancement de Docker étant un peu longue, il est possible de créer un fichier docker-compose.yml qui liste ces différentes options. Le
démarrage du conteneur se fait alors ensuite simplement par un docker-compose up .
|
mysql:
image: mysql/5.7
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: monpwdroot
MYSQL_DATABASE: mabdd
MYSQL_USER: monuser
MYSQL_PASSWORD: monpwd
On aurait également sans doute pu créer notre image en créant notre propre fichier Dockerfile, mais je n’ai pas exploré cette piste.. |
Le piège du volume
On vérifie que notre nouvelle image fonctionne. Pour cela, on stoppe le conteneur, et on en crée un nouveau à partir de notre image.
docker stop mysql
docker run --name mysql2 -p 3306:3306 -d monappli/mabdd:bug
Surprise ! Ca ne fonctionne absolument pas comme je m’y attendais. Le conteneur démarre bien, mais le répertoire de données MySQL
/var/lib/mysql semble avoir disparu.
|
Ce dignostic est confirmé par la commande "docker images". La nouvelle image est bien listée à côté de celle de MySQL, mais les deux images font exactement la même taille (alors que l’image de la base chargée aurait dû peser quelques dizaines de Go).
En cherchant, il apparaît que ce comportement est tout à fait normal. Il provient du fait que le
répertoire /var/lib/mysql
a été déclaré en tant que volume dans l’image MySQL. Lorsque le premier conteneur a été démarré, Docker a donc créé un
volume de données qu’il a attaché au conteneur (comme un point de montage). Or, les volumes ne sont pas inclus lorsqu’on enregistre une nouvelle image.
Contournement
Première idée : appliquer un "docker commit" sur le volume de données, pour pouvoir revenir au jeu de test souhaité. Mais cette commande n’existe pas.
Selon la documentation, la sauvegarde/restauration d’un
volume de données passe par des archives tar.gz
classiques.
Vu la taille du volume, je n’ai pas poussé plus loin l’analyse de cette solution, la décompression de dizaines de Go d’une archive étant justement ce que je voulais éviter pour avoir un rechargement rapide.
Il existe un projet Convoy permettant de prendre des snapshots des volumes Docker de données, et de les restaurer, mais je ne l’ai pas testé. |
Puisque le problème initial provient du fait que /var/lib/mysql
est un volume, je me suis juste débarassé du volume en question. Pour cela, je suis
reparti du Dockerfile MySQL.
mkdir /tmp/mysql
cd /tmp/mysql
# Récupération des fichiers nécessaires à la construction de l'image MySQL.
wget https://raw.githubusercontent.com/docker-library/mysql/77f0a50ecd54edafe48ce3a2a328c22e9e7564a8/5.7/Dockerfile
wget https://raw.githubusercontent.com/docker-library/mysql/77f0a50ecd54edafe48ce3a2a328c22e9e7564a8/5.7/docker-entrypoint.sh
# Correction des droits du fichier entrypoint.
chmod +x docker-entrypoint.sh
# Hack de l'image, pour supprimer le volume qui nous pose problème.
sed -i 's/^VOLUME \/var\/lib\/mysql$//' Dockerfile
# Construction de la nouvelle image MySQL.
docker build -t monappli/mabdd:init .
Revanche
On refait le test initial, mais en utilisant notre nouvelle image sans volume. Après avoir supprimé l’image monappli/mabdd:bug
, on exécute :
docker run --name mysql3 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=monpwdroot -e MYSQL_DATABASE=mabdd -e MYSQL_USER=monuser -e MYSQL_PASSWORD=monpwd -d monappli/mabdd:init
mysql -h 127.0.0.1 -p 3306 -u monuser -Pmonpwd mabdd < mondump.sql
docker commit mysql3 monappli/mabdd:bug
docker stop mysql3
docker run --name mysql4 -p 3306:3306 -d monappli/mabdd:bug
Cette fois, ça marche. Le conteneur démarre en quelques secondes, et contient bien la base avec le jeu de données permettant de reproduire le cas de test que nous voulons corriger. Une fois celui-ci "consommé", on réinitialise en moins d’une minute la base et notre cas de test :
docker stop mysql4
docker rm mysql4
docker run --name mysql4 -p 3306:3306 -d monappli/mabdd:bug
Conclusion
Dans ce cas de figure, Docker nous permet de très rapidement réinitialiser une base de données, et nous fait ainsi gagner beaucoup de temps sur l’analyse des problèmes de production, en nous permettant de les rejouer autant de fois que nécessaire.
Je n’ai pas du tout la prétention d’être un expert Docker, et le "hack" du Dockerfile MySQL, ne me paraît pas être une solution très propre (même si elle s’avère être efficace et répond bien au besoin). Si quelqu’un a une solution plus élégante au problème, je suis curieux de la connaître. Profitez donc des commentaires Disqus mis en place spécialement pour l’occasion. |