Alto consumo de memoria y cursores de datos

Sorpresas te llevas en la vida, siempre. A pesar de lo que parezca, hoy no ando sermoneador ni nada por el estilo. Es sólo que no se me ocurre como comenzar este post así que escribo lo primero que se me ocurre [:)]. Total, lo interesante viene ahora.


Viaje de emergencia, aplicación ASP con [OOM], servicio interrumpido.
Resumen: problemas… un poco de entretención para unos meses muy aburridos.


Síntomas


Como mencionaba, tenemos una aplicación ASP que de vez en cuando lanza excepciones por falta de memoria, mas conocidos como error 500 ASP 147. Obviamente con el reinicio del proceso, todo vuelve a la normalidad, pero luego de la calma, llega la tormenta.


Como ya es costumbre, se capturaron algunos dumps de memoria cuando se estaba produciendo el error y se analizaron. Los resultados fueron sorprendentes, los que pasan a ser mostrados ahora.


Lo primero muy interesante es que el dump apenas sobrepasaba los 100 megabytes. Un dump contiene, sin entrar en grandes detalles, los datos privados del proceso y las librerías cargadas entre otras cosas. Si son un poco más de 100 megabytes, ¿cómo es posible que haya falta de memoria?. El administrador de tareas confirmaba que el proceso estaba utilizando algo más de 100 megabytes en working set y un poco menos en memoria privada


¿Entonces?


[windbg] entra a ayudarnos. Revisando el estado de cada heap, nos encontramos con lo que muestra el bloque de más abajo. Hay muchas columnas y muchos datos, pero fijemos la atención en las columnas que hacen mención a la memoria reservada y comprometida.


Recordemos que en un sistema operativo Windows, la memoria puede estar en tres estados: libre, reservada y comprometida. Para que una aplicación la pueda utilizar necesita primero reservarla, y luego comprometerla. Después de usarla, la debe des-comprometer (fea palabra, lo sé) y luego liberar (des-reservar, también es fea).





  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast
(k) (k) (k) (k) length blocks cont. heap
—————————————————————————–
00080000 00000002 15360 13424 14512 2362 514 137 0 9d L
External fragmentation 17 % (514 free blocks)
00180000 00008000 64 12 12 10 1 1 0 0
002b0000 00001002 22080 9560 17536 1629 231 124 25 5b L
External fragmentation 17 % (231 free blocks)
Virtual address fragmentation 45 % (124 uncommited ranges)
00550000 00000002 1024 20 20 2 1 1 0 0 L
00690000 00001002 256 32 32 2 1 1 0 0 L
01bb0000 00001002 256 12 12 4 1 1 0 0 L
01bf0000 00001002 39872 11200 29608 722 62 60 0 7 LFH
01c30000 00001002 256 12 12 4 1 1 0 0 L
01c70000 00001002 256 12 12 4 1 1 0 0 L
<recortado>
02630000 00001002 256 12 12 2 1 1 0 0 L
02670000 00001002 256 12 12 4 1 1 0 0 L
02730000 00001002 64 32 32 4 1 1 0 0 L
02a20000 00001002 3328 2084 2396 110 23 16 0 0 LFH
02a60000 00001002 19968 7164 8076 35 6 10 21 0 LFH
02f60000 00001003 1280 1152 1152 2 1 1 0 bad
03470000 00001003 1280 512 512 1 1 1 0 bad
034b0000 00001003 1280 524 524 2 1 1 0 bad
034f0000 00001003 256 96 96 0 0 1 0 bad
03730000 00001003 1280 356 356 1 1 1 0 bad
03970000 00001003 1280 264 264 0 0 1 0 bad
049b0000 00001003 256 204 204 3 1 1 0 bad
049f0000 00001003 130304 128 300 63 11 12 0 bad
04a30000 00001003 441600 112 188 101 11 12 0 bad <-éste
04a70000 00001003 167204 352 424 264 49 67 0 bad
04ab0000 00001003 465716 128 36532 103 20 27 0 bad
04af0000 00001003 469708 164 1696 92 13 32 0 bad
04b30000 00001003 46312 324 328 254 47 65 0 bad
04b70000 00001003 9700 372 372 348 62 64 0 bad
039c0000 00001002 64 16 16 2 1 1 0 0 L
04c30000 00001003 256 148 148 92 36 1 0 bad
<recortado>

En el listado anterior, vemos que hay un par de heaps que han reservado (memoria en estado reservado) más de 400 megabytes, pero que sólo están utilizando (memoria en estado comprometido) un poco más de 100 kilobytes. Entre varios, el heap 04a30000, indicado más arriba en negrilla y con la palabra “<- éste”, es uno de los más grandes.


Veamos el detalle de este heap y sus segmentos, listados a continuación.





