Quando Sql Server non è sufficiente per salvare le sessioni utente di IIS
IL PROBLEMA
Quando si ha la necessitò di far girare un’applicazione web in un ambiente multiserver si ha la necessità di gestire le sessioni degli utenti, e fin dalle sue origini ASP.NET mette a disposizione due modalità: lo state server e la possibilità di appoggiarsi ad un database Sql Server opportunamente configurato. Lo stesso discorso vale quando si utilizza il Web Garden; questa è la situazione iniziale di un problema al quale ho lavorato recentemente: Web Garden + Sql Server.
Per qualche motivo, alcune variabili statiche utilizzate dall’applicazione e salvate in sessione risultavano improvvisamente vuote non appena veniva aumentato il numero di worker process per ogni application pool, che è esattamente quello che non dovrebbe succedere quando si usa <sessionState mode=”SqlServer”… /> . Inizialmente ho pensato ad un problema di configurazione sui server ma con qualche veloce verifica nei vari file .config di .NET e nel metabase di IIS abbiamo subito scartato questa eventualità; nonostante i miei tentativi non sono però riuscito a riprodurre il problema su un mio server di test, quindi tutto continuava a far pensare a qualcosa di specifico sui server in questione, o nel codice dell’applicazione.
Avendo a disposizione il codice sorgente la scelta più logica era quella di usare Visual Studio per debuggarlo e dopo pochi minuti, mentre stavo verificando il valore di alcune variabili ed improvvisamente ho ottenuto la stessa eccezione riportata dal cliente come parte del problema, un alert javascript che creata all’interno di un blocco catch nella pagina. Sulla mia macchina questo era dovuto all’health monioring di IIS che ha riciclato l’application pool a causa di un lungo periodo di inattività, ma il cliente non lamentava nessun crash o riciclo inatteso dell’application pool ne ce n’era traccia negli event log… inoltre il repro che avevo a disposizione non utilizzava nemmeno Session() o Cache() , tutti dettagli che tra l’altro sono in contrasto tra loro, se non si fa uso della Sessione è difficile poterla perdere… L’esperienza però mi ha insegnato che bisogna sempre avere un occhio di riguardo per gli oggetti statici, soprattutto quando si ha a che fare con sessioni perse e variabili che cambiano inspiegabilmente valore (esiste una sola istanza di un oggetto statico valida per tutta l’applicazione, e se un utente modifica il valore di una variabile statica questo avrà un effetto su tutti gli utenti che la utilizzano), ma anche in questo caso la mia teoria è stata smentita dal fatto che il cliente era anche in grado di riprodurre il problema in un ambiente con un singolo server e con un solo utente collegato.
Ma gli oggetti statici hanno anche un’altra caratteristica da tenere conto: il loro scopo si riferisce all’AppDomain.
Chi ha l’istinto del detective probabilmente starà già immaginando dove voglio andare a parare… “le informazioni sul tipo di una classe statica vengono caricate da .NET Framework Common Language Runtime (CLR) durante il caricamento del programma che fa riferimento alle classi” (da http://msdn.microsoft.com/it-it/library/79b3xss3.aspx) e la prima volta che un utente richiede una pagina le variabili statiche vengono inizializzate. Nel nostro caso l’oggetto statico era un array i cui valori erano presi da un database al primo caricamento di una pagina tramite Page.IsPostBack: ai successivi POST si da per scontato che l’array sa già stato valorizzato precedentemente e pronto all’uso, e tutto funziona correttamente. Questo è veri fino a quando IIS decide che il nostro POST deve essere servito dallo stessa istanza di w3wp.exe che ha servito la nostra prima richiesta (GET). Ma… cosa succede se siamo invece dirottati ad una nuova istanza di w3wp.exe? Beh, si tratterà sempre di un POST che però verrà servito da una nuova istanza di w3wp.exe che magari caricherà (per la prima volta) il Common Language Runtime, inizializzerà l’applicazione, creerà gli AppDomain necessari, gli oggetti statici, inizierà l’esecuzione della pagina richiesta… e Page.IsPostBack sarà sempre true, il codice per valorizzare l’array non verrà eseguito ma si presumerà che sia valido (e l’oggetto array è effettivamente valido, altrimenti avremmo una NullReferenceException, è semplicemente vuoto) ed invece avremo una IndexOutOfRangeException quando cercheremo di leggerne il contenuto.
UN PO’ DI TEORIA
Questo post di Chris Brumme (ed anche questo per maggiori dettagli sugli AppDomain) discute le basi della isolation:
By default, static fields are scoped to AppDomains. In other words, each AppDomain gets its own copy of all the static fields for the types that are loaded into that AppDomain . This is independent of whether the code was loaded as domain-neutral or not. Loading code as domain neutral affects whether we can share the code and certain other runtime structures. It is not supposed to have any effect other than performance
I processi sono isolati per definizione, è il Sistema Operativo a garantirlo: se pensiamo a come i messaggi in Windows vengono scambiati, alla complessa struttura a livello Kernel per garantire la comunicazione tra i processi (Windows è un sistema operativo basato sui messaggi, che vengono inviati a finestre e processi attraverso API di basso livello) è facile capire come l’isolation sia essenziale per garantire sicurezza e dare stabilità all’intero Sistema Operativo ed ai processi che esso ospita. Dal momento che un AppDomain viene caricato all’interno di un processo e lo scopo principale di un AppDomain è garantire ancora una volta isolation (ad esempio per impedire che un’eccezione in un’applicazione abbia un impatto anche sulle altre che girano all’interno dello stesso processo) è anche chiaro che quando si carica un AppDomain all’interno di una specifica istanza di un processo, questo non possa essere acceduto da altri AppDomain caricati in differenti istanze del processo (ovviamente a meno che non sia il programmatore a volerlo esplicitamente, ad esempio usando Remoting).
D’altra parte come si dovrebbero gestire oggetti, indirizzi di memoria, thread, risorse ecc… condivisi da più processi? Se un processo lancia un’eccezione che intacca la porzione di memoria condivisa, l’errore potrebbe causare il crash di tutti i processi che condividono quella zona di memoria… questo è l’opposto dell’isolation. Se poi pensiamo ad un ambiente multiserver, come si dovrebbero condividere queste risorse (memoria, thread ecc…) attraverso le diverse macchine? Come si dovrebbe replicare una variabile statica attraverso tutte le macchine ed i processi?
Quindi, un AppDomain è specifico dell’istanza di processo che lo ospita, i membri statici sono specifici dell’AppDomain… Sql Server non è la soluzione per il problema con il quale abbiamo iniziato questa discussione: questo è piuttosto un problema di design dell’applicazione. Inoltre nel caso specifico il cliente voleva utilizzare il Web Garden per incrementare le performance della sua applicazione web ed in effetti questa è l’idea con il quale il Web Garden è stato pensato, ma solo per circostanze molto specifiche. La pratica dimostra che usando il Web Garden si rischia di perdere performance piuttosto che guadagnarne nel 90% dei casi (a causa dei meccanismi interni di sincronizzazione e gestione dei processi da parte di IIS, la gestione della cache ecc…) ed in ogni caso non più servire per superare le limitazioni imposte dall’architettura del Sistema Operativo. Il Web Garden aiuta ad aumentare le performance dell’applicazione solamente in casi nei quali l’applicazione stessa non fa uso di Sessione o Cache e dove l’applicazione non sia stressante per la CPU: sostanzialmente si ha un impatto positivo con un sito composto principalmente di pagine statiche e senza stato.
CONCLUSIONE
Quindi la soluzione migliore per il problema iniziale non è il Web Garden, ma se proprio non si vuole abbandonare l’idea di avere più processi che servono l’application pool sarà necessario un po’ di riscrittura del codice per trasformare gli oggetti statici in oggetti privati ed eventualmente salvarli in Session() o Cache() che a questo punto verrà servita perfettamente da Sql Server. Niente oggetti statici se si usa il Web Garden, oppure assicuratevi sempre che i dati siano validi e non semplicemente presumere che sia tutto a posto semplicemente perché ci si trova in un postback.