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

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ò si perdano su di te, come era per me in un primo momento, questo in pratica significa quanto segue:

  • Ogni volta che una pagina Web ASP.Net impiega molto tempo per essere caricata (forse a causa di una lenta chiamata al database o altro), e l’utente decide di passare a una pagina diversa perché è stanca di aspettare, NON POSSONO! Il blocco della sessione ASP.Net forza la nuova richiesta di pagina ad attendere fino a quando la richiesta originale ha terminato il suo carico dolorosamente lento. Arrrgh.

  • Ogni volta che UpdatePanel si sta caricando lentamente e l’utente decide di navigare verso una pagina diversa prima che UpdatePanel abbia completato l’aggiornamento … NON POSSONO! Il blocco della sessione ASP.net forza la nuova richiesta di pagina ad attendere fino a quando la richiesta originale ha terminato il suo carico dolorosamente lento. Double Arrrgh!

Quindi quali sono le opzioni? Finora ho escogitato:

  • Implementare un SessionStateDataStore personalizzato, supportato da ASP.Net. Non ne ho trovati troppi da copiare, e sembra un rischio elevato e facile da incasinare.
  • 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 (credo).
  • Non usare Session! 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. Di nuovo sembra un po ‘estremo.

Non riesco davvero a credere che il team Microsoft di ASP.Net avrebbe lasciato un collo di bottiglia così enorme nel framework alla versione 4.0! Mi manca qualcosa di ovvio? Quanto sarebbe difficile usare una raccolta ThreadSafe per la sessione?

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

< % @Page EnableSessionState="ReadOnly" %> 

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

 < % @Page EnableSessionState="False" %> 

Se nessuna delle tue pagine utilizza le variabili di sessione, basta distriggersre lo stato di sessione nel web.config.

  

Sono curioso, cosa pensi che “una collezione ThreadSafe” farebbe diventare thread-safe, se non usa le serrature?

Edit: Probabilmente dovrei spiegare con cosa intendo per “distriggersre la maggior parte di questo blocco”. È ansible elaborare contemporaneamente un numero qualsiasi di pagine di sola lettura o di non sessione per una determinata sessione senza bloccarsi a vicenda. Tuttavia, una pagina di sessione di lettura-scrittura non può iniziare l’elaborazione finché tutte le richieste di sola lettura non sono state completate e, mentre è in esecuzione, deve disporre dell’accesso esclusivo alla sessione di quell’utente per mantenere la coerenza. Il blocco dei singoli valori non funzionerebbe, perché cosa accadrebbe se una pagina modificasse un insieme di valori correlati come gruppo? Come faresti a garantire che le altre pagine in esecuzione allo stesso tempo ottengano una visione coerente delle variabili di sessione dell’utente?

Ti suggerirei di provare a minimizzare la modifica delle variabili di sessione una volta che sono state impostate, se ansible. Ciò consentirebbe di rendere la maggior parte delle pagine di sola lettura di sessione, aumentando la possibilità che più richieste simultanee dallo stesso utente non si blocchino reciprocamente.

OK, così grandi puntelli a Joel Muller per tutto il suo contributo. La mia soluzione definitiva era usare Custom 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)
  • Utilizzato un sacco della gestione standard della sessione ASP.Net (tramite la class SessionStateUtility)

Ciò ha reso ENORME la differenza con la sensazione di “snapiness” alla nostra applicazione. Non riesco ancora a credere che l’implementazione personalizzata di ASP.Net Session blocchi la sessione per l’intera richiesta. Ciò aggiunge una tale 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 molto esperti), molte persone hanno riscontrato questo problema, ma pochissime persone sono mai riuscite ad arrivare fino in fondo alla causa. Forse scriverò una lettera a Scott Gu …

Spero che questo aiuti alcune persone là fuori!

Ho iniziato a utilizzare AngiesList.Redis.RedisSessionStateModule , che oltre a utilizzare il server Redis (molto veloce) per l’archiviazione (sto utilizzando la porta di Windows – sebbene esista 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 coerenti e bloccati come parte della sessione, dovresti implementare in modo specifico un controllo di blocco / concorrenza da solo.

La decisione di MS, secondo la quale ogni sessione ASP.NET dovrebbe essere bloccata di default solo per gestire una ctriggers progettazione dell’applicazione, è una decisione sbagliata, secondo me. Soprattutto perché sembra che la maggior parte degli sviluppatori non si sia nemmeno resa conto che le sessioni sono state bloccate, per non parlare del fatto che le app devono essere strutturate in modo da poter fare lo stato di sessione di sola lettura il più ansible (opt-out, ove ansible) .

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

Ho anche fatto delle 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 vuoi implementare il supporto di Memcached o Redis, installa questo pacchetto. Quindi eredita la class LockFreeSessionStateModule e implementa metodi astratti.

Il codice non è ancora testato in produzione. Inoltre, è necessario migliorare la gestione degli errori. Le eccezioni non sono prese nell’implementazione corrente.

Alcuni provider di sessione senza blocco che utilizzano Redis:

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

  1. Non usare affatto la sessione
  2. Usa la sessione così com’è ed esegui la messa a punto come menzionato da Joel.

Session non è solo thread-safe, ma anche sicuro per lo stato, in modo che tu sappia che fino a quando la richiesta corrente non viene completata, ogni variabile di sessione non cambierà da un’altra richiesta triggers. Affinché ciò accada, è necessario assicurarsi che la sessione venga BLOCCATA fino al completamento della richiesta corrente.

È ansible creare un comportamento simile alla sessione 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 evitare esecuzioni e attese non necessarie sul client, sebbene non possa risolvere completamente questo problema, in quanto può essere utilizzato solo da un pooling e non da asincrono.

Per ASPNET MVC, abbiamo fatto quanto segue:

  1. Per impostazione predefinita, imposta SessionStateBehavior.ReadOnly su tutte le azioni del controllore sovrascrivendo DefaultControllerFactory
  2. Sulle azioni del controller che richiedono la scrittura nello stato della sessione, contrassegnare con l’attributo per impostarlo su SessionStateBehaviour.Required

Creare ControllerFactory personalizzato e sovrascrivere 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 { } 

Collega la factory controller creata in global.asax.cs

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

Ora, possiamo avere sia lo stato di sessione di read-only read-write che di read-write 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à di sola lettura e non genera alcuna eccezione (non si blocca per garantire la coerenza), pertanto dobbiamo fare attenzione a contrassegnare AcquireSessionLock nelle azioni del controller che richiedono la scrittura dello stato della sessione .

Contrassegnare lo stato della sessione di un controller come readonly o disabled risolverà il problema.

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

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

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

  • Predefinito
  • Disabilitato
  • Sola lettura
  • necessario

Solo per aiutare chiunque abbia questo problema (bloccare le richieste quando si esegue un altro dalla stessa sessione) …

Oggi ho iniziato a risolvere questo problema e, dopo alcune ore di ricerca, l’ho risolto rimuovendo il metodo Session_Start (anche se vuoto) dal file Global.asax .

Funziona in tutti i progetti che ho provato.