SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Analisi delle prestazioni e ottimizzazione — uno strumento potente per valutare l'adeguatezza delle prestazioni per i clienti.

L'analisi delle prestazioni può essere utilizzata per identificare i colli di bottiglia nel programma, impiegando un approccio scientifico per valutare gli esperimenti di ottimizzazione. Questo articolo delinea un approccio generale all'analisi delle prestazioni e all'ottimizzazione usando un server web Go come esempio.

Go è particolarmente adatto, poiché dispone di strumenti di profiling. pprof nella libreria standard.

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Strategia

Creiamo un riepilogo per la nostra analisi strutturale. Cercheremo di utilizzare alcuni dati per prendere decisioni invece di apportare modifiche basate su intuizioni o congetture. Ecco come procederemo:

  • Definiamo i confini dell'ottimizzazione (requisiti);
  • Calcoliamo il carico di transazione per il sistema;
  • Eseguiamo il test (generiamo dati);
  • Osserviamo;
  • Analizziamo: vengono rispettati tutti i requisiti?
  • Ottimizziamo scientificamente, formuliamo un'ipotesi;
  • Eseguiamo un esperimento per verificare questa ipotesi.

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Architettura di un semplice server HTTP

Per questo articolo utilizzeremo un piccolo server HTTP scritto in Golang. Tutto il codice di questo articolo è disponibile. qui.

L'applicazione analizzata è un server HTTP che interroga Postgresql per ogni richiesta. Inoltre, ci sono Prometheus, node_exporter e Grafana per raccogliere e visualizzare le metriche dell'applicazione e del sistema.

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

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

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Definiamo gli obiettivi

In questo passaggio definiamo l'obiettivo. Cosa stiamo cercando di analizzare? Come sappiamo quando è il momento di concludere? In questo articolo presupponiamo di avere clienti e che il nostro servizio dovrà gestire 10.000 richieste al secondo.

In Google SRE Book esamineremo in dettaglio i metodi di selezione e modellazione. Procederemo nello stesso modo, creando modelli:

  • Latenza: il 99% delle richieste deve essere completato in meno di 60 ms;
  • Costo: il servizio deve consumare la minore somma di denaro che riteniamo ragionevolmente possibile. Per questo, massimizziamo la capacità di throughput;
  • Pianificazione delle capacità: è necessario comprendere e documentare quanti esemplari dell'applicazione saranno necessari, inclusa la funzione generale di scalabilità, così come quanti esemplari serviranno per soddisfare i requisiti iniziali di carico e garantire ridondanza n+1..

La latenza può richiedere ottimizzazione oltre all'analisi, ma il throughput deve essere chiaramente analizzato. Utilizzando il processo SRE, il requisito di latenza proviene dal cliente o dall'azienda, rappresentato dal product owner. E il nostro servizio si impegnerà a rispettarlo fin dall'inizio, senza alcuna configurazione!

Configuriamo l'ambiente di test

Con l'ambiente di test saremo in grado di fornire un carico dosato nel nostro sistema. Per l'analisi saranno generati dati sulle prestazioni del web service.

Carico di transazione

Questo ambiente utilizza Vegeta per generare una frequenza personalizzabile di richieste HTTP, fino a quando non viene interrotta:

$ 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 report

Osservazione

Durante l'esecuzione verrà applicato un carico transazionale. Oltre alle metriche dell'applicazione (numero di richieste, latenza di risposta) e del sistema operativo (memoria, CPU, IOPS), verrà eseguito un profiling dell'applicazione per comprendere dove si presentano problemi e come viene consumato il tempo della CPU.

Profiling

Il profiling è un tipo di misurazione che consente di vedere dove viene speso il tempo della CPU durante l'esecuzione dell'applicazione. Permette di identificare esattamente dove e quanto tempo CPU viene speso:

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Questi dati possono essere utilizzati durante l'analisi per ottenere una visione del tempo della CPU sprecato e del lavoro non necessario eseguito. Go (pprof) può generare profili e visualizzarli sotto forma di flame graph, utilizzando un set di strumenti standard. Spiegherò come utilizzarli e fornirò indicazioni su come configurarli più avanti nell'articolo.

Esecuzione, osservazione, analisi.

Effettueremo un esperimento. Eseguiremo, osserveremo e analizzeremo finché le prestazioni non ci soddisferanno. Sceglieremo un carico inizialmente basso per applicarlo nel nostro primo round di osservazioni. In ogni passaggio successivo, aumenteremo il carico usando un fattore di scalabilità selezionato con una certa variabilità. Ogni esecuzione del test di carico verrà effettuata regolando il numero di richieste: make load-test LOAD_TEST_RATE=X.

50 richieste al secondo

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Nota i due grafici in alto. Il grafico in alto a sinistra mostra che la nostra applicazione gestisce 50 richieste al secondo (secondo il suo parere), mentre quello in alto a destra mostra la durata di ciascuna richiesta. Entrambi i parametri ci aiutano a osservare e analizzare se stiamo rimanendo entro i nostri limiti di prestazioni o meno. La linea rossa sul grafico Latenza delle richieste HTTP mostra l'SLO a 60ms. Dalla linea, possiamo vedere che siamo ben al di sotto del nostro tempo massimo di risposta.

Guardiamo sotto l'aspetto dei costi:

10000 richieste al secondo / 50 richieste per server = 200 server + 1

Possiamo ancora migliorare questo valore.

500 richieste al secondo

