L'analisi delle prestazioni e la configurazione sono strumenti potenti per verificare la conformità delle prestazioni per i clienti.
L'analisi delle prestazioni può essere utilizzata per identificare i colli di bottiglia nel programma, applicando un approccio scientifico nella verifica degli esperimenti di configurazione. Questo articolo definisce un approccio generale all'analisi delle prestazioni e alla configurazione utilizzando come esempio un server web in Go.
Go è particolarmente adatto in questo caso, poiché offre strumenti di profilazione pprof nella libreria standard.

Strategia
Creiamo un elenco riassuntivo per la nostra analisi strutturale. Cercheremo di utilizzare dei dati per prendere decisioni anziché apportare modifiche basate sull'intuizione o sulle congetture. A tal fine, procediamo così:
- Definiamo i confini dell'ottimizzazione (requisiti);
- Calcoliamo il carico di transazione per il sistema;
- Eseguiamo il test (creiamo dati);
- Osserviamo;
- Analizziamo: tutti i requisiti sono rispettati?
- Configuriamo scientificamente, formuliamo un'ipotesi;
- Eseguiamo un esperimento per verificare questa ipotesi.

Architettura di un semplice server HTTP
Per questo articolo utilizzeremo un piccolo server HTTP in Golang. Tutto il codice di questo articolo è disponibile .
L'applicazione analizzata è un server HTTP che interroga Postgresql per ogni richiesta. Inoltre, sono presenti Prometheus, node_exporter e Grafana per la raccolta e la visualizzazione delle metriche dell'applicazione e del sistema.

Per semplificare, assumiamo che per la scalabilità orizzontale (e per semplificare i calcoli) ogni servizio e database vengano distribuiti insieme:

Definiamo gli obiettivi
In questo passaggio definiamo l'obiettivo. Cosa stiamo cercando di analizzare? Come capiremo quando è il momento di fermarsi? In questo articolo presumeremo di avere clienti e che il nostro servizio elaborerà 10.000 richieste al secondo.
In illustra in dettaglio i metodi di selezione e modellazione. Procederemo allo stesso modo, costruendo modelli:
- Ritardo: il 99% delle richieste deve essere eseguito in meno di 60 ms;
- Costo: il servizio deve consumare la minima somma di denaro che ci sembri ragionevolmente possibile. Per questo, massimizziamo la capacità di throughput;
- Pianificazione delle capacità: è necessaria una comprensione e documentazione di quante istanze dell'applicazione saranno necessarie per avviarsi, inclusa la funzionalità generale di scaling, così come quante istanze saranno necessarie per soddisfare i requisiti iniziali di carico e garantire .
La latenza potrebbe richiedere ottimizzazione oltre all'analisi, ma la larghezza di banda deve essere chiaramente analizzata. Nell'utilizzo del processo SRE, il requisito di latenza proviene dal cliente e/o dal business, rappresentato dal product owner. E il nostro servizio rispetterà questo impegno fin dall'inizio senza alcuna configurazione!
Configurare l'ambiente di test
Con l'aiuto dell'ambiente di test saremo in grado di fornire un carico dosato al nostro sistema. Verranno creati dati sulle prestazioni del web service per l'analisi.
Carico transazionale
Questo ambiente utilizza per generare una frequenza di richieste HTTP configurabile, finché non viene fermato:
$ make load-test LOAD_TEST_RATE=50
echo "POST http://localhost:8080" | vegeta attack -body tests/fixtures/age_no_match.json -rate=50 -duration=0 | tee results.bin | vegeta reportMonitoraggio
Durante l'esecuzione verrà applicato un carico transazionale. Oltre alle metriche dell'applicazione (numero di richieste, latenza della risposta) e del sistema operativo (memoria, CPU, IOPS), verrà avviato un profilo dell'applicazione per capire dove ci sono problemi e come viene consumato il tempo della CPU.
Profilazione
La profilazione è un tipo di misura che permette di vedere dove va a finire il tempo della CPU mentre l'applicazione è in esecuzione. Consente di determinare esattamente dove e quanto tempo della CPU viene speso:

