Analizza le prestazioni di produzione con Cloud Profiler

1. Panoramica

Mentre gli sviluppatori di app client e web frontend utilizzano comunemente strumenti come Android Studio CPU Profiler o gli strumenti di profilazione inclusi in Chrome per migliorare le prestazioni del codice, le tecniche equivalenti non sono state altrettanto accessibili o adottate da chi lavora sui servizi di backend. Cloud Profiler offre le stesse funzionalità agli sviluppatori di servizi, indipendentemente dal fatto che il loro codice venga eseguito sulla piattaforma Google Cloud o altrove.

95c034c70c9cac22.png

Lo strumento raccoglie informazioni sull'utilizzo della CPU e sull'allocazione della memoria dalle tue applicazioni di produzione. Attribuisce queste informazioni al codice sorgente dell'applicazione, aiutandoti a identificare le parti dell'applicazione che consumano più risorse e a comprendere meglio le caratteristiche di rendimento del codice. Il basso overhead delle tecniche di raccolta utilizzate dallo strumento lo rende adatto all'uso continuo negli ambienti di produzione.

In questo codelab, imparerai a configurare Cloud Profiler per un programma Go e a familiarizzare con il tipo di informazioni sulle prestazioni dell'applicazione che lo strumento può presentare.

Cosa imparerai a fare

  • Come configurare un programma Go per la profilazione con Cloud Profiler.
  • Come raccogliere, visualizzare e analizzare i dati sul rendimento con Cloud Profiler.

Che cosa ti serve

  • Un progetto Google Cloud
  • Un browser, ad esempio Chrome o Firefox
  • Familiarità con gli editor di testo standard di Linux, ad esempio Vim, EMAC o Nano

Come utilizzerai questo tutorial?

Leggilo e basta Leggilo e completa gli esercizi

Come valuteresti la tua esperienza con Google Cloud Platform?

Principiante Intermedio Avanzato

2. Configurazione e requisiti

Configurazione dell'ambiente autonomo

  1. Accedi alla console Cloud e crea un nuovo progetto o riutilizzane uno esistente. Se non hai ancora un account Gmail o Google Workspace, devi crearne uno.

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

Ricorda l'ID progetto, un nome univoco tra tutti i progetti Google Cloud (il nome sopra è già stato utilizzato e non funzionerà per te, mi dispiace). In questo codelab verrà chiamato PROJECT_ID.

  1. Successivamente, dovrai abilitare la fatturazione in Cloud Console per utilizzare le risorse Google Cloud.

L'esecuzione di questo codelab non dovrebbe costare molto, se non nulla. Assicurati di seguire le istruzioni riportate nella sezione "Pulizia", che ti consiglia come arrestare le risorse in modo da non incorrere in addebiti oltre questo tutorial. I nuovi utenti di Google Cloud possono beneficiare del programma prova senza costi di 300$.

Google Cloud Shell

Anche se Google Cloud può essere gestito da remoto dal tuo laptop, per semplificare la configurazione in questo codelab utilizzeremo Google Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.

Attiva Cloud Shell

  1. Nella console Cloud, fai clic su Attiva Cloud Shell 4292cbf4971c9786.png.

bce75f34b2c53987.png

Se non hai mai avviato Cloud Shell, viene visualizzata una schermata intermedia (sotto la piega) che ne descrive le funzionalità. In questo caso, fai clic su Continua e non comparirà più. Ecco come si presenta la schermata intermedia:

70f315d7b402b476.png

Bastano pochi istanti per eseguire il provisioning e connettersi a Cloud Shell.

fbe3a0674c982259.png

Questa macchina virtuale è caricata con tutti gli strumenti per sviluppatori di cui hai bisogno. Offre una home directory permanente da 5 GB e viene eseguita in Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. Gran parte del lavoro per questo codelab, se non tutto, può essere svolto semplicemente con un browser o con Chromebook.

Una volta eseguita la connessione a Cloud Shell, dovresti vedere che il tuo account è già autenticato e il progetto è già impostato sul tuo ID progetto.

  1. Esegui questo comando in Cloud Shell per verificare che l'account sia autenticato:
gcloud auth list

Output comando

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Esegui questo comando in Cloud Shell per verificare che il comando gcloud conosca il tuo progetto:
gcloud config list project

Output comando

[core]
project = <PROJECT_ID>

In caso contrario, puoi impostarlo con questo comando:

gcloud config set project <PROJECT_ID>

Output comando

Updated property [core/project].

3. Vai a Cloud Profiler

Nella console Cloud, vai all'interfaccia utente di Profiler facendo clic su "Profiler" nella barra di navigazione a sinistra:

37ad0df7ddb2ad17.png

In alternativa, puoi utilizzare la barra di ricerca della console Cloud per accedere all'interfaccia utente di Profiler: digita "Cloud Profiler" e seleziona l'elemento trovato. In entrambi i casi, dovresti visualizzare l'interfaccia utente di Profiler con il messaggio "Nessun dato da visualizzare", come mostrato di seguito. Il progetto è nuovo, quindi non sono ancora stati raccolti dati di profilazione.

