Acabo de descubrir por qué todos los sitios web de ASP.Net son lentos, y estoy tratando de averiguar qué hacer al respecto.

¡Acabo de descubrir que cada solicitud en una aplicación web ASP.Net obtiene un locking de sesión al comienzo de una solicitud, y luego la lanza al final de la solicitud!

En caso de que las implicaciones de esto se pierdan en usted, como lo fue para mí al principio, esto básicamente significa lo siguiente:

  • Cada vez que una página web ASP.Net tarda mucho en cargarse (tal vez debido a una lenta llamada a la base de datos o lo que sea), y el usuario decide que quiere navegar a otra página porque está cansado de esperar, ¡NO PUEDEN! El locking de sesión de ASP.Net obliga a la nueva solicitud de página a esperar hasta que la solicitud original haya finalizado su carga extremadamente lenta. Arrrgh.

  • Cada vez que un UpdatePanel se carga lentamente, y el usuario decide navegar a una página diferente antes de que UpdatePanel haya terminado de actualizar … ¡NO PUEDEN! El locking de sesión de ASP.net obliga a la nueva solicitud de página a esperar hasta que la solicitud original haya finalizado su carga extremadamente lenta. Doble Arrrgh!

Así que cuales son las opciones? Hasta ahora he llegado a:

  • Implemente una SessionStateDataStore personalizada, que ASP.Net admite. No he encontrado demasiados para copiar, y parece un poco arriesgado y fácil de estropear.
  • Lleve un registro de todas las solicitudes en curso, y si llega una solicitud del mismo usuario, cancele la solicitud original. Parece un poco extremo, pero funcionaría (creo).
  • ¡No use Session! Cuando necesito algún tipo de estado para el usuario, podría usar Caché y elementos clave en el nombre de usuario autenticado, o algo similar. De nuevo parece un poco extremo.

¡Realmente no puedo creer que el equipo de Microsoft ASP.Net hubiera dejado un cuello de botella de rendimiento tan grande en el marco en la versión 4.0! ¿Me estoy perdiendo algo obvio? ¿Qué tan difícil sería usar una colección ThreadSafe para la sesión?

Si su página no modifica ninguna variable de sesión, puede cancelar la mayoría de este locking.

<% @Page EnableSessionState="ReadOnly" %> 

Si su página no lee ninguna variable de sesión, puede optar por salir de este locking por completo, para esa página.

 <% @Page EnableSessionState="False" %> 

Si ninguna de sus páginas usa variables de sesión, simplemente desactive el estado de la sesión en web.config.

  

Tengo curiosidad, ¿qué crees que “una colección ThreadSafe” haría para convertirse en hilo seguro, si no usa lockings?

Editar: Probablemente explique lo que quiero decir con “no participar de la mayor parte de este locking”. Se puede procesar cualquier número de páginas de sesión de solo lectura o sin sesión para una sesión determinada al mismo tiempo sin bloquear entre sí. Sin embargo, una página de lectura-escritura-sesión no puede iniciar el procesamiento hasta que se hayan completado todas las solicitudes de solo lectura, y mientras se está ejecutando debe tener acceso exclusivo a la sesión de ese usuario para mantener la coherencia. El locking de valores individuales no funcionaría, porque ¿qué ocurre si una página cambia un conjunto de valores relacionados como un grupo? ¿Cómo se aseguraría de que otras páginas que se ejecutan al mismo tiempo obtengan una vista coherente de las variables de sesión del usuario?

Sugeriría que intente minimizar la modificación de las variables de sesión una vez que se hayan establecido, si es posible. Esto le permitiría hacer que la mayoría de sus páginas sean páginas de solo lectura, lo que aumenta las posibilidades de que múltiples solicitudes simultáneas del mismo usuario no se bloqueen entre sí.

De acuerdo, grandes accesorios para Joel Muller por todos sus comentarios. Mi última solución fue utilizar el SessionStateModule personalizado detallado al final de este artículo de MSDN:

