Debugging avanzado en .NET – Parte 1

Diagnosticar y corregir errores en las aplicaciones siempre ha sido difícil, sobre todo cuando estos errores no tienen una causa aparente o no se pueden reproducir fácilmente. Seguramente a muchos de ustedes esta situación les resulte familiar: luego de mucho meses de desarrollo y pruebas en distintos ambientes, nuestra aplicación (que ha sido diseñada teniendo en cuenta buenas prácticas de arquitectura y ha sido sometida muchas instancias de pruebas) es puesta en producción, al tiempo (inmediatamente o después de meses o años) empieza “colgarse”, consume demasiada memoria (incluso hasta que termina abruptamente) o se vuelve lenta sin estar sometida a demasiada carga. Todo esto sin una explicación aparente y lo que es peor aun en forma aleatoria no pudiendo reproducir estos problemas en un entorno distinto al productivo. La primera reacción de la mayoría de nosotros es decir “¡no puede ser, en mi maquina funciona!”, luego del shock inicial tratamos de diagnosticar el problema en forma “artesanal”, la mayoría de las veces con poco éxito, y aplicando soluciones parciales que atacan el síntoma y no el problema. Aunque no lo crean he visto muchas instalaciones en donde la solución a casos como los que he descripto consistía en un operador monitoreando la aplicación para reiniciarla cuando detecta que esta “colgada”.

En esta serie de artículos voy a explicar técnicas y herramientas poco difundidas para diagnosticar y resolver este tipo de problemas. Además, estas técnicas, sirven para ayudar a mejorar la calidad de las aplicaciones aun cuando estas no presenten problemas graves (por ejemplo la aplicación consume demasiada memoria pero termina en forma abrupta). Este primer artículo contiene una introducción a las diferentes técnicas de debugging, las ventajas y desventajas de cada una, las distintas herramientas a utilizar, como obtenerlas, como configurarlas y como utilizarlas.

