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

275

¡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 para 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 llamada lenta a 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 ASP.Net obliga a la nueva solicitud de página a esperar hasta que la solicitud original haya finalizado su carga lenta y dolorosa. Arrrgh

  • Cada vez que 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 lenta y dolorosa. Doble Arrrgh!

Así que cuales son las opciones? Hasta ahora se me ocurrió:

  • Implemente un SessionStateDataStore personalizado, que ASP.Net admite. No he encontrado demasiados para copiar, y parece de alto riesgo y fácil de estropear.
  • Realice un seguimiento 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í. Nuevamente parece un poco extremo.

¡Realmente no puedo creer que el equipo de ASP.Net Microsoft hubiera dejado un enorme cuello de botella de rendimiento 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?

James
fuente
40
Te das cuenta de que este sitio está construido sobre .NET. Dicho esto, creo que escala bastante bien.
wheaties
77
Bien, estaba siendo un poco gracioso con mi título. Aún así, en mi humilde opinión, el rendimiento de bloqueo que impone la implementación inmediata de la sesión es sorprendente. Además, apuesto a que los muchachos de Stack Overflow han tenido que hacer un poco de desarrollo altamente personalizado para obtener el rendimiento y la escalabilidad que han logrado, y felicitaciones a ellos. Por último, Stack Overflow es una aplicación MVC, no WebForms, que apuesto a que ayuda (aunque es cierto que todavía usaba la misma infraestructura de sesión).
James
44
Si Joel Mueller le dio la información para solucionar su problema, ¿por qué no marcó su respuesta como la respuesta correcta? Solo un pensamiento.
ars265
1
@ ars265 - Joel Muller proporcionó mucha información buena, y quería agradecerle por eso. Sin embargo, finalmente tomé una ruta diferente a la sugerida en su publicación. Por lo tanto, marcar una publicación diferente como la respuesta.
James

Respuestas:

201

Si su página no modifica ninguna variable de sesión, puede inhabilitar la mayor parte de este bloqueo.

<% @Page EnableSessionState="ReadOnly" %>

Si su página no lee ninguna variable de sesión, puede inhabilitar este bloqueo 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 sesión en web.config.

<sessionState mode="Off" />

Tengo curiosidad, ¿qué crees que "una colección ThreadSafe" haría para ser seguro para subprocesos, si no usa cerraduras?

Editar: Probablemente debería explicar por lo que quiero decir con "optar por salir de la mayoría de este bloqueo". Se puede procesar cualquier cantidad de páginas 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 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. Bloquear valores individuales no funcionaría, porque ¿qué pasa 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?

Sugeriría 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 páginas de solo lectura, aumentando la posibilidad de que múltiples solicitudes simultáneas del mismo usuario no se bloqueen entre sí.

Joel Mueller
fuente
2
Hola Joel. Gracias por tu tiempo en esta respuesta. Estas son algunas buenas sugerencias y algo de reflexión. No entiendo su razonamiento para decir que todos los valores de una sesión deben estar exclusivamente bloqueados en toda la solicitud. Los valores de ASP.Net Cache pueden ser alterados en cualquier momento por cualquier hilo. ¿Por qué debería ser diferente para la sesión? Como comentario aparte, un problema que tengo con la opción de solo lectura es que si un desarrollador agrega un valor a la sesión cuando está en modo de solo lectura, falla silenciosamente (sin excepción). De hecho, mantiene el valor para el resto de la solicitud, pero no más allá.
James
55
@James: solo estoy adivinando las motivaciones de los diseñadores aquí, pero imagino que es más común que varios valores dependan unos de otros en la sesión de un solo usuario que en un caché que se puede eliminar por falta de uso o bajo razones de memoria en cualquier momento. Si una página establece 4 variables de sesión relacionadas, y otra las lee después de que solo dos hayan sido modificadas, eso podría conducir fácilmente a algunos errores muy difíciles de diagnosticar. Me imagino que los diseñadores eligieron ver "el estado actual de la sesión de un usuario" como una sola unidad para fines de bloqueo por ese motivo.
Joel Mueller
2
Entonces, ¿desarrolle un sistema que atienda a los programadores de mínimo común denominador que no pueden resolver el bloqueo? ¿El propósito es habilitar granjas web que comparten un almacén de sesiones entre instancias de IIS? ¿Puedes dar un ejemplo de algo que almacenarías en una variable de sesión? No se me ocurre nada.
Jason Goemaat
2
Sí, este es uno de los propósitos. Reconsidere los diversos escenarios cuando se implementa el equilibrio de carga y la redundancia en la infraestructura. Cuando el usuario trabaja en la página web, es decir, está ingresando datos en el formulario, durante, digamos, 5 minutos, y algo en la granja de servidores se bloquea (la fuente de energía de un nodo se hincha), el usuario NO debe notar eso. No puede ser expulsado de la sesión solo porque su sesión se perdió, simplemente porque su proceso de trabajo ya no existe. Esto significa que para manejar el equilibrio perfecto / redundancia, las sesiones deben ser exteriorizados de nodos de trabajo ..
quetzalcoatl
77
Otro nivel útil de exclusión está <pages enableSessionState="ReadOnly" />en web.config y usa @Page para habilitar la escritura solo en páginas específicas.
MattW
84