http://msdn.microsoft.com/en-us/library/system.web.sessionstate.sessionstateutility.aspx

Esto era:

  • Muy rápido de implementar (en realidad parecía más fácil que ir por la ruta del proveedor)
  • Usé mucho de la sesión estándar de ASP.Net manejando de fábrica (a través de la clase SessionStateUtility)

Esto ha marcado una GRAN diferencia en la sensación de “brusquedad” de nuestra aplicación. Todavía no puedo creer que la implementación personalizada de la sesión ASP.Net bloquee la sesión para toda la solicitud. Esto agrega una gran cantidad de lentitud a los sitios web. A juzgar por la cantidad de investigación en línea que tuve que hacer (y las conversaciones con varios desarrolladores de ASP.Net realmente experimentados), mucha gente ha experimentado este problema, pero muy pocas personas han llegado al fondo de la causa. Tal vez voy a escribir una carta a Scott Gu …

¡Espero que esto ayude a algunas personas!

Empecé a utilizar AngiesList.Redis.RedisSessionStateModule , que además de usar el servidor (muy rápido) Redis para el almacenamiento (estoy usando el puerto de Windows , aunque también hay un puerto MSOpenTech ), no bloquea absolutamente la sesión .

En mi opinión, si su aplicación está estructurada de manera razonable, esto no es un problema. Si realmente necesita datos consistentes y bloqueados como parte de la sesión, debe implementar específicamente una comprobación de locking / concurrencia por su cuenta.

La decisión de MS de que cada sesión de ASP.NET debería estar bloqueada de manera predeterminada solo para manejar el diseño deficiente de la aplicación es una mala decisión, en mi opinión. Especialmente porque parece que la mayoría de los desarrolladores ni siquiera se dieron cuenta de que las sesiones estaban bloqueadas, y menos aún que las aplicaciones aparentemente necesitan ser estructuradas para que puedas hacer el estado de sesión de solo lectura tanto como sea posible (opt-out, donde sea posible) .

Preparé una biblioteca basada en enlaces publicados en este hilo. Utiliza los ejemplos de MSDN y CodeProject. Gracias a James.

También hice modificaciones aconsejadas por Joel Mueller.

El código está aquí:

https://github.com/dermeister0/LockFreeSessionState

Módulo HashTable:

 Install-Package Heavysoft.LockFreeSessionState.HashTable 

Módulo ScaleOut StateServer:

 Install-Package Heavysoft.LockFreeSessionState.Soss 

Módulo personalizado:

 Install-Package Heavysoft.LockFreeSessionState.Common 

Si desea implementar soporte de Memcached o Redis, instale este paquete. A continuación, herede la clase LockFreeSessionStateModule e implemente métodos abstractos.

El código aún no se ha probado en producción. También es necesario mejorar el manejo de errores. Las excepciones no quedan atrapadas en la implementación actual.

Algunos proveedores de sesión sin locking que usan Redis:

A menos que su aplicación tenga necesidades especiales, creo que tiene 2 enfoques:

  1. No use sesión en absoluto
  2. Use la sesión tal como está y realice un ajuste fino como mencionó joel.

La sesión no solo es segura para subprocesos sino también segura para el estado, de modo que usted sepa que hasta que se complete la solicitud actual, cada variable de sesión no cambiará de otra solicitud activa. Para que esto suceda, debe asegurarse de que la sesión SE BLOQUEARÁ hasta que se complete la solicitud actual.

Puede crear una sesión como comportamiento de muchas maneras, pero si no bloquea la sesión actual, no será ‘sesión’.

Para los problemas específicos que mencionaste, creo que deberías verificar HttpContext.Current.Response.IsClientConnected . Esto puede ser útil para evitar ejecuciones innecesarias y esperas en el cliente, aunque no puede resolver este problema por completo, ya que esto se puede usar solo de forma agrupada y no asincrónica.

