Cu 10 ani în urmă, Google se confrunta cu un blocaj critic cauzat de timpii de compilare C++ extrem de lungi și avea nevoie de o modalitate complet nouă de rezolvare a problemei. Inginerii Google au abordat provocarea prin crearea unui nou limbaj numit Go (cunoscut și ca Golang). Noul limbaj Go împrumută cele mai bune părți ale limbajului C++, (mai ales caracteristicile sale de performanță și securitate) și le combină cu viteza Python pentru a permite Go să utilizeze rapid mai multe nuclee, fiind în același timp capabil să implementeze concurența.

Aici, la Coralogix, analizăm jurnalele clienților noștri pentru a le oferi informații în timp real, alerte și metadate despre jurnalele lor. Pentru a face acest lucru, faza de parsare, care este foarte complexă și încărcată cu tone de reguli pentru fiecare serviciu de linie de jurnal, trebuie să fie extrem de rapidă. Acesta este unul dintre motivele pentru care am decis să folosim Go lang.

Noul serviciu funcționează acum cu normă întreagă în producție și, deși vedem rezultate foarte bune, trebuie să ruleze pe mașini performante. Peste zeci de miliarde de jurnale sunt analizate în fiecare zi de acest serviciu Go care rulează pe o instanță AWS m4.2xlarge cu 8 procesoare și 36 GB de memorie.

În acest stadiu, am fi putut încheia ziua simțindu-ne foarte bine că totul merge bine, dar nu așa funcționăm noi aici la Coralogix. Am vrut mai multe caracteristici (performanță, etc.) folosind mai puțin (instanțe AWS). Pentru a ne îmbunătăți, trebuia mai întâi să înțelegem natura blocajelor noastre și cum putem să le reducem sau să le eliminăm complet.

Am decis să rulăm niște profilări Golang pe serviciul nostru și să verificăm ce anume a cauzat consumul ridicat de CPU pentru a vedea dacă putem optimiza.

În primul rând, am actualizat la cea mai recentă versiune stabilă Go (o parte esențială a ciclului de viață al software-ului). Am fost pe versiunea Go v1.12.4, iar cea mai recentă a fost 1.13.8. Versiunea 1.13, conform documentației, avea îmbunătățiri majore în biblioteca de execuție și alte câteva componente care utilizau în principal utilizarea memoriei. În concluzie, lucrul cu cea mai recentă versiune stabilă a fost util și ne-a economisit destul de multă muncă →

Astfel, consumul de memorie s-a îmbunătățit de la aproximativ ~800MB la ~180MB.

În al doilea rând, pentru a înțelege mai bine procesul nostru și pentru a înțelege unde cheltuim timp și resurse, am început să facem profiluri.

Profilarea diferitelor servicii și limbaje de programare poate părea complexă și intimidantă, dar este de fapt destul de ușoară în Go și poate fi descrisă în câteva comenzi. Go are un instrument dedicat numit „pprof” care ar trebui să fie activat în aplicația dvs. prin ascultarea unei rute (port implicit- 6060) și să utilizați pachetul Go pentru gestionarea conexiunilor http:

import _ "net/http/pprof"

Apoi inițializați următoarele în funcția principală sau în pachetul de rute:

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

Acum puteți să vă porniți serviciul și să vă conectați la

http://localhost:6060/debug/pprof

Documentația completă de Go poate fi găsită aici.

Profilarea implicită pentru pprof va fi o eșantionare de 30 de secunde a utilizării CPU. Există câteva căi diferite care permit eșantionarea pentru utilizarea CPU, utilizarea heap și altele.

Noi ne-am concentrat pe utilizarea CPU, așa că am făcut o profilare de 30 de secunde în producție și am descoperit ceea ce vedeți în imaginea de mai jos (reamintim: acest lucru se întâmplă după ce am actualizat versiunea noastră Go și am redus la minimum părțile interne ale Go):

Profilare Go – Coralogix

După cum puteți vedea, am descoperit o mulțime de activitate a pachetelor în timpul execuției, care a indicat în mod specific activitatea GC → Aproape 29% din CPU-ul nostru (doar primele 20 de obiecte cele mai consumate) este folosit de GC. Deoarece Go GC este destul de rapid și destul de optimizat, cea mai bună practică este să nu îl schimbăm sau să îl modificăm și, din moment ce consumul nostru de memorie a fost foarte mic (în comparație cu versiunea Go anterioară), principalul suspect a fost o rată mare de alocare a obiectelor.

Dacă acesta este cazul, există două lucruri pe care le putem face:

  • Ajustați activitatea Go GC pentru a se adapta la comportamentul serviciului nostru, adică – amânați declanșarea sa pentru a activa GC mai puțin frecvent. Acest lucru ne va forța să compensăm cu mai multă memorie.
  • Căutați funcția, zona sau linia din codul nostru care alocă prea multe obiecte.

Urmărind tipul nostru de instanță, era clar că aveam multă memorie de rezervă și că în prezent suntem limitați de CPU-ul mașinii. Așa că am schimbat pur și simplu acest raport. Golang, încă din primele sale zile, are un indicator pe care majoritatea dezvoltatorilor nu îl cunosc, numit GOGC. Acest indicator, cu o valoare implicită de 100, spune pur și simplu sistemului când să declanșeze GC. Valoarea implicită va declanșa procesul GC ori de câte ori heap-ul ajunge la 100% din dimensiunea sa inițială. Schimbarea acestei valori cu un număr mai mare va întârzia declanșarea GC, iar scăderea ei va declanșa GC mai devreme. Am început să analizăm câteva valori diferite și cea mai bună performanță pentru scopul nostru a fost obținută atunci când am folosit: GOGC=2000.

Aceasta a crescut imediat utilizarea memoriei de la ~200MB la ~2,7GB (Asta după ce consumul de memorie a scăzut datorită actualizării versiunii Go) și a scăzut utilizarea CPU cu ~10%.
Următoarea captură de ecran demonstrează rezultatele benchmark-ului:

GOGC =2000 results – Coralogix benchmark

Principalele 4 funcții care consumă CPU sunt funcțiile serviciului nostru, ceea ce are sens. Utilizarea totală a GC este acum ~13%, mai puțin de jumătate din consumul anterior(!)

Am fi putut să ne oprim aici, dar am decis să descoperim unde și de ce alocăm atât de multe obiecte. De multe ori, există un motiv întemeiat pentru asta (de exemplu, în cazul procesării fluxurilor în care creăm o mulțime de obiecte noi pentru fiecare mesaj pe care îl primim și trebuie să scăpăm de el pentru că este irelevant pentru următorul mesaj), dar există cazuri în care există o modalitate ușoară de a optimiza și de a diminua drastic crearea de obiecte.

Pentru a începe, să rulăm aceeași comandă ca și înainte cu o mică modificare pentru a lua heap dump:

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

Pentru a interoga fișierul rezultat, puteți rula următoarea comandă în folderul de cod pentru a analiza heap-ul:

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

Copia noastră instantanee arăta astfel:

Toate păreau rezonabile, cu excepția celui de-al treilea rând, care este o funcție de monitorizare care raportează exportatorului nostru prometheus la sfârșitul fiecărei faze de analiză a regulilor Coralogix. Pentru a intra în profunzime, am rulat următoarea comandă:

list <FunctionName>

De exemplu:

list reportRuleExecution

Și apoi am obținut următoarele:

Leave a comment

Adresa ta de email nu va fi publicată.