Variables de sesión y costos escondidos

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 aspnet

Es 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.


 


Módulos y Handlers aspnet


 


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 caso

Volviendo 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  Gen
0x0106fad0      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.SessionDictionary
MethodTable 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 0x79ba0d74
EEClass 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 0x00c32018
Size 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/NameObjectEntry
MethodTable 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 0x79b925c8
EEClass 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


http://msdn2.microsoft.com/en-us/library/aa479328.aspx


Saludos,
Patrick

4 Replies to “Variables de sesión y costos escondidos”

  1. 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???

Leave a Reply

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