Traza de asp.net y el consumo de memoria

El siguiente caso a presentar está relacionado con el alto consumo de memoria de una aplicación. Como el título lo dice, está relacionado con el uso de la traza de asp.net (trace en web.config.)


El escenario era similar a lo descrito ahora. La aplicación analizada empezaba a consumir memoria y aunque tenía momentos donde la liberaba, la impresión general era que en el largo plazo, siempre subía. Este es un típico comportamiento de un memory leak o pérdida de memoria; la tendencia al alza, aunque con momentos donde baja un poco.


Como ya es tradición, un dump del proceso y mi amigo [windbg] nos darían las pistas necesarias. También nos apoyamos en performance monitor, pero hay veces que ya sabes que es una pérdida de memoria y éste no te dirá mucho más de lo que ya sabes.


Con el dump en mis manos, hacemos una revisión del proceso afectado y el estado de la memoria virtual del éste.


Recolección de información general


Tamaño del dump: ~300 MB. No es muy grande, pero tampoco es descartable. Tradicionalmente se espera a que la memoria privada del proceso llegue al menos hasta 500 MB para declararlo como sospechoso.


Información del proceso:





System Uptime: 180 days 15:52:41.140
Process Uptime: 0 days 2:08:23.000
Kernel time: 0 days 0:00:13.000
User time: 0 days 0:08:48.000

El sistema operativo lleva corriendo 180 días sin reiniciarse. El proceso en cuestión lleva del orden de 2 horas y 8 minutos corriendo y que ha consumido 13 segundos en modo privilegiado (kernel) y 8 minutos y 48 segundos en modo usuario.

Con 8 minutos de procesamiento ya llegó a 300 MB. Interesante.







El estado de la memoria virtual concuerda con el tamaño del dump, reflejado gráficamente en la imagen de la derecha, la cual pueden pinchar para agrandar.

Efectivamente hay aproximadamente 300 MB de memoria virtual, de los cuales casi 190 son de memoria manejada.


Memoria manejada


Haciendo una revisión de los heaps del GC, obtenemos la información de la siguiente lista. El servidor tiene 4 procesadores por lo que se crean cuatro heaps del GC, cada uno con sus propias generaciones, heaps efímeros y heaps de objetos grandes.





Number of GC Heaps: 4
——————————
Heap 0 (0x000d01c8)
generation 0 starts at 0x121135c8
generation 1 starts at 0x12109a44
generation 2 starts at 0x102d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x102d0000 0x102d0030 0x121135d4 0x01e435a4(31,733,156) 0x00d79000
Large object heap starts at 0x202d0030
segment begin allocated size reserved
0x202d0000 0x202d0030 0x205d2168 0x00302138(3,154,232) 0x00cdd000
Heap Size 0x2245770(35,936,112)
——————————
Heap 1 (0x000d08d8)
generation 0 starts at 0x16009a28
generation 1 starts at 0x15eaacac
generation 2 starts at 0x142d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x142d0000 0x142d0030 0x16009a34 0x01d39a04(30,644,740) 0x014c7000
Large object heap starts at 0x212d0030
segment begin allocated size reserved
0x212d0000 0x212d0030 0x21449a38 0x00179a08(1,546,760) 0x00e66000
Heap Size 0x1f2b4a0(32,683,168)
——————————
Heap 2 (0x000d13d8)
generation 0 starts at 0x24461404
generation 1 starts at 0x243e6e2c
generation 2 starts at 0x182d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x182d0000 0x182d0030 0x1998c7dc 0x016bc7ac(23,840,684) 0x028ef000
0x242d0000 0x242d0030 0x24461410 0x001913e0(1,643,488) 0x033c4000
Large object heap starts at 0x2a2d0030
segment begin allocated size reserved
0x2a2d0000 0x2a2d0030 0x2a2d0030 0x00000000(0) 0x047df000
Heap Size 0x198fb8c(26,803,084)
——————————

Heap 3 (0x000d1ca0)
generation 0 starts at 0x087954cc
generation 1 starts at 0x083dd820
generation 2 starts at 0x1c2d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x1c2d0000 0x1c2d0030 0x1df48b8c 0x01c78b5c(29,854,556) 0x02310000
0x08330000 0x08330030 0x087954d8 0x004654a8(4,609,192) 0x01fff000
Large object heap starts at 0x232d0030
segment begin allocated size reserved
0x232d0000 0x232d0030 0x2344ed50 0x0017ed20(1,568,032) 0x00e61000
0x0edc0000 0x0edc0030 0x0f195f40 0x003d5f10(4,022,032) 0x00c2a000
Heap Size 0x277ac50(41,397,328)
——————————
Reserved segments:
——————————
GC Heap Size 0x827b3ec(136,819,692)