OK, tan grandes apoyos a Joel Muller por toda su aportación. Mi solución final fue usar el Custom SessionStateModule 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 seguir la ruta del proveedor)
  • Usó gran parte de la sesión estándar de ASP.Net que se maneja fuera de la caja (a través de la clase SessionStateUtility)

Esto ha marcado una GRAN diferencia en la sensación de "rapidez" en nuestra aplicación. Todavía no puedo creer que la implementación personalizada de ASP.Net Session 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 investigación en línea que tuve que hacer (y las conversaciones con varios desarrolladores 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!

James
fuente
19
Esa referencia es un hallazgo interesante, pero debo advertirle sobre algunas cosas: el código de muestra tiene algunos problemas: en primer lugar, ReaderWriterLockha quedado en desuso a favor ReaderWriterLockSlim, debe usarlo en su lugar. En segundo lugar, lock (typeof(...))también ha quedado en desuso: en su lugar, debe bloquear una instancia de objeto estático privado. En tercer lugar, la frase "Esta aplicación no impide que las solicitudes web simultáneas utilicen el mismo identificador de sesión" es una advertencia, no una función.
Joel Mueller
3
Creo que puede hacer que esto funcione, pero debe reemplazar el uso de SessionStateItemCollectionen el código de muestra con una clase segura para subprocesos (quizás basada en ConcurrentDictionary) si desea evitar errores difíciles de reproducir bajo carga.
Joel Mueller
3
Acabo de investigar esto un poco más, y desafortunadamente ISessionStateItemCollectionrequiere que la Keyspropiedad sea de tipo System.Collections.Specialized.NameObjectCollectionBase.KeysCollection, que no tiene constructores públicos. Gee, gracias chicos. Eso es muy conveniente.
Joel Mueller
2
OK, creo que finalmente tengo una implementación de bloqueo de sesión completa, segura para subprocesos y sin lectura del funcionamiento de la sesión. Los últimos pasos involucraron la implementación de una colección SessionStateItem segura para subprocesos personalizada, que se basó en el artículo MDSN vinculado en el comentario anterior. La pieza final del rompecabezas con esto fue crear un enumerador seguro para subprocesos basado en este gran artículo: codeproject.com/KB/cs/safe_enumerable.aspx .
James
26
James: obviamente, este es un tema bastante antiguo, pero me preguntaba si podrías compartir tu solución definitiva. He tratado de seguir usando el hilo de comentarios anterior, pero hasta ahora no he podido obtener una solución que funcione. Estoy bastante seguro de que no hay nada fundamental en nuestro uso limitado de la sesión que requiera bloqueo.
bsiegel
31

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 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 bloqueados y consistentes como parte de la sesión, debe implementar específicamente una verificación de bloqueo / concurrencia por su cuenta.

En mi opinión, la decisión de MS de que cada sesión 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 / ni siquiera se dieron cuenta de que las sesiones estaban bloqueadas, y mucho menos que las aplicaciones aparentemente necesitan estructurarse para que pueda hacer el estado de sesión de solo lectura tanto como sea posible (optar por salir, cuando sea posible) .

Gregmac
fuente
Su enlace de GitHub parece estar 404 muerto. bibliotecas.io/github/angieslist/AL-Redis parece ser la nueva URL?
Uwe Keim
Parece que el autor quería eliminar la biblioteca, incluso desde el segundo enlace. Dudaría en usar una biblioteca abandonada, pero hay un tenedor aquí: github.com/PrintFleet/AL-Redis y una biblioteca alternativa vinculada desde aquí: stackoverflow.com/a/10979369/12534
Christian Davén
21

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 el soporte de Memcached o Redis, instale este paquete. Luego 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. No se detectan excepciones en la implementación actual.

Algunos proveedores de sesiones sin bloqueo que usan Redis:

Der_Meister
fuente
Requiere bibliotecas de la solución ScaleOut, que no es gratuita.
Hoàng Long
1
Sí, creé la implementación solo para SOSS. Puede usar los proveedores de sesión de Redis mencionados, es gratis.
Der_Meister
Tal vez Hoàng Long perdió el punto de que puede elegir entre la implementación de HashTable en memoria y ScaleOut StateServer.
David De Sloovere
Gracias por su contribución :) Lo intentaré para ver cómo actúa sobre los pocos casos de uso que tenemos con SESSION involucrado.
Agustin Garzon
Dado que muchas personas han mencionado que se bloquea un elemento específico en la sesión, podría ser bueno señalar que la implementación de respaldo debe devolver una referencia común al valor de la sesión a través de las llamadas para permitir el bloqueo (e incluso eso no funcionará con servidores con equilibrio de carga). Dependiendo de cómo use el estado de la sesión, aquí hay posibilidades de condiciones de carrera. Además, me parece que hay bloqueos en su implementación que en realidad no hacen nada porque solo envuelven una sola llamada de lectura o escritura (corríjame si me equivoco aquí).
nw.
11

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

  1. No use sesión en absoluto
  2. Utilice la sesión como está y realice un ajuste fino como se menciona en Joel