d275a5f61ed31fb2.png

Ora è il momento di profilare qualcosa.

4. Profilare il benchmark

Utilizzeremo una semplice applicazione Go sintetica disponibile su GitHub. Nel terminale Cloud Shell ancora aperto (e mentre nell'interfaccia utente di Profiler è ancora visualizzato il messaggio "Nessun dato da visualizzare"), esegui questo comando:

$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...

Quindi passa alla directory dell'applicazione:

$ cd ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/profiler/hotapp

La directory contiene il file "main.go", che è un'app sintetica con l'agente di profilazione abilitato:

main.go

...
import (
        ...
        "cloud.google.com/go/profiler"
)
...
func main() {
        err := profiler.Start(profiler.Config{
                Service:        "hotapp-service",
                DebugLogging:   true,
                MutexProfiling: true,
        })
        if err != nil {
                log.Fatalf("failed to start the profiler: %v", err)
        }
        ...
}

Per impostazione predefinita, l'agente di profilazione raccoglie i profili di CPU, heap e thread. Il codice qui consente la raccolta di profili mutex (noti anche come "contesa").

Ora esegui il programma:

$ go run main.go

Durante l'esecuzione del programma, l'agente di profilazione raccoglierà periodicamente i profili dei cinque tipi configurati. La raccolta viene eseguita in modo casuale nel tempo (con una velocità media di un profilo al minuto per ciascun tipo), quindi potrebbero essere necessari fino a tre minuti per raccogliere ciascun tipo. Il programma ti comunica quando crea un profilo. I messaggi sono abilitati dal flag DebugLogging nella configurazione precedente; in caso contrario, l'agente viene eseguito in modalità silenziosa:

$ go run main.go
2018/03/28 15:10:24 profiler has started
2018/03/28 15:10:57 successfully created profile THREADS
2018/03/28 15:10:57 start uploading profile
2018/03/28 15:11:19 successfully created profile CONTENTION
2018/03/28 15:11:30 start uploading profile
2018/03/28 15:11:40 successfully created profile CPU
2018/03/28 15:11:51 start uploading profile
2018/03/28 15:11:53 successfully created profile CONTENTION
2018/03/28 15:12:03 start uploading profile
2018/03/28 15:12:04 successfully created profile HEAP
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:04 successfully created profile THREADS
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:25 successfully created profile HEAP
2018/03/28 15:12:25 start uploading profile
2018/03/28 15:12:37 successfully created profile CPU
...

L'UI si aggiornerà poco dopo la raccolta del primo profilo. Dopo questo aggiornamento, non verrà eseguito automaticamente, quindi per visualizzare i nuovi dati dovrai aggiornare manualmente l'interfaccia utente di Profiler. Per farlo, fai clic due volte sul pulsante Ora nel selettore dell'intervallo di tempo:

650051097b651b91.png

Dopo l'aggiornamento della UI, vedrai qualcosa di simile a questo:

47a763d4dc78b6e8.png

Il selettore del tipo di profilo mostra i cinque tipi di profilo disponibili:

b5d7b4b5051687c9.png

Ora esaminiamo ciascun tipo di profilo e alcune importanti funzionalità della UI, quindi conduciamo alcuni esperimenti. A questo punto, non hai più bisogno del terminale Cloud Shell, quindi puoi uscire premendo Ctrl + C e digitando "exit".

5. Analizzare i dati del profiler

Ora che abbiamo raccolto alcuni dati, esaminiamoli più da vicino. Utilizziamo un'app sintetica (il cui codice sorgente è disponibile su GitHub) che simula comportamenti tipici di diversi tipi di problemi di prestazioni in produzione.

Codice che richiede un uso intensivo della CPU

Seleziona il tipo di profilo CPU. Dopo il caricamento dell'interfaccia utente, nel grafico a fiamma vedrai i quattro blocchi foglia per la funzione load, che rappresentano collettivamente tutto il consumo di CPU:

fae661c9fe6c58df.png

Questa funzione è scritta appositamente per consumare molti cicli della CPU eseguendo un ciclo stretto:

main.go

func load() {
        for i := 0; i < (1 << 20); i++ {
        }
}

La funzione viene chiamata indirettamente da busyloop() tramite quattro percorsi di chiamata: busyloop → {foo1, foo2} → {bar, baz} → load. La larghezza di una casella di funzione rappresenta il costo relativo del percorso di chiamata specifico. In questo caso, tutti e quattro i percorsi hanno lo stesso costo. In un programma reale, devi concentrarti sull'ottimizzazione dei percorsi di chiamata più importanti in termini di rendimento. Il grafico a fiamma, che mette in evidenza visivamente i percorsi più costosi con caselle più grandi, rende questi percorsi facili da identificare.

Puoi utilizzare il filtro dei dati del profilo per perfezionare ulteriormente la visualizzazione. Ad esempio, prova ad aggiungere un filtro "Mostra stack" specificando "baz" come stringa di filtro. Dovresti visualizzare una schermata simile a quella riportata di seguito, in cui vengono visualizzati solo due dei quattro percorsi di chiamata a load(). Questi due percorsi sono gli unici che passano attraverso una funzione con la stringa "baz" nel nome. Questo filtro è utile quando vuoi concentrarti su una parte di un programma più grande (ad esempio, perché ne possiedi solo una parte).

eb1d97491782b03f.png

Codice che utilizza molta memoria

Ora passa al tipo di profilo "Heap". Assicurati di rimuovere tutti i filtri creati negli esperimenti precedenti. Ora dovresti vedere un grafico a fiamma in cui allocImpl, chiamato da alloc, viene visualizzato come principale consumatore di memoria nell'app:

f6311c8c841d04c4.png

La tabella di riepilogo sopra il grafico a fiamma indica che la quantità totale di memoria utilizzata nell'app è in media di circa 57,4 MiB, la maggior parte dei quali allocati dalla funzione allocImpl. Ciò non sorprende, data l'implementazione di questa funzione:

main.go

func allocImpl() {
        // Allocate 64 MiB in 64 KiB chunks
        for i := 0; i < 64*16; i++ {
                mem = append(mem, make([]byte, 64*1024))
        }
}

La funzione viene eseguita una volta, allocando 64 MiB in blocchi più piccoli, quindi memorizzando i puntatori a questi blocchi in una variabile globale per proteggerli dalla garbage collection. Tieni presente che la quantità di memoria mostrata come utilizzata da Profiler è leggermente diversa da 64 MiB: Go heap profiler è uno strumento statistico, quindi le misurazioni sono a basso overhead, ma non accurate al byte. Non sorprenderti se noti una differenza di circa il 10% come questa.

Codice con uso intensivo di I/O

Se scegli "Thread" nel selettore del tipo di profilo, la visualizzazione passerà a un grafico a fiamma in cui la maggior parte della larghezza è occupata dalle funzioni wait e waitImpl:

ebd57fdff01dede9.png

Nel riepilogo sopra il grafico a fiamma, puoi vedere che ci sono 100 goroutine che aumentano lo stack di chiamate dalla funzione wait. È esattamente così, dato che il codice che avvia queste attese ha questo aspetto:

main.go

func main() {
        ...
        // Simulate some waiting goroutines.
        for i := 0; i < 100; i++ {
                go wait()
        }

Questo tipo di profilo è utile per capire se il programma trascorre un tempo imprevisto in attesa (ad esempio I/O). Questi stack di chiamate in genere non vengono campionati dal profiler CPU, in quanto non consumano una parte significativa del tempo della CPU. Spesso è consigliabile utilizzare i filtri "Nascondi stack " con i profili dei thread, ad esempio per nascondere tutti gli stack che terminano con una chiamata a gopark,, poiché si tratta spesso di goroutine inattive e meno interessanti di quelle che attendono l'I/O.

Il tipo di profilo dei thread può anche aiutare a identificare i punti del programma in cui i thread attendono a lungo un mutex di proprietà di un'altra parte del programma, ma il tipo di profilo seguente è più utile a questo scopo.

Codice con elevata contesa

Il tipo di profilo Contention identifica le serrature più "richieste" del programma. Questo tipo di profilo è disponibile per i programmi Go, ma deve essere attivato esplicitamente specificando "MutexProfiling: true" nel codice di configurazione dell'agente. La raccolta funziona registrando (nella metrica "Contese") il numero di volte in cui un blocco specifico, quando viene sbloccato da una goroutine A, ha un'altra goroutine B in attesa che il blocco venga sbloccato. Registra anche (nella metrica "Ritardo") il tempo di attesa della goroutine bloccata per il blocco. In questo esempio, è presente un singolo stack di contesa e il tempo di attesa totale per il blocco è stato di 10,5 secondi:

83f00dca4a0f768e.png

Il codice che genera questo profilo è costituito da quattro goroutine che si contendono un mutex:

main.go

func contention(d time.Duration) {
        contentionImpl(d)
}

func contentionImpl(d time.Duration) {
        for {
                mu.Lock()
                time.Sleep(d)
                mu.Unlock()
        }
}
...
func main() {
        ...
        for i := 0; i < 4; i++ {
                go contention(time.Duration(i) * 50 * time.Millisecond)
        }
}

6. Riepilogo

In questo lab hai imparato come configurare un programma Go per l'utilizzo con Cloud Profiler. Hai anche imparato a raccogliere, visualizzare e analizzare i dati sul rendimento con questo strumento. Ora puoi applicare la tua nuova competenza ai servizi reali che esegui su Google Cloud.

7. Complimenti!

Hai imparato a configurare e utilizzare Cloud Profiler.

Scopri di più

Licenza

Questo lavoro è concesso in licenza ai sensi di una licenza Creative Commons Attribution 2.0 Generic.