Ottimizzare un'app Go
In questo tutorial esegui il deployment di un'applicazione Go intenzionalmente inefficiente configurata per raccogliere i dati del profilo. Utilizza l'interfaccia di Profiler per visualizzare i dati del profilo e identificare potenziali ottimizzazioni. Poi modifichi l'applicazione, la esegui il deployment e valuti l'effetto della modifica.
Prima di iniziare
- Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
-
In the Google Cloud console, on the project selector page, select or create a Google Cloud project.
-
Enable the required API.
- Per aprire Cloud Shell, fai clic su Attiva Cloud Shell nella barra degli strumenti della console Google Cloud:
Dopo qualche istante, si apre una sessione di Cloud Shell all'interno della console Google Cloud:
Applicazione di esempio
L'obiettivo principale è massimizzare il numero di query al secondo che il server può elaborare. Un obiettivo secondario è ridurre l'utilizzo della memoria eliminando le allocazioni di memoria non necessarie.
Il server, utilizzando un framework gRPC, riceve una parola o una frase e poi restituisce il numero di volte che la parola o la frase compare nelle opere di Shakespeare.
Il numero medio di query al secondo che il server può gestire è determinato dal test di carico del server. Per ogni ciclo di test, viene chiamato un simulatore di client e viene richiesto di emettere 20 query sequenziali. Al termine di un round, vengono visualizzati il numero di query inviate dal simulatore del client, il tempo trascorso e il numero medio di query al secondo.
Il codice del server è intenzionalmente inefficiente.
Esecuzione dell'applicazione di esempio
Scarica ed esegui l'applicazione di esempio:
In Cloud Shell, esegui i seguenti comandi:
git clone https://github.com/GoogleCloudPlatform/golang-samples.git cd golang-samples/profiler/shakesapp
Esegui l'applicazione con la versione impostata su
1
e il numero di giri impostato su 15:go run . -version 1 -num_rounds 15
Dopo un paio di minuti, vengono visualizzati i dati del profilo. I dati del profilo sono simili all'esempio seguente:
Nello screenshot, noterai che il Tipo di profilo è impostato su
CPU time
. Ciò indica che i dati sull'utilizzo della CPU vengono visualizzati nel grafico a forma di fiamma.Di seguito è riportato l'output di esempio stampato in Cloud Shell:
$ go run . -version 1 -num_rounds 15 2020/08/27 17:27:34 Simulating client requests, round 1 2020/08/27 17:27:34 Stackdriver Profiler Go Agent version: 20200618 2020/08/27 17:27:34 profiler has started 2020/08/27 17:27:34 creating a new profile via profiler service 2020/08/27 17:27:51 Simulated 20 requests in 17.3s, rate of 1.156069 reqs / sec 2020/08/27 17:27:51 Simulating client requests, round 2 2020/08/27 17:28:10 Simulated 20 requests in 19.02s, rate of 1.051525 reqs / sec 2020/08/27 17:28:10 Simulating client requests, round 3 2020/08/27 17:28:29 Simulated 20 requests in 18.71s, rate of 1.068947 reqs / sec ... 2020/08/27 17:44:32 Simulating client requests, round 14 2020/08/27 17:46:04 Simulated 20 requests in 1m32.23s, rate of 0.216849 reqs / sec 2020/08/27 17:46:04 Simulating client requests, round 15 2020/08/27 17:47:52 Simulated 20 requests in 1m48.03s, rate of 0.185134 reqs / sec
L'output di Cloud Shell mostra il tempo trascorso per ogni iterazione e la frequenza media delle richieste. Quando l'applicazione viene avviata, la voce "20 richieste simulate in 17,3 secondi, frequenza di 1,156069 richieste / sec" indica che il server sta eseguendo circa 1 richiesta al secondo. Nell'ultimo round, la voce "20 richieste simulate in 1 minuto e 48,03 secondi, frequenza di 0,185134 richieste / sec" indica che il server sta eseguendo circa una richiesta ogni 5 secondi.
Utilizzo dei profili di tempo della CPU per massimizzare le query al secondo
Un approccio per massimizzare il numero di query al secondo è identificare i metodi che richiedono un uso intensivo della CPU e ottimizzarne le implementazioni. In questa sezione, utilizzi i profili di tempo della CPU per identificare un metodo che richiede molta CPU nel server.
Identificazione dell'utilizzo del tempo della CPU
Il frame principale del grafico a forma di fiamma elenca il tempo totale di CPU utilizzato dall'applicazione nell'intervallo di raccolta di 10 secondi:
In questo esempio, il servizio ha utilizzato 2.37 s
. Quando il sistema viene eseguito su un singolo nucleo, un utilizzo del tempo della CPU di 2,37 secondi corrisponde al 23,7% di utilizzo di quel nucleo. Per ulteriori informazioni, consulta
Tipi di profilazione disponibili.
Modificare l'applicazione
Passaggio 1: quale funzione richiede molto tempo della CPU?
Un modo per identificare il codice che potrebbe dover essere ottimizzato è visualizzare la tabella delle funzioni e identificare le funzioni avide:
- Per visualizzare la tabella, fai clic su list Elenco funzioni di messa a fuoco.
- Ordina la tabella in base al valore Totale. La colonna etichettata
Totale mostra l'utilizzo del tempo di CPU di una funzione e delle relative funzioni secondarie.
In questo esempio,
GetMatchCount
è la prima funzioneshakesapp/server.go
elencata. Questa funzione ha utilizzato 1,7 secondi del tempo di CPU totale, ovvero il 72% del tempo di CPU totale delle applicazioni. È noto che questa funzione gestisce le richieste gRPC.Il grafico a fiamme mostra che la funzione
shakesapp/server.go
GetMatchCount
chiamaMatchString
, che a sua volta trascorre la maggior parte del suo tempo chiamandoCompile
:
Passaggio 2: come puoi utilizzare ciò che hai appreso?
- Affidati alle tue competenze linguistiche.
MatchString
è un metodo di espressione regolare. Sai che l'elaborazione delle espressioni regolari è molto flessibile, ma non necessariamente la soluzione più efficace per ogni problema. - Affidati alle tue competenze in materia di applicazioni. Il client genera una parola o una frase e il server la cerca.
- Cerca nell'implementazione del
metodo
shakesapp/server.go
GetMatchCount
gli utilizzi diMatchString
e poi determina se una funzione più semplice ed efficiente potrebbe sostituire quella chiamata.
Passaggio 3: come puoi modificare la richiesta?
Nel file shakesapp/server.go
, il codice esistente contiene una chiamata a MatchString
:
isMatch, err := regexp.MatchString(query, line) if err != nil { return resp, err } if isMatch { resp.MatchCount++ }
Un'opzione è sostituire la logica MatchString
con una logica equivalente che utilizza strings.Contains
:
if strings.Contains(line, query) { resp.MatchCount++ }
Assicurati di rimuovere l'istruzione di importazione per il pacchetto regexp
.
Valutazione della modifica
Per valutare la modifica:
Esegui l'applicazione con la versione dell'applicazione impostata su
2
:go run . -version 2 -num_rounds 40
Una sezione successiva mostra che, con l'ottimizzazione, il tempo necessario per eseguire un singolo round è molto inferiore a quello dell'applicazione non modificata. Per garantire che l'applicazione venga eseguita per un periodo di tempo sufficiente a raccogliere e caricare i profili, il numero di round viene aumentato.
Attendi il completamento dell'applicazione, quindi visualizza i dati del profilo per questa versione dell'applicazione:
- Fai clic su ORA per caricare i dati del profilo più recenti. Per ulteriori informazioni, consulta Intervallo di tempo.
- Nel menu Versione, seleziona 2.
Ad esempio, il grafico a forma di fiamma è il seguente:
In questa figura, il frame principale mostra un valore 7.8 s
. A seguito della
modifica della funzione di corrispondenza delle stringhe, il tempo della CPU utilizzato dall'applicazione
è aumentato da 2,37 secondi a 7,8 secondi oppure l'applicazione è passata dall'utilizzo del
23,7% di un core della CPU all'utilizzo del 78% di un core della CPU.
La larghezza del frame è una misura proporzionale dell'utilizzo del tempo della CPU. In questo
esempio, la larghezza del frame per GetMatchCount
indica che la funzione
utilizza circa il 49% di tutto il tempo di CPU utilizzato dall'applicazione.
Nel grafico a forma di fiamma originale,
lo stesso frame corrispondeva a circa il 72% della larghezza del grafico.
Per visualizzare l'utilizzo esatto del tempo della CPU, puoi utilizzare la descrizione comando del frame o l'elenco delle funzioni per lo stato attivo:
L'output in Cloud Shell mostra che la versione modificata completa circa 5,8 richieste al secondo:
$ go run . -version 2 -num_rounds 40 2020/08/27 18:21:40 Simulating client requests, round 1 2020/08/27 18:21:40 Stackdriver Profiler Go Agent version: 20200618 2020/08/27 18:21:40 profiler has started 2020/08/27 18:21:40 creating a new profile via profiler service 2020/08/27 18:21:44 Simulated 20 requests in 3.67s, rate of 5.449591 reqs / sec 2020/08/27 18:21:44 Simulating client requests, round 2 2020/08/27 18:21:47 Simulated 20 requests in 3.72s, rate of 5.376344 reqs / sec 2020/08/27 18:21:47 Simulating client requests, round 3 2020/08/27 18:21:51 Simulated 20 requests in 3.58s, rate of 5.586592 reqs / sec ... 2020/08/27 18:23:51 Simulating client requests, round 39 2020/08/27 18:23:54 Simulated 20 requests in 3.46s, rate of 5.780347 reqs / sec 2020/08/27 18:23:54 Simulating client requests, round 40 2020/08/27 18:23:58 Simulated 20 requests in 3.4s, rate of 5.882353 reqs / sec
La piccola modifica all'applicazione ha avuto due effetti diversi:
Il numero di richieste al secondo è aumentato da meno di 1 al secondo a 5,8 al secondo.
Il tempo della CPU per richiesta, calcolato dividendo l'utilizzo della CPU per il numero di richieste al secondo, è sceso al 13,4% dal 23,7%.
Tieni presente che il tempo della CPU per richiesta è diminuito anche se l'utilizzo del tempo della CPU è aumentato da 2,37 secondi, che corrispondono al 23,7% di utilizzo di un singolo core della CPU, a 7,8 secondi, ovvero il 78% di un core della CPU.
Utilizzo dei profili dell'heap allocato per migliorare l'utilizzo delle risorse
Questa sezione illustra come utilizzare i profili dell'heap e dell'heap allocato per identificare un metodo che richiede un'allocazione intensiva nell'applicazione:
I profili dell'heap mostrano la quantità di memoria allocata nell'heap del programma nell'istante in cui viene raccolto il profilo.
I profili dell'heap allocato mostrano la quantità totale di memoria allocata nell'heap del programma durante l'intervallo in cui è stato raccolto il profilo. Dividendo questi valori per 10 secondi, ovvero l'intervallo di raccolta dei profili, puoi interpretarli come tassi di allocazione.
Attivazione della raccolta del profilo heap
Esegui l'applicazione con la versione dell'applicazione impostata su
3
e attiva la raccolta dei profili dell'heap e dell'heap allocato.go run . -version 3 -num_rounds 40 -heap -heap_alloc
Attendi il completamento dell'applicazione, quindi visualizza i dati del profilo per questa versione dell'applicazione:
- Fai clic su ORA per caricare i dati del profilo più recenti.
- Nel menu Versione, seleziona 3.
- Nel menu Tipo di profiler, seleziona Heap allocato.
Ad esempio, il grafico a forma di fiamma è il seguente:
Identificazione del tasso di allocazione heap
Il frame principale mostra la quantità totale di heap allocata durante i 10 secondi in cui è stato raccolto un profilo, calcolata in media su tutti i profili. In questo esempio, il frame principale mostra che, in media, sono stati allocati 1,535 GiB di memoria.
Modificare l'applicazione
Passaggio 1: vale la pena ridurre al minimo la frequenza di allocazione dell'heap?
L'utilizzo del tempo di CPU della funzione di garbage collection del background Go,
runtime.gcBgMarkWorker.*
, può essere utilizzato per determinare se vale la pena ottimizzare l'applicazione per ridurre i costi di garbage collection:
- Salta l'ottimizzazione se l'utilizzo del tempo della CPU è inferiore al 5%.
- Esegui l'ottimizzazione se il tempo di utilizzo della CPU è almeno del 25%.
Per questo esempio, l'utilizzo del tempo della CPU da parte del garbage collector in background è pari al 16,8%. Questo valore è abbastanza elevato da consigliare di provare a ottimizzare shakesapp/server.go
:

