Affaire déclassée : une fuite mémoire dans e-Chauffeur
21/12/2023 - 5 minutes
(NB : Précisons que les IP dans les screenshots ne sont plus actives… ou au moins plus chez nous, raison pour laquelle j’ai préféré l’esthétisme de screenshots non floutés)
Qu’est-ce à dire ?
Parlons rapidement du projet : e-Chauffeur est une application de réservation de véhicule avec chauffeur, sans brique de paiement, à destination des personnels des Armées. (Plus d’informations sur sa fiche beta.gouv.fr). Son code est libre et disponible sur gitlab.com.
Pour rester très générique, une fuite mémoire résulte d’une accumulation de données dans la mémoire vive d’un ordinateur entrainant sa saturation. Je pense pouvoir affirmer que nous l’avons tous expérimenté au moins une fois sans savoir ce que c’était : Votre ordinateur se mets soudain à ralentir alors que vous l’utilisez, et relancer un logiciel ou l’ordinateur suffit à résoudre le problème… jusqu’à ce que cela recommence.
Dans le développement logiciel, un programme requête un espace mémoire auprès du système d’exploitation, afin d’y stocker une donnée. Ensuite, le programme doit libérer l’espace mémoire quand il n’en a plus besoin. A partir de là, plusieurs stratégies existent suivant les langages de programmation, citons en trois :
- La gestion de cette mémoire est entièrement la responsabilité du développeur, à lui de savoir quand son programme n’a plus besoin de cette entrée mémoire. C’est le cas du C dit “vanilla“.
- Le compilateur est responsable de savoir la durée de vie de ces données, et ajoute au code compilé les instructions pour nettoyer la mémoire. C’est le cas du Rust.
- Le programme embarque un Garbage Collector (Collecteur de miettes en bon français). Son rôle est de vérifier régulièrement quelles variables ne sont plus utilisées, pour les libérer. C’est le cas d’une majeure partie des langages, dont celui qui nous intéresse ici : Javascript (via NodeJS).
Pour peu qu’on analyse son graph d’utilisation mémoire, on peut presque distinguer un programme avec un Garbage Collector : les moments où il va libérer de la mémoire sont cycliques. Par exemple, on peut imaginer que chaque seconde, le Garbage Collector va regarder les adresses mémoires réputées inaccessibles et les libérer. Cette fréquence est généralement bien plus rapide que “chaque seconde”, et est bien souvent configurable.
Comment une fuite mémoire est possible avec un Garbage Collector ? Ce dernier déduit un graph d’utilisation des entrées mémoire. Quand des points du graph sont coupés de la racine, c’est qu’ils sont inaccessibles et sont à nettoyer. Tant que les variables sont accessibles, et reliées à la racine de ce graph, alors le Garbage Collector va garder les variables pour éviter une erreur du programme.
Dans notre cas, le problème était lié à la méthode utilisée pour ajouter des plugins dans le programme. Ces plugins sont prévus pour être ajoutés une fois dans la partie rendu serveur, au lancement du serveur. Dans notre cas, ils étaient ajoutés à chaque requête reçue.
Ainsi, les éléments étaient ajoutés dans un tableau de plugins, et ce tableau était utilisé régulièrement, et donc maintenu dans les variables “utiles” au programme. Ce tableau devenait de plus en plus grand, jusqu’à saturer la mémoire.
Point intéressant, plus le tableau était grand, plus la puissance de calcul nécessaire à le parcourir était grande également, ainsi le monitoring montre une augmentation similaire du processeur.
Une fuite invisible
Nous n’avons pas vu cette fuite mémoire avant un paquet de temps, faute en est à notre infrastructure, qui fonctionnait comme prévu : plusieurs conteneurs (au moins 3) fonctionnent en même temps, dans le cas où une erreur est détectée, le conteneur est redémarré (et ça va vite), dans le cas où un conteneur est indisponible, les requêtes ne lui parviennent plus (les autres prennent la charge).
Une erreur possible, c’est que la mémoire dépasse l’espace qui lui est alloué. À ce moment là, Kubernetes va relancer (détruire et recréer serait plus exacte) le conteneur.
Une fois le correctif déployé, on constate rapidement que ce motif en dents de scie cesse, et que les conteneurs ne redémarrent plus.
Peut être vous demandez vous pourquoi corriger s’il n’y a pas de problème pour l’utilisateur, étant donné que c’est compensé par l’infrastructure ? Et bien déjà, permettez moi de le mettre en premier : pour le principe ! C’est une anomalie, elle doit être corrigée. Autrement, il y a de meilleures raisons : la pollution des logs, ces derniers vont être pollués par des lignes indiquant le redémarrage des services, inutilement. Si vous avez un système automatique de détection d’erreurs dans les logs, basé sur un apprentissage, et bien il va se mettre à considérer que quand le conteneur redémarre, c’est normal. Ensuite, la pollution des évènements est également une bonne raison : on préfère avoir des évènements utiles dans les informations de Kubernetes.
Conclusion
La leçon à tirer de tout ça ? Et bien il faut regarder les graphiques de monitoring des environnements de reviews (qui sont déployés à la volée) avant de mettre en production des modifications ! Quand ça fonctionne, c’est pas nécessairement que tout fonctionne correctement.