Logo UGIdotNET

Discussione 'Pooling e RAM'

# Pubblicato il 27 gen 2003 10.45 - Rispondi
Alessandro Rizzi
Pooling e RAM
Scusate per la lunghezza del messaggio ....

Tempo fa avevo fatto dei test con l'object pooling scoprendo che c'erano dei problemi di "abuso" di RAM.
La situazione e' questa: un oggetto transazionale con un metodo autocomplete con l'object pooling. Se faccio un ciclo che insanzia l'oggetto e chiama questo metodo 5000 volte vedo che la RAM continua a crescere. Inoltre, alla fine del ciclo, non c'e' nessun calo, finche' non va in timeout DLLHOST.
A suo tempo ne ho parlato con A. Bini, il quale mi ha spiegato che la cosa dipende dal Remoting (non entro nel dettaglio) e dal fatto che il tempo di default prima che un oggetto "remoto" sia deallocato e' 5 minuti, per cui nessuno dei miei 5000 oggetti veniva deallocato in tempo (DLLHOST cade dopo 3 minuti). Pertanto, sempre su suggerimento di A.Bini ho ridefinito il metodo InitializeLifetimeService, restituendo un oggetto ILease con "lease.InitialLeaseTime = TimeSpan.FromSeconds(5);"

In questo modo le cose cambiano, ma non come speravo. Segue una tabella con i tempi e l'uso della RAM (scusate ma qui non riesco a scriverla meglio, salvatela in un file .htm e apritela con explorer):

<TABLE id="Table1" style="Z-INDEX: 101; LEFT: 104px; POSITION: absolute; TOP: 65px" cellSpacing="1" cellPadding="1" width="300" border="1">
<TR>
<TD></TD>
<TD></TD>
<TD>Secondi</TD>
<TD>RAM</TD>
<TD>Secondi</TD>
<TD>RAM</TD>
<TD>Secondi</TD>
<TD>RAM</TD>
<TD>Secondi</TD>
<TD>RAM</TD>
</TR>
<TR>
<TD>Pool</TD>
<TD>NotDispose</TD>
<TD>33,43</TD>
<TD>29M</TD>
<TD>34,09</TD>
<TD>46M</TD>
<TD>33,26</TD>
<TD>27M</TD>
<TD>31,71</TD>
<TD>36M</TD>
</TR>
<TR>
<TD>Pool</TD>
<TD>Dispose</TD>
<TD>75,62</TD>
<TD>17M</TD>
<TD>71,78</TD>
<TD>22M</TD>
<TD>75,68</TD>
<TD>16M</TD>
<TD>74,54</TD>
<TD>18M</TD>
</TR>
<TR>
<TD>NotPool</TD>
<TD>NotDispose</TD>
<TD>36,43</TD>
<TD>31M</TD>
<TD>34,84</TD>
<TD>49M</TD>
<TD>34,39</TD>
<TD>29M</TD>
<TD>32,48</TD>
<TD>35M</TD>
</TR>
<TR>
<TD>NotPool</TD>
<TD>Dispose</TD>
<TD>74,96</TD>
<TD>19M</TD>
<TD>73,37</TD>
<TD>25M</TD>
<TD>76,35</TD>
<TD>16M</TD>
<TD>73,75</TD>
<TD>19M</TD>
</TR>
</TABLE>

La prima riga si rifesce ad un oggetto con il Pooling attivato, di cui il client non chiama il metodo Dispose().
La seconda riga si rifesce ad un oggetto con il Pooling attivato, di cui il client chiama il metodo Dispose().
La terza riga si rifesce ad un oggetto con il Pooling disattivato, di cui il client non chiama il metodo Dispose().
La quarta riga si rifesce ad un oggetto con il Pooling disattivato, di cui il client chiama il metodo Dispose().

Le prime due colonne indicano rispettivamente tempi e RAM per i primi 5000 oggetti, le seconde due colonne per i secondi 5000 oggetti.
Le ultime quattro colonne si riferiscono alla 2a fase dell'esperimento in cui e' stato ridefinito il metodo InitializeLifetimeService.

Le mie conclusioni, esaminando questi risultati sono le seguenti:
- Il dispose fa risparmiare parecchia RAM, e sprecare parecchio tempo ... il solito compromesso, ma dall'esempio in questione conviene decisamente usarlo, se non si vuole esaurire la RAM molto velocemente.
- Il pooling serve a poco, sia in termini di tempo, sia di memoria (lo so che se nel costruttore faccio qualcosa di pesante, il pooling serve, ma quando mai posso fare qualcosa nel costruttore di un oggetto che va bene per tutte le instanze di quell'oggetto, in un'applicazione realmente complessa ... 0.5% dei casi?)
- Ridefinire InitializeLifetimeService serve effettivamente a ridurre i problemi di RAM, ma non completamente. O forse ho sbagliato qualcosa io?

Se ho un'applicazione Server che non viene mai spenta, cosa succede dopo 1 mese di lavoro ... occupa 1Giga di RAM?
(ok ci sono i trucchi di COM+ 1.5, che riavvia "dolcemente" le applicazioni ... ma ... )

Saluti a tutti
Alessandro Rizzi
# Pubblicato il 04 feb 2003 17.26 - Rispondi
Andrea Bini [MS]
Re: Pooling e RAM
A questo punto mi trovo costretto a chiedere scusa anch'io per la lunghezza della risposta....

I punti che hai messo in evidenza potrebbero rissumersi nei seguenti:
- utilità o meno di chiamare Dispose()
- utilità del pooling
- tempi di esecuzione delle chiamate a componenti COM+
- andamento della memoria nel tempo per un'applicazione COM+

Prima di procedere con l'esame dei punti specifici, è importante definire esattamente lo scenario che si vuole testare. Se ricordo bene dalle nostre precedenti discussioni sull'argomento, lo scenario è il seguente:
- un componente derivato da ServicedComponent configurato in un'applicazione COM+ di tipo server
- un client managed eseguito sulla stessa macchina del server che utilizza il componente secondo questo pattern:
for (int i=0; i < 5000; i++)
{
ComponentA obj = new ComponentA();
obj.Foo();
obj.Dispose();
}

Per quanto riguarda la Dipose(), in questo scenario è fondamentale se si vogliono tenere le risorse server (memoria, canale RPC per DCOM, etc) sotto controllo. Quindi non metterei in discussione questa scelta.

Per il pooling, confermo quanto detto da te: la sua utilità non sta nel risparmio di memoria che si potrebbe ottenere, bensì nel risparmio dei tempi di inizializzazione per oggetti che ad esempio devono creare strutture dati complesse (matrici per eseguire calcoli), o che devono acquisire risorse (ad esempio connessioni a Mainframe) il cui tempo di collegamento sia dell'ordine dei secondi. La scelta se utilizzare o meno il pooling dipende quindi dal contesto in cui ci si trova e dal tipo di design che si vuole implementare. Valgono tutte le considerazioni che si facevano su COM+ prima di .NET.

Gli altri due argomenti sono sicuramente più interessanti: performance e memoria.

Per quanto riguarda le performance, se ho capito bene i risultati dei tuoi test, nel caso "peggiore" si impiegano 75 secondi per effettuare 5000 chiamate di attivazione / utilizzo dell'oggetto / disattivazione. Questo corrisponde a una media di 15 ms per ogni ciclo, che mi sembrano un tempo OTTIMO.
Tieni presente che nel tuo test stai misurando sotto stress le performances pure della sola infrastruttura. Nel caso reale, il tuo componente eseguirà delle attività (chiamate ad altri componenti e/o verso un database) che impiegano mediamente tempi dell'ordine dei 100 ms.
Se inserisci nel metodo chiamato anche solo una sleep o una finta elaborazione di quest'ordine di grandezza, noterai che i tempi di esecuzione nel caso un cui utilizzi la Dispose() sono sostanzialmente identici al caso in cui non la utilizzi. Questo conferma l'indicazione di chiamare sempre e comunque la Dispose().

La questione RAM è un pochino più complessa. Per farti un'idea di persona di quello che succede puoi utilizzare dei tool di profiling che traccino le attività di allocazione della memoria (per la memoria managed puoi utilizzare l'Allocation Profiler citato nella sezione Goodies di questo sito).
Sostanzialmente si vede che ci sono due fattori che contribuiscono all'aumento della memoria occupata dal processo DLLHOST (devi guardare la colonna VM size, non Mem Usage su Task Manager):
- il LeaseManager tiene in vita gli oggetti anche quando hai chiamato Dispose(). Con le impostazioni di default, ci sono circa 1000-2000 oggetti che restano in vita e contribuiscono a circa il 90% dell'heap managed. Modificando i valori di lease come riportato nel tuo messaggio si abbassa il tempo in cui un oggetto viene tenuto in vita dal LeaseManager, quindi diminuisce il numero di oggetti presenti in memoria in un dato istante. Per un approfondimento dell'argomento relativo ai lease consiglio la lettura dell'articolo http://www.gotdotnet.com/team/xmlentsvcs/espaper.aspx, sezione "Remote components" verso la fine.
- ci sono alcune strutture dati un-managed a livello di CLR che vengono allocate e non rilasciate (ad esempio i SyncBlock, che sono strutture interne di sincronizzazione del CLR). Anche in questo caso però ho paura che l'allocazione di queste strutture sia una conseguenza del fatto che stiamo mettendo sotto stress il runtime senza fargli eseguire un'attività specifica. Inserendo delle attività (sleep o altro) all'interno del metodo del componente si nota che queste strutture si stabilizzano senza crescere ulteriormente.

Ovviamente non chiedo a nessuno di credere sulla parola, per cui se fate dei test che sono in contrasto con quanto esposto parliamone pure. Non escludiamo che potrebbero esserci in particolari situazioni dei bug o dei memory leaks.
Se ritenete l'argomento interessante possiamo approfondirlo nel workshop (segnalatelo in tal caso nel forum apposito)

Andrea Bini [MS]
Il presente posting viene fornito “così come é”, senza garanzie, e non conferisce alcun diritto

© 2001 User Group Italiano UGIdotNET. Tutti i diritti riservati. Note legali. - Partita IVA 01927050185