Questi dati possono essere utilizzati durante l'analisi per ottenere un'idea del tempo della CPU sprecato inutilmente e del lavoro inutile eseguito. Go (pprof) può generare profili e visualizzarli sotto forma di flame graph, utilizzando un set standard di strumenti. Parlerò del loro utilizzo e delle istruzioni per la configurazione un po' più avanti nell'articolo.
Esecuzione, monitoraggio, analisi.
Eseguiamo un esperimento. Ci occuperemo di eseguire, osservare e analizzare finché le prestazioni non ci soddisfano. Sceglieremo un valore di carico arbitrariamente basso per applicarlo al fine di ottenere risultati delle prime osservazioni. A ciascun passo successivo, aumenteremo il carico con un coefficiente di scaling scelto con una certa variabilità. Ogni esecuzione del test di carico viene eseguita con la regolazione del numero di richieste: make load-test LOAD_TEST_RATE=X.
50 richieste al secondo

Nota i due grafici in alto. Il grafico in alto a sinistra mostra che la nostra applicazione gestisce 50 richieste al secondo (secondo la sua opinione), mentre il grafico in alto a destra mostra la durata di ogni richiesta. Entrambi i parametri ci aiutano a osservare e analizzare: ci stiamo mantenendo nei nostri limiti di prestazioni o no. La linea rossa nel grafico Latenza della richiesta HTTP mostra un SLO di 60 ms. La linea indica che siamo ben al di sotto del nostro tempo massimo di risposta.
Guardiamo dal punto di vista dei costi:
10000 richieste al secondo / su 50 richieste per server = 200 server + 1
Possiamo ancora migliorare questo indicatore.
500 richieste al secondo
Iniziano a verificarsi eventi più interessanti quando il carico arriva a 500 richieste al secondo:

Ancora una volta, nel grafico in alto a sinistra si vede che l'applicazione rileva un carico normale. Se non è così, c'è un problema con il server su cui è in esecuzione l'applicazione. Il grafico della latenza di risposta, situato in alto a destra, mostra che 500 richieste al secondo hanno portato a una latenza di risposta di 25-40 ms. Il 99° percentile si mantiene ancora magnificamente all'interno dello SLO di 60 ms scelto sopra.
Dal punto di vista dei costi:
10000 richieste al secondo / su 500 richieste per server = 20 server + 1
C'è ancora margine di miglioramento.
1000 richieste al secondo

Ottimo avvio! L'applicazione mostra di aver elaborato 1000 richieste al secondo, tuttavia il limite di latenza è stato superato dal lato dello SLO. Questo è visibile dalla linea p99 nel grafico in alto a destra. Anche se la linea p100 è molto più alta, le latenze reali superano il massimo di 60 ms. Immergiamoci nel profiling per scoprire cosa fa effettivamente l'applicazione.
Profilazione
Per il profiling impostiamo il carico a 1000 richieste al secondo, poi utilizziamo pprof per raccogliere dati per scoprire dove l'applicazione spende tempo CPU. Questo può essere fatto attivando l'endpoint HTTP pprof, dopo di che, con il carico, salva i risultati utilizzando curl:
$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.profI risultati possono essere visualizzati in questo modo:
$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof
Il grafico mostra dove e quanto il programma consuma tempo di CPU. Dalla descrizione di :
Sull'asse X c'è il riempimento del profilo dello stack, ordinato in ordine alfabetico (non è tempo), l'asse Y mostra la profondità dello stack, contando da zero in [top]. Ogni rettangolo è un frame dello stack. Più largo è il frame, più spesso appare negli stack. Ciò che è in alto è in esecuzione sulla CPU, mentre ciò che è sotto sono gli elementi figlio. I colori, in genere, non significano nulla, ma sono scelti a caso per distinguere i frame.
Analisi - ipotesi
Per la configurazione, ci concentreremo sul tentativo di trovare sprechi inutili di tempo di CPU. Cercheremo le fonti più grandi di spese inutili e le rimuoveremo. Tuttavia, dato che il profiling rivela in modo piuttosto preciso dove il programma spende il suo tempo CPU, potrebbe essere necessario farlo più volte e modificare il codice sorgente dell'applicazione, riavviare i test e osservare che le prestazioni si avvicinano a quanto previsto.
Seguendo i consigli di Brendan Gregg, leggeremo il grafico dall'alto verso il basso. Ogni riga rappresenta un frame dello stack (chiamata di funzione). La prima riga è il punto di ingresso nel programma, genitore di tutte le altre chiamate (in altre parole, tutte le altre chiamate lo avranno nel proprio stack). La riga successiva già differisce:
![]()
Se si passa il cursore sul nome della funzione nel grafico, verrà mostrato il tempo totale in cui è stata nello stack durante il debugging. La funzione HTTPServe è stata presente per il 65% del tempo, le altre funzioni runtime, runtime.mcall, mstart e gc, hanno occupato il resto del tempo. Un fatto interessante: il 5% del tempo totale è stato speso per le richieste DNS:

