🤖 Computer Science

Comment nous avons résolu les fuites de mémoire de nos instances backend

date
Jan 22, 2023
slug
comment-nous-avons-resolu-les-fuites-de-memoire-de-nos-instances-backends
author
status
Public
tags
Optimisation
Momory
Threads
summary
La gestion efficace de la mémoire est un aspect crucial pour garantir des performances optimales des applications et des serveurs.
type
Post
thumbnail
0_bHJc2tbv1pM_kLTU.jpg
category
🤖 Computer Science
updatedAt
Dec 3, 2024 09:34 AM
La gestion efficace de la mémoire est un aspect crucial pour assurer des performances optimales des applications et des serveurs. Une consommation excessive de mémoire peut entraîner des ralentissements, des temps de réponse prolongés, voire des plantages du système.
 
Au lancement de Nimba SMS, nous avions configuré 6 workers Celery pour exécuter des tâches distribuées, envoyant des centaines de SMS par minuite vers des terminaux mobiles, répartis sur deux serveurs principaux. Ces instances étaient dotées de consommateurs (consumers) abonnés à un broker RabbitMQ, permettant la lecture et l'exécution de tâches, qu'elles soient immédiates ou programmées.
 
Deux mois après le lancement de la plateforme, nous avons constaté une augmentation significative de la consommation de mémoire. En moyenne, chaque worker utilisait entre 100 et 200 Mo de mémoire, avec des pics atteignant parfois 400 Mo. Plus problématique encore, les instances avaient du mal à libérer la mémoire occupée après l'exécution des tâches, créant une accumulation progressive. Cela posait un sérieux problème, notamment lorsque des milliers de tâches devaient être traitées, entraînant une fuite de mémoire.
 
Ce comportement problématique était étroitement lié à notre utilisation de Celery, une bibliothèque Python dédiée à la gestion des tâches distribuées et asynchrones. Celery repose sur un système de workers pour exécuter les tâches et utilise un broker de messages, tel que RabbitMQ ou Redis, pour coordonner leur traitement. Dans notre architecture, Celery jouait un rôle central : il orchestrait l’envoi massif de SMS via des workers abonnés à RabbitMQ. Cependant, les fuites de mémoire observées au niveau des workers compromettaient leur efficacité et menaçaient la stabilité globale de la plateforme.
 
Nous avons commencé à investiguer et à expérimenter quelques solutions,
  1. Limité le nombre de tâches que chaque worker pouvait exécuter avant d’être redémarré et remplacé par une autre instance. worker_max_tasks_per_child
  1. Quantité maximale de mémoire résidente, en kilo-octets, qui peut être consommée par un worker avant qu'il ne soit remplacé par un nouveau worker fraîche. Si une tâche amenait un worker à déplacer cette limite la tâche sera achevée et remplacer un autre. worker_max_memory_per_child
 
Celery explique ses deux concepts dans sa documentation. https://docs.celeryq.dev/en/stable/userguide/optimizing.html#optimizing-prefetch-limit
 
Malheureusement ses configurations n’ont apporté que des améliorations peu significatives. Il nous fallait quelques choses qui pouvait libérer rapidement la mémoire réservée par une tache dès la fin de la tâche.
C’est à ce moment-là que nous avons découvert libjemalloc2
jemalloc
jemallocUpdated Dec 7, 2024
 
libjemalloc2 est une bibliothèque de gestion de la mémoire pour les systèmes d'exploitation Unix et Linux. Elle est conçue comme une alternative au gestionnaire de mémoire standard de la bibliothèque C standard (libc). Le but principal de jemalloc est de fournir une performance améliorée et une fragmentation réduite, surtout dans les environnements multithread ou lors de l'utilisation de grandes quantités de mémoire, initialement développé pour FreeBSD et a depuis été adopté par de nombreux autres systèmes et applications en raison de ses performances robustes et de sa gestion efficace de la mémoire. Cette bibliothèque est particulièrement populaire dans les environnements de serveur et est utilisée par des projets importants comme Firefox et Redis.
 
Fonctionnalités clés de jemalloc :
  1. Allocation de mémoire efficace : Elle optimise l'utilisation de la mémoire et réduit la fragmentation, ce qui est crucial pour les applications qui utilisent beaucoup de mémoire.
  1. Multithreading : Elle offre des performances solides dans les environnements multithread, réduisant la contention entre les threads pour l'accès à la mémoire.
  1. Débogage et profilage : jemalloc fournit des outils pour le débogage et le profilage de l'utilisation de la mémoire, ce qui est utile pour identifier les fuites de mémoire et optimiser l'allocation de mémoire.
Quand utiliser jemalloc ?
  • Dans des applications multithread intensives.
  • Lorsque la gestion de la mémoire est un goulot d'étranglement pour les performances.
  • Pour des applications nécessitant une gestion fine de la mémoire avec une faible fragmentation.
 
Nous avons intégré jemalloc dans le runtime Python pour optimiser la gestion de la mémoire.
  1. Installation jemalloc dans le runtime de python,
sudo apt update sudo apt libjemalloc2
  1. Chargement de la bibliothèque avec la variable d'environnement LD_PRELOAD
ENV LD_PRELOAD /usr/lib/x86_64-linux-gnu/libjemalloc.so.2
 
LD_PRELOAD est une variable d'environnement utilisée sous Linux et les systèmes similaires pour injecter une bibliothèque partagée dans le processus d'exécution des programmes. Dans ce cas, elle force l'utilisation de jemalloc comme alternative aux fonctions d'allocation dynamique de mémoire standard (malloc, calloc, realloc, free).
Ainsi, toute application ou processus exécuté dans le conteneur Docker utilisera jemalloc à la place de l'allocateur mémoire par défaut, améliorant potentiellement les performances et la gestion de la mémoire.
 
Après implémentation, les améliorations ont été significatives :
  • Réduction de 60% de la consommation mémoire moyenne
  • Libération plus rapide des ressources
  • Stabilisation générale du système
 

Ressource utiles
Si vous rencontrez des problématiques similaires, voici quelques ressources utiles :
N'hésitez pas à partager vos retours d'expérience sur ce sujet dans les commentaires.