Ho appena scoperto perché tutti i siti Web ASP.Net sono lenti e sto cercando di capire cosa fare al riguardo


275

Ho appena scoperto che ogni richiesta in un'applicazione Web ASP.Net ottiene un blocco della sessione all'inizio di una richiesta e quindi la rilascia alla fine della richiesta!

Nel caso in cui le implicazioni di ciò vadano perse per te, com'era inizialmente per me, ciò significa sostanzialmente quanto segue:

  • Ogni volta che una pagina Web ASP.Net impiega molto tempo a caricarsi (forse a causa di una chiamata al database lenta o altro) e l'utente decide di voler navigare su una pagina diversa perché sono stanchi di aspettare, NON POSSONO! Il blocco della sessione ASP.Net impone alla nuova richiesta di pagina di attendere fino a quando la richiesta originale ha terminato il suo carico dolorosamente lento. Arrrgh.

  • Ogni volta che UpdatePanel si carica lentamente e l'utente decide di passare a una pagina diversa prima che UpdatePanel abbia terminato l'aggiornamento ... NON POSSONO! Il blocco della sessione ASP.net forza la richiesta della nuova pagina ad attendere fino a quando la richiesta originale ha terminato il suo carico dolorosamente lento. Doppio Arrrgh!

Quindi quali sono le opzioni? Finora ho escogitato:

  • Implementare un SessionStateDataStore personalizzato, supportato da ASP.Net. Non ne ho trovati troppi là fuori da copiare, e sembra un po 'ad alto rischio e facile da sbagliare.
  • Tieni traccia di tutte le richieste in corso e se una richiesta arriva dallo stesso utente, annulla la richiesta originale. Sembra un po 'estremo, ma funzionerebbe (penso).
  • Non usare la sessione! Quando ho bisogno di un qualche tipo di stato per l'utente, potrei semplicemente usare Cache e gli elementi chiave sul nome utente autenticato, o qualcosa del genere. Ancora una volta sembra un po 'estremo.

Non riesco davvero a credere che il team di ASP.Net Microsoft avrebbe lasciato un enorme collo di bottiglia nelle prestazioni alla versione 4.0! Mi sto perdendo qualcosa di ovvio? Quanto sarebbe difficile utilizzare una raccolta ThreadSafe per la sessione?


40
Ti rendi conto che questo sito è costruito su .NET. Detto questo, penso che ridimensiona abbastanza bene.
Wheaties

7
OK, quindi ero un po 'faceto con il mio titolo. Tuttavia, IMHO le prestazioni che l'implementazione immediata della sessione impone sono sorprendenti. Inoltre, scommetto che i ragazzi di Stack Overflow hanno dovuto fare un bel po 'di sviluppatori altamente personalizzati per ottenere le prestazioni e la scalabilità che hanno raggiunto - e complimenti per loro. Infine, Stack Overflow è un'APP MVC, non WebForms, che scommetto aiuta (anche se, ammettiamolo, questa utilizza ancora la stessa infrastruttura di sessione).
James,


4
Se Joel Mueller ti ha fornito le informazioni per risolvere il problema, perché non hai contrassegnato la sua risposta come la risposta corretta? Solo un pensiero.
ars265,

1
@ ars265 - Joel Muller ha fornito molte buone informazioni e volevo ringraziarlo per questo. Tuttavia, alla fine ho seguito una strada diversa da quella suggerita nel suo post. Quindi, contrassegnare un post diverso come risposta.
James,

Risposte:


201

Se la tua pagina non modifica alcuna variabile di sessione, puoi disattivare la maggior parte di questo blocco.

<% @Page EnableSessionState="ReadOnly" %>

Se la tua pagina non legge alcuna variabile di sessione, puoi disattivare completamente questo blocco per quella pagina.

<% @Page EnableSessionState="False" %>

Se nessuna delle tue pagine utilizza variabili di sessione, disattiva lo stato della sessione in web.config.

<sessionState mode="Off" />