Gli indirizzi che il programma cerca appartengono a Postgresql. Facciamo clic su FindByAge:

È interessante notare che il programma mostra che ci sono essenzialmente tre principali fonti che aggiungono ritardi: apertura e chiusura delle connessioni, richiesta di dati e connessione al database. Nel grafico si vede che le richieste DNS, l'apertura e la chiusura delle connessioni occupano circa il 13% del tempo totale di esecuzione.
Ipotesi: Il riutilizzo delle connessioni tramite un pool dovrebbe ridurre il tempo di una singola richiesta HTTP, consentendo una maggiore larghezza di banda e minori ritardi..
Configurazione dell'applicazione - esperimento
Aggiorniamo il codice sorgente, proviamo a rimuovere la connessione a Postgresql per ogni richiesta. Prima opzione - utilizzo a livello di applicazione. In questo esperimento noi il pool di connessioni utilizzando il driver sql per go:
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
if err != nil {
return nil, err
}Esecuzione, monitoraggio, analisi
Dopo aver riavviato il test con 1000 richieste al secondo si nota che p99 per i ritardi è tornato nei limiti con SLO di 60ms!
Quali sono i costi?
10000 richieste al secondo / 1000 richieste per server = 10 server + 1
Facciamo ancora meglio!
2000 richieste al secondo

Il raddoppio del carico mostra lo stesso risultato, il grafico in alto a sinistra dimostra che l'applicazione riesce a gestire 2000 richieste al secondo, p100 sotto i 60ms, p99 soddisfa SLO.
Dal punto di vista dei costi:
10000 richieste al secondo / 2000 richieste per server = 5 server + 1
3000 richieste al secondo

Qui l'applicazione può gestire 3000 richieste con un ritardo p99 inferiore a 60ms. Lo SLO non è violato, i costi sono così stabiliti:
10000 richieste al secondo / 3000 richieste per server = 4 server + 1 (l'autore ha arrotondato per eccesso, nota del traduttore)
Proviamo a fare un altro round di analisi.
Analisi - ipotesi
Raccogliamo e visualizziamo i risultati del debug dell'applicazione a 3000 richieste al secondo:

Ancora il 6% del tempo viene speso per stabilire connessioni. La configurazione del pool ha migliorato le prestazioni, ma si nota ancora che l'applicazione continua a creare nuove connessioni al database.
Ipotesi: Le connessioni, nonostante la presenza di un pool, vengono comunque rifiutate e liberate, quindi l'applicazione deve reinstallarle. Impostare il numero di connessioni in attesa uguale alla dimensione del pool dovrebbe aiutare a ridurre i ritardi minimizzando il tempo che l'applicazione spende per stabilire una connessione..
Configurazione dell'applicazione - esperimento
Proviamo a impostare uguale alla dimensione del pool (anche descritto ):
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
return nil, err
}Esecuzione, monitoraggio, analisi
3000 richieste al secondo

p99 inferiore a 60ms con un p100 significativamente più basso!

Il controllo del flame graph mostra che l'impostazione della connessione non è più visibile! Verifichiamo più dettagliatamente pg(*conn).query — non notiamo nemmeno l'impostazione della connessione qui.

Conclusione
L'analisi delle prestazioni è fondamentale per comprendere se le aspettative dei clienti e i requisiti non funzionali sono soddisfatti. L'analisi confrontando le osservazioni con le aspettative dei clienti può aiutare a stabilire quali siano accettabili e quali no. Go offre strumenti efficaci, integrati nella libreria standard, che semplificano e rendono accessibile l'analisi.
Fonte: habr.com