De esta información se puede ver que hay muchos objetos en generación 2, lo que significa que hay objetos que no se han liberado y han ido envejeciendo más de lo necesario.


¿Qué hay en la memoria?


Haciendo un vaciado estadístico de los objetos, utilizando [windbg] y sos, obtenemos un listado como el de a continuación, donde se ha eliminado parte del comienzo y sólo se ha dejado el final, que contiene la información relevante para el caso.





MT        Count TotalSize Class Name
…..<recortado por el bien de esta página>…
0x028665f0 22,807 273,684 System.Xml.Xsl.EndEvent
0x02784adc 14,800 296,000 System.Data.DataRowCollection
0x027844dc 14,800 296,000 System.Data.DataRowBuilder
0x027862d4 14,911 298,220 System.Data.ColumnQueue
0x02782a74 11,824 331,072 System.ComponentModel.CollectionChangeEventHandler
0x0230159c 14,866 356,784 System.Xml.NameTable/Entry
0x02925aec 7,847 408,044 System.Xml.Xsl.XsltCompileContext
0x02926bfc 11,589 417,204 System.Xml.XPath.XPathChildIterator
0x02863d04 8,692 417,216 System.Xml.XPath.ChildrenQuery
0x027846d4 14,800 473,600 System.Data.RecordManager
0x0278001c 14,800 473,600 System.Data.ConstraintCollection
0x027842e4 14,913 477,216 System.ComponentModel.PropertyDescriptorCollection
0x0280cddc 16,754 670,160 System.Xml.XPath.XPathWhitespace
0x0280c924 10,044 682,992 System.Xml.XPath.XPathElement
0x02866564 22,807 729,824 System.Xml.Xsl.BeginEvent
0x0252ebe4 14,800 769,600 System.Data.DataColumnCollection
0x79bda488 14,813 888,780 System.Threading.ReaderWriterLock
0x79bab93c 17,498 909,896 System.Collections.Hashtable
0x02923770 28,877 924,064 System.Xml.DocumentXPathNavigator
0x027882e4 40,203 964,872 System.Data.Common.StringStorage
0x0280cba4 21,157 1,015,536 System.Xml.XPath.XPathAttribute
0x0278489c 29,600 1,184,000 System.Data.DataRelationCollection/DataTableRelationCollection
0x01ba2c28 17,561 2,839,896 System.Collections.Hashtable/bucket[]
0x0252e4b0 14,800 3,433,600 System.Data.DataTable
0x79ba2ee4 145,618 3,494,832 System.Collections.ArrayList
0x0278670c 54,976 3,518,464 System.Data.DataColumnPropertyDescriptor
0x01ba2964 24,879 5,024,760 System.Int32[]
0x0278513c 136,364 5,454,560 System.Data.DataRow
0x0252f5d0 54,760 7,009,280 System.Data.DataColumn
0x01ba26b0 1,262 8,513,840 System.Byte[]
0x000cff48 2,829 14,897,824 Free
0x79b94638 238,884 23,296,388 System.String
0x01ba209c 235,235 40,966,392 System.Object[]
Total 1,509,853 objects, Total size: 136,798,976

Existe un poco más de 1,5 millones de objetos en memoria, que consumen cerca de 136 MB, similar al resultado entregado en la lista anterior.


De esos 1,5 millones de objetos, existe una gran cantidad de strings y arreglos de objeto, como también de filas de datos y columnas de datos. Este comportamiento se presenta cuando se han creado Datasets y no se han liberado.


Un DataSet contiene filas y columnas, y en cada celda encontramos objetos de los tipos básicos como strings, enteros, decimales, objetos, etc. Por lo tanto, estos objetos que se ven al final no viven solos sino que están referenciados desde otro objeto, como podría ser un dataset. No significa que todos estén referenciados, pero con 136 mil filas (DataRow) y 54 mil columnas (DataColumn), varios de esos miles de objetos si lo están.


¿Cuantos DataSets hay?