Index   Address  Name      Debugging options enabled
111: 04a30000
Segment at 04a30000 to 04a70000 (00010000 bytes committed)
Segment at 0f940000 to 0fa40000 (00003000 bytes committed)
Segment at 0fa40000 to 0fc40000 (00001000 bytes committed)
Segment at 100d0000 to 104d0000 (00001000 bytes committed)
Segment at 104d0000 to 10cd0000 (00001000 bytes committed)
Segment at 10cd0000 to 11cd0000 (00001000 bytes committed)
Segment at 11cd0000 to 13cd0000 (00001000 bytes committed)
Segment at 13cd0000 to 17cd0000 (00001000 bytes committed)
Segment at 17ed0000 to 1fed0000 (00001000 bytes committed)
Segment at 4dbd0000 to 55bd0000 (00001000 bytes committed)
Segment at 5bb60000 to 5eb60000 (00001000 bytes committed)

Mmm… mmm…mmm…mmm (esto me recuerda una canción de hace unos años), la mayoría de ellos no tiene más de 4 kilobytes usados para bloques de varios megabytes reservados. Si las matemáticas no te ayudan ahora, 1000 en hexadecimal es equivalente a 4096 en decimal.


Análisis de la situación


Recordemos que el manejo de la memoria lo realiza generalmente el sistema operativo aunque algunas aplicaciones pueden utilizar sus propios manejadores de memoria. Desde código ASP (VBScript) o Visual Basic 6.0, como también desde código manejado NO es posible trabajar a este nivel con la memoria. Lo anterior es un problema en un manejador de memoria.


Si no es ASP, VB. 6.0, ¿qué puede ser? (considerando que no hay componentes desarrollados por el cliente en C o C++)


La respuesta la da [DebugDiag]. Quien creo el heap es “Microsoft Data Access Runtime”, es decir, MDAC. Revisando la versión instalada, comprobamos que es la última con Windows Server 2003 SP2. El camino se pone difícil.


Investigación y resolución


Involucrando a las personas adecuadas, aprendimos que este comportamiento es considerado “esperado” cuando se cumplen las siguientes condiciones:



  • Se utilizan recordset del lado del cliente (client-side cursor)

  • Se obtienen muchos datos, muchos datos de una tabla

Ok ¿client-side cursor?¿que significa “muchos datos”?


“Cliente” es quien consulta la base de datos, que para este caso es IIS/ASP. En ese caso, los datos se llevan al cliente para ser luego procesados.


Después de investigar en el código, se encontró que una consulta estaba retornando más de 2 millones de registros. Eso es mucho [:)]


Reproducción


Decidido a demostrarlo, procedí a hacer unas pruebas con el siguiente código en mi “servidor.”



Y le agregué a mi tabla algo así como 4 millones de registros.


Después de varias ejecuciones, tanto en paralelo como en serie, los contadores de memoria reservada, comprometida y utilización de procesador mostraron esto:



Se puede ver que la memoria comprometida (verde) llegó como mucho hasta 300 megabytes, pero la memoria reservada (roja) aumentó sin mostrar intención de disminuir, llegando casi hasta 900 megabytes.


¿Cuál es la explicación a que no reutilice la memoria reservada y siga reservando más? Al menos yo no tengo la respuesta.


¿Que sucede cuando llegue a 2 gigabytes? [OOM]


Conclusiones


1.- Nunca desplegar “muchos” registros en una página. Mejor aún, nunca pedir muchos registros a la base de datos.


2.- Utilicen server-side cursors. Hagan la prueba con el mismo código y comparen los resultados. [;)]


Saludos,
Patrick

5 Replies to “Alto consumo de memoria y cursores de datos”

  1. Es impresionante este blog de alto contenido técnico, el 90% de las cosas no las entiendo pero aún asi me parece un trabajo bastante interesante, al autor del blog solo comentarle cual fue su formación técnica para tener los conocimientos q tiene sobre el analisis de aplicaciones? Enhorabuena por el blog!!

  2. Hola,

    gracias por tus palabras. Si tuviese que definir cual ha sido uno de los mejores libros que te pueden ayudar a realizar este trabajo, elegiría “Windows Internals” de Mark Russinovich y David Solomon.

    Entender el funcionamiento de Windows es la base para el resto. IIS, SQL, ASP, ASPNET y otros productos usan las funcionalidades que el sistema operativo expone.

    Para este caso en especial, está involucrado el memory manager de windows, y ojo que no significa que funcione mal, pero entendiendo como funciona la memoria en windows, sabes adonde apuntar cuando estás buscando los soluciones para las aplicaciones con problemas.

    Hay otros libros muy útiles también, pero lo mas importante es entender “el ambiente” (el sistema operativo) donde corren las aplicaciones.

    Saludos

    Patrick

  3. Supongo que sera fundamental como dices y que te habrán hecho falta conocimientos muy sólidos de sistemas operativos para poder ver como funciona todo el tinglado.
    Como sugerencia te animo a que en alguna ocasión y ya que esta tan de moda el J2EE y Java escribas algun post con algún análisis sobre rendimiento y monitoreo de las aplicaciones en la JVM.
    Un saludo desde España y felicitaciones de nuevo por el blog, francamente interesante.

  4. I’m getting a new computer but don’t want to lose my Firefox bookmarks. Is there an easy way to save a record of all the URLs in my Bookmarks and then quickly upload them to Firefox on my new computer?.

Leave a Reply

Your email address will not be published. Required fields are marked *