10 anni fa, Google stava affrontando un collo di bottiglia critico causato da tempi di compilazione C++ estremamente prolungati e aveva bisogno di un modo totalmente nuovo per risolverlo. Gli ingegneri di Google hanno affrontato la sfida creando un nuovo linguaggio chiamato Go (aka Golang). Il nuovo linguaggio di Go prende in prestito le parti migliori del C++, (in particolare le sue prestazioni e le caratteristiche di sicurezza) e le combina con la velocità di Python per consentire a Go di utilizzare rapidamente più core ed essere in grado di implementare la concorrenza.

Qui, a Coralogix, stiamo analizzando i log dei nostri clienti per dare loro approfondimenti in tempo reale, avvisi e metadati sui loro log. Per fare questo, la fase di parsing, che è molto complessa e caricata con tonnellate di regole per ogni servizio di log line, deve essere estremamente veloce. Questo è uno dei motivi per cui abbiamo deciso di usare il Go lang.

Il nuovo servizio è ora in esecuzione a tempo pieno in produzione e mentre stiamo vedendo grandi risultati, ha bisogno di funzionare su macchine ad alte prestazioni. Oltre decine di miliardi di log vengono analizzati ogni giorno da questo servizio Go che gira su un’istanza AWS m4.2xlarge con 8 CPU e 36 GB di memoria.

A questo punto, avremmo potuto chiudere la giornata sentendoci bene, ma non è così che facciamo qui a Coralogix. Volevamo più funzioni (prestazioni, ecc.) usando meno (istanze AWS). Per migliorare, avevamo prima bisogno di capire la natura dei nostri colli di bottiglia e come possiamo ridurli o eliminarli completamente.

Abbiamo deciso di eseguire alcuni profili Golang sul nostro servizio e controllare cosa esattamente ha causato un alto consumo di CPU per vedere se possiamo ottimizzare.

Prima di tutto, abbiamo aggiornato all’ultima versione stabile di Go (una parte fondamentale del ciclo di vita del software). Eravamo sulla versione Go v1.12.4, e l’ultima era la 1.13.8. La versione 1.13, secondo la documentazione, aveva importanti miglioramenti nella libreria runtime e in alcuni altri componenti che utilizzavano principalmente l’uso della memoria. In conclusione, lavorare con l’ultima versione stabile è stato utile e ci ha fatto risparmiare un bel po’ di lavoro →

Così, il consumo di memoria è migliorato da circa ~800MB a ~180MB.

In secondo luogo, al fine di ottenere una migliore comprensione del nostro processo e capire dove stiamo spendendo tempo e risorse, abbiamo iniziato a fare il profiling.

Profilare diversi servizi e linguaggi di programmazione può sembrare complesso e intimidatorio, ma in realtà è piuttosto facile in Go e può essere descritto in pochi comandi. Go ha uno strumento dedicato chiamato ‘pprof’ che dovrebbe essere abilitato nella tua app ascoltando una rotta (porta predefinita – 6060) e usa il pacchetto Go per gestire le connessioni http:

import _ "net/http/pprof"

Poi inizializza quanto segue nella tua funzione principale o sotto il tuo pacchetto di rotta:

go func() {log.Println(http.ListenAndServe("localhost:6060", nil))}()

Ora puoi avviare il tuo servizio e connetterti a

http://localhost:6060/debug/pprof

La documentazione completa di Go può essere trovata qui.

Il profiling predefinito per pprof sarà un campionamento di 30 secondi dell’utilizzo della CPU. Ci sono alcuni percorsi diversi che abilitano il campionamento per l’uso della CPU, l’uso dell’heap e altro.

Ci siamo concentrati sul nostro utilizzo della CPU, quindi abbiamo fatto un profiling di 30 secondi in produzione e abbiamo scoperto quello che vedete nell’immagine qui sotto (promemoria: questo è dopo aver aggiornato la nostra versione di Go e diminuito le parti interne di Go al minimo):

Go profiling – Coralogix

Come potete vedere, abbiamo trovato un sacco di attività del pacchetto runtime, che indica specificamente l’attività GC → Quasi il 29% della nostra CPU (solo i primi 20 oggetti più consumati) è usato dal GC. Poiché Go GC è abbastanza veloce e ottimizzato, la pratica migliore è quella di non cambiarlo o modificarlo e poiché il nostro consumo di memoria era molto basso (rispetto alla nostra precedente versione di Go) il principale sospetto era un alto tasso di allocazione degli oggetti.

Se questo è il caso, ci sono due cose che possiamo fare:

  • Tune Go GC activity to adapt to our service behavior, meaning – delay its trigger in order to activate the GC less frequently. Questo ci costringerà a compensare con più memoria.
  • Trovare la funzione, l’area o la linea nel nostro codice che alloca troppi oggetti.

Guardando il nostro tipo di istanza, era chiaro che avevamo molta memoria a disposizione e siamo attualmente legati alla CPU della macchina. Quindi abbiamo semplicemente cambiato il rapporto. Golang, fin dai suoi primi giorni, ha un flag che la maggior parte degli sviluppatori non conosce, chiamato GOGC. Questa bandiera, con un default di 100, dice semplicemente al sistema quando innescare il GC. Il valore predefinito farà scattare il processo GC ogni volta che l’heap raggiunge il 100% della sua dimensione iniziale. Cambiando questo valore con un numero più alto si ritarderà l’attivazione del GC e abbassandolo si attiverà il GC prima. Abbiamo iniziato a testare alcuni valori diversi e le migliori prestazioni per il nostro scopo sono state ottenute usando: GOGC=2000.

Questo ha immediatamente aumentato il nostro utilizzo della memoria da ~200MB a ~2.7GB (Questo dopo che il consumo di memoria è diminuito a causa del nostro aggiornamento della versione di Go) e ha diminuito il nostro utilizzo della CPU di ~10%.
La seguente schermata dimostra i risultati del benchmark:

Risultati GOGC =2000 – Coralogix benchmark

Le prime 4 funzioni che consumano CPU sono quelle del nostro servizio, il che ha senso. L’utilizzo totale del GC è ora ~13%, meno della metà del suo consumo precedente(!)

Potevamo fermarci qui, ma abbiamo deciso di scoprire dove e perché allociamo così tanti oggetti. Molte volte, c’è una buona ragione per questo (per esempio nel caso dell’elaborazione dei flussi dove creiamo molti nuovi oggetti per ogni messaggio che riceviamo e abbiamo bisogno di liberarcene perché è irrilevante per il prossimo messaggio), ma ci sono casi in cui c’è un modo semplice per ottimizzare e diminuire drasticamente la creazione di oggetti.

Per iniziare, eseguiamo lo stesso comando di prima con una piccola modifica per prendere l’heap dump:

http://localhost:6060/debug/pprof/heap

Per interrogare il file dei risultati è possibile eseguire il seguente comando all’interno della cartella del codice per analizzare il dump:

go tool pprof -alloc_objects <HEAP.PROFILE.FILE>

la nostra istantanea appariva così:

Tutto sembrava ragionevole tranne la terza riga, che è una funzione di monitoraggio che riporta al nostro esportatore prometheus alla fine di ogni fase di analisi delle regole Coralogix. Per approfondire, abbiamo eseguito il seguente comando:

list <FunctionName>

Per esempio:

list reportRuleExecution

E poi abbiamo ottenuto quanto segue:

Leave a comment

Il tuo indirizzo email non sarà pubblicato.