Para ASPNET MVC, hicimos lo siguiente:

  1. De forma predeterminada, establezca SessionStateBehavior.ReadOnly en todas las acciones del controlador anulando DefaultControllerFactory
  2. En las acciones del controlador que necesitan escritura en el estado de la sesión, marque con atributo para establecerlo en SessionStateBehaviour.Required

Cree Custom ControllerFactory y anule GetControllerSessionBehaviour .

  protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType) { var DefaultSessionStateBehaviour = SessionStateBehaviour.ReadOnly; if (controllerType == null) return DefaultSessionStateBehaviour; var isRequireSessionWrite = controllerType.GetCustomAttributes(inherit: true).FirstOrDefault() != null; if (isRequireSessionWrite) return SessionStateBehavior.Required; var actionName = requestContext.RouteData.Values["action"].ToString(); MethodInfo actionMethodInfo; try { actionMethodInfo = controllerType.GetMethod(actionName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); } catch (AmbiguousMatchException) { var httpRequestTypeAttr = GetHttpRequestTypeAttr(requestContext.HttpContext.Request.HttpMethod); actionMethodInfo = controllerType.GetMethods().FirstOrDefault( mi => mi.Name.Equals(actionName, StringComparison.CurrentCultureIgnoreCase) && mi.GetCustomAttributes(httpRequestTypeAttr, false).Length > 0); } if (actionMethodInfo == null) return DefaultSessionStateBehaviour; isRequireSessionWrite = actionMethodInfo.GetCustomAttributes(inherit: false).FirstOrDefault() != null; return isRequireSessionWrite ? SessionStateBehavior.Required : DefaultSessionStateBehaviour; } private static Type GetHttpRequestTypeAttr(string httpMethod) { switch (httpMethod) { case "GET": return typeof(HttpGetAttribute); case "POST": return typeof(HttpPostAttribute); case "PUT": return typeof(HttpPutAttribute); case "DELETE": return typeof(HttpDeleteAttribute); case "HEAD": return typeof(HttpHeadAttribute); case "PATCH": return typeof(HttpPatchAttribute); case "OPTIONS": return typeof(HttpOptionsAttribute); } throw new NotSupportedException("unable to determine http method"); } 

AcquireSessionLockAttribute

 [AttributeUsage(AttributeTargets.Method)] public sealed class AcquireSessionLock : Attribute { } 

global.asax.cs fábrica del controlador creado en global.asax.cs

 ControllerBuilder.Current.SetControllerFactory(typeof(DefaultReadOnlySessionStateControllerFactory)); 

Ahora, podemos tener estados de sesión de read-only read-write y de read-write en un único Controller .

 public class TestController : Controller { [AcquireSessionLock] public ActionResult WriteSession() { var timeNow = DateTimeOffset.UtcNow.ToString(); Session["key"] = timeNow; return Json(timeNow, JsonRequestBehavior.AllowGet); } public ActionResult ReadSession() { var timeNow = Session["key"]; return Json(timeNow ?? "empty", JsonRequestBehavior.AllowGet); } } 

Nota: El estado de la sesión ASPNET aún se puede escribir en modo solo lectura y no arrojará ninguna forma de excepción (simplemente no se bloquea para garantizar la coherencia), así que debemos tener cuidado de marcar AcquireSessionLock en las acciones del controlador que requieren escribir el estado de la sesión .

Marcar el estado de sesión de un controlador como de solo lectura o desactivado resolverá el problema.

Puede decorar un controlador con el siguiente atributo para marcarlo como de solo lectura:

 [SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)] 

System.Web.SessionState.SessionStateBehavior enum tiene los siguientes valores:

  • Defecto
  • Discapacitado
  • Solo lectura
  • Necesario

Solo para ayudar a cualquier persona con este problema (bloqueando solicitudes al ejecutar otra de la misma sesión) …

Hoy comencé a resolver este problema y, después de algunas horas de investigación, lo resolví eliminando el método Session_Start (aunque esté vacío) del archivo Global.asax .

Esto funciona en todos los proyectos que he probado.