Sono curioso, cosa pensi che farebbe "una collezione ThreadSafe" per diventare thread-safe, se non utilizza i lucchetti?

Modifica: probabilmente dovrei spiegare cosa intendo per "rinunciare alla maggior parte di questo blocco". È possibile elaborare contemporaneamente un numero qualsiasi di pagine di sola lettura o di nessuna sessione per una determinata sessione senza bloccarsi a vicenda. Tuttavia, una pagina di sessione di lettura / scrittura non può iniziare l'elaborazione fino a quando tutte le richieste di sola lettura non sono state completate e, mentre è in esecuzione, deve avere accesso esclusivo alla sessione di quell'utente per mantenere la coerenza. Il blocco su singoli valori non funzionerebbe, perché cosa succede se una pagina modifica un insieme di valori correlati come gruppo? Come assicureresti che altre pagine in esecuzione contemporaneamente ottengano una visione coerente delle variabili di sessione dell'utente?

Suggerirei di provare a minimizzare la modifica delle variabili di sessione una volta impostate, se possibile. Ciò consentirebbe di creare la maggior parte delle pagine di pagine di sola lettura, aumentando la possibilità che più richieste simultanee dello stesso utente non si blocchino.


2
Ciao Joel Grazie per il tuo tempo dedicato a questa risposta. Questi sono alcuni buoni suggerimenti e alcuni spunti di riflessione. Non capisco il tuo ragionamento per dire che tutti i valori di una sessione devono essere bloccati esclusivamente nell'intera richiesta. I valori della cache ASP.Net possono essere modificati in qualsiasi momento da qualsiasi thread. Perché questo dovrebbe essere diverso per la sessione? A parte questo, un problema che ho con l'opzione readonly è che se uno sviluppatore aggiunge un valore alla sessione quando è in sola modalità, fallisce silenziosamente (senza eccezioni). In realtà mantiene il valore per il resto della richiesta, ma non oltre.
James,

5
@James - Sto solo indovinando le motivazioni dei designer qui, ma immagino che sia più comune avere più valori che dipendono l'uno dall'altro in una sessione di un singolo utente che in una cache che può essere eliminata per mancanza di utilizzo o bassa- motivi di memoria in qualsiasi momento. Se una pagina imposta 4 variabili di sessione correlate e un'altra le legge dopo che solo due sono state modificate, ciò potrebbe facilmente portare ad alcuni bug molto difficili da diagnosticare. Immagino che i progettisti abbiano scelto di visualizzare "lo stato corrente della sessione di un utente" come una singola unità per scopi di blocco per quel motivo.
Joel Mueller,

2
Quindi sviluppare un sistema che si rivolge ai programmatori con il minimo comune denominatore che non riescono a capire il blocco? Lo scopo è abilitare le Web farm che condividono un archivio sessioni tra istanze IIS? Puoi fare un esempio di qualcosa che vorresti memorizzare in una variabile di sessione? Non riesco a pensare a niente.
Jason Goemaat il

2
Sì, questo è uno degli scopi. Ripensare i vari scenari quando il bilanciamento del carico e la ridondanza sono implementati nell'infrastruttura. Quando l'utente lavora sulla pagina Web, ovvero sta inserendo dati nel modulo, per, diciamo, 5 minuti, e qualcosa nel webfarm si arresta in modo anomalo - la fonte di alimentazione di un nodo non funziona - l'utente NON dovrebbe notarlo. Non può essere espulso dalla sessione solo perché la sessione è andata persa, solo perché il suo processo di lavoro non esiste più. Ciò significa che per gestire il perfetto bilanciamento / ridondanza, le sessioni devono essere esternalizzate dai nodi di lavoro ..
quetzalcoatl

7
Un altro utile livello di opt-out è <pages enableSessionState="ReadOnly" />in web.config e usa @Page per abilitare la scrittura solo su pagine specifiche.
Matt,

84

