Une fois encore, un message de Sarah vous accueille à l’ouverture de votre messagerie :
Liam nous a demandé de mettre en place l’infrastructure de préproduction. J’en ai profité pour commencer à mettre en place des sondes d’analyse de performance. Il faut qu’on parle des ressources utilisées par l’application…
Sarah soulève un point essentiel : comprendre et gérer les ressources consommées par vos conteneurs est central pour éviter des surcharges et des inefficacités qui pourraient affecter l’ensemble de votre infrastructure. Avant de pouvoir optimiser l’usage de celles-ci, il est primordial d’analyser quelles ressources sont en jeu. Dans ce chapitre, nous verrons ensemble comment réaliser cet inventaire et comment mettre en application les meilleures stratégies pour améliorer l’efficacité de votre infrastructure globale.
Identifiez les différentes ressources exploitées par votre déploiement
Dans un environnement de production, les ressources sont souvent limitées. Cela inclut non seulement l'espace disque, la RAM, et les CPU, mais aussi d'autres ressources comme la bande passante réseau. Il est donc crucial de gérer ces ressources de manière efficace pour assurer la stabilité et la performance de vos applications conteneurisées.
Réalisons un petit inventaire des principales ressources utilisées par les conteneurs, et les impacts potentiels qu’une mauvaise gestion de celles-ci peut provoquer sur une infrastructure :
Espace disque :
Images Docker : Les images Docker peuvent occuper beaucoup d'espace disque, surtout si elles sont volumineuses ou si vous avez plusieurs versions de la même image.
Conteneurs en cours d'exécution : Les conteneurs utilisent également de l'espace disque pour stocker les données générées pendant leur exécution.
Volumes : Les volumes Docker permettent de stocker des données persistantes. Leur utilisation peut rapidement consommer de l'espace disque si les données ne sont pas gérées correctement.
Journaux de conteneurs : Les logs générés par les conteneurs peuvent devenir volumineux avec le temps, surtout si les applications produisent beaucoup de log.
CPU :
Consommation CPU : Les conteneurs partagent les ressources CPU de l'hôte. Une consommation élevée de CPU par un ou plusieurs conteneurs peut affecter la performance globale de la machine hôte.
Cycles CPU : Chaque processus à l'intérieur d'un conteneur utilise des cycles CPU. La gestion de la priorité des processus peut aider à équilibrer la charge CPU.
RAM :
Allocation de mémoire : Chaque conteneur utilise une partie de la mémoire RAM disponible sur l'hôte. Une utilisation excessive de la RAM peut provoquer des ralentissements ou des crashs si la mémoire disponible est épuisée.
Fuites de mémoire : Les applications peuvent avoir des fuites de mémoire qui, si elles ne sont pas détectées et corrigées, peuvent épuiser les ressources de mémoire.
Bande passante réseau :
Trafic réseau : Les conteneurs peuvent générer un trafic réseau important, ce qui peut impacter la performance du réseau, surtout dans les environnements où la bande passante est limitée.
Limitation de la bande passante : Gérer et limiter la bande passante utilisée par chaque conteneur peut aider à maintenir une performance réseau optimale.
Il est crucial de gérer ces ressources de manière efficace pour assurer la stabilité et la performance de vos applications conteneurisées. Votre objectif est donc de réduire l’empreinte technique de celles-ci et limiter les risques d’emballement.
Voyons tout de suite un premier levier d’action concernant l’utilisation de l’espace disque via la réduction de la taille de vos images de conteneur.
Réduisez la taille de vos images
Optimiser la taille des images Docker est central pour améliorer l'efficacité de votre infrastructure et réduire les temps de déploiement.
Une image plus petite utilise moins d'espace disque, se transfère plus rapidement sur le réseau et démarre plus vite. Dans cette section, nous allons explorer comment fonctionne la création d'images Docker, ainsi que des méthodes pour réduire leur taille.
Fonctionnement des layers et impact sur la taille finale des images
Comme nous l’avons vu dans les chapitres précédents, une image Docker est composée de plusieurs couches. Lorsqu'une image est modifiée, seule la couche modifiée est mise à jour, ce qui permet de minimiser la taille des téléchargements lors des mises à jour.
Or, ce système induit une subtilité qui peut avoir une grande importance quant à la taille finale d’une image: installer puis supprimer un fichier dans l’image sur deux layers différents ne réduira jamais la taille finale de l’image ! Par exemple :
FROM ubuntu:noble # Installation de NodeJS comme dépendance de construction, par exemple pour générer une version de production d’une application Javascript RUN apt-get update && apt-get install -y nodejs # <installation et compilation des sources de l’application Javascript> # Désinstallation de NodeJS puisque non-nécessaire dans l’image finale # RUN apt-get remove nodejs
Même si NodeJS ne sera plus disponible dans l’image finale, la taille de celle-ci correspondra toujours à une version qui la contiendrait ! Pourquoi me direz-vous ? Parce que dans un système de fichier par couches, lorsqu’un fichier est supprimé, il n’est en réalité pas véritablement supprimé mais seulement “marqué” comme étant absent.
Il est donc important d’identifier ce type d’opérations afin d’éviter de créer des images inutilement lourdes, qui seront plus longues à être transférées et donc déployées.
Mais comment faire alors pour supprimer réellement des fichiers ?
Deux possibilités : soit vous pouvez réduire les opérations en une seule instructionRUN
, soit vous utilisez le mécanisme d’images “multi-stages” !
Structurer le Dockerfile pour tirer parti du cache
Cette structure en couches est également très utile, si vous savez comment l’utiliser, pour améliorer les temps de construction de vos images.
En effet, Docker utilise un mécanisme de cache pour les couches d'image afin d'éviter de reconstruire des parties de l'image qui n'ont pas changé, ce qui permet de gagner du temps et de réduire les ressources utilisées. Pour maximiser les bénéfices de ce cache, il est important de suivre certaines pratiques lors de la rédaction de votre Dockerfile :
Ordre des instructions : Placez les instructions qui changent le moins fréquemment en haut du Dockerfile. Par exemple, les instructions
FROM
,ENV
, etCOPY
pour les fichiers de configuration qui ne changent pas souvent doivent être en premier. Les instructions qui changent fréquemment, commeRUN
, doivent être placées vers la fin. Cette organisation permet à Docker de réutiliser le plus de couches possibles à chaque build.Minimiser les couches : Combinez les commandes dans une seule instruction
RUN
pour réduire le nombre de couches et tirer pleinement parti du cache Docker. Par exemple :
# Copie du fichier d’empreintes COPY SHA256SUMS ./SHA256SUMS # Téléchargement d’un binaire exécutable, vérification de la conformité de l’empreinte du fichier et application des droits d’exécution en une seule commande RUN wget -O ./my-app https://s3.libra.io/app \ && sha256sum -c SHA256SUMS \ && chmod +x ./my-app
Utiliser des
ARG
pour les versions des dépendances : En utilisant des arguments de build (ARG), vous pouvez rendre les versions de vos dépendances configurables. Cela permet de contrôler précisément quand le cache doit être invalidé, par exemple lors d'une mise à jour d'une dépendance spécifique.
En plus des instructions constituant votre Dockerfile, Docker utilise les fichiers présents dans le contexte de construction de votre image afin d’identifier si des éléments ont changé depuis la dernière construction, notamment lorsque vous utilisez des instructions du typeCOPY
. Il est donc important de savoir gérer cet aspect afin d’optimiser votre processus de construction.
Utiliser un fichier .dockerignore pour éviter les fichiers inutiles
Le fichier.dockerignore
permet d'exclure des fichiers et des dossiers lors du processus de build Docker. Cela réduit la taille du contexte de construction et évite d'inclure des fichiers inutiles dans les couchesCOPY
etADD
, ce qui améliore à la fois la vitesse de construction et la taille finale de l'image.
En excluant des fichiers inutiles comme les fichiers de configuration de versionnement (exemple :.git
), les fichiers temporaires, les artefacts de construction, etc., vous réduisez le nombre de fichiers que Docker doit envoyer au daemon lors de la création d'une image. Et moins de fichiers dans le contexte signifie que Docker peut créer les couches d'image plus rapidement, ce qui permet d'optimiser les temps de construction !
Exemple de fichier.dockerignore
:
# Ignorer les répertoires de build et les fichiers de configuration non pertinents node_modules dist *.log .git
Nettoyer les caches et fichiers temporaires
La plupart des systèmes de gestion de paquets ou de dépendances utilisent des répertoires servant de “cache”. Dans le contexte d’usage d’un système d’exploitation classique, ce type de système permet d'accélérer les futures mises à jour en utilisant les données en cache local lorsque cela est possible.
Cependant, dans le cadre de la construction d’une image de conteneur, ce mode de fonctionnement n’a que peu d’intérêt puisque le système de fichier du conteneur n’aura pas vocation à être mis à jour durant son temps de vie, mais remplacé par une nouvelle version de son image.
Il est donc préférable de nettoyer ces répertoires après utilisation des gestionnaires de paquet afin d’éviter cet usage d’espace disque inutile.
Chaque gestionnaire de dépendance est différent, cependant voici un exemple pourapt
, le gestionnaire de paquet pour Ubuntu :
# Installation de deux paquets factices package1 et package2 # puis nettoyage du cache “apt” RUN apt-get update && apt-get install -y \ package1 \ package2 \ && apt-get clean && rm -rf /var/lib/apt/lists/*
La taille d’une image est bien entendu qu’une composante des différentes ressources utilisées par notre environnement conteneurisé, dont l’impact est principalement restreint au déploiement. Intéressons nous maintenant aux ressources utilisées pendant l’exécution des conteneurs.
Limitez les ressources utilisables par vos conteneurs
En définissant des limites pour les ressources critiques comme l'espace disque, le CPU et la RAM, vous pouvez prévenir les incidents liés à l'épuisement des ressources et garantir un fonctionnement fluide de vos applications.
Prévenir l’épuisement de l’espace disque
Pour empêcher l'écriture sur le système de fichiers et éviter que vos conteneurs ne consomment trop d'espace disque, vous pouvez monter des volumes en mode lecture seule.
Par exemple, dans un fichierdocker-compose.yml
, vous pouvez spécifier un volume en lecture seule avec l'option:ro
.
services:
my_service:
image: my_image
volumes:
- /data:/data:ro
Cette configuration permet au conteneur de lire les données du volume/data
, mais empêche toute écriture, ce qui protège contre l'épuisement involontaire de l'espace disque.
Mais mon conteneur peut aussi écrire dans son système de fichier local !
Tout à fait ! Pour empêcher l’écriture sur le système de fichier local du conteneur, vous pouvez utiliser le flag--read-only
lors du lancement de votre conteneur. Sachez cependant que l’usage d’un système de fichier complet en lecture entraîne souvent des complications qui peuvent être longues à contourner…
Limiter les risques d’emballement d’exécution d’un processus
Si vous vous rappelez du début de notre cours, vous savez que les conteneurs sont rendus possibles par deux technologies liées au noyau Linux, en particulier les cgroups.
Les cgroups permettent notamment de définir des quotas d’utilisation de ressources, par exemple pour l’usage CPU ainsi que la mémoire RAM. Voyons ensemble comment utiliser Docker pour manipuler ces quotas.
Dans cette vidéo, nous avons vu :
L’usage du flag
--cpus=
de la commandedocker run
afin de définir des quotas d’usage du CPU pour notre conteneurL’usage du flag
--memory=
de la commandedocker run
afin de définir des quotas d’usage de la mémoire RAM pour notre conteneur
À vous de jouer
Contexte
Sarah, une fois encore, a identifié un problème potentiel avant même que celui-ci devienne dangereux pour le projet. Elle vous a chargé de réduire la taille de l’image de l’application Libra afin d’améliorer le temps de déploiement de celle-ci et l’espace disque occupé sur les serveurs en production !
Votre mission est d’appliquer les bonnes pratiques décrites dans ce cours afin d’atteindre cet objectif, en sélectionnant celles qui vous permettront de diminuer la taille de l’image finale.
Consignes
Retravailler le fichier Dockerfile issu du chapitre précédent et améliorez-le pour réduire le nombre d’instructions présentes dans celui-ci.
Vérifiez que la taille de l’image finale a bien été réduite via l’exécution de la commande
docker images
et en comparant la taille de l’image avec celle de la version précédente.
En résumé
Inventorier les différentes ressources utilisées par vos conteneurs: (espace disque, CPUs, RAM, bande passante réseau) et identifier l’impact de l’usage de celles-ci sur le bon fonctionnement de votre infrastructure.
Combiner les instructions dans les Dockerfiles et nettoyer les fichiers temporaires pour minimiser l'espace disque utilisé.
Définir des quotas stricts pour la consommation de CPU et de mémoire afin de prévenir les abus de ressources et garantir une performance stable.
Utiliser des volumes en lecture seule pour empêcher les écritures non nécessaires et éviter l'épuisement de l'espace disque.
Vous savez optimiser les images et l’usage des ressources de vos containers. Voyons maintenant comment optimiser la répartition de ceux-ci au sein de votre cluster !