tag - Acabo de descubrir por qué todos los sitios web de ASP.Net son lentos y estoy tratando de averiguar qué hacer al respecto.
asp route tag helper (8)
¡Acabo de descubrir que cada solicitud en una aplicación web ASP.Net obtiene un bloqueo de sesión al comienzo de una solicitud y luego la libera 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 de ASP.Net tarda mucho en cargarse (tal vez debido a una llamada lenta de la base de datos o lo que sea), y el usuario decide que quiere navegar a una página diferente porque está cansado de esperar, ¡NO PUEDEN! El bloqueo 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 actualizarse ... ¡NO PUEDEN! El bloqueo 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 con:
- Implemente un SessionStateDataStore personalizado, que ASP.Net admite. No he encontrado demasiados por ahí para copiar, y parece un poco alto riesgo y fácil de desordenar.
- 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 uses la sesión! Cuando necesito algún tipo de estado para el usuario, podría usar Cache en su lugar y elementos clave en el nombre de usuario autenticado, o algo así. De nuevo parece algo 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 de 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?
A menos que su aplicación tenga necesidades especiales, creo que tiene 2 enfoques:
- No utilizar sesión en absoluto
- Utilice la sesión como está y realice la optimización fina como se mencionó a Joel.
La sesión no solo es segura para subprocesos, sino también segura para el estado, de una manera que sabe 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 SERÁ BLOQUEADA 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 revisar HttpContext.Current.Response.IsClientConnected . Esto puede ser útil para evitar ejecuciones y esperas innecesarias en el cliente, aunque no puede resolver este problema por completo, ya que solo se puede usar de forma agrupada y no asíncrona.
Comencé a usar AngiesList.Redis.RedisSessionStateModule , que además de usar el servidor Redis (muy rápido) para el almacenamiento (estoy usando el puerto de Windows , aunque también hay un puerto MSOpenTech ), no hace ningún bloqueo en la sesión. .
En mi opinión, si su aplicación está estructurada de una manera razonable, esto no es un problema. Si realmente necesita datos bloqueados y consistentes como parte de la sesión, debe implementar específicamente un control de bloqueo / concurrencia por su cuenta.
MS, en mi opinión, decidir que cada sesión de ASP.NET debería estar bloqueada por defecto solo para manejar un diseño deficiente de la aplicación es una mala decisión. Especialmente porque parece que la mayoría de los desarrolladores no se dieron cuenta de que las sesiones estaban bloqueadas, y mucho menos que las aplicaciones aparentemente deben estar estructuradas para que pueda hacer el estado de sesión de solo lectura tanto como sea posible (opción de exclusión, cuando sea posible) .
Marcar el estado de sesión de un controlador como de solo lectura o deshabilitado resolverá el problema.
Puede decorar un controlador con el siguiente atributo para marcarlo como de solo lectura:
[SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)]
La enumeración System.Web.SessionState.SessionStateBehavior tiene los siguientes valores:
- Defecto
- Discapacitado
- Solo lectura
- Necesario
OK, tan grandes apoyos a Joel Muller por todos sus comentarios. Mi última solución fue usar el SessionStateModule personalizado 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 seguir la ruta del proveedor)
- Usó gran parte del manejo de sesión estándar de ASP.Net fuera de la caja (a través de la clase SessionStateUtility)
Esto ha hecho una gran diferencia en la sensación de "rapidez" a nuestra aplicación. Todavía no puedo creer que la implementación personalizada de la sesión de ASP.Net bloquee la sesión para toda la solicitud. Esto agrega una enorme cantidad de lentitud a los sitios web. A juzgar por la cantidad de investigaciones en línea que tuve que hacer (y conversaciones con varios desarrolladores de ASP.Net realmente experimentados), muchas personas han experimentado este problema, pero muy pocas personas han llegado al fondo de la causa. Tal vez le escriba una carta a Scott Gu ...
Espero que esto ayude a algunas personas por ahí!
Para ASPNET MVC, hicimos lo siguiente:
- De forma predeterminada, configure
SessionStateBehavior.ReadOnly
en todas las acciones del controlador anulandoDefaultControllerFactory
- En las acciones del controlador que necesitan escribirse en el estado de la sesión, marque con el atributo para establecerlo en
SessionStateBehaviour.Required
Cree ControllerFactory personalizado y anule GetControllerSessionBehaviour
.
protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType)
{
var DefaultSessionStateBehaviour = SessionStateBehaviour.ReadOnly;
if (controllerType == null)
return DefaultSessionStateBehaviour;
var isRequireSessionWrite =
controllerType.GetCustomAttributes<AcquireSessionLock>(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<AcquireSessionLock>(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 de controladores creada en global.asax.cs
ControllerBuilder.Current.SetControllerFactory(typeof(DefaultReadOnlySessionStateControllerFactory));
Ahora, podemos tener un estado de sesión de read-only
read-write
y de read-write
en un solo 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 de solo lectura y no generará ningún tipo de excepción (simplemente no se bloquea para garantizar la coherencia), por lo que debemos tener cuidado de marcar
AcquireSessionLock
en las acciones del controlador que requieren un estado de sesión de escritura .
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 hashable:
Install-Package Heavysoft.LockFreeSessionState.HashTable
Módulo de ScaleOut StateServer:
Install-Package Heavysoft.LockFreeSessionState.Soss
Módulo personalizado:
Install-Package Heavysoft.LockFreeSessionState.Common
Si desea implementar el soporte de Memcached o Redis, instale este paquete. Luego hereda la clase LockFreeSessionStateModule e implementa métodos abstractos.
El código no está probado en producción todavía. También es necesario mejorar el manejo de errores. Las excepciones no están atrapadas en la implementación actual.
Algunos proveedores de sesión sin bloqueo que utilizan Redis:
- AngiesList.Redis.RedisSessionStateModule (sugerido por gregmac en este hilo).
- https://github.com/welegan/RedisSessionProvider (NuGet: RedisSessionProvider)
- https://github.com/efaruk/playground/tree/master/UnlockedStateProvider (NuGet: UnlockedStateProvider.Redis)
Si su página no modifica ninguna de las variables de sesión, puede optar por salir de la mayoría de este bloqueo.
<% @Page EnableSessionState="ReadOnly" %>
Si su página no lee ninguna variable de sesión, puede optar por salir de este bloqueo por completo, para esa página.
<% @Page EnableSessionState="False" %>
Si ninguna de sus páginas utiliza variables de sesión, simplemente desactive el estado de sesión en web.config.
<sessionState mode="Off" />
Tengo curiosidad, ¿qué crees que haría una "colección ThreadSafe" para ser segura para subprocesos, si no utiliza bloqueos?
Editar: Probablemente debería explicarlo con lo que quiero decir con "optar por salir de la mayoría de este bloqueo". Se puede procesar cualquier cantidad de páginas de sesión de solo lectura o sin sesión para una sesión determinada al mismo tiempo sin bloquearse entre sí. Sin embargo, una página de sesión de lectura-escritura no puede comenzar a procesarse hasta que todas las solicitudes de solo lectura se hayan completado, y mientras se ejecuta, debe tener acceso exclusivo a la sesión de ese usuario para mantener la coherencia. Bloquear valores individuales no funcionaría, porque ¿qué pasaría si una página cambia un conjunto de valores relacionados como grupo? ¿Cómo se aseguraría de que otras páginas que se ejecutan al mismo tiempo obtendrían una vista coherente de las variables de sesión del usuario?
Le sugiero que intente minimizar la modificación de las variables de sesión una vez que se hayan configurado, si es posible. Esto le permitiría hacer que la mayoría de sus páginas sean de solo lectura, lo que aumenta la posibilidad de que múltiples solicitudes simultáneas del mismo usuario no se bloqueen entre sí.
Solo para ayudar a cualquiera con este problema (bloquear solicitudes cuando se ejecuta otro desde 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
(incluso si estaba vacío) del archivo Global.asax .
Esto funciona en todos los proyectos que he probado.