MT         Count TotalSize Class Name
0x0252ce84 1,597 127,760 System.Data.DataSet
Total 1,597 objects, Total size: 127,760

Bueno, tomemos uno al azar y veamos donde nos lleva. Elegimos el objeto en la posición de memoria 0x0834d148.





Name: System.Data.DataSet
MethodTable 0x0252ce84
EEClass 0x0275b43c
Size 80(0x50) bytes
GC Generation: 2
mdToken: 0x0200003b (c:\windows\assembly\gac\system.data\1.0.5000.0__b77a5c561934e089\system.data.dll)
FieldDesc*: 0x0252c4b4
MT Field Offset Type Attr Value Name
0x0252c23c 0x4000583 0x4 CLASS instance 0x00000000 site
0x0252c23c 0x4000584 0x8 CLASS instance 0x00000000 events
0x0252c23c 0x4000582 0 CLASS shared static EventDisposed
>> Domain:Value 0x000c4ac8:NotInit 0x00138380:0x1c331c58 <<
0x0252ce84 0x40003d3 0xc CLASS instance 0x15cee704 defaultViewManager
0x0252ce84 0x40003d4 0x10 CLASS instance 0x0834d198 tableCollection
0x0252ce84 0x40003d5 0x14 CLASS instance 0x0834d220 relationCollection
0x0252ce84 0x40003d6 0x18 CLASS instance 0x00000000 extendedProperties
0x0252ce84 0x40003d7 0x1c CLASS instance 0x1c331c30 dataSetName
0x0252ce84 0x40003d8 0x20 CLASS instance 0x102d0224 _datasetPrefix
0x0252ce84 0x40003d9 0x24 CLASS instance 0x102d0224 namespaceURI
0x0252ce84 0x40003da 0x40 System.Boolean instance 0 caseSensitive
0x0252ce84 0x40003db 0x28 CLASS instance 0x1c2e69c8 culture
0x0252ce84 0x40003dc 0x41 System.Boolean instance 1 enforceConstraints
0x0252ce84 0x40003dd 0x42 System.Boolean instance 0 fInReadXml
0x0252ce84 0x40003de 0x43 System.Boolean instance 0 fInLoadDiffgram
0x0252ce84 0x40003df 0x44 System.Boolean instance 0 fTopLevelTable
0x0252ce84 0x40003e0 0x45 System.Boolean instance 0 fInitInProgress
0x0252ce84 0x40003e1 0x46 System.Boolean instance 1 fEnableCascading
0x0252ce84 0x40003e2 0x47 System.Boolean instance 0 fIsSchemaLoading
0x0252ce84 0x40003e3 0x2c CLASS instance 0x00000000 rowDiffId
0x0252ce84 0x40003e4 0x48 System.Boolean instance 0 fBoundToDocument
0x0252ce84 0x40003e5 0x30 CLASS instance 0x00000000 onPropertyChangingDelegate
0x0252ce84 0x40003e6 0x34 CLASS instance 0x00000000 onMergeFailed
0x0252ce84 0x40003e7 0x38 CLASS instance 0x00000000 onDataRowCreated
0x0252ce84 0x40003e8 0x3c CLASS instance 0x00000000 onClearFunctionCalled
0x0252ce84 0x40003e9 0 CLASS shared static zeroTables
>> Domain:Value 0x000c4ac8:NotInit 0x00138380:0x1c331c20 <<

Mmm, nada más que un dataset. No se si podría haber encontrado otra cosa [:)]. Veamos el nombre de éste, en la posición 0x1c331c30 (línea blanca arriba).





String: NewDataSet

[:s]. El nombre genérico no da pistas para encontrar alguna parte del código que lo esté creando. Vemos entonces quién lo está apuntando.