OK, così tanti oggetti di scena a Joel Muller per tutto il suo contributo. La mia soluzione definitiva era quella di utilizzare il modulo personalizzato SessionStateModule dettagliato alla fine di questo articolo MSDN:

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

Questo era:

  • Molto veloce da implementare (in realtà sembrava più facile che seguire la rotta del provider)
  • Ha utilizzato gran parte della sessione standard ASP.Net per la gestione immediata (tramite la classe SessionStateUtility)

Ciò ha fatto una GRANDE differenza nella sensazione di "rapidità" per la nostra applicazione. Non riesco ancora a credere che l'implementazione personalizzata di ASP.Net Session blocchi la sessione per l'intera richiesta. Ciò aggiunge un'enorme quantità di lentezza ai siti Web. A giudicare dalla quantità di ricerche online che ho dovuto fare (e dalle conversazioni con diversi sviluppatori ASP.Net davvero esperti), molte persone hanno riscontrato questo problema, ma pochissime persone sono mai arrivate in fondo alla causa. Forse scriverò una lettera a Scott Gu ...

Spero che questo aiuti alcune persone là fuori!


19
Quel riferimento è una scoperta interessante, ma devo avvertirti di alcune cose - il codice di esempio ha alcuni problemi: in primo luogo ReaderWriterLockè stato deprecato a favore di ReaderWriterLockSlim- dovresti invece usarlo. In secondo luogo, lock (typeof(...))è stato anche deprecato: è necessario bloccare invece un'istanza di oggetto statico privato. In terzo luogo, la frase "Questa applicazione non impedisce alle richieste Web simultanee di utilizzare lo stesso identificatore di sessione" è un avvertimento, non una funzionalità.
Joel Mueller,

3
Penso che tu possa far funzionare questo, ma devi sostituire l'uso di SessionStateItemCollectionnel codice di esempio con una classe thread-safe (forse basata su ConcurrentDictionary) se vuoi evitare errori difficili da riprodurre sotto carico.
Joel Mueller,

3
Ho appena esaminato un po 'di più questo, e purtroppo ISessionStateItemCollectionrichiede che la Keysproprietà sia di tipo System.Collections.Specialized.NameObjectCollectionBase.KeysCollection- che non ha costruttori pubblici. Accidenti, grazie ragazzi. È molto conveniente.
Joel Mueller,

2
OK, credo di avere finalmente un'implementazione di blocco thread-safe e non read di Session funzionante. Gli ultimi passaggi hanno riguardato l'implementazione di una raccolta SessionStateItem thread thread sicura, basata sull'articolo MDSN collegato al commento sopra. Il pezzo finale del puzzle con questo era la creazione di un enumeratore thread-safe basato su questo fantastico articolo: codeproject.com/KB/cs/safe_enumerable.aspx .
James,

26
James - ovviamente questo è un argomento abbastanza vecchio, ma mi chiedevo se tu fossi in grado di condividere la tua soluzione definitiva? Ho provato a seguire usando il thread di commenti sopra ma finora non sono stato in grado di ottenere una soluzione funzionante. Sono abbastanza certo che non ci sia nulla di fondamentale nel nostro uso limitato della sessione che richiederebbe il blocco.
bsiegel,

31

Ho iniziato a utilizzare AngiesList.Redis.RedisSessionStateModule , che oltre a utilizzare il server Redis (molto veloce) per l'archiviazione (sto usando la porta di Windows - anche se c'è anche una porta MSOpenTech ), non ha assolutamente alcun blocco sulla sessione .

Secondo me, se la tua applicazione è strutturata in modo ragionevole, questo non è un problema. Se in realtà hai bisogno di dati bloccati e coerenti come parte della sessione, devi implementare in modo specifico un controllo di blocco / concorrenza da solo.

