Windows Workflow Foundation e Thread Pool
Riprendo quanto segnalato da Paolo nel primo paragrafo del suo ottimo sul problema della rientranza delle chiamate in WF per ribadire il concetto sul numero dei thread utilizzabili in WF.
Lo scheduler di default di Windows Workflow Foundation (DefaultSchedulerService) lavora in asincrono rispetto al thread che avvia l'istanza di workflow dall'host.
Il parametro MaxSimultaneousWorkflows del DefaultSchedulerService determina quanti thread massimi simultanei sono consentiti dallo scheduler. Se il limite è impostato a 3, il DefaultWorkflowSchedulerService può lavorare con 3 thread del thread pool di .NET per eseguire i workflow. Quando viene avviata una nuova istanza di workflow, se i tre thread stanno già eseguendo altre istanze, la nuova istanza viene inserita in una coda interna al DefaultSchedulerService ed eseguita quando un thread dei 3 ritorna disponibile. Questo accodamento avviene anche dopo una sospensione o recupero del workflow tramite il PersistenceService.
I valori di default a prima vista sono molto bassi (un po' come accade per i 25 thread di ASP.NET che sembrano non pochi, ma praticamente inesistenti :-)): il numero, rappresentato da un Int32 è 5 nel caso di macchine mono-processore e calcolato con questo algoritmo nel caso di macchine multi-processore: (int)(5 * Environment.ProcessorCount * .8). A prima vista l'algoritmo restituisce gli stessi risultati di "4 * numeroprocessori", ma visto che si lavora con una regola aritmetica sugli interi non sempre restituisce lo stesso numero della semplice moltiplicazione per 4.
Alzare il numero dei thread sembra essere la soluzione per far girare più workflow in contemporanea, ma è bene ricordare che:
1) Host in ASP.NET: anche ASP.NET ha bisogno di thread del thread pool di .NET per poter servire le richieste che arrivano. Alzare quindi il numero di workflow contemporanei, se da una parte può migliorare il parallelismo fra workflow, sicuramente fa calare drasticamente il numero di thread disponibili in asp.net per servire le varie richieste all'applicazione per le varie pagine. Non tutte le pagine tra l'altro utilizzeranno workflow e quindi otteniamo un rallentamento generale dell'applicazione ingiustificato: una pagina velocissima da eseguire deve aspettare che si liberi un thread del thread pool per poter essere accolta. Inoltre è probabile che la singola richiesta per una pagina ASP.NET (una insert, un'update o una lettura di dati) sia più veloce dell'esecuzione di un flusso di operazioni di un workflow, non perchè il workflow rutime di per se sia più lento (anche se un po' di overhead lo aggiunge, ma ritengo che i benefici vadano ben oltre), ma perchè probabilmente in un workflow dobbiamo eseguire varie operazioni con una certa logica.
Questo problema non è confinato solamente a Windows Workflow Foundation: i generale tutte le operazioni complesse in ASP.NET occupano un thread del thread pool per molto tempo. Se ci mettiamo anche 10 thread, ad esempio, per lo scheduler del workflow (sui 25 totali di default) abbiamo veramente pochi thread a disposizione per le pagine.
Per una trattazione approfondita delle tematiche ASP.NET e l'esecuzione delle pagine in modo asincrono si veda l'articolo da me pubblicato su Computer Programmin nel 2006. E' diviso in due parti: Parte 1 e Parte 2.
Queste problematiche riguardano anche l'esecuzione di Workflow su un application server che condivide le richieste a più client.
2) Host fuori da ASP.NET e Application Server. Anche se non ospitiamo il workflow runtime in applicazioni server side, alzare il numero di workflow contemporanei, può non essere una buona idea.
Il motivo è lo stesso: questi thread vengono recuperati dal thread pool di .NET, lo stesso thread pool che viene utilizzato da altre operazioni nel codice. Ad esempio se usiamo un oggetto Transaction (manuale o automatico dal TransactionScope) nel codice del workflow o anche nel codice dell'host, questo signore utilizza il thread pool di .NET, di conseguenza andiamo a pestare i piedi (nel senso di cercare di utilizzare) allo stesso pool di thread utilizzato dai workflow: in pratica andiamo gestire le transazioni (che credo importanti :-)) aspettando che si liberi un thread alla fine di un workflow solo perchè vorremmo vedere tanti workflow in contemporanea. Il problema è enorme se pensate che le transazioni hanno (E DEVONO AVERE) un timeout...non è bello avere dei rollback per timeout solo perchè ci piace vedere il Concurrent Workflow ad un valore alto pensando che significhi alte prestazioni.
Come sempre un sistema deve esere guardato nel suo complesso, altrimenti toccando le singole impostazioni per migliorare una delle sue parti, si rischia di rallentare TUTTO.
Una "chicca" finale: sapete con cosa viene gestita dietro le quinte (ed è giusto che sia così) la persistenza di un workflow su DB ? Con un oggetto Transaction :-)
Quindi diventa facile andare in timeout durante la persistenza di workflow (con conseguente retry probabile che necessita sempre di un thread libero del thread pool) se aumentiamo il numero di workflow contemporanei. L'espressione tecnica per indicare questo "stallo" è: Il cane che si morde la coda.
La storia potrebbe essere questa:
Ho una applicazione ASP.NET che usa WF: spesso non riesco a eseguire quanti workflow vorrei. Alzo il numero di istanze possibili (MaxSimultaneousWorkflows a 20). Se chiamo i 20 WF da un singolo client vado come i fulmini. Penso di aver risolto il problema. Se non provo sotto carico (bastano 3/4 richieste concorrenti) non mi accorgo di niente. Poi gli utenti si lamentano della lentezza...vedo che ASP.NET ha solo 25 thread usabili e alzo il valore a 100. Penso di aver risolto...ma ho tanti thread switch che causano operazioni onerose e mi si rallenta ulteriormente il tutto...Alla fine compro un server da 200K così non ho problemi :-)...si ma per gestire un carico che potrebbe essere gestito da un server di fascia molto più bassa.
La soluzione necessita di attenzione su tutte le varie componenti: ormai i software sono molto legati l'uno all'altro e occorre fare molta attenzione nello spostare qualche flag isolato di una delle componenti. Ad esempio, se il nostro DB fa di tutto, anche le analisi comparative sul fatturato dei tre anni precedenti con query da 18 Join (magari senza indici perchè il DB lo abbiamo ereditato da sviluppatori precedenti), non possiamo pretendere che la nostra applicazioni vada veloce su quel db anche se il codice ASP.NET è performance e i thread sono pochi. Se appoggiamo i Workflow sullo stesso DB come persistenza probabilmente peggioriamo il tutto: sono tante le operazioni che il PersistenceService deve fare sul DB, soprattutto per applicazioni ASP.NET dove ogni richiesta è una toccata e fuga; il WF deve essere persistito molte volte in una singola operazioni di un utente. Magari abbiamo deciso di usare anche il TrackingService per tracciare tutto quello che ci serve...e magari lo abbiamo fatto sullo stesso DB peggiorando anche le statistiche di cui sopra.
Le soluzioni ci sono e constano nel calibrare bene i parametri. Molto spesso basta spostare un po' i carichi di lavoro, magari usando servizi di accodamento (MSMQ, Sql 2205 Broker Service, Oracle Advanced Queuing, MQSeries) rendendo il più possibile asincrono il lavoro già a partire da pagine .aspx e service .asmx, .svc, self-hosted di WCF. Su questi argomenti ci confronteremo a DevCon dove, ad oggi, in agenda abbiamo pesato ad una sessione tripla su queste tecniche. Il titolo della sessione dovrebbe essere esplicativo: Server-side Async: With some tricks your 5K Server can perform as a 40K server. Per il contenuto: http://devcon2007.devleap.com/sessioni.aspx#ALL01.
Nota conclusiva: esiste anche un ManualWorkflowSchedulerService, che lavora in sincrono risolvendo molti dei problemi accennati da Paolo nel suo post e da me in questo post. In alcuni post sul web si indica questa come soluzione. NON E' DETTO CHE SIA LA SOLUZIONE, ANZI, ma ne parliamo un'altra volta....