Diagnosticando en distintos ambientes
El principal motivo para depurar una aplicación es encontrar la causa de un problema de performance, operación, configuración o una condición de error anormal que impida el normal funcionamiento del sistema. Esta definición aplica tanto al ambiente de desarrollo como al ambiente de producción, pero desde el punto de vista del debugging ambos ambientes tienen características y posibilidades muy distintas.
En el ambiente de desarrollo se tiene control total, la seguridad es menos estricta y se pueden instalar herramientas de diagnostico invasivas (básicamente un IDE con un debugger). En este ambiente se utiliza la técnica de debugging más común y antigua: Live Debugging, es decir ejecutar el código de la aplicación línea por línea verificando el valor de las variables y el contexto de ejecución hasta encontrar el error.
Producción es un ambiente mucho más controlado en el que no se pueden instalar herramientas de diagnostico invasivas y en el que normalmente el equipo de desarrollo ni siquiera tiene acceso, por lo que iniciar una sesión de Live Debugging local o remoto es muy difícil. Aun cuando dispongamos del nivel de acceso necesario para instalar herramientas de diagnostico o iniciar sesiones de Live Debugging esto podría interrumpir la normal ejecución de las partes de la aplicación que no presentan errores o de otras aplicaciones que se ejecutan los mismos equipos, situación para nada deseable en un entorno productivo.
Por otra parte la técnica de Live Debugging es útil para diagnosticar errores que se pueden reproducir siguiendo un patrón, pero la mayoría de los errores que pueden producir que una aplicación deje de operar totalmente no se pueden reproducir mediante un patrón y seguramente solo ocurren en el ambiente productivo únicamente. La dificultad para reproducir problemas en el entorno de desarrollo se debe generalmente a que el hardware, software y configuraciones son distintos a los utilizados en producción y a que la carga a la que está sometida la aplicación es también distinta, no pudiéndose reproducir fácilmente en desarrollo los mismos niveles de uso que se registran en producción.
El tiempo es también un factor importante. Cuando una condición de error se produce en desarrollo generalmente se dispone de más tiempo para diagnosticar su causa y corregir el problema. En cambio en producción el tiempo desde que se produce a la falla hasta que la aplicación vuelve a operar normalmente debe ser el menor posible. Esto es así fundamentalmente porque cada segundo que la aplicación esta fuera de línea (total o parcialmente) la organización pierde dinero. Por este motivo en producción es más importante aplicar soluciones que hagan que la aplicación vuelva a estar 100% operacional lo más rápido posible, aun cuando estas no sean del todo “elegantes”, para luego con más tiempo poder encarar una “refactorizacíon” más profunda.
El diagnostico y análisis de errores en producción es, en esencia, la habilidad de diagnosticar un problema sin utilizar herramientas intrusivas (como un IDE) y sin interrumpir la normal operación de las partes de la aplicación que funcionan normalmente, u otras aplicaciones ejecutando en el mismo hardware. Estás técnicas también sirven para depurar en desarrollo cuando las herramientas no aportan información o bien es un problema de reproducción esporádica.
Fases del proceso de diagnostico
Al diagnosticar un problema en una aplicación hay dos fases:
·         Discovery Phase: El objetivo de esta fase es obtener información del estado de la aplicación y el servidor al momento de producirse el error y luego permitir que continúen su funcionamiento. Esta fase a menudo no es tenida en cuenta pero es tan importante como la siguiente.
·         Debugging Phase: En esta fase se utilizan dos técnicas.
o   Live Debugging.
o   Post-Mortem Debugging: Consiste en analizar la información recolectada en la fase anterior para encontrar la causa del problema. La ventaja de esta técnica es que se trabaja con información obtenida de un problema real en vez de tratar de encontrar los pasos necesarios para reproducir el problema.
En cada fase y para cada técnica se utilizan herramientas distintas.
Herramientas para la fase de Discovery
Incluidas dentro de Windows hay dos herramientas muy útiles para obtener información en la fase de Discovery:
·         Task Manager: Esta muy conocida herramienta permite obtener importantes métricas en tiempo real como la utilización de CPU, memoria física o memoria virtual.
·         Performance Monitor: Permite registrar los valores de los diferentes contadores de performance, en tiempo real o en un archivo para su posterior análisis.
También existen otras herramientas que permiten obtener valiosa información para el diagnostico de un problema:
·         Autodump+ (ADPlus): Esta herramienta está incluida dentro del paquete Debugging Tools for Windows, disponible para su descarga desde http://www.microsoft.com/whdc/devtools/debugging/default.mspx. Es un script (VBScript) que utiliza el Core Debugger para obtener vuelcos de memoria y logs que para el análisis post-mortem. Se puede utilizar para obtener información en forma inmediata o para monitorear una aplicación y tomar datos en el momento de su caída. Al ser un script su principal ventaja es su facilidad de instalación, pero tiene como desventaja que si queremos dejar monitoreando una aplicación es necesario que una sesión interactiva este iniciada en el servidor.
·         Debug Diagnostics: Originalmente pensada analizar problemas de memoria en Internet Information Server, esta herramienta sirve además para obtener y analizar datos y vuelcos de memoria en cualquier tipo de proceso en Windows. Lo interesante de esta herramienta es que se instala como un servicio lo que permite obtener  datos sin que tener que dejar una sesión interactiva abierta en el servidor. También dispone de una consola grafica de administración y una serie de wizards que diagnostican automáticamente los problemas de memoria más comunes. Viene incluida dentro del paquete Internet Information Services Diagnostic Tools, disponible para su descarga desde http://www.microsoft.com/windowsserver2003/iis/diagnostictools/default.mspx.
Herramientas para la fase de Debugging
Las siguientes herramientas permiten hacer Live Debugging o analizar la información obtenida en la fase de Discovery (Post-Mortem Debugging):
·         WinDbg: Es un debugger de código nativo con interfaz grafica que viene incluido dentro del paquete Debugging Tools for Windows. Se puede utilizar para Live Debugging de procesos locales o remotos, permite analizar post-mortem vuelcos de memoria y además soporta extensiones para ampliar sus capacidades. WinDbg utiliza DBGENG.DLL que es una biblioteca que ofrece servicios de debugging y sobre la cual se basan casi todos las UI de debugging que se incluyen en el paquete Debugging Tools for Windows (como el Core Debugger, cdb.exe, que es un debugger de consola que también utiliza los servicios de DBGENG.DLL).
·         CorDbg: es un debugger de código manejado con interfaz de consola. Viene incluido en el SDK de .NET Framework (la versión 1.1 del SDK se puede descargar desde http://www.microsoft.com/downloads/details.aspx?FamilyId=9B3A2CA6-3647-4070-9F41-A333C6B9181D&displaylang=en y la versión 2.0 del SDK desde http://www.microsoft.com/downloads/details.aspx?FamilyID=FE6F2099-B7B4-4F47-A244-C96D69C35DEC&displaylang=en). Es muy útil para debugging en ambientes productivos ya que ocupa muy poco tamaño, no requiere instalación y tiene un número pequeño de dependencias.
·         SOS.dll (Son of Strike): Es una extensión que permite depurar código manejado utilizando WinDbg. Incluye una gran cantidad de comandos muy útiles para visualizar información del stack manejado, objetos, threads, Garbage Collector, etc. Viene incluida dentro de .NET Framework (se encuentra en el directorio de instalación). Es importante destacar que existe una versión de SOS para cada versión de .NET Framework y para cada plataforma. De esta manera si estamos debuggeando una aplicación .NET 1.1 debemos usar la versión de SOS incluida dentro de .NET Framework 1.1. Lo mismo si, por ejemplo, estamos analizando un vuelco de memoria de una aplicación .NET 2.0 x64, debemos utilizar la versión SOS incluida en .NET Framework 2.0 x64.
·         SieExtPub.dll: Es otra extensión para WinDbg que se utiliza para visualizar información de threading de COM. Se encuentra dentro del paquete Debugging Tools (que también incluye una versión SOS para .NET Framework 1.0 y una versión de AutoDump+,) y se puede descargar desde http://www.microsoft.com/downloads/details.aspx?FamilyID=7c6ec49c-a8f7-4323-b583-6a7a6aeb5e66&DisplayLang=en.
·         Visual Studio .NET 2003/Visual Studio 2005 debuggers: Son debugger de código nativo y manejado que soportan el debugging al mismo tiempo de código escrito en diferentes lenguajes y scripts. Soportan debugging remoto pero para esto es necesario instalar las herramientas de remote debugging (incluidas en el paquete de instalación de Visual Studio) en la maquina remota que se desea depurar.
Utilizando las herramientas
Para demostrar el uso de algunas de las herramientas que he mencionado anteriormente utilizare como ejemplo un programa WinForm escrito en .NET Framework 2.0. Este programa tiene dos botones, el primero de ellos crea y destruye objetos de 20 MB, y el segundo botón llama al método GC.Collect para forzar que los objetos que no están siendo utilizados se eliminen de la memoria.
Pero este simple programa tiene un problema. Si lo ejecutamos y abrimos el Task Manageren la solapa Process, seleccionado las columnas Memory – Working Set y Memory – Private Working Set, podemos observar que siempre que presionamos el primer botón la cantidad de memoria utilizada aumenta, por más que los objetos se crean e inmediatamente se eliminan.  La cantidad de memoria utilizada tampoco disminuye al forzar la recolección de basura mediante el segundo botón.
Si continuamos presionando el primer botón luego de un tiempo obtendremos una excepción del tipo System.OutOfMemoryException. Esta excepción se produce cuando una alocación de memoria falla, en la mayoría de los casos por que ya no hay más memoria disponible. Luego de producirse la excepción el programa termina abruptamente.
Este error es fácilmente reproducible, es claro que se produce al presionar repetidas veces el primer botón y además al ser una aplicación WinForm sin demasiadas dependencias es fácilmente diagnosticable utilizando Live Debugging . Pero supongamos que el problema no sea reproducible en un ambiente controlado, o no podemos instalar herramientas de debugging en el entorno productivo, o que simplemente no dispongamos del código fuente para poder hacer Live Debugging. En estos casos es donde debemos tratar de obtener la mayor cantidad de información en el momento que se produce el problema.
Una fuente importante de información son los contadores de performance. La herramienta Performance Monitor nos permite seleccionar contadores y visualizar sus valores en tiempo real o registrarlos en un archivo para poder analizarlos posteriormente.
En la próxima entrega de este artículo explicaré en detalle como diagnosticar problemas de memoria, por el momento mencionaré que los contadores que generalmente se utilizan para analizar este tipo de errores son los del grupo .NET CLR Memory, Process\Virtual Bytes y Process\Private Byte.
Otra fuente muy importante de información son los vuelcos de memoria (memory dumps). Como ya he mencionado, la mayoría de las veces es imposible depurar la aplicación en el momento en que el problema se produce. En este tipo de circunstancias los vuelcos de memoria nos permiten tener una “foto” exacta de lo que estaba pasando en el sistema al momento del error. Además por la naturaleza misma del entorno de ejecución de .NET Framework, el CLR, un vuelvo de memoria de un programa .NET tiene mucha más información que un vuelco de memoria de un proceso tradicional, lo que ayuda muchísimo en el diagnostico.
Para obtener vuelcos de memoria podemos utilizar AutoDump+. Esta herramienta  tiene dos modos de utilización: hang y crash. El modo hang permite tomar un vuelco de memoria inmediatamente sin interrumpir la normal ejecución del proceso. Para hacerlo debemos ejecutar desde la consola: adplus.vbs –hang –pn <nombre del proceso> o adplus.vbs –hang –p <identificador del proceso>. En el modo crash AutoDump+ se queda monitoreando hasta que detecta una excepción que hace que el proceso termine en forma abrupta y en ese momento toma un vuelco de memoria. Para activar el modo crash debemos usar el comando: adplus.vbs –crash –pn <nombre del proceso> o adplus.vbs –crash –p <identificador del proceso>.
Junto con los vuelcos de memoria AutoDump+ genera una serie de archivos de texto con información de los registros de CPU, threading y contexto de ejecución al momento de generar el vuelco.
También podemos tomar vuelcos de memoria utilizando Debug Diagnostics. Para ello al ejecutar Debug Diagnostics se inicia un Wizard que nos permite realizar esta tarea. En este Wizard debemos seleccionar la opción Crash, A specific process, el proceso a monitorear y por último si queremos comenzar a monitorear inmediatamente o en otro momento.
Ejecutando el Wizard se creará una “regla de diagnóstico”, podemos tener tantas reglas monitoreando procesos como se quiera y además las reglas se ejecutan mediante el servicio Debug Diagnostic Service (DbgSvc.exe) con lo cual se puede cerrar la consola de administración, la sesión interactiva y hasta reiniciar el sistema operativo y las reglas seguirán activas.
Una vez que obtuvimos un vuelvo de memoria utilizaremos WinDbg y SOS para analizarlo. Para ello primero deberemos configurar la variable de entorno NT_SYMBOL_PATH. Esta variable de entorno permite configurar todas las rutas (separadas por 😉 donde WinDbg va a buscar los archivos de símbolos cuando los necesite. Los símbolos son archivos de datos que contienen información sobre el código ejecutable con el que están asociados. Esta información incluye los nombres de las funciones y su ubicación en memoria, los nombres de las variables globales y locales y su ubicación en memoria, información sobre el código fuente (números de línea, nombres de los archivos, etc.) y otra información importante para el debugging. Los archivos de símbolos de todas las versiones de Windows se pueden descargar desde http://www.microsoft.com/whdc/devtools/debugging/symbolpkg.mspx y los de las diferentes versions.NET Framework vienen incluidos en sus respectivos SDK’s. Pero si contamos con conexión a Internet mientras estamos analizando un vuelco de memoria podemos configurar la variable NT_SYMBOL_PATH para que WinDbg descargue automáticamente los símbolos del servidor público de símbolos de Microsoft a medida que se necesiten, para ello debemos agregar el siguiente texto como si fuera una ruta más: *<directorio de descarga>*http://msdl.microsoft.com/download/symbols.
Ya estamos listos para utilizar WinDbg. Para abrir un vuelco de memoria debemos ir al menú File\Open Crash Dump y elegir el archivo que contiene el vuelco. Para comprobar si se el vuelco se ha generado correctamente podemos usar el comando ~ que lista información sobre todos los threads del proceso.
Una vez abierto el vuelco podemos cargar extensión SOS utilizando el comando .load <ruta de instalación de .NET Framework\SOS.dll>. Para probar SOS podemos utilizar el comando !EEVersion que lista la versión de .NET utilizada en el vuelco y el modo de ejecución del Garbage Collector.
En la ayuda de WinDbg se encuentra la referencia completa de todos sus comandos. La referencia de los comandos de SOS esta en el directorio SDK\v1.1\Tool Developers Guide\Samples\SOS dentro del directorio de instalación de Visual Studio .NET 2003. En la continuación de este artículo explicaré como utilizar alguno de estos comandos para diagnosticar el problema que tiene el programa de ejemplo con el manejo de memoria.
Con WinDbg y SOS también podemos hacer Live Debugging utilizando el mismo entorno y comandos que al analizar un vuelvo de memoria. Para esto se utiliza el menú File\Attach to a Process que despliega una ventana para que seleccionar cual de los procesos que actualmente se están ejecutando queremos depurar.
Conclusión
Como hemos visto hay cierto tipo de errores en una aplicación que no se pueden diagnosticar utilizando los métodos usuales. En estos casos la depuración tradicional línea por líneas no alcanza. Afortunadamente en la plataforma Windows hay disponibles poderosas herramientas que, aunque poco difundidas, son de mucha ayuda para diagnosticar efectivamente esta clase de situaciones.
En esta primera parte he presentado algunas de estas técnicas y herramientas. En la segunda parte de este articulo pondremos en práctica estas herramientas para diagnosticar problemas de memoria en aplicaciones .NET, para ello además repasaremos como el CLR administra la memoria y porque a pesar de contar con el Garbage Collector un programa .NET puede consumir memoria en forma excesiva.
El codigo de ejemplo utilizado en este articulo se puede descargar desde http://prototypes.shockbyte.com.ar/Misc/MemoryCrashProblem.zip

Leave a Reply