Secondo me, la decisione di MS che ogni sessione ASP.NET debba essere bloccata per impostazione predefinita solo per gestire una progettazione di applicazioni scadente è una decisione sbagliata. Soprattutto perché sembra che la maggior parte degli sviluppatori non abbia realizzato / nemmeno realizzato che le sessioni fossero bloccate, figuriamoci che le app apparentemente devono essere strutturate in modo da poter fare lo stato della sessione in sola lettura il più possibile (opt-out, ove possibile) .


Il tuo link GitHub sembra essere morto 404. libraries.io/github/angieslist/AL-Redis sembra essere il nuovo URL?
Uwe Keim,

Sembra che l'autore abbia voluto rimuovere la libreria, anche dal secondo link. Vorrei essere riluttanti a utilizzare una libreria abbandonata, ma c'è un bivio qui: github.com/PrintFleet/AL-Redis e una libreria alternativa legata da qui: stackoverflow.com/a/10979369/12534
Christian Daven

21

Ho preparato una libreria basata sui link pubblicati in questa discussione. Utilizza gli esempi da MSDN e CodeProject. Grazie a James.

Ho anche apportato modifiche consigliate da Joel Mueller.

Il codice è qui:

https://github.com/dermeister0/LockFreeSessionState

Modulo HashTable:

Install-Package Heavysoft.LockFreeSessionState.HashTable

Modulo ScaleOut StateServer:

Install-Package Heavysoft.LockFreeSessionState.Soss

Modulo personalizzato:

Install-Package Heavysoft.LockFreeSessionState.Common

Se si desidera implementare il supporto di Memcached o Redis, installare questo pacchetto. Quindi ereditare LockFreeSessionStateModule classe e implementare metodi astratti.

Il codice non è ancora stato testato in produzione. Inoltre, è necessario migliorare la gestione degli errori. Le eccezioni non vengono rilevate nell'attuale implementazione.

Alcuni provider di sessioni senza blocco che utilizzano Redis:


Richiede librerie dalla soluzione ScaleOut, che non è gratuita?
Hoàng Long,

1
Sì, ho creato l'implementazione solo per SOSS. Puoi utilizzare i citati provider di sessioni Redis, è gratuito.
Der_Meister

Forse Hoàng Long ha perso il punto che hai una scelta tra l'implementazione HashTable in memoria e ScaleOut StateServer.
David De Sloovere,

Grazie per il tuo contributo :) Proverò a vedere come agisce sui pochi casi d'uso che abbiamo con SESSION coinvolti.
Agustin Garzon,

Dal momento che molte persone hanno menzionato il blocco di un elemento specifico nella sessione, potrebbe essere utile sottolineare che l'implementazione di supporto deve restituire un riferimento comune al valore della sessione attraverso get chiamate per consentire il blocco (e persino che non funzionerà con server con bilanciamento del carico). A seconda di come usi lo stato della sessione, qui c'è il potenziale per le condizioni di gara. Inoltre, mi sembra che ci siano blocchi nella tua implementazione che in realtà non fanno nulla perché avvolgono una sola chiamata di lettura o scrittura (correggimi se sbaglio qui).
nw.

11

A meno che la tua applicazione non abbia esigenze particolari, penso che tu abbia 2 approcci:

  1. Non usare affatto la sessione
  2. Utilizzare la sessione così com'è ed eseguire la messa a punto come Joel menzionato.

Session non è solo thread-safe, ma anche sicuro dallo stato, in modo da sapere che fino al completamento della richiesta corrente, ogni variabile di sessione non cambierà da un'altra richiesta attiva. Affinché ciò accada, è necessario assicurarsi che la sessione venga BLOCCATA fino al completamento della richiesta corrente.

È possibile creare una sessione come comportamento in molti modi, ma se non blocca la sessione corrente, non sarà "sessione".

Per i problemi specifici che hai citato, penso che dovresti controllare HttpContext.Current.Response.IsClientConnected . Ciò può essere utile per prevenire esecuzioni e attese non necessarie sul client, sebbene non sia in grado di risolvere completamente questo problema, poiché può essere utilizzato solo in modo raggruppato e non asincrono.


10

