Cómo cazar una fuga de memoria en .Net (I)
Hablaba en un post anterior de como me las he tenido que ver con una fuga de memoria en .Net recientemente, e incluso ponía un pequeño caso mínimo representativo del problema que he tenido. Hoy toca hablar de como llegue a la conclusión de que estaba ante una fuga de memoria y a la conclusión de que se trataba de una fuga de memoria manejada.
Para diagnosticar una fuga de memoria en .Net debemos comenzar por responder dos preguntas, que a continuación veremos. Si diré que la herramienta para contestar ambas preguntas es el monitor de rendimiento.
La primera cuestión que tendremos que dilucidar es: ¿Estamos ante una fuga de memoria o ante un consumo excesivo de memoria?
No es extraño encontrar aplicaciones de .Net que por un mal diseño hacen un uso excesivo de memoria. Una situación habitual es por ejemplo, convertir la sesión de las aplicaciones Asp.net en un cajón de sastre en el que todo cabe y en el que se acumula un motón de información que lógicamente tiene un coste en consumo de memoria. Pero esto es diferente de una fuga de memoria y tiene mejor solución: añadir más memoria a nuestro servidor puede ser una medida paliativa suficiente e incluso más rentable, en términos económicos, que dedicar esfuerzo a optimizar el consumo de memoria. Cuando nos enfrentamos a una fuga de memoria la solución de añadir más memoria al servidor solo es prolongar la agonía.
Para saber si estamos ante una fuga de memoria debemos observar los bytes privados del proceso sospecho. Para ello basta establece una traza del monitor de rendimiento que recoja la evolución del contador de bytes privados que se encuentra en la categoría proceso.
El contador Proceso/Bytes privados recoge el número de bytes que nuestro proceso ha reservado y que no pueden ser compartidos con ningún otro proceso. Por lo tanto si nuestro proceso se está guardando memoria de manera egoísta este contador será nuestro chivato.
Si tras un periodo de muestreo razonable (lo que es razonable depende de la naturaleza de las actividades que lleve a cabo el proceso) vemos que la tendencia de ese contador es siempre creciente, sin duda nos encontramos ante una fuga de memoria. Si este contador no muestra una tendencia creciente, aunque sea elevado o puntualmente elevado, no nos encontraremos ante una fuga de memoria.
Fijaros en la traza de bytes privados que dejaba el servicio en el que yo sufría la fuga de memoria.
Viendo esa traza de casi dos días se hace evidente que estamos fugando memoria, sobre todo teniendo en cuenta que como ya comenté, con el paso del tiempo, acababa sufriendo un ‘casque’.
La segunda cuestión a desvelar es: ¿Se trata de una fuga de memoria nativa o manejada?
Sin duda saber si lo que estamos fugando es un recurso no manejado o una manejada nos va a dar muchas pistas sobre donde hemos de buscar en nuestro código la fuga de memoria.
Para esto vamos a usar otros dos contadores, que nos dan, para el caso que nos ocupa una información similar: cuanta memoria se está consumiento en la pila manejada del proceso (Memoria de .Net CLR/# Bytes en todos los montones) y cuantas colecciones de generación 2 tenemos (Memoria de .Net CLR/# de colecciones de gen. 2), es decir cuantos objetos de nuestro proceso están sobreviviendo durante demasiado tiempo al recolector de basura. Al final los estos dos contadores nos informa de si estamos dejando atrás recursos maneajados sin liberar.
Logicamente, si estos contadores crecen de manera paralela a los bytes privados de nuestro proceso, estarémos ante una fuga de memoria manejada, sin embargo si crecen los bytes privados pero no crecen los bytes en la pila manejada lo que estaremos fugando es un recurso no manejado.
En la siguiente imagen podéis ver que en el caso que a mí me ocupaba claramente estabamos fugando memoria manejada, pues hay una clarísima correlación entre los tres contadores.
Ahora ya tengo claro que estoy fugando memoria y que es memoria manejada ¿que puedo hacer?.
Básicamente tengo dós opciones, usar WinDBG para ir obtieniendo la situación de la memoria de mi proceso y ver que objetos son los que estoy fugando opción que David Salgado explico explendidamente y que exije un dominio de WinDBG notable o usar una opción para meros mortales: el CLR Profiler.
WinDBG es una herramienta potentísima y que sin duda todo desarrollador que quiera poder resolver problemas peliagudos con aplicaciones de Windows tiene que saber manejar. Cuando las cosas se ponen realmente feas, WinDBG es nuestro mejor aliado y a menudo el único que nos puede sacar de atolladero. Os recomiendo los post sobre WinDBG de mi compañero en Plain Concepts, Pablo Alvarez (el tiempo que paso en Microsoft dando soporte ha dejado su huella), y los de David Salgado sobre el mismo tema. Otro recurso impresionante, eso sí en inglés, sobre depuración en .Net con WinDBG son las .NET Debugging Demos de Tess Ferrandez, Escalation Engineer en Microsof Suiza.
En un próximo post, veremos como usé el CLR Profiler (opte por la opción facil) para diagnosticar donde, exactamente, se estaba produciendo la fuga de memoria.
Si queréis aun más información sobre los temas tratados en este post, os dejo la siguiente lectura recomendada:
Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework I y II
Investigating Memory Issues
Identify And Prevent Memory Leaks In Managed Code
.NET Debugging Demos Lab 3: Memory
.NET Debugging Demos Lab 3: Memory - Review