Arnaud TAMAILLON
Apr 5 · 9 min read

Cette journée commençait pourtant bien. C’était sans compter sur les alertes Slack, les mails Application Insights, et le regard noir de votre DevOps en arrivant au boulot.

Consommation mémoire de l’app service sur une quinzaine de jours

Manifestement, le seul garbage collector qui récupère effectivement la mémoire dans notre cas, c’est le redémarrage du process. Assez loin de la promesse d’un langage managé…

Cet article vise à décrire une méthode permettant d’investiguer cette fuite mémoire sur un webjob, de la collecte d’informations à la résolution effective.

Toute ressemblance avec une situation réelle est loin d’être fortuite.

Récupérer un dump mémoire du webjob

Un point fondamental dans l’analyse d’un problème est la collecte d’informations sur celui-ci.

Bien souvent, le problème que l’on constate en production n’a pas été détecté lors du développement ou des tests, car les conditions de reproduction ne sont pas remplies sur les environnements de qualification ou de développement.

Par ailleurs, ces conditions ne sont en général pas identifiées au début de la phase de recherche, et formuler des hypothèses et les tester peut nécessiter un temps précieux.

Fort heureusement, dans le cas des problèmes de fuite mémoire (mais pas seulement), il existe un outil précieux pour collecter de la donnée et l’analyser, à tête reposée, une fois la production remise en fonctionnement (même temporairement) : le dump mémoire.

Le dump mémoire est une capture de la mémoire du process à un moment donné. Idéalement, celle-ci aura lieu lors d’une phase de consommation mémoire importante pour maximiser la visibilité du problème dans les données recueillies.

Il faut noter que, le temps de la capture, l’exécution du processus sera gelée, et la disponibilité de l’application en sera affectée. C’est un paramètre important à prendre en compte, même si prendre une minute pour avoir une information fiable est très souvent préférable à gagner du temps sur la relance et en être réduit à des conjectures lors de la phase de recherche.

Dans notre cas, on parle d’un processus qui traite des messages en tâche de fond, de manière asynchrone ; sa capture n’aura pour conséquence qu’un léger retard de traitement de ceux-ci.

Il existe de nombreux moyens pour prendre un dump d’un process. En revanche, il existe également pléthore d’options qui configurent la quantité d’informations recueillies. Problèmes de nommage propres à l’informatique oblige, il faut noter qu’un Full Dump est moins complet qu’un Mini Dump avec toutes les options activées (https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/user-mode-dump-files).

L’idéal pour couvrir toutes les options, est de prendre un Mini Dump avec toutes options, le /ma de la documentation. Je vais ici en présenter une, reposant sur Kudu (disponible sur toutes les WebApps Azure), qui a l’avantage d’éviter les erreurs, et de rester assez simple.

Pour accéder à Kudu, sur un AppService, rechercher Advanced Tools, et cliquer sur le lien Go pour ouvrir l’interface Kudu.

Récupérer l’identifiant de process

Dans Kudu, l’onglet Process explorer présente les informations recherchées.

Récupérer l’identifiant de process via Kudu

Capturer le dump

L’onglet Debug console donne accès à une console interactive, qui va nous permettre d’exécuter les commandes nécessaires. Nous allons pour cela utiliser les excellents outils SysInternals, que Microsoft installe par défaut sur les webapps.

Il est maintenant possible de télécharger le dump localement. Pour permettre aux outils d’analyse de fonctionner au mieux, il est conseillé de récupérer également un autre fichier, et de le stocker à côté de ce dump.

  • Pour les applications .Net Framework, mscordacwks.dll, dans D:\Windows\Microsoft.NET\Framework[64]\v4.0.30319
  • Pour les applications .Net Core, mscordaccore.dll, dans D:\Program Files\dotnet\shared\Microsoft.NETCore.App\version

Kudu permet bien évidemment là aussi de récupérer ce fichier, qui constitue la couche d’abstraction au dessus de l’implémentation de la CLR de .Net.

Analyser le dump

Il existe un certain nombre d’outils pour réaliser une analyse du contenu de ce fichier. Nous allons en voir trois ici, deux nécessitant une licence (Visual Studio Enterprise et dotMemory), et un gratuit, WinDbg.

Microsoft Visual Studio Enterprise

Si les éditions inférieures de Visual Studio permettent également d’ouvrir les fichiers dump, seule l’édition Enterprise permet d’analyser le contenu mémoire de celui-ci.

Pour commencer, il faut ouvrir le fichier avec File > Open > File. Visual Studio propose alors différentes actions relatives à l’analyse.

La fenêtre principale du dump

Comme notre problème concerne une fuite mémoire, l’action Debug Managed Memory parait indiquée (c’est cette option qui n’est disponible qu’en édition Enterprise). Visual Studio va alors traiter le dump pour en déterminer l’ensemble des objets .Net présents en mémoire dans le process au moment où le dump a été pris, ainsi que la chaîne de références qui les relie à une racine du garbage collector.

Après un temps d’analyse dépendant de la taille du fichier (quelques minutes pour un dump de 5 Go sur une machine de développement récente), la fenêtre suivante s’affiche :

On constate assez vite qu’il y a beaucoup de références liées à Entity Framework en mémoire, et notamment, plus de 600 000 contextes, dont on s’attend pourtant à ce qu’ils soient recyclés régulièrement.

En cliquant sur la ligne correspondante, on obtient le graphe de références pointant vers des objets de ce type :

Nous tenons le coupable : le conteneur d’injection de dépendances de Microsoft. Celui-ci référence les contextes via une liste d’objets disposables, probablement dans l’optique de les disposer, mais n’en fait rien, ce qui est fâcheux… A noter la présence d’un ServiceProviderEngineScope, qui semble indiquer l’utilisation d’un unique scope pour l’ensemble du webjob. Cela va nous donner une piste de recherche.

Il semble que les autres références constituent les grappes d’objets Entity Framework liés aux contextes, et ne constituent probablement pas un problème en soi, à ce moment de l’analyse.

JetBrains dotMemory

dotMemory est un profiler mémoire, qui peut être utilisé pour inspecter un process en temps réel, mais qui sait également exploiter les fichiers dump.

Le menu Import Dump permet d’importer le fichier capturé précédemment

Une fois ouvert, un passage sur l’onglet Types permet d’obtenir la vue des objets présents en mémoire, classés par type.

En cliquant sur la ligne correspondant à l’objet Contexte, on obtient la vue relative à ce type. L’onglet Similar Retention permet alors d’inspecter de manière graphique les liens aux racines du garbage collector.

On constate ainsi à nouveau que la majorité des contextes sont retenus par le biais du conteneur d’injection de dépendances.

WinDbg

Il existe deux versions de WinDbg. L’une est distribuée dans le SDK Windows, l’autre (WinDbg Preview) via le Store Windows (!).

Lorsque c’est possible, et que vous êtes donc sur un poste utilisateur, je recommande l’utilisation de WinDbg Preview, qui retire (un peu) de rudesse à l’interface de l’outil.

Une fois le fichier ouvert, il faut charger l‘extension SOS, qui fournit de nombreuses fonctions utiles au debug .Net. De manière standard, cela se fait via la commande suivante :

# pour .Net Framework
.loadby sos clr
# pour .Net Core
.loadby sos coreclr

Il peut arriver que cette commande échoue si l’emplacement du .Net framework est différent de la machine où le dump a été pris. C’est typiquement le cas avec les dumps pris sur Azure, où le framework est installé sur le disque D.

The call to LoadLibrary(D:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos) failed, Win32 error 0n126
"The specified module could not be found."
Please check your debugger configuration and/or network access.

Dans ce cas, il suffit de charger la dll par son chemin explicite, via la commande suivante :

.load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos

On va alors pouvoir utiliser la commande dumpheap, qui va lister les objets présents en mémoire, en lui ajoutant le paramètre -stat, qui fournira une vue agrégée par type de la mémoire, similaire à ce qu’on peut obtenir dans les deux outils vus précédemment :

!dumpheap -stat
Résultat de la commande dumpheap -stat

Sur la ligne correspondant au contexte, en cliquant sur le lien représentant le type, WinDbg va énumérer les instances du type. Cela peut prendre un certain temps vu le nombre d’instances en mémoire.

La liste d’instances

Chaque ligne correspond à une instance, et on trouve successivement l’adresse de l’objet en mémoire, le type, et la taille mémoire occupée.

On va pouvoir alors inspecter quelques objets par échantillonnage via la commande GCRoot, qui va remonter le lien aux racines du garbage collector.

!GCRoot 000001b631a5eb58
Sortie de la commande GCRoot

On voit ainsi à nouveau apparaître le conteneur d’injection de dépendance.

Ne nous en cachons pas, l’expérience est plus rustique, mais l’outil permet de scripter des opérations, et est déployable via une simple copie de fichier si d’aventure il fallait débuguer directement sur une VM en production (ce qu’on ne souhaite à personne par ailleurs).

Corriger le problème

Maintenant qu’une hypothèse est identifiée sur la fuite mémoire, il est possible de la valider sur le poste de développement. Ainsi, intégrer un breakpoint dans la méthode Dispose de l’objet MyAccountReadWriteContext (quitte à en rajouter une pour l’occasion) montre effectivement une absence flagrante d’appels.

En creusant plus avant dans le code du webjob, on trouve alors une ligne correspondant à une définition de factory pour le traitement de messages ServiceBus, référençant le fameux contexte, qui se présente ainsi :

Ainsi, à chaque message, un nouveau Consumer est instancié depuis le conteneur, par la librairie qui traite les messages, mais cette librairie n’a pas connaissance du conteneur, qui n’est référencé que dans la lambda.

En se rappelant le scope d’injection unique dans la chaîne de références du contexte, on se rend compte que la librairie n’a aucun moyen de signaler la fin de l’utilisation du Consumer au conteneur, et donc de demander qu’il soit disposé.

Fort heureusement, la librairie utilisée supporte le conteneur de Microsoft, via une autre méthode d’enregistrement :

La méthode Consumer, fournie par le framework, prend en paramètre le conteneur, et se chargera de la résolution du type concret.

Ainsi, cette librairie sera à même de gérer le cycle de vie de ces objets, via la création d’un scope dédié au traitement de chaque message. La destruction du scope pourra ainsi déclencher le nettoyage des instances créées dans ce scope.

Une fois la modification effectuée, on constate que la méthode Dispose du contexte est bien appelée après chaque traitement de message, ce qui va nous permettre de corriger la fuite (en espérant que cela soit la seule !)

Valider la correction

Parce qu’une image vaut mieux qu’un long discours :

On voit ainsi qu’après livraison de la correction, la consommation mémoire s’est bien assagie !

Cet exemple démontre qu’il existe de nombreux outils permettant d’investiguer à froid des données de production. C’est un outil précieux pour le développeur qui peut exploiter les données récoltées lors d’un souci de production, une fois celui-ci corrigé.

Et pour se quitter sur une touche sécurité/GDPR: le dump mémoire d’un process, par définition, va contenir des données sensibles (mots de passes, clés, données personnelles …). A ce titre, il est important de traiter ces fichiers avec tout le respect dû à ce type de données !

YounitedTech

Le blog Tech de Younited, où l’on parle de développement, d’architecture, de microservices, de cloud, de data… Et de comment on s’organise pour faire tout ça. Ah, et on recrute aussi, on vous a dit ?

Arnaud TAMAILLON

Written by

YounitedTech

Le blog Tech de Younited, où l’on parle de développement, d’architecture, de microservices, de cloud, de data… Et de comment on s’organise pour faire tout ça. Ah, et on recrute aussi, on vous a dit ?

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade