Benoît Courtine



    Navigation
     » Accueil
     » A propos
     » XML Feed

    Docker à la rescousse

    20 Aug 2016 » docker, mysql

    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.

    Création de l’image
    # 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.
    docker-compose.yml
    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.

    Test 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.

    Hack du Dockerfile MySQL et construction de l’image
    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 :

    Création de l’image (bis)
    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
    Test de notre image
    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 :

    Réinitialisation de la base 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.