Fatti più interessanti iniziano a verificarsi quando il carico raggiunge 500 richieste al secondo:

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Ancora una volta, nel grafico in alto a sinistra si può vedere che l'applicazione registra un carico normale. Se non è così, c'è un problema con il server su cui è in esecuzione l'applicazione. Il grafico con la latenza delle risposte è posizionato in alto a destra e mostra che 500 richieste al secondo hanno portato a una latenza di risposta di 25-40ms. Il percentile 99 è ancora ben dentro l'SLO di 60ms scelto sopra.

Dal punto di vista dei costi:

10000 richieste al secondo / 500 richieste per server = 20 server + 1

C'è ancora spazio per miglioramenti.

1000 richieste al secondo

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Ottimo avvio! L'applicazione mostra di aver gestito 1000 richieste al secondo, tuttavia il limite sulla latenza è stato superato dal lato dell'SLO. Questo è evidente dalla linea p99 nel grafico in alto a destra. Nonostante la linea p100 sia molto più alta, le latenze effettive superano il massimo di 60ms. Approfondiamo il profiling per capire cosa sta realmente facendo l'applicazione.

Profiling

Per il profiling, impostiamo un carico di 1000 richieste al secondo, quindi utilizziamo pprof per raccogliere dati su come l'applicazione spende il tempo del processore. Questo può essere fatto attivando l'endpoint HTTP pprof, dopodiché, sotto carico, salviamo i risultati usando curl:

$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.prof

I risultati possono essere visualizzati in questo modo:

$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Il grafico mostra dove e quanto l'applicazione spende tempo della CPU. Dalla descrizione di Brendan Gregg:

Sull'asse X ci sono i profili degli stack, ordinati in ordine alfabetico (questo non è tempo), mentre l'asse Y mostra la profondità dello stack, contando da zero in [top]. Ogni rettangolo rappresenta un frame dello stack. Più largo è il frame, più frequentemente appare negli stack. Quello in alto utilizza la CPU, mentre quelli sotto sono gli elementi figli. I colori, di solito, non hanno alcun significato e vengono scelti casualmente per distinguere i frame.

Analisi - ipotesi

Per l'ottimizzazione, ci concentreremo sul tentativo di trovare sprechi di tempo della CPU. Cerceremo le fonti più grandi di sprechi inutili e le rimuoveremo. E dato che il profiling rivela molto precisamente dove l'applicazione spende il suo tempo del processore, potrebbe essere necessario farlo più volte e richiedere modifiche al codice sorgente dell'applicazione, riavviare i test e osservare se le prestazioni si avvicinano agli obiettivi predefiniti.

Seguendo le raccomandazioni 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, il genitore di tutte le altre chiamate (in altre parole, tutte le altre chiamate avranno questa nel loro stack). La riga successiva è già diversa:

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Se si passa il cursore sul nome della funzione nel grafico, verrà visualizzato il tempo totale in cui è stata nello stack durante il debug. La funzione HTTPServe è stata lì per il 65% del tempo, mentre 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 richieste DNS:

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

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

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Curioso, il programma mostra che ci sono fondamentalmente tre fonti principali che aggiungono latenza: apertura e chiusura delle connessioni, richiesta di dati e connessione al database. Nel grafico si può osservare 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 ogni richiesta HTTP, consentendo una maggiore capacità e minori latenze..

Ottimizzazione dell'applicazione - esperimento

Aggiorniamo il codice sorgente, proviamo a rimuovere la connessione a Postgresql per ogni richiesta. La prima opzione è utilizzare un pool di connessioni a livello applicativo. In questo esperimento, configureremo 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, osservazione, analisi

Esecuzione, monitoraggio, analisi

Dopo il riavvio del test con 1000 richieste al secondo, si può notare che il p99 dei ritardi è tornato nella norma con SLO di 60 ms!

Qual è il costo?

10000 richieste al secondo / su 1000 richieste per server = 10 server + 1

Facciamo ancora meglio!

2000 richieste al secondo

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

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, con p100 sotto i 60 ms e p99 che soddisfa l'SLO.

Dal punto di vista dei costi:

10000 richieste al secondo / su 2000 richieste per server = 5 server + 1

3000 richieste al secondo

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

In questo caso, l'applicazione può gestire 3000 richieste con ritardo p99 inferiore a 60 ms. L'SLO non è violato, e il costo è stato accettato come:

10000 richieste al secondo / su 3000 richieste per server = 4 server + 1 (l'autore ha arrotondato per eccesso, nota del traduttore)

Proviamo un altro round di analisi.

Analisi - ipotesi

Raccogliamo e mostriamo i risultati del debug dell'applicazione a 3000 richieste al secondo:

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Ancora il 6% del tempo è dedicato all'installazione delle connessioni. La configurazione del pool ha migliorato le prestazioni, ma si vede ancora che l'applicazione continua a creare nuove connessioni con il database.

Ipotesi: Le connessioni, nonostante la presenza del pool, vengono ancora rifiutate e pulite, quindi l'applicazione deve reinstallarle. Impostare il numero di connessioni in attesa alla dimensione del pool dovrebbe aiutare a ridurre il ritardo, minimizzando il tempo che l'applicazione spende per creare una connessione..

Ottimizzazione dell'applicazione - esperimento

Proviamo a impostare MaxIdleConns uguale alla dimensione del pool (anche descritto qui):

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

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

p99 inferiore a 60 ms con un p100 significativamente ridotto!

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

Il controllo del flame graph mostra che l'installazione della connessione non è più evidente! Controlliamo più nel dettaglio pg(*conn).query — non notiamo nemmeno l'installazione della connessione qui.

SRE: Analisi delle prestazioni. Metodo di configurazione utilizzando un semplice server web in Go

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 determinare cosa sia accettabile e cosa no. Go fornisce strumenti efficaci, integrati nella libreria standard, che rendono l'analisi semplice e accessibile.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster