Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Si applica a: .NET Core 2.1, .NET Core 3.1, .NET 5
Questo articolo illustra come analizzare i problemi di prestazioni e descrive come usare createdump e ProcDump per acquisire manualmente i file di dump della memoria .NET Core in Linux.
Prerequisiti
I requisiti minimi per seguire questi lab per la risoluzione dei problemi sono i seguenti:
- Un'applicazione ASP.NET Core per illustrare problemi di prestazioni e arresto anomalo della CPU a bassa CPU e elevata.
- Il debugger lldb, installato e configurato per caricare l'estensione SOS all'apertura di un dump principale.
Se sono state seguite le parti precedenti di questa serie, è necessario avere la configurazione seguente pronta per procedere:
- Nginx è configurato per ospitare due siti Web:
- La prima è in ascolto delle richieste usando l'intestazione host myfirstwebsite (
http://myfirstwebsite
) e le richieste di routing alla demo ASP.NET'applicazione Core in ascolto sulla porta 5000. - Il secondo è in ascolto delle richieste usando l'intestazione host buggyamb (
http://buggyamb
) e le richieste di routing alla seconda applicazione buggy di esempio ASP.NET Core in ascolto sulla porta 5001.
- La prima è in ascolto delle richieste usando l'intestazione host myfirstwebsite (
- Entrambe le applicazioni core ASP.NET devono essere eseguite come servizi che vengono riavviati automaticamente quando il server viene riavviato o l'applicazione smette di rispondere.
- Il firewall locale Linux è abilitato e configurato per consentire il traffico SSH e HTTP.
Note
Se la configurazione non è pronta, passare a "Parte 2 Creare ed eseguire ASP.NET app core".
Obiettivo di questo lab
Finora in questa serie di risoluzione dei problemi è stato analizzato un problema di arresto anomalo. In questo lab si avrà la possibilità di analizzare un problema di prestazioni e di imparare a usare createdump e ProcDump per acquisire manualmente i file di dump della memoria.
Riprodurre il problema
In una parte precedente è stato testato il primo scenario "lento" selezionando il collegamento Lento . Quando si esegue questa operazione, la pagina viene caricata correttamente, ma molto più lentamente del previsto. In questa parte si userà la funzionalità Generatore di carico BuggyAmb per risolvere questo problema di prestazioni. Si tratta di una funzionalità "sperimentale" che invia fino a sei richieste simultanee a qualsiasi risorsa problematica. È limitato a sei perché usa chiamate jQuery e Ajax per inviare le richieste. I Web browser impostano un limite per la maggior parte delle richieste Ajax a sei richieste simultanee a un determinato URL. Per informazioni su come usare il generatore di carico per riprodurre scenari diversi, vedere Sperimentale "Generatore di carico".
Per riprodurre il problema, aprire Pagine dei problemi, selezionare Generatore di carico e quindi inviare sei richieste nello scenario Lento.
L'elenco seguente mostra cosa si dovrebbe, alla fine, vedere nel browser. I tempi di risposta visualizzati sono elevati. Il tempo di risposta previsto è inferiore a un secondo. Questo è ciò che si può aspettare di vedere quando si seleziona il collegamento Risultati previsti dalla pagina di destinazione dell'applicazione.
Si tratta del problema che si risolverà.
Monitorare i sintomi
Tenere presente che una buona sessione di risoluzione dei problemi inizia definendo il problema e comprendendo i sintomi. Si userà htop
per monitorare l'utilizzo della memoria del processo e della CPU per il processo che ospita l'applicazione ASP.NET Core quando si tenta di riprodurre il problema generando il carico. Se non si ricorda cos'è htop
, controllare le parti precedenti della serie.
Prima di provare a riprodurre il problema, iniziare impostando una linea di base per la modalità di esecuzione dell'applicazione. Selezionare Risultati previsti o inviare più richieste allo scenario dei risultati previsti usando la funzionalità Generatore di carico. Controllare quindi l'aspetto dell'utilizzo della CPU e della memoria quando il problema non si manifesta. Si userà htop
per controllare l'utilizzo della CPU e della memoria.
Eseguire htop
e filtrarlo per visualizzare solo i processi che appartengono all'utente con cui viene eseguita l'applicazione buggy. L'utente dell'applicazione di destinazione ASP.NET Core in questo caso è www-data. Premere il tasto U per selezionare l'utente www-data dall'elenco. Premere anche MAIUSC+H per nascondere i thread. Come si può notare, esistono quattro processi in esecuzione nel contesto di www-data e due di essi sono i processi Nginx. Gli altri sono per l'applicazione buggy e l'applicazione demo creata durante la configurazione dell'ambiente.
Poiché non è stato ancora riprodotto il problema di prestazioni, si noti che tutte le statistiche di utilizzo della CPU e della memoria sono attualmente basse.
Tornare ora al browser client e inviare sei richieste allo scenario Lento usando il generatore di carico. Successivamente, tornare rapidamente al dispositivo Linux e osservare il consumo delle risorse di processo in htop
. Si noterà che l'utilizzo della CPU dell'applicazione buggy è aumentato notevolmente e l'utilizzo della memoria varia verso l'alto e verso il basso.
Note
Poiché questo output viene ricavato da una macchina virtuale dotata di due CPU logiche, htop
mostra più del 100% di utilizzo della CPU.
Dopo l'elaborazione di tutte le richieste, l'utilizzo della CPU e della memoria diminuisce. Sia le tendenze di utilizzo della CPU che della memoria dovrebbero far sospettare che durante l'elaborazione delle richieste si verifichi un utilizzo GC elevato (Garbage Collector) nell'applicazione.
Raccogliere i file di dump di base
Quando si risolve un problema di prestazioni, i file di dump di memoria consecutivi vengono acquisiti e analizzati. L'idea alla base dell'acquisizione di più file di dump è semplice: un dump di processo è uno snapshot della memoria del processo. Non contiene informazioni passate. Per risolvere i problemi di prestazioni, è necessario acquisire più file di dump della memoria manuale o file di dump core in modo da poter confrontare i thread e gli heap e così via.
Usare le opzioni consigliate seguenti per acquisire i file di dump della memoria manuali su richiesta:
- Createdump
- Procdump
- Dotnet-dump
Createdump
Createdump è incluso insieme al runtime di .NET Core. Si trova nella directory di runtime. È possibile trovare i percorsi della directory di runtime usando il dotnet --list-runtimes
comando .
Poiché l'applicazione buggy è un'applicazione .NET Core 3.1, il percorso completo di createdump è /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.10/createdump.
La forma più semplice di questo comando è createdump <PID>
. Verrà scritto un dump di base per il processo di destinazione. È possibile indicare allo strumento dove creare i file di dump aggiungendo l'opzione -f
: createdump <PID> -f <filepath>
. Per questo esercizio, creare i file di dump nella ~/dumps/ directory .
Verranno acquisiti due file di dump di memoria consecutivi del processo BuggyAmb a distanza di 10 secondi. È necessario acquisire i file di dump mentre si riproduce il problema di "risposta lenta delle richieste". Per iniziare, è prima necessario trovare il PID del processo. Usare il htop
comando o systemctl status buggyamb.service
. Negli elenchi seguenti il processo PID è 11724.
Per creare i file di dump, seguire questa procedura:
- Creare il primo file:
sudo /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.10/createdump 11724 -f ~/dumps/coredump.manual.1.%d
. - Attendere 10 secondi dopo la scrittura del primo file di dump.
- Creare il secondo file:
sudo /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.10/createdump 11724 -f ~/dumps/coredump.manual.2.%d
Alla fine, dovrebbero essere presenti due file di dump della memoria. Prendere nota delle dimensioni di ogni file di dump.
Analizzare i file di dump in lldb
Si dovrebbe già sapere come aprire i file di dump in lldb. Aprire entrambi i file in lldb in due sessioni SSH diverse.
L'obiettivo è sviluppare una teoria su ciò che potrebbe causare il problema delle prestazioni. Si sa già che l'utilizzo della CPU e della memoria è elevato quando si verifica il problema. Per controllare la memoria gestita, è possibile usare il dumpheap -stat
comando . Prima di iniziare, esaminare rapidamente il primo file di dump.
Eseguire il clrthreads
comando per ottenere un elenco di thread gestiti.
Note
Uno dei thread ha la modalità GC impostata su Cooperative e le altre sono impostate su Preemptive.
Se la modalità GC di un thread è impostata su Preemptive, significa che GC può sospendere questo thread in qualsiasi momento. Al contrario, la modalità cooperativa indica che il GC deve attendere che il thread passi alla modalità Preemptive prima di sospenderlo. Quando il thread esegue codice gestito, è in modalità cooperativa.
Per iniziare, esaminare il thread in modalità cooperativa. L'ID thread del debugger per il thread cooperativo è 19 nell'elenco di esempio. L'ID sarà diverso quando si ripete l'esercizio. Passare al thread eseguendo thread select 19
e quindi eseguire clrstack
per elencare lo stack di chiamate gestite. La pagina "lenta" dell'applicazione buggy sta eseguendo un'operazione di concat di stringa.
Questo dovrebbe far sospettare perché è necessario sapere che le operazioni di concat di stringa sono costose. Ciò è dovuto al fatto che gli oggetti stringa in .NET non sono modificabili, vale a dire che i relativi valori non possono essere modificati dopo l'assegnazione. Si consideri questo frammento di codice pseudo:
string myText = "Debugging";
myText = myText + " .NET Core";
myText = myText + " is awesome";
Questo codice crea più stringhe in memoria: Debugging
, Debugging .NET Core
e Debugging .NET Core is awesome
. È necessario creare tre oggetti stringa diversi per generare (concatenare) una stringa finale. Se ciò si verifica con una frequenza sufficiente, potrebbe creare una pressione di memoria in modo che il processo GC possa essere attivato.
Questa teoria sembra promettente. Tuttavia, è consigliabile provare a verificare che sia corretto. Prima di esaminare l'heap gestito e, mentre si è già posizionati nel contesto del thread, esaminare gli oggetti a cui viene fatto riferimento da questo thread per determinare quali valori di stringa e string[]
oggetti sono. Eseguire dso
e concentrarsi su matrici di stringhe e stringhe.
Provare a esaminare la matrice di stringhe. Eseguire dumpobj
usando l'indirizzo dell'oggetto . Tuttavia, tenere presente che questo dimostra solo che l'oggetto in questione è una matrice. SOS fornisce un dumparray
comando per analizzare le matrici. Eseguire dumparray 00007faf309528c8
per ottenere l'elenco degli elementi nella matrice. Tenere presente che l'indirizzo dell'oggetto matrice sarà diverso nel file di dump che si sta esaminando.
Eseguire di nuovo il dumpobj
comando usando gli indirizzi stringa risultanti contenuti all'interno della matrice. Scegliere alcuni indirizzi e esaminarli.
Queste stringhe sono simili alle stringhe presenti nella tabella product visualizzata nella pagina.
Si noti che lldb (o SOS) potrebbe non visualizzare il valore stringa se le stringhe sono di grandi dimensioni. In questi casi, una delle opzioni consiste nell'usare i comandi nativi di lldb per esaminare l'indirizzo di memoria nativa. Questa operazione è simile all'uso dei d*
comandi (ad esempio, dc
) in WinDbg.
Il comando seguente legge la memoria nativa nella posizione di memoria specificata e mostra i primi 384 byte. L'elenco usa uno degli indirizzi stringa per dimostrarlo. Il comando in esecuzione è memory read -c 384 00007fb14d5da040
.
Il numero di stringhe a cui fa riferimento lo stack del thread sembra confermare che la teoria del problema di concat della stringa sta causando il problema di prestazioni.
Tuttavia, l'indagine non è ancora terminata. Sono disponibili due file di dump della memoria. Di conseguenza, si confronterà l'heap di memoria gestita e si verificherà il modo in cui l'heap è cambiato nel tempo.
Eseguire il dumpheap -stat
comando in ogni file di dump. Di seguito è riportato il primo file. Nell'elenco seguente sono presenti 105.401 oggetti stringa e la dimensione totale degli oggetti stringa è di circa 480 MB. Si noti anche che la memoria è probabilmente frammentata e che la causa della frammentazione sembra essere correlata agli oggetti e System.Data.DataRow
agli oggetti della matrice di stringhe.
Continuare eseguendo lo stesso dumpheap -stat
comando nel secondo file di dump. Dovrebbe essere visualizzata una modifica nelle statistiche di frammentazione, ma non è importante nel contesto di questa indagine. La parte importante è il numero di oggetti stringa e l'aumento significativo delle dimensioni di questi oggetti.
Allo stesso tempo, aumenta anche il numero di System.Data.DataRow
oggetti.
Si potrebbe sospettare che si verifichi un problema che coinvolge un heap di oggetti di grandi dimensioni (LOH). È quindi possibile esaminare gli oggetti LOH. In questo caso, è necessario eseguire i dumpheap -stat -min 85000
comandi. L'elenco seguente contiene le statistiche di LOH per il primo dump della memoria.
Di seguito sono riportate le statistiche di LOH per il secondo dump della memoria.
Ciò mostra chiaramente anche l'aumento dell'heap. Tutto ciò sembra essere correlato agli string
oggetti .
Infine, cosa succede se si scegliesse un oggetto "live" da LOH per trovare la radice? "Live", in questo caso, significa che l'oggetto è rooted da qualche parte e, pertanto, viene usato attivamente dall'applicazione in modo che il processo GC non lo elimini.
La gestione di questa situazione è facile. Eseguire dumpheap -stat -min 85000 -live
. Questo comando visualizza solo gli oggetti che sono rooted da qualche parte. In questo esempio sono presenti solo istanze corrette di string
oggetti che risiedono nel file LOH.
Utilizzare l'indirizzo MT dell'oggetto string
per ottenere l'elenco degli indirizzi di tali oggetti attivi. Eseguire dumpheap -mt 00007fb1602c0f90 -min 85000 -live
.
Selezionare ora un indirizzo in modo casuale dall'elenco risultante. Nello screenshot seguente viene visualizzato il terzo indirizzo nell'elenco. È possibile provare a esaminare l'indirizzo scelto eseguendo dumpobj
. Tuttavia, poiché si tratta di un oggetto di grandi dimensioni, il debugger non visualizzerà il valore. Esaminare invece l'indirizzo di memoria nativa ancora una volta e si noterà che si tratta di un string
oggetto simile a quello che è possibile trovare nell'elenco della tabella dei prodotti nella pagina che risponde lentamente.
Esaminare la radice dell'oggetto elencato. A tale scopo, usare il comando SOS gcroot
. Questo comando accetta semplicemente l'indirizzo dell'oggetto come parametro nella sua forma più semplice. Come si può notare, questo string
è rooted al thread in cui viene eseguita la pagina "lenta". Dovrebbero essere visualizzati anche il nome del file di origine e le informazioni sul numero di riga.
Note
La visualizzazione delle informazioni sul nome del file di origine e sul numero di riga dipende dalla posizione in cui si stanno risolvendo i problemi e dal fatto che i simboli siano impostati correttamente. Nello scenario peggiore, è possibile recuperare almeno l'ID del thread. Nell'elenco seguente b6c è un ID thread gestito. Se si esegue clrthreads
, è possibile trovare l'ID thread corrispondente.
Come illustrato nello screenshot precedente, l'ID del thread del debugger per l'ID del thread gestito b6c è 23. Passare al thread 23 e controllare lo stack di chiamate gestite. Come si è visto in precedenza, questo thread dovrebbe anche eseguire operazioni di concat di stringa.
Se si esamina lo stack di chiamate native usando il bt
comando , è possibile notare che il processo GC sta allocando memoria per questo thread.
Questa evidenza conferma la teoria che il problema è correlato a un numero elevato di operazioni di concatenazione di stringhe che creano stringhe sempre più grandi che vengono attivate durante l'elaborazione di una pagina "lenta".
La soluzione per un problema di questo tipo non rientra nell'ambito di questa serie. Tenere tuttavia presente che la soluzione è facile da implementare usando un'istanza StringBuilder
di classe anziché le operazioni concat di stringa.