Hace algunas semanas estuve de visita en un cliente, en donde me encontré con una aplicación que cada cierto tiempo, experimentaba excepciones de escasez de memoria (Out Of Memory).
Como vimos en el post sobre la analogía entre la memoria de un servidor y un restaurant, http://msmvps.com/blogs/pmackay/archive/2007/02/02/netadmin.aspx, una de las causas por las que se producen las excepciones por falta de memoria, es por la no liberación de los objetos, entre otras.
Después de tomar unos dumps de la memoria del proceso y realizar los análisis pertinentes, encontré gran cantidad de la memoria referenciada en variables de sesión, con variables de tamaño superior a algunos megabytes en algunos casos.
Variables de sesión y el pipeline http de aspnetEs sabido que no se debe almacenar objetos de gran tamaño en las variables de sesión debido al costo que implica su acceso. Recordemos que cada vez que el servidor recibe un requerimiento, éste es llevado a través de todo el pipeline http de ASP.NET y los módulos que están habilitados en él hasta llegar al handler que procesará finalmente el requerimiento. A su vez, cuando el handler termina de procesar el requerimiento, la respuesta debe traspasar el mismo pipeline en sentido opuesto, y una vez terminado el proceso, ésta es enviada al cliente por el servidor.
Uno de estos módulos es el de las variables de sesión. Si está habilitado, como ocurre por defecto, ASP.NET carga las variables de sesión desde el repositorio (InProc, StateServer o SQL Server) cada vez que se hace un requerimiento a una página, independientemente si se van a usar o no, y las almacena de vuelta en el repositorio al descargar la página. Técnicamente hablando, en el caso de InProc no “carga” nada porque estas ya están cargadas en el proceso.
Para tener un control más fino del uso de variables de sesión en una página o en un sitio completo, se puede utilizar la propiedad de la página llamada EnableSessionState (http://msdn2.microsoft.com/en-us/library/ms178581.aspx).
Esta propiedad permite habilitar el acceso a las variables de sesión en dos modalidades. Las modalidades disponibles son lectura solamente y lectura y escritura. También es posible deshabilitar el acceso a las variables de sesión, liberando de realizar trabajo al servidor cada vez que vaya a procesarse esa página.
Volvamos al casoVolviendo al caso que les mencionaba. Por motivos de confidencialidad no puedo exponer la información que obtuve en el caso, pero podemos repetirlo de forma muy simple.
Al revisar el dump obtenido del proceso aspnet_wp.exe, el listado los objetos contenedores de las variables de sesión es el siguiente.
Address MT Size Gen0x0106fad0 0x03749abc 48 2 System.Web.SessionState.InProcSessionState
total 1 objects
sizeof(0x106fad0) = 1,442,100 (0x160134) bytes (System.Web.SessionState.InProcSessionState)
Se puede ver fácilmente que el objeto InProcSessionState tiene un peso de 1,44 MB, que se encuentra en la generación 2 (más antigua) y que existe solo un objeto de este tipo, lo cual es correcto ya que estoy sólo en mi máquina haciendo las pruebas. Extraño sería ver más de 1.
Al analizar internamente el objeto InProcSessionState, podremos ver el contenido y ver que causa su gran tamaño.
Name: System.Web.SessionState.InProcSessionState
MethodTable 0x03749abc
EEClass 0x037599c0Size 48(0x30) bytes
GC Generation: 2
mdToken: 0x02000131 (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x03749a10
MT Field Offset Type Attr Value Name
0x03749abc 0x40009ee 0x4 CLASS instance 0x00feb4c0 dict
0x03749abc 0x40009ef 0x8 CLASS instance 0x00000000 staticObjects
0x03749abc 0x40009f0 0xc System.Int32 instance 20 timeout
0x03749abc 0x40009f1 0x18 System.Boolean instance 0 isCookieless
0x03749abc 0x40009f2 0x10 System.Int32 instance 0 streamLength
0x03749abc 0x40009f3 0x19 System.Boolean instance 0 locked
0x03749abc 0x40009f4 0x1c VALUETYPE instance start at 0106faec utcLockDate
0x03749abc 0x40009f5 0x14 System.Int32 instance 2 lockCookie
0x03749abc 0x40009f6 0x24 VALUETYPE instance start at 0106faf4 spinLock
Este objeto tiene una instancia de SessionDictionary en una variable llamada dict. El contenido de ésta es:
Name: System.Web.SessionState.SessionDictionaryMethodTable 0x03749614EEClass 0x0375963c
Size 44(0x2c) bytes
GC Generation: 2
mdToken: 0x0200013a (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x037494f0
MT Field Offset Type Attr Value Name
0x031ab170 0x4000a8b 0x24 System.Boolean instance 0 _readOnly
0x031ab170 0x4000a8c 0x4 CLASS instance 0x00feb6c0 _entriesArray
0x031ab170 0x4000a8d 0x8 CLASS instance 0x00feb6a8 _hashProvider
0x031ab170 0x4000a8e 0xc CLASS instance 0x00feb6b4 _comparer
Continuando la búsqueda, revisemos que contiene la variable interna _entriesArray, donde está cada variable de sesión de un usuario.
Name: System.Collections.ArrayListMethodTable 0x79ba0d74EEClass 0x79ba0eb0Size 24(0x18) bytesGC Generation: 2
mdToken: 0x020000ff (c:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
FieldDesc*: 0x79ba0f14
MT Field Offset Type Attr Value Name
0x79ba0d74 0x400035b 0x4 CLASS instance 0x00feb6d8 _items
0x79ba0d74 0x400035c 0xc System.Int32 instance 1 _size
0x79ba0d74 0x400035d 0x10 System.Int32 instance 1 _version
0x79ba0d74 0x400035e 0x8 CLASS instance 0x00000000 _syncRoot
_items contiene:
Name: System.Object[]MethodTable 0x00c3209cEEClass 0x00c32018Size 80(0x50) bytes
GC Generation: 2
Array: Rank 1, Type CLASS
Element Type: System.Object
Content: 16 items
—— Will only dump out valid managed objects —-
Address MT Class Name
0x00fec2a0 0x031ab5bc System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry
Y el valor del objeto en el arreglo es:
Name: System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntryMethodTable 0x031ab5bc
EEClass 0x02fbb5c8
Size 16(0x10) bytes
GC Generation: 2
mdToken: 0x0200017a (c:\windows\assembly\gac\system\1.0.5000.0__b77a5c561934e089\system.dll)
FieldDesc*: 0x031ab578
MT Field Offset Type Attr Value Name
0x031ab5bc 0x4000a94 0x4 CLASS instance 0x00fec224 Key
0x031ab5bc 0x4000a95 0x8 CLASS instance 0x01074740 Value
Como bien sabemos, las variables de sesión se almacenan utilizando un identificador, en este caso, el key, y el valor asociado a éste.
Para ver el valor del identificador y el valor de la variable de sesión, vemos el contenido de ambos objetos, acompañado del código fuente de la página.
Name: System.StringMethodTable 0x79b925c8EEClass 0x79b92914
Size 48(0x30) bytes
GC Generation: 2
mdToken: 0x0200000f (c:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
String: pruebavariable
FieldDesc*: 0x79b92978
private void Button1_Click(object sender, System.EventArgs e)
{
this.Session[“pruebavariable”]= Button1;
}
Name: System.Web.UI.WebControls.ButtonMethodTable 0x03746800
EEClass 0x0373f054
Size 96(0x60) bytes
GC Generation: 2
mdToken: 0x02000203 (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x03746670
MT Field Offset Type Attr Value Name
0x032f590c 0x4000b3a 0x4 CLASS instance 0x00000000 _dataBindings
0x032f590c 0x4000b3b 0x8 CLASS instance 0x00febb9c _id
0x032f590c 0x4000b3c 0xc CLASS instance 0x00febb9c _cachedUniqueID
0x032f590c 0x4000b3d 0x10 CLASS instance 0x010744e8 _parent
0x032f590c 0x4000b3e 0x14 CLASS instance 0x00000000 _site
0x032f590c 0x4000b3f 0x18 CLASS instance 0x01080d58 _events
0x032f590c 0x4000b40 0x1c CLASS instance 0x00000000 _controls
0x032f590c 0x4000b41 0x38 System.Int32 instance 5 _controlState
0x032f590c 0x4000b42 0x20 CLASS instance 0x00000000 _renderMethod
0x032f590c 0x4000b43 0x24 CLASS instance 0x010747a0 _viewState
0x032f590c 0x4000b44 0x28 CLASS instance 0x00000000 _controlsViewState
0x032f590c 0x4000b45 0x2c CLASS instance 0x00000000 _namedControls
0x032f590c 0x4000b46 0x3c System.Int32 instance 0 _namedControlsID
0x032f590c 0x4000b47 0x30 CLASS instance 0x01073acc _namingContainer
0x032f590c 0x4000b48 0x34 CLASS instance 0x01073acc _page…cortado para abreviar
El objeto almacenado en sesión es un botón de web, del tipo System.Web.UI.WebControls.Button. Veamos el tamaño del objeto almacenado.
sizeof(0x1074740) = 1,442,148 (0x160164) bytes (System.Web.UI.WebControls.Button)¿Impresionante, no?, ¿Cómo es posible que un botón tenga un tamaño de 1,44 MB?
El tamaño real del botón, o lo que podemos entender como un simple botón, no es 1,44 MB. El problema se presenta ya que al momento de almacenar éste en una variable de sesión, en modalidad InProc , se agrega una referencia del objeto al arreglo interno utilizado para almacenar las variables. La referencia del objeto incluye también todos los objetos que sobre los cuales este tiene una referencia. Entre estos objetos podemos incluir la página misma, el viewstate e incluso el HTTPRuntime. Existe otro problema con almacenar este tipo de objetos en sesión, el cual ya lo abordaremos en otra oportunidad.
Para confirmar lo anterior, es cosa de seguir “navegando” los objetos a los que el botón hace referencia, actividad que queda para ustedes ya que escapa al objetivo de este post.
Name: System.Web.UI.WebControls.Button
MethodTable 0x03746800EEClass 0x0373f054
Size 96(0x60) bytes
GC Generation: 2
mdToken: 0x02000203 (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x03746670
MT Field Offset Type Attr Value Name
0x032f590c 0x4000b3a 0x4 CLASS instance 0x00000000 _dataBindings
0x032f590c 0x4000b3b 0x8 CLASS instance 0x00febb9c _id
0x032f590c 0x4000b3c 0xc CLASS instance 0x00febb9c _cachedUniqueID
0x032f590c 0x4000b3d 0x10 CLASS instance 0x010744e8 _parent
0x032f590c 0x4000b3e 0x14 CLASS instance 0x00000000 _site
0x032f590c 0x4000b3f 0x18 CLASS instance 0x01080d58 _events
0x032f590c 0x4000b40 0x1c CLASS instance 0x00000000 _controls
0x032f590c 0x4000b41 0x38 System.Int32 instance 5 _controlState
0x032f590c 0x4000b42 0x20 CLASS instance 0x00000000 _renderMethod
0x032f590c 0x4000b43 0x24 CLASS instance 0x010747a0 _viewState
0x032f590c 0x4000b44 0x28 CLASS instance 0x00000000 _controlsViewState
0x032f590c 0x4000b45 0x2c CLASS instance 0x00000000 _namedControls
0x032f590c 0x4000b46 0x3c System.Int32 instance 0 _namedControlsID
0x032f590c 0x4000b47 0x30 CLASS instance 0x01073acc _namingContainer
0x032f590c 0x4000b48 0x34 CLASS instance 0x01073acc _page…
cortado para abreviar
Conclusión
El uso de variables de sesión es conocido y utilizado mundialmente. A su vez, aplicaciones hacen un uso indiscriminado de éstas, muchas veces sin considerar ni reparar en lo que realmente están almacenando.
Para el caso presentado, no debe concluirse que el error está en cómo el objeto botón es almacenado en una variable de sesión, que para el caso de InProc, es sólo la referencia a éste. Tampoco podemos esperar que almacene solamente el botón y no “el resto”, ya que esa definición de “resto” no existe. Desde el punto de vista del CG (Garbage Collector), se almacena la referencia del objeto (en modalidad InProc) y por consecuencia, el grafo de objetos que parten de éste seguirán vivos. Si esto no ocurriese así, una vez que requiera obtenerse el objeto para volver a ser utilizado, el resultado de cualquier ejecución sobre él sería inesperado.
Es importante recalcar en este punto que el resultado de “pegar” este control generado en una página anterior sobre una nueva página, que aunque sea del mismo tipo, no es la misma instancia, podrá llevar a resultados impredecibles.
Al almacenar objetos en variables de sesión, como también en el caso del caché, variables de aplicación o cualquier “repositorio”, se debe tener extremo cuidado en conocer las implicancias de todo lo que se está almacenando y los otros objetos que se almacenan de forma imperceptible. Estos costos escondidos pueden degradar el rendimiento de nuestra aplicación, pudiendo ser necesario invertir muchos recursos para determinar la causa.
Para finalizar el post, ¿Qué ocurriría si se utilizan variables de sesión almacenadas fuera del proceso, como por ejemplo SQL Server o StateServer?
Recursos adicionales
Pipeline http de asp.net.
http://msdn.microsoft.com/msdnmag/issues/02/09/HTTPPipelines/default.aspx
Saludos,
Patrick
bla bla bla, cuanto puede almacenar una variable de sesión?
y creo que bla bla
como puedes resolver el problema desde el cliente, es decir sin modificar el codigo de la aplicación existente, puedes acceder a eliminar o cerrar dichas sesiones para liberar espacio???
Angie, eso no se puede hacer desde el cliente.