Scan Thread 10 (0x1cac)
Scan Thread 16 (0xa00)
Scan Thread 17 (0x1908)
Scan Thread 5 (0xa0c)
Scan Thread 4 (0x1dcc)
Scan Thread 19 (0x1a6c)
Scan Thread 3 (0x1584)
Scan Thread 2 (0x444)
Scan Thread 20 (0x15f4)
ESP:36ff548:Root:0x1c6b59a8(System.Threading.Thread)->0x15cecad4(System.Runtime.Remoting.Messaging.IllogicalCallContext)->0x15cecae0(System.Collections.Hashtable)->0x15cecb14(System.Collections.Hashtable/bucket[])->0x15cec81c(System.Web.HttpContext)->0x15cec608(System.Web.Hosting.ISAPIWorkerRequestInProcForIIS6)->0x182d81c4(System.Web.HttpWorkerRequest/EndOfSendNotification)->0x14339e70(System.Web.HttpRuntime)->0x182d7fac(System.Web.Util.Profiler)->0x14358454(System.Collections.ArrayList)->0x1d89d7b0(System.Object[])->0x834d148(System.Data.DataSet)
Scan Thread 22 (0x1098)
Scan Thread 24 (0xcd8)
…..<recortado por el bien de esta página>…
Scan Thread 67 (0x1938)
Scan Thread 54 (0x618)
Scan HandleTable 0xc9ec0
Scan HandleTable 0xcd008
Scan HandleTable 0x147008

El thread 20, que está procesando un requerimiento en la extensión ISAPI de asp.net en IIS6 (System.Web.Hosting.ISAPIWorkerRequestInProcForIIS6) tiene una referencia fuerte al objeto. Vemos también un HashTable, el Contexto y Runtime de http, los cuales se pueden considerar normales en el ciclo de vida de un requerimiento web.


Sin embargo, hay un invitado especial en esta lista de referencias. Éste es System.Web.Util.Profiler, del cual no hay mucha información disponible, pero después de examinarlo, ya podemos inferir de quién se trata. Veamos su contenido.