Passaggio 2: quale funzione alloca molta memoria?
Il file shakesapp/server.go
contiene due funzioni che potrebbero essere scelti come target per l'ottimizzazione: GetMatchCount
e readFiles
. Per determinare
la percentuale di allocazione della memoria per queste funzioni, imposta
Tipo di profilo su Heap allocato e poi utilizza
list
Elenco funzioni in primo piano.
In questo esempio, l'allocazione heap totale per readFiles.func1
durante la raccolta del profilo di 10 secondi è, in media, di 1,045 GiB o il 68% della
memoria allocata totale. L'allocazione heap autonomo durante la raccolta del profilo di 10 secondi è pari a 255,4 MiB:

In questo esempio, il metodo Go makeSlice
ha allocato in media 798, 7 MiB durante la raccolta del profilo di 10 secondi.
Il modo più semplice per ridurre queste allocazioni
è ridurre le chiamate a makeSlice
. La funzione readFiles
chiama makeSlice
tramite un metodo della libreria.
Il risultato di questa analisi suggerisce che potrebbe essere possibile ridurre
il tasso di allocazioni dell'heap ottimizzando readFiles
.
Passaggio 3: come puoi modificare la richiesta?
Un'opzione è modificare l'applicazione in modo che legga i file una volta e poi riutilizzi i contenuti. Ad esempio, puoi apportare le seguenti modifiche:
- Definisci una variabile globale
files
per memorizzare i risultati della lettura iniziale del file:var files []string
- Modifica
readFiles
in modo che ritorni in anticipo quando è definitofiles
:func readFiles(ctx context.Context, bucketName, prefix string) ([]string, error) { // return if defined if files != nil { return files, nil } // Existing type resp struct { s string err error } ... // Save the result in the variable files files = make([]string, len(paths)) for i := 0; i < len(paths); i++ { r := <-resps if r.err != nil { return nil, r.err } files[i] = r.s } return files, nil }
Valutazione della modifica
Per valutare la modifica:
Esegui l'applicazione con la versione dell'applicazione impostata su
4
:go run . -version 4 -num_rounds 60 -heap -heap_alloc
Attendi il completamento dell'applicazione, quindi visualizza i dati del profilo per questa versione dell'applicazione:
- Fai clic su ORA per caricare i dati del profilo più recenti.
- Nel menu Versione, seleziona 4.
- Nel menu Tipo di profiler, seleziona Heap allocato.
Per quantificare l'effetto della modifica di
readFiles
sul tasso di allocazione dell'heap, confronta i profili dell'heap allocati per la versione 4 con quelli raccolti per la 3:La descrizione comando del frame principale mostra che con la versione 4 la quantità media di memoria allocata durante la raccolta dei profili è diminuita di 1.301 GiB rispetto alla versione 3. La descrizione comando di
readFiles.func1
mostra una diminuzione di 1045 GiB:Per quantificare l'effetto sulla garbage collection, configura un confronto dei profili di tempo della CPU. Nello screenshot seguente viene applicato un filtro per mostrare le le pile per il garbage collector di Go
runtime.gcBgMarkWorker.*
. Lo screenshot mostra che l'utilizzo della CPU per la garbage collection è diminuito dal 16,8% al 4,97%.Per determinare se la modifica influisce sul numero di richieste al secondo gestite dall'applicazione, visualizza l'output in Cloud Shell. In questo esempio, la versione 4 completa fino a 15 richieste al secondo, un numero notevolmente superiore alle 5,8 richieste al secondo della versione 3:
$ go run . -version 4 -num_rounds 60 -heap -heap_alloc 2020/08/27 21:51:42 Simulating client requests, round 1 2020/08/27 21:51:42 Stackdriver Profiler Go Agent version: 20200618 2020/08/27 21:51:42 profiler has started 2020/08/27 21:51:42 creating a new profile via profiler service 2020/08/27 21:51:44 Simulated 20 requests in 1.47s, rate of 13.605442 reqs / sec 2020/08/27 21:51:44 Simulating client requests, round 2 2020/08/27 21:51:45 Simulated 20 requests in 1.3s, rate of 15.384615 reqs / sec 2020/08/27 21:51:45 Simulating client requests, round 3 2020/08/27 21:51:46 Simulated 20 requests in 1.31s, rate of 15.267176 reqs / sec ...
L'aumento delle query al secondo servite dall'applicazione potrebbe essere dovuto a un minore tempo impiegato per la raccolta dei rifiuti.
Puoi ottenere una comprensione più completa dell'effetto della modifica a
readFiles
visualizzando i profili dell'heap. Un confronto dei profili dell'heap per la versione 4 e per la versione 3 mostra che l'utilizzo dell'heap è diminuito da 70,95 MiB a 18,47 MiB:
Riepilogo
In questa guida rapida sono stati utilizzati i profili di tempo della CPU e dell'heap allocato per identificare potenziali ottimizzazioni di un'applicazione. Gli obiettivi erano massimizzare il numero di richieste al secondo ed eliminare le allocazioni non necessarie.
Utilizzando i profili di tempo della CPU, è stata identificata una funzione che utilizza molta CPU. Dopo aver applicato una semplice modifica, il tasso di richieste del server è aumentato a 5,8 al secondo, rispetto a circa 1 al secondo.
Utilizzando i profili dell'heap allocato, è stato rilevato che la
shakesapp/server.go
funzionereadFiles
ha un tasso di allocazione elevato. Dopo aver ottimizzatoreadFiles
, la tasso di richieste del server è aumentata a 15 richieste al secondo e la quantità media di memoria allocata durante la raccolta del profilo di 10 secondi è diminuita di 1,301 GiB.
Passaggi successivi
Per informazioni su come vengono raccolti e inviati al tuo Google Cloud progetto, consulta Raccolta dei profili.
Leggi le nostre risorse su DevOps ed esplora il nostro programma di ricerca.
- Profilazione delle applicazioni Go
- Profilazione delle applicazioni Java
- Profilazione delle applicazioni Node.js
- Profilazione delle applicazioni Python
- Profilazione delle applicazioni in esecuzione al di fuori di Google Cloud