La sesión no solo es segura para subprocesos, sino también 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 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 mencionó, creo que debería consultar 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 solo se puede usar de forma conjunta y no asíncrona.

George Mavritsakis
fuente
10

Si está utilizando la versión actualizada Microsoft.Web.RedisSessionStateProvider(a partir de 3.0.2), puede agregarla a su web.configpara permitir sesiones concurrentes.

<appSettings>
    <add key="aspnet:AllowConcurrentRequestsPerSession" value="true"/>
</appSettings>

Fuente

rabz100
fuente
No estoy seguro de por qué esto fue en 0. +1. Muy útil.
Pangamma
¿Funciona esto en el grupo de aplicaciones de modo clásico? github.com/Azure/aspnet-redis-providers/issues/123
Rusty
¿funciona esto con el proveedor de servicios de estado predeterminado inProc o Session?
Nick Chan Abdullah
Tenga en cuenta que el póster hace referencia si está utilizando RedisSessionStateprovider, pero también podría funcionar con estos nuevos proveedores AspncSessionState Async (para SQL y Cosmos) porque también está en su documentación: github.com/aspnet/AspNetSessionState Mi conjetura es que funcionaría en modo clásico si SessionStateProvider ya funciona en modo clásico, es probable que el estado de la sesión ocurra dentro de ASP.Net (no IIS). Con InProc puede que no funcione, pero sería un problema menor porque resuelve un problema de contención de recursos que es un problema mayor con escenarios fuera de proceso.
madamission
4

Para ASPNET MVC, hicimos lo siguiente:

  1. De forma predeterminada, configure SessionStateBehavior.ReadOnlytodas las acciones del controlador anulandoDefaultControllerFactory
  2. En las acciones del controlador que deben escribirse en el estado de la sesión, marque con el atributo para establecerlo en SessionStateBehavior.Required

Crear ControllerFactory personalizado y anular GetControllerSessionBehavior.

    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
{ }

Conecte la fábrica de controladores creada en global.asax.cs

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

Ahora, podemos tener ambos estados read-onlyy read-writeestado de sesión 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 incluso en modo de solo lectura y no arrojará ninguna forma de excepción (simplemente no se bloquea para garantizar la coherencia), por lo que debemos tener cuidado de marcar AcquireSessionLocklas acciones del controlador que requieren escribir el estado de la sesión.

Misterhex
fuente
3

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
Michael King
fuente
0

Solo para ayudar a cualquiera con este problema (bloquear solicitudes al ejecutar otra desde la misma sesión) ...

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

Esto funciona en todos los proyectos que he probado.

graduenz
fuente
IDK en qué tipo de proyecto se trataba, pero el mío no tiene un Session_Startmétodo y aún se bloquea
Denis G. Labrecque
0

Después de luchar con todas las opciones disponibles, terminé escribiendo un proveedor de SessionStore basado en token JWT (la sesión viaja dentro de una cookie y no se necesita almacenamiento de back-end).

http://www.drupalonwindows.com/en/content/token-sessionstate

Ventajas:

  • Reemplazo directo, no se necesitan cambios en su código
  • Escale mejor que cualquier otra tienda centralizada, ya que no se necesita un servidor de almacenamiento de sesión.
  • Más rápido que cualquier otro almacenamiento de sesión, ya que no es necesario recuperar datos de ningún almacenamiento de sesión
  • No consume recursos del servidor para el almacenamiento de la sesión.
  • Implementación predeterminada sin bloqueo: la solicitud concurrente no se bloqueará entre sí y mantendrá un bloqueo en la sesión
  • Escale horizontalmente su aplicación: debido a que los datos de la sesión viajan con la solicitud en sí misma, puede tener varios cabezales web sin preocuparse por compartir la sesión.
David
fuente