Se si sta utilizzando l'aggiornamento Microsoft.Web.RedisSessionStateProvider(a partire da 3.0.2), è possibile aggiungerlo al proprio web.configper consentire sessioni simultanee.

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

fonte


Non so perché questo fosse a 0. +1. Molto utile.
Pangamma,

Funziona nel pool di app in modalità classica? github.com/Azure/aspnet-redis-providers/issues/123
Rusty

funziona con il provider di servizi stato inProc o Session predefinito?
Nick Chan Abdullah,

Si noti che il poster fa riferimento se si utilizza RedisSessionStateprovider, ma potrebbe funzionare anche con questi nuovi provider AsNetc AspNetSessionState (per SQL e Cosmos) perché è anche nella loro documentazione: github.com/aspnet/AspNetSessionState La mia ipotesi è che funzionerebbe in classicmode se SessionStateProvider funziona già in modalità classica, probabilmente le cose sullo stato della sessione avvengono all'interno di ASP.Net (non IIS). Con InProc potrebbe non funzionare, ma sarebbe meno un problema perché risolve un problema di contesa di risorse che è un grosso problema con scenari fuori processo.
madamission,

4

Per ASPNET MVC, abbiamo fatto quanto segue:

  1. Per impostazione predefinita, imposta SessionStateBehavior.ReadOnlytutte le azioni del controller eseguendo l'overrideDefaultControllerFactory
  2. Sulle azioni del controller che devono essere scritte nello stato della sessione, contrassegnare con l'attributo per impostarlo su SessionStateBehavior.Required

Crea ControllerFactory personalizzato e sostituisci 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
{ }

Collega la fabbrica di controller creata in global.asax.cs

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

Ora, possiamo avere entrambi read-onlye lo read-writestato della sessione in un singolo 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: lo stato della sessione ASPNET può ancora essere scritto anche in modalità sola lettura e non genererà alcuna forma di eccezione (non si blocca solo per garantire coerenza), quindi dobbiamo fare attenzione a contrassegnare AcquireSessionLockle azioni del controller che richiedono la scrittura dello stato della sessione.



3

Contrassegnare lo stato della sessione di un controller come di sola lettura o disabilitato risolverà il problema.

Puoi decorare un controller con il seguente attributo per contrassegnarlo in sola lettura:

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

l' enum System.Web.SessionState.SessionStateBehavior ha i seguenti valori:

  • Predefinito
  • Disabilitato
  • Sola lettura
  • necessario

0

Solo per aiutare chiunque abbia questo problema (bloccare le richieste quando si esegue un'altra dalla stessa sessione) ...

Oggi ho iniziato a risolvere questo problema e, dopo alcune ore di ricerca, l'ho risolto rimuovendo il Session_Startmetodo (anche se vuoto) da Global.asax file .

Questo funziona in tutti i progetti che ho testato.


IDK che tipo di progetto era attivo, ma il mio non ha un Session_Startmetodo e si blocca ancora
Denis G. Labrecque

0

Dopo aver lottato con tutte le opzioni disponibili, ho finito per scrivere un provider SessionStore basato su token JWT (la sessione viaggia all'interno di un cookie e non è necessario alcun archivio back-end).

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

vantaggi:

  • Sostituzione drop-in, non sono necessarie modifiche al codice
  • Ridimensiona meglio di qualsiasi altro archivio centralizzato, poiché non è necessario alcun backend di archiviazione delle sessioni.
  • Più veloce di qualsiasi altro archivio di sessioni, poiché non è necessario recuperare dati da alcun archivio di sessioni
  • Non consuma risorse del server per l'archiviazione della sessione.
  • Implementazione predefinita non bloccante: la richiesta simultanea non si bloccherà a vicenda e manterrà un blocco sulla sessione
  • Ridimensionare orizzontalmente l'applicazione: poiché i dati della sessione viaggiano con la richiesta stessa, è possibile avere più web head senza preoccuparsi della condivisione della sessione.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.