Name: System.Web.Util.Profiler
MethodTable 0x021c49d0
EEClass 0x0215912c
Size 28(0x1c) bytes
GC Generation: 2
mdToken: 0x0200029d (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x021c4704
MT Field Offset Type Attr Value Name
0x021c49d0 0x4001076 0x8 System.Int32 instance 1478 _requestNum
0x021c49d0 0x4001077 0xc System.Int32 instance 7500 _requestsToProfile
0x021c49d0 0x4001078 0x4 CLASS instance 0x14358454 _requests
0x021c49d0 0x4001079 0x14 System.Boolean instance 0 _pageOutput
0x021c49d0 0x400107a 0x15 System.Boolean instance 1 _isEnabled
0x021c49d0 0x400107b 0x16 System.Boolean instance 1 _oldEnabled
0x021c49d0 0x400107c 0x17 System.Boolean instance 0 _localOnly
0x021c49d0 0x400107d 0x10 System.Int32 instance 0 _outputMode

Recordando las propiedades de la traza de asp.net, podemos ver asombrosas similitudes con las propiedades. Veamos la definición de la traza en el archivo web.config. La información oficial dice:





<trace
enabled=”true|false”
localOnly=”true|false”
pageOutput=”true|false”
requestLimit=”integer”
mostRecent=”true|false”
writeToDiagnosticsTrace=”true|false”
traceMode=”SortByTime|SortByCategory”
/>

El dump dice que la traza estaba habilitada (_isEnabled), no estaba local (_localOnly), que tenía marcado para almacenar hasta 7.500 requerimientos (segunda línea blanca) y que habían 1478 almacenados (primera línea blanca). El resto de las propiedades las pueden chequear ustedes. Recordemos que habían 1597 DataSets en memoria.


El objeto _requests es un System.Collections.ArrayList y los 1478 objetos dentro suman casi la totalidad de memoria manejada, que si bien recordarán, sumaba cerca de 136 MB.





sizeof(0x14358454) = 91,258,512 (0x5707e90) bytes (System.Collections.ArrayList)

Entonces, la condición de memoria elevada, se da en parte porque hay ~90 MB de objetos referenciados por la traza de asp.net. También hay una cantidad de importante de memoria (~70 MB) consumida por dlls y ensamblados (ver la primera imagen.)


Conclusiones


Las conclusiones que se pueden obtener en este caso son:




  1. No todos las pérdidas de memoria son producto de errores en la codificación.



  2. Si vas a habilitar la traza en producción, recuerda deshabilitarla una vez que termines.



  3. También, si no vas a poder leer la información de 7500 trazas, configúralo para que almacene menos.


Saludos,
Patrick

Signos vitales de un servidor: Parte II (procesador)

Continuando con los signos vitales de un servidor, hoy día veremos en la segunda parte, el procesador.


El análisis del procesador es bastante más simple que el de la memoria. Hay dos contadores importantes. Uno de ellos corresponde al uso del procesador, con dos matices especiales; uso del procesador en modo usuario y el otro en modo kernel. El otro contador corresponde a la cantidad de requerimientos encolados del procesador.


Como ya hemos visto en otros posts, Windows utiliza dos modos de ejecución. Uno de ellos es el modo usuario, donde corren las aplicaciones que nosotros ejecutamos y el otro es el modo Kernel, en el cual corre el kernel del sistema operativo.


¿Por qué es importante esta explicación?


Si logramos identificar quién está consumiendo el procesador, podremos inferir y tener mejores pistas para encontrar al culpable del consumo de éste.


Si te preguntas ¿para qué necesitas saber el modo (usuario o kernel) que está consumiendo el procesador, si mirando el task manager sabrás fácilmente qué aplicación es responsable?


Bueno. Hay un par de razones. Veamos cada una de ellas.


1.- Task manager te dirá la aplicación que está consumiendo el procesador, pero no te dirá más. Solo la aplicación. Perfmon por otra parte te podrá entregar más información. [procexp] de Sysinternals es una excelente herramienta.
2.- Task manager no muestra siempre el consumo del kernel.


El segundo punto no está tan relacionado con aplicaciones, pero vale la pena aclarar que hay ciertas actividades del kernel que no son cuantificadas por Task Manager, o son cuantificadas pero no desplegadas. Recordemos que el kernel corresponde al proceso llamado System, siempre con el PID 4. Entonces puede parecer que tu sistema tiene bajo uso del procesador, pero el kernel está trabajando activamente. Para mejores resultados en el despliegue de los recursos consumidos, puedes utilizar [procexp].


Resumiendo


Podemos decir que para una aplicación específica:


El porcentaje de consumo en modo kernel corresponde a las actividades que debe realizar el kernel para cumplir con los requerimientos de la aplicación, como por ejemplo, trabajar con archivos, mover memoria, escribir en sockets, pintar ventanas, etc.


El porcentaje de consumo en modo usuario corresponde a la ejecución de código sin interacción con el kernel, es decir, algo así como “el código sin acceso a recursos externos.” Si estuviésemos utilizando un profiler, diríamos entonces que corresponde a lo que se llama “exclusive time.”


Contadores


La siguiente es la lista de contadores de perfmon a utilizar, que como podrán notar, es muy breve, repetitiva y casi no requiere mayor explicación.




  • Objeto System, contador Processor Queue Length: muestra la cantidad de requerimientos encolados para el procesador.



  • Objeto Processor, contador % Privileged Time: muestra el porcentaje de tiempo utilizado por el kernel, para todo el procesador



  • Objeto Processor, contador % User Time: similar al anterior, pero para modo usuario



  • Objeto Process, contador % Privileged Time, instancia específica. muestra el porcentaje de tiempo utilizado por el kernel, para la aplicación seleccionada en el campo instancia



  • Objeto Process, contador % User Time, instancia específica: similar al anterior, pero para modo usuario



  • Objeto Thread, contador % Privileged Time, instancia específica: muestra el porcentaje de tiempo utilizado por el kernel, para un thread especifico de la aplicación seleccionada



  • Objeto Thread, contador % User Time, instancia especifica: similar al anterior, pero para modo usuario


Probablemente te podrás estar preguntando la utilidad de la información de los últimos 2 contadores. ¿de que te serviría saber cuál es el thread responsable si no se puede hacer mucho con esa información?


Si se puede hacer bastante. Puedes determinar si es un único thread que está ocupando mucho el procesador o es un conjunto de ellos en que todos aportan. Te sirve para obtener pistas.


Podría ser el thread o los threads del GC, el thread del Finalizador, un thread que se quedó pegado en un ciclo, etc. [procexp] entrega mucha información referente a tipos de procesos e informacón de los threads.


Ok, ya tengo todos los datos…


¿Contra qué comparar la información?


Salvo para el primer contador, no hay punto de comparación. Si el uso del procesador llega a 80% o más, probablemente sea hora de proveer alivio al servidor, ya sea corrigiendo el código o haciendo un upgrade del hardware.


El primer contador no debe ser mayor a 2 multiplicado por la cantidad de procesadores (cores) de tu servidor. Para el caso de HT (Hyper Threading), no estoy seguro, pero creo que no se deben contabilizar.


Ya vendrá una tercera entrega relacionada con el disco duro.


Saludos,
Patrick

Signos vitales de un servidor: Parte I (memoria)

¿Qué sucede si uno no se siente bien?; en condiciones normales, uno visita al doctor para saber qué sucede y éste hace una revisión general. Si el doctor encuentra algo interesante, te pide hacer unos exámenes para saber con mayor detalle qué está sucediendo.


Llevando este mismo ejemplo al área de la informática, si tienes un servidor y presientes (o tienes total certeza) que algo no funciona como debiera, un buen punto de partida es determinar si los signos vitales de éste están bien.


El objetivo de este post es dar una pauta general para determinar cuando un servidor Windows 2003, cualquier versión, puede tener algún problema que afecte sus signos vitales. Hoy veremos los signos vitales de la memoria. Los siguientes posts abordarán otras áreas.


Un poco de contexto


Si has escuchado muchas veces la palabra contadores, y no tienes claro a que se refieren cuando lo escuchas, esta sección debiera aclararte un poco. Si tienes claro a que se refiere, puedes saltarte  esta sección.


Los contadores son entidades u objetos que mantienen información del funcionamiento de algún programa o sistema operativo (SO de ahora en adelante). Cada programa que utilizas —todos debieran, pero no siempre lo hacen— genera información con estadística y en tiempo real del funcionamiento de éste, la cual se le informa al SO. A su vez, el SO también genera información que almacena para que nosotros podamos consultarla y saber cómo está funcionando.


Los contadores se categorizan por objetos (por ejemplo, memoria, sistema, sql server, iis) y se pueden o no aplicar a instancias (procesos, sitios web, bases de datos, etc.)


Por ejemplo, un contador de importante SO es la cantidad de memoria disponible que le queda. Este se categoriza en el objeto Memory, y el contador se llama Available MBytes, que nos dice la cantidad de megabytes de memoria disponible. Los nombres de los contadores dependen del idioma del programa o SO. Si estás ejecutando un SO en español, el objeto podría llamarse Memoria y el contador, cantidad de MB disponibles.


La siguiente es una imagen de cómo se agrega este contador utilizando performance monitor (perfmon).



Luego, después de habar agregado los contadores, podrás ver una pantalla como ésta, donde la primera es la representación gráfica y la otra la textual, para la misma información.


 


 


Suponiendo que ya se entiende la idea, vamos entonces a ver la primera parte de cómo chequear la salud de un sistema operativo.


Grupos de contadores


Aunque los contadores ya están agrupados de acuerdo a los objetos a los cuales pertenecen, los volveremos a agrupar en clasificaciones más grandes, más generales. Tendremos entonces un grupo para colocar los contadores de memoria, uno para los del procesador, otro para el disco duro, otro para la tarjeta de red y otro para el sistema operativo. En otra oportunidad veremos los asociados a productos específicos como SQL, IIS o .NET.


Contadores de memoria


Los contadores de memoria son variados. Podemos encontrar desde la memoria utilizada por una aplicación, la memoria libre del servidor y la memoria utilizada por los componentes del Kernel del SO. Todas estas son importantes y están de alguna forma relacionadas.


Veamos cada uno de los contadores mencionados y contra qué compararlos (cuando aplica).


Memoria del sistema (incluido kernel)


La memoria libre del sistema la entrega el contador Available MBytes, del objeto Memory. El mismo contador existe retornando el valor en otras unidades como KBytes y Bytes. De acuerdo a la cantidad de memoria que hoy en día tiene un servidor, medir en bytes o kilobytes puede no ser necesario.


Diversa documentación podrán encontrar referente a este contador. Algunos dirán que con 10 MB disponibles, es momento de preocuparse. Otros dirán que 25 MB es una señal de preocupación. En general, menos del 10% de la memoria total instalada ya es reflejo de que al sistema le falta memoria. Por ejemplo, si mi servidor tiene 2GB instalados y le quedan 150 MB disponibles, significa que estoy consumiendo el 92,5% de la memoria. Sin embargo, otros contadores te ayudarán a determinar si esto es cierto o no (si hay o no un problema). Puede ser que tengas mucha memoria ocupada, pero el sistema funcione bien.


Otro contador importante es Pages/sec del objeto Memory. Este refleja la tasa de lecturas y escrituras a disco para resolver problemas de páginas no encontradas (hard faults) en la memoria física, y por ende, tiene que ir al archivo de paginación a buscarla (y guardar otra para hacer espacio).


La información disponible públicamente es muy variada. Desde sitios que dicen que valores de más de 100 son preocupantes. Otros dicen que más de 5 de forma constante es preocupante. Lamentablemente ninguna de esas medidas es agnóstica y depende de otros factores (como el disco). Una medida agnóstica se obtiene al multiplicar este contador por Avg. Disk sec/Transfer del objeto Physical Disk, en la instancia (disco) donde está el archivo de paginación. El resultado de multiplicar ambos corresponde al porcentaje de tiempo de acceso a disco. Si es mayor a 10%, está paginando mucho, lo que puede ser producto de falta de memoria.


Los contadores de memoria del kernel se deben comparar contra valores en unas tablas específicas. Estos son Pool Paged Bytes y Pool Nonpaged Bytes del objeto Memory. Las tablas se encuentran a continuación, tanto para Windows 2000 como Windows 2003 (clic para agrandar).







Windows 2003

Windows 2000 SP4

Los valores obtenidos para cada unos de los contadores, tanto para paged como nonpaged, corresponden a la cantidad de bytes de cada pool que ha sido utilizado. Si es menor al 50% del pool, todo está bien. Mayor a 50% y menor a 80%, es hora de preocuparse. Si es mayor a 80%, hay problemas.


¿Cómo se mide?


Si obtuviste un valor de 56 MB en el contador de paged pool bytes y tu sistema es Windows 2003 con 4 GB y estás usando /3GB, el tamaño total del paged pool bytes es de 258 MB (revisa la tabla). La tasa de uso (56/258) es cercana al 21%, lo que nos dice que nuestro sistema está bien. Lo mismo para nonpaged pool bytes, pero considerando el otro valor en la tabla.


El último contador relacionado con la memoria en el kernel corresponde a Free System Page Table Entries (PTEs), del objeto Memory. Los PTEs son estructuras internas utilizadas por el componente del kernel llamado Memory Manager y que tiene como objetivo administrar la memoria.


Si este contador se reduce mucho (menos de 15 mil o 10 mil), habrá problemas. Usualmente este contador se ve afectado por la utilización de la opción /3GB sin utilizar la opción /userva. Esta anécdota ocurrió en un caso donde no se utilizó /userva.


Después de haber visto las opciones para el sistema y kernel, podemos ver las opciones de memoria disponibles para las aplicaciones.


Memoria de aplicaciones


Para medir la memoria de una aplicación específica, tengo que agregar los siguientes contadores, y asociarlos con el proceso que quiero medir (instancia en la parte derecha de la ventana):




  • Objeto Process, contador Private Bytes



  • Objeto Process, contador Virtual Bytes



  • Objeto Process, contador Working Set


El primer contador corresponde a la memoria privada el proceso. El segundo contador, a la memoria virtual del proceso. Para un mejor entendimiento de los tipos de memoria que miden, te recomiendo la lectura de estos posts:



El working set corresponde a la cantidad de memoria privada (sumada a la memoria usada por dlls y otras estructuras) que está cargada en memoria real (física). A grosso modo, la suma de los working set de todos los procesos no podrá superar la memora física de tu máquina.


Lamentablemente no hay valores contra qué compararlos, pero de acuerdo a lo que vimos en el segundo post de la lista de más arriba, la memoria virtual no podrá superar los 2 GB en un sistema de 32 bits.


Si virtual bytes es muy elevado en comparación con private bytes, podría haber un problema de fragmentación de memoria. ¿Qué significa muy elevado?; tampoco hay una respuesta, pero unas 10 veces debiera ser preocupante, como también es preocupante acercarse a los 2 GB. Claro que las 10 veces dependerá de la memoria que use nuestro proceso. Si un proceso utiliza 10 MB de memoria privada, es normal que utilice 100 MB de memoria virtual.


Hay muchas cosas relativas, pero lo importante es monitorearlos en el tiempo.


Si virtual bytes sube y sube sin descender (o descendiendo menos de lo que aumenta), habrá un problema de fragmentación de memoria, aunque dependerá de cómo aumente private bytes también.


Si private bytes sube y sube sin descender (o descendiendo menos de lo que aumenta), tendrás un problema de pérdida de memoria o memory leak. Private bytes no podrá ser superior al máximo de memoria virtual de un proceso (2GB).


Otros contadores


Sin duda, hay muchos más contadores, pero para determinar signos vitales, con éstos basta.


Desde Santiago de Chile,
Patrick