Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Vă sugerez să citiți transcrierea raportului de la sfârșitul anului 2019 de Alexander Valyalkin „Go optimizări în VictoriaMetrics”

VictoriaMetrics — un SGBD rapid și scalabil pentru stocarea și procesarea datelor sub forma unei serii temporale (înregistrarea formează timpul și un set de valori corespunzătoare acestui timp, de exemplu, obținute prin sondarea periodică a stării senzorilor sau colectarea de metrici).

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Iată un link către videoclipul acestui raport - https://youtu.be/MZ5P21j_HLE

Diapozitive

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Spune-ne despre tine. Eu sunt Alexander Valyalkin. Aici contul meu GitHub. Sunt pasionat de Go și optimizarea performanței. Am scris o mulțime de biblioteci utile și nu atât de utile. Încep cu oricare fast, sau cu quick prefix.

În prezent lucrez la VictoriaMetrics. Ce este și ce fac acolo? Voi vorbi despre asta în această prezentare.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Schema raportului este următoarea:

  • În primul rând, vă voi spune ce este VictoriaMetrics.
  • Apoi vă voi spune ce serii cronologice sunt.
  • Apoi vă voi spune cum funcționează o bază de date cu serii de timp.
  • În continuare, vă voi spune despre arhitectura bazei de date: în ce constă.
  • Și apoi să trecem la optimizările pe care le are VictoriaMetrics. Aceasta este o optimizare pentru indexul inversat și o optimizare pentru implementarea seturilor de biți în Go.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Știe cineva din audiență ce este VictoriaMetrics? Wow, mulți oameni știu deja. Este o veste bună. Pentru cei care nu știu, aceasta este o bază de date cu serii de timp. Se bazează pe arhitectura ClickHouse, pe câteva detalii ale implementării ClickHouse. De exemplu, pe cum ar fi: MergeTree, calcul paralel pe toate nucleele de procesor disponibile și optimizarea performanței prin lucrul pe blocuri de date care sunt plasate în memoria cache a procesorului.

VictoriaMetrics oferă o comprimare a datelor mai bună decât alte baze de date în serie de timp.

Se scalează pe verticală - adică puteți adăuga mai multe procesoare, mai multă RAM pe un singur computer. VictoriaMetrics va utiliza cu succes aceste resurse disponibile și va îmbunătăți productivitatea liniară.

VictoriaMetrics se scalează și pe orizontală - adică puteți adăuga noduri suplimentare la clusterul VictoriaMetrics, iar performanța acestuia va crește aproape liniar.

După cum ați ghicit, VictoriaMetrics este o bază de date rapidă, pentru că nu pot scrie altele. Și este scris în Go, așa că vorbesc despre asta la această întâlnire.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Cine știe ce este o serie temporală? De asemenea, cunoaște o mulțime de oameni. O serie temporală este o serie de perechi (timestamp, значение), unde aceste perechi sunt sortate după timp. Valoarea este un număr în virgulă mobilă – float64.

Fiecare serie temporală este identificată în mod unic printr-o cheie. În ce constă această cheie? Este format dintr-un set nevid de perechi cheie-valoare.

Iată un exemplu de serie temporală. Cheia acestei serii este o listă de perechi: __name__="cpu_usage" este numele valorii, instance="my-server" - acesta este computerul pe care este colectată această valoare, datacenter="us-east" - acesta este centrul de date unde se află acest computer.

Am ajuns să avem un nume de serie cronologică format din trei perechi cheie-valoare. Această cheie corespunde unei liste de perechi (timestamp, value). t1, t3, t3, ..., tN - acestea sunt marcaje de timp, 10, 20, 12, ..., 15 — valorile corespunzătoare. Aceasta este utilizarea CPU la un moment dat pentru o serie dată.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Unde pot fi folosite seriile temporale? Are cineva idee?

  • În DevOps, puteți măsura CPU, RAM, rețea, rps, numărul de erori etc.
  • IoT - putem măsura temperatura, presiunea, coordonatele geografice și altceva.
  • De asemenea, finanțe – putem monitoriza prețurile pentru tot felul de acțiuni și valute.
  • În plus, seriile de timp pot fi utilizate în monitorizarea proceselor de producție din fabrici. Avem utilizatori care folosesc VictoriaMetrics pentru a monitoriza turbinele eoliene, pentru roboți.
  • Seriile temporale sunt, de asemenea, utile pentru colectarea de informații de la senzorii diferitelor dispozitive. De exemplu, pentru un motor; pentru măsurarea presiunii în pneuri; pentru măsurarea vitezei, distanței; pentru măsurarea consumului de benzină etc.
  • Serii temporale pot fi folosite și pentru a monitoriza aeronavele. Fiecare aeronavă are o cutie neagră care colectează serii temporale pentru diferiți parametri ai sănătății aeronavei. Seriile temporale sunt folosite și în industria aerospațială.
  • Asistența medicală este tensiunea arterială, pulsul etc.

S-ar putea să fie mai multe aplicații de care am uitat, dar sper că înțelegeți că seriile de timp sunt folosite în mod activ în lumea modernă. Iar volumul utilizării lor crește în fiecare an.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

De ce aveți nevoie de o bază de date cu serii de timp? De ce nu poate fi folosită o bază de date relațională obișnuită pentru a stoca serii de timp?

Deoarece seriile de timp conțin de obicei o cantitate mare de informații, care este dificil de stocat și procesat în bazele de date convenționale. Prin urmare, au apărut baze de date specializate pentru serii temporale. Aceste baze stochează efectiv puncte (timestamp, value) cu cheia dată. Ele furnizează un API pentru citirea datelor stocate după cheie, printr-o singură pereche cheie-valoare sau prin mai multe perechi cheie-valoare sau prin expresie regulată. De exemplu, doriți să găsiți încărcarea CPU a tuturor serviciilor dvs. într-un centru de date din America, apoi trebuie să utilizați această pseudo-interogare.

În mod obișnuit, bazele de date cu serii de timp oferă limbaje de interogare specializate, deoarece SQL-ul serii de timp nu este foarte potrivit. Deși există baze de date care acceptă SQL, acesta nu este foarte potrivit. Limbi de interogare precum PromQL, InfluxQL, Flux, Q. Sper că cineva a auzit cel puțin una dintre aceste limbi. Mulți oameni au auzit probabil despre PromQL. Acesta este limbajul de interogare Prometheus.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Așa arată o arhitectură modernă a bazelor de date în serie de timp folosind VictoriaMetrics ca exemplu.

Este format din două părți. Aceasta este stocarea pentru indexul inversat și stocarea pentru valorile serii de timp. Aceste depozite sunt separate.

Când o înregistrare nouă ajunge în baza de date, accesăm mai întâi indexul inversat pentru a găsi identificatorul seriei temporale pentru un anumit set. label=value pentru o metrică dată. Găsim acest identificator și salvăm valoarea în depozitul de date.

Când vine o solicitare pentru a prelua date de la TSDB, mergem mai întâi la indexul inversat. Să luăm totul timeseries_ids înregistrări care se potrivesc cu acest set label=value. Și apoi obținem toate datele necesare din depozitul de date, indexate de timeseries_ids.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Să ne uităm la un exemplu despre modul în care o bază de date în serie de timp procesează o interogare de selecție primită.

  • În primul rând ea primește totul timeseries_ids dintr-un index inversat care conţine perechile date label=value, sau satisface o expresie regulată dată.
  • Apoi preia toate punctele de date din stocarea de date la un interval de timp dat pentru cele găsite timeseries_ids.
  • După aceasta, baza de date efectuează câteva calcule pe aceste puncte de date, conform solicitării utilizatorului. Și după aceea returnează răspunsul.

În această prezentare vă voi povesti despre prima parte. Aceasta este o căutare timeseries_ids prin indice inversat. Puteți urmări a doua parte și a treia parte mai târziu Surse VictoriaMetrics, sau așteptați până voi pregăti alte rapoarte :)

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Să trecem la indicele inversat. Mulți ar putea crede că acest lucru este simplu. Cine știe ce este un index inversat și cum funcționează? Oh, nu mai mulți oameni. Să încercăm să înțelegem despre ce este vorba.

Este de fapt simplu. Este pur și simplu un dicționar care mapează o cheie la o valoare. Ce este o cheie? Acest cuplu label=valueUnde label и value - acestea sunt linii. Iar valorile sunt un set timeseries_ids, care include perechea dată label=value.

Indexul inversat vă permite să găsiți rapid totul timeseries_ids, care au dat label=value.

De asemenea, vă permite să găsiți rapid timeseries_ids serii temporale pentru mai multe perechi label=value, sau pentru cupluri label=regexp. Cum se întâmplă asta? Prin găsirea intersecției mulțimii timeseries_ids pentru fiecare pereche label=value.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Să ne uităm la diferite implementări ale indexului inversat. Să începem cu cea mai simplă implementare naivă. Ea arată așa.

Funcție getMetricIDs primește o listă de șiruri. Fiecare linie contine label=value. Această funcție returnează o listă metricIDs.

Cum functioneaza? Aici avem o variabilă globală numită invertedIndex. Acesta este un dicționar obișnuit (map), care va mapa șirul pentru a tăia int. Linia contine label=value.

Implementarea funcției: get metricIDs pentru primul label=value, apoi trecem prin toate celelalte label=value, am luat metricIDs pentru ei. Și apelați funcția intersectInts, despre care se va discuta mai jos. Și această funcție returnează intersecția acestor liste.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

După cum puteți vedea, implementarea unui index inversat nu este foarte complicată. Dar aceasta este o implementare naivă. Ce dezavantaje are? Principalul dezavantaj al implementării naive este că un astfel de index inversat este stocat în RAM. După repornirea aplicației, pierdem acest index. Nu există nicio salvare a acestui index pe disc. Este puțin probabil ca un astfel de index inversat să fie potrivit pentru o bază de date.

Al doilea dezavantaj este legat și de memorie. Indexul inversat trebuie să se potrivească în RAM. Dacă depășește dimensiunea RAM, atunci evident că vom ieși din eroare de memorie. Și programul nu va funcționa.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Această problemă poate fi rezolvată folosind soluții gata făcute, cum ar fi LevelDBSau RocksDB.

Pe scurt, avem nevoie de o bază de date care să ne permită să facem trei operațiuni rapid.

  • Prima operațiune este înregistrarea ключ-значение la această bază de date. Ea face asta foarte repede, unde ключ-значение sunt șiruri arbitrare.
  • A doua operație este o căutare rapidă a unei valori folosind o anumită cheie.
  • Și a treia operație este o căutare rapidă pentru toate valorile după un prefix dat.

LevelDB și RocksDB - aceste baze de date au fost dezvoltate de Google și Facebook. Mai întâi a venit LevelDB. Apoi băieții de la Facebook au luat LevelDB și au început să-l îmbunătățească, au făcut RocksDB. Acum aproape toate bazele de date interne funcționează pe RocksDB în interiorul Facebook, inclusiv cele care au fost transferate în RocksDB și MySQL. L-au numit MyRocks.

Un index inversat poate fi implementat folosind LevelDB. Cum să o facă? Salvăm ca cheie label=value. Și valoarea este identificatorul seriei temporale în care este prezentă perechea label=value.

Dacă avem multe serii cronologice cu o pereche dată label=value, atunci vor exista multe rânduri în această bază de date cu aceeași cheie și diferită timeseries_ids. Pentru a obține o listă cu toate timeseries_ids, care încep cu asta label=prefix, facem o scanare interval pentru care această bază de date este optimizată. Adică, selectăm toate liniile care încep cu label=prefix și obțineți necesarul timeseries_ids.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Iată un exemplu de implementare a cum ar arăta în Go. Avem un indice inversat. Acesta este LevelDB.

Funcția este aceeași ca și pentru implementarea naivă. Se repetă implementarea naivă aproape linie cu linie. Singurul punct este că, în loc să apelăm la map accesăm indicele inversat. Primim toate valorile pentru prima label=value. Apoi trecem prin toate perechile rămase label=value și obțineți seturile corespunzătoare de metricID-uri pentru ei. Apoi găsim intersecția.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Totul pare să fie bine, dar această soluție are dezavantaje. VictoriaMetrics a implementat inițial un index inversat bazat pe LevelDB. Dar până la urmă a trebuit să renunț.

De ce? Deoarece LevelDB este mai lentă decât implementarea naivă. Într-o implementare naivă, dată fiind o cheie dată, recuperăm imediat întreaga felie metricIDs. Aceasta este o operațiune foarte rapidă - întreaga felie este gata de utilizare.

În LevelDB, de fiecare dată când este apelată o funcție GetValues trebuie să treci prin toate rândurile care încep cu label=value. Și obțineți valoarea pentru fiecare linie timeseries_ids. De așa fel timeseries_ids aduna o felie din acestea timeseries_ids. Evident, acest lucru este mult mai lent decât simpla accesare a unei hărți obișnuite prin tastă.

Al doilea dezavantaj este că LevelDB este scris în C. Apelarea funcțiilor C din Go nu este foarte rapidă. Este nevoie de sute de nanosecunde. Acest lucru nu este foarte rapid, deoarece în comparație cu un apel de funcție obișnuit scris în go, care durează 1-5 nanosecunde, diferența de performanță este de zeci de ori. Pentru VictoriaMetrics acesta a fost un defect fatal :)

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Așa că am scris propria mea implementare a indexului inversat. Și a sunat-o mergeset.

Mergeset se bazează pe structura de date MergeTree. Această structură de date este împrumutată de la ClickHouse. Evident, mergeset ar trebui să fie optimizat pentru căutare rapidă timeseries_ids conform cheii date. Mergeset este scris în întregime în Go. Poti sa vezi Surse VictoriaMetrics pe GitHub. Implementarea mergeset este în folder /lib/mergeset. Puteți încerca să vă dați seama ce se întâmplă acolo.

API-ul mergeset este foarte asemănător cu LevelDB și RocksDB. Adică, vă permite să salvați rapid noi înregistrări acolo și să selectați rapid înregistrările după un prefix dat.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Despre dezavantajele mergeset vom vorbi mai târziu. Acum să vorbim despre ce probleme au apărut cu VictoriaMetrics în producție la implementarea unui index inversat.

De ce au apărut?

Primul motiv este rata ridicată de abandon. Tradusă în rusă, aceasta este o schimbare frecventă a seriei cronologice. Acesta este momentul în care se termină o serie temporală și începe o nouă serie sau încep multe serii cronologice noi. Și asta se întâmplă des.

Al doilea motiv este numărul mare de serii cronologice. La început, când monitorizarea câștiga popularitate, numărul de serii cronologice era mic. De exemplu, pentru fiecare computer trebuie să monitorizați procesorul, memoria, rețeaua și încărcarea discului. 4 serii temporale per computer. Să presupunem că aveți 100 de computere și 400 de serii cronologice. Acest lucru este foarte puțin.

De-a lungul timpului, oamenii și-au dat seama că ar putea măsura informații mai granulare. De exemplu, măsurați încărcarea nu a întregului procesor, ci separat a fiecărui nucleu de procesor. Dacă aveți 40 de nuclee de procesor, atunci aveți de 40 de ori mai multe serii de timp pentru a măsura încărcarea procesorului.

Dar asta nu este tot. Fiecare nucleu de procesor poate avea mai multe stări, cum ar fi inactiv, atunci când este inactiv. Și, de asemenea, lucrați în spațiul utilizatorului, lucrați în spațiul kernel și în alte state. Și fiecare astfel de stare poate fi măsurată și ca o serie temporală separată. Acest lucru crește suplimentar numărul de rânduri de 7-8 ori.

Dintr-o măsurătoare am obținut 40 x 8 = 320 de valori pentru un singur computer. Înmulțind cu 100, obținem 32 în loc de 000.

Apoi a apărut Kubernetes. Și s-a înrăutățit, deoarece Kubernetes poate găzdui multe servicii diferite. Fiecare serviciu din Kubernetes este format din mai multe pod-uri. Și toate acestea trebuie monitorizate. În plus, avem o implementare constantă de noi versiuni ale serviciilor dumneavoastră. Pentru fiecare versiune nouă, trebuie create noi serii temporale. Ca urmare, numărul de serii temporale crește exponențial și ne confruntăm cu problema unui număr mare de serii temporale, care se numește cardinalitate ridicată. VictoriaMetrics face față cu succes în comparație cu alte baze de date cu serii de timp.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Să aruncăm o privire mai atentă asupra ratei ridicate de abandon. Ce cauzează o rată ridicată de abandon în producție? Pentru că unele semnificații ale etichetelor și etichetelor se schimbă în mod constant.

De exemplu, luați Kubernetes, care are conceptul deployment, adică atunci când o nouă versiune a aplicației dvs. este lansată. Din anumite motive, dezvoltatorii Kubernetes au decis să adauge ID-ul de implementare la etichetă.

La ce a dus asta? Mai mult, cu fiecare nouă implementare, toate seriile de timp vechi sunt întrerupte și, în locul lor, serii temporale noi încep cu o nouă valoare de etichetă. deployment_id. Pot exista sute de mii și chiar milioane de astfel de rânduri.

Lucrul important despre toate acestea este că numărul total de serii temporale crește, dar numărul de serii temporale care sunt active în prezent și care primesc date rămâne constant. Această stare se numește rata ridicată de abandon.

Principala problemă a ratei ridicate de abandon este asigurarea unei viteze de căutare constantă pentru toate seriile de timp pentru un anumit set de etichete pe un anumit interval de timp. De obicei, acesta este intervalul de timp pentru ultima oră sau ultima zi.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Cum se rezolvă această problemă? Iată prima opțiune. Aceasta este pentru a împărți indicele inversat în părți independente în timp. Adică, trece un interval de timp, terminăm de lucru cu indexul invers invers. Și creați un nou index inversat. Mai trece un interval de timp, creăm altul și altul.

Și la eșantionarea din acești indici inversați, găsim un set de indici inversați care se încadrează în intervalul dat. Și, în consecință, selectăm id-ul seriei temporale de acolo.

Acest lucru economisește resurse, deoarece nu trebuie să ne uităm la părți care nu se încadrează în intervalul dat. Adică, de obicei, dacă selectăm date pentru ultima oră, atunci pentru intervalele de timp anterioare omitem solicitările.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Există o altă opțiune pentru a rezolva această problemă. Aceasta este pentru a stoca pentru fiecare zi o listă separată de ID-uri ale seriilor temporale care au avut loc în ziua respectivă.

Avantajul acestei soluții față de soluția anterioară este că nu duplicăm informații din seria temporală care nu dispar în timp. Sunt prezenti constant si nu se schimba.

Dezavantajul este că o astfel de soluție este mai dificil de implementat și mai dificil de depanat. Și VictoriaMetrics a ales această soluție. Așa s-a întâmplat istoric. Această soluție funcționează, de asemenea, bine în comparație cu cea anterioară. Pentru că această soluție nu a fost implementată din cauza faptului că este necesară duplicarea datelor în fiecare partiție pentru serii de timp care nu se modifică, adică care nu dispar în timp. VictoriaMetrics a fost optimizat în primul rând pentru consumul de spațiu pe disc, iar implementarea anterioară a înrăutățit consumul de spațiu pe disc. Dar această implementare este mai potrivită pentru a minimiza consumul de spațiu pe disc, așa că a fost aleasă.

A trebuit să mă lupt cu ea. Lupta a fost că în această implementare mai trebuie să alegeți un număr mult mai mare timeseries_ids pentru date decât atunci când indexul inversat este partiționat în timp.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Cum am rezolvat această problemă? Am rezolvat-o într-un mod original - prin stocarea mai multor identificatori de serie cronologică în fiecare intrare de index inversată în loc de un singur identificator. Adică avem o cheie label=value, care apare în fiecare serie temporală. Și acum salvăm mai multe timeseries_ids într-o singură intrare.

Iată un exemplu. Anterior aveam N intrări, dar acum avem o intrare al cărei prefix este același cu toate celelalte. Pentru intrarea anterioară, valoarea conține toate ID-urile seriilor temporale.

Acest lucru a făcut posibilă creșterea vitezei de scanare a unui astfel de index inversat de până la 10 ori. Și ne-a permis să reducem consumul de memorie pentru cache, pentru că acum stocăm șirul label=value doar o dată în cache împreună de N ori. Și această linie poate fi mare dacă stocați linii lungi în etichete și etichete, pe care Kubernetes îi place să le împingă acolo.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

O altă opțiune pentru accelerarea căutării pe un index inversat este fragmentarea. Crearea mai multor indici inversați în loc de unul și împărțirea datelor între ei prin cheie. Acesta este un set key=value aburi. Adică obținem mai mulți indecși inversați independenți, pe care îi putem interoga în paralel pe mai multe procesoare. Implementările anterioare au permis operarea numai în modul cu un singur procesor, adică scanarea datelor pe un singur nucleu. Această soluție vă permite să scanați date pe mai multe nuclee simultan, așa cum îi place să facă ClickHouse. Acesta este ceea ce intenționăm să implementăm.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Acum să revenim la oile noastre - la funcția de intersecție timeseries_ids. Să luăm în considerare ce implementări pot exista. Această funcție vă permite să găsiți timeseries_ids pentru un set dat label=value.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Prima opțiune este o implementare naivă. Două bucle imbricate. Aici obținem intrarea funcției intersectInts doua felii - a и b. La ieșire, ar trebui să ne returneze intersecția acestor felii.

O implementare naivă arată așa. Iterăm peste toate valorile de la felie a, în interiorul acestei bucle parcurgem toate valorile slice b. Și le comparăm. Dacă se potrivesc, atunci am găsit o intersecție. Și salvează-l în result.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Care sunt dezavantajele? Complexitatea pătratică este principalul său dezavantaj. De exemplu, dacă dimensiunile dvs. sunt slice a и b câte un milion, atunci această funcție nu vă va returna niciodată un răspuns. Pentru că va trebui să facă un trilion de iterații, ceea ce este mult chiar și pentru computerele moderne.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

A doua implementare se bazează pe hartă. Cream harta. Am pus toate valorile de la felie în această hartă a. Apoi trecem prin felie într-o buclă separată b. Și verificăm dacă această valoare este din slice b în hartă. Dacă există, adăugați-l la rezultat.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Care sunt beneficiile? Avantajul este că există doar complexitate liniară. Adică, funcția se va executa mult mai rapid pentru felii mai mari. Pentru o porțiune de un milion, această funcție se va executa în 2 milioane de iterații, spre deosebire de trilioanele de iterații ale funcției anterioare.

Dezavantajul este că această funcție necesită mai multă memorie pentru a crea această hartă.

Al doilea dezavantaj este supraîncărcarea mare pentru hashing. Acest dezavantaj nu este foarte evident. Și pentru noi nici nu era foarte evident, așa că la început în VictoriaMetrics implementarea intersecției a fost printr-o hartă. Dar apoi profilarea a arătat că timpul procesorului principal este cheltuit scriind pe hartă și verificând prezența unei valori în această hartă.

De ce se pierde timpul CPU în aceste locuri? Pentru că Go efectuează o operație de hashing pe aceste linii. Adică calculează hash-ul cheii pentru a o accesa apoi la un index dat din HashMap. Operația de calcul hash este finalizată în zeci de nanosecunde. Acest lucru este lent pentru VictoriaMetrics.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Am decis să implementez un set de biți optimizat special pentru acest caz. Așa arată acum intersecția a două felii. Aici creăm un set de biți. Adăugăm elemente din prima felie. Apoi verificăm prezența acestor elemente în a doua felie. Și adăugați-le la rezultat. Adică, nu este aproape deloc diferit de exemplul anterior. Singurul lucru aici este că am înlocuit accesul la hartă cu funcții personalizate add и has.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

La prima vedere, se pare că acest lucru ar trebui să funcționeze mai lent, dacă anterior a fost folosită o hartă standard acolo, iar apoi sunt apelate și alte funcții, dar profilarea arată că acest lucru funcționează de 10 ori mai repede decât harta standard în cazul VictoriaMetrics.

În plus, utilizează mult mai puțină memorie în comparație cu implementarea hărții. Pentru că stocăm biți aici în loc de valori de opt octeți.

Dezavantajul acestei implementări este că nu este atât de evidentă, nici banală.

Un alt dezavantaj pe care mulți s-ar putea să nu-l observe este că această implementare poate să nu funcționeze bine în unele cazuri. Adică este optimizat pentru un caz specific, pentru acest caz de intersecție a ID-urilor seriei temporale VictoriaMetrics. Acest lucru nu înseamnă că este potrivit pentru toate cazurile. Dacă este folosit incorect, nu vom obține o creștere a performanței, ci o eroare de memorie lipsită și o încetinire a performanței.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Să luăm în considerare implementarea acestei structuri. Dacă vrei să cauți, se află în sursele VictoriaMetrics, în folder lib/uint64set. Este optimizat special pentru cazul VictoriaMetrics, unde timeseries_id este o valoare de 64 de biți, unde primii 32 de biți sunt practic constanți și doar ultimii 32 de biți se modifică.

Această structură de date nu este stocată pe disc, funcționează doar în memorie.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Iată API-ul său. Nu este foarte complicat. API-ul este adaptat în mod specific unui exemplu specific de utilizare a VictoriaMetrics. Adică, nu există funcții inutile aici. Iată funcțiile care sunt utilizate în mod explicit de VictoriaMetrics.

Există funcții add, care adaugă noi valori. Există o funcție has, care verifică pentru noi valori. Și există o funcție del, care elimină valori. Există o funcție de ajutor len, care returnează dimensiunea setului. Funcţie clone clonează foarte mult. Și funcționalitate appendto convertește acest set în felie timeseries_ids.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Așa arată implementarea acestei structuri de date. setul are doua elemente:

  • ItemsCount este un câmp de ajutor pentru a returna rapid numărul de elemente dintr-un set. Ar fi posibil să se facă fără acest câmp auxiliar, dar a trebuit să fie adăugat aici, deoarece VictoriaMetrics interogează adesea lungimea setului de biți în algoritmii săi.

  • Al doilea câmp este buckets. Aceasta este o felie din structură bucket32. Fiecare structură stochează hi camp. Aceștia sunt cei 32 de biți superiori. Și două felii - b16his и buckets de bucket16 structurilor.

Aici sunt stocați primii 16 biți ai celei de-a doua părți a structurii pe 64 de biți. Și aici seturile de biți sunt stocate pentru cei 16 biți inferiori ai fiecărui octet.

Bucket64 constă dintr-o matrice uint64. Lungimea este calculată folosind aceste constante. Într-una bucket16 maxim poate fi stocat 2^16=65536 pic. Dacă împărțiți acest lucru la 8, atunci este de 8 kiloocteți. Dacă împărțiți din nou la 8, este 1000 uint64 sens. Acesta este Bucket16 – aceasta este structura noastră de 8 kiloocteți.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Să vedem cum este implementată una dintre metodele acestei structuri pentru adăugarea unei noi valori.

Totul începe cu uint64 sensuri. Calculăm cei 32 de biți superiori, calculăm cei 32 de biți inferiori. Să trecem prin toate buckets. Comparăm primii 32 de biți din fiecare găleată cu valoarea adăugată. Și dacă se potrivesc, atunci numim funcția add în structura b32 buckets. Și adăugați cei 32 de biți inferiori acolo. Și dacă s-a întors true, atunci asta înseamnă că am adăugat o asemenea valoare acolo și nu am avut o asemenea valoare. Dacă se întoarce false, atunci un astfel de sens exista deja. Apoi creștem numărul de elemente din structură.

Dacă nu l-am găsit pe cel de care aveți nevoie bucket cu valoarea înaltă necesară, apoi numim funcția addAlloc, care va produce unul nou bucket, adăugându-l la structura găleții.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Aceasta este implementarea funcției b32.add. Este similar cu implementarea anterioară. Calculăm cei mai semnificativi 16 biți, cei mai puțin semnificativi 16 biți.

Apoi trecem prin toți cei 16 biți de sus. Găsim meciuri. Și dacă există o potrivire, numim metoda add, pentru care o vom lua în considerare pe pagina următoare bucket16.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Și aici este cel mai scăzut nivel, care ar trebui optimizat cât mai mult posibil. Calculăm pentru uint64 valoarea id în slice bit și, de asemenea bitmask. Aceasta este o mască pentru o valoare dată de 64 de biți, care poate fi folosită pentru a verifica prezența acestui bit sau pentru a-l seta. Verificăm dacă acest bit este setat și îl setăm și returnăm prezența. Aceasta este implementarea noastră, care ne-a permis să grăbim de 10 ori operațiunea de intersectare a ID-urilor seriilor temporale în comparație cu hărțile convenționale.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Pe lângă această optimizare, VictoriaMetrics are multe alte optimizări. Cele mai multe dintre aceste optimizări au fost adăugate dintr-un motiv, dar după profilarea codului în producție.

Aceasta este regula principală de optimizare - nu adăugați optimizare presupunând că va exista un blocaj aici, deoarece s-ar putea dovedi că nu va exista un blocaj acolo. Optimizarea degradează de obicei calitatea codului. Prin urmare, merită optimizat numai după profilare și de preferință în producție, astfel încât acestea să fie date reale. Dacă cineva este interesat, puteți să vă uitați la codul sursă VictoriaMetrics și să explorați alte optimizări care există.

Accesați optimizările în VictoriaMetrics. Alexandru Valyalkin

Am o întrebare despre bitset. Foarte similar cu implementarea C++ vector bool, set de biți optimizat. Ai luat implementarea de acolo?

Nu, nu de acolo. Când am implementat acest set de biți, am fost ghidat de cunoștințele despre structura acestor serii temporale de id-uri, care sunt utilizate în VictoriaMetrics. Și structura lor este astfel încât cei 32 de biți superiori sunt practic constanți. Cei 32 de biți inferiori pot fi modificați. Cu cât bitul este mai mic, cu atât se poate schimba mai des. Prin urmare, această implementare este optimizată special pentru această structură de date. Implementarea C++, din câte știu eu, este optimizată pentru cazul general. Dacă optimizați pentru cazul general, aceasta înseamnă că nu va fi cel mai optim pentru un caz specific.

De asemenea, vă sfătuiesc să urmăriți reportajul lui Alexey Milovid. Acum aproximativ o lună, a vorbit despre optimizarea în ClickHouse pentru specializări specifice. El spune doar că, în cazul general, o implementare C++ sau o altă implementare este adaptată pentru a funcționa bine în medie într-un spital. Poate funcționa mai rău decât o implementare specifică cunoștințelor precum a noastră, unde știm că primii 32 de biți sunt în mare parte constanti.

Am o a doua întrebare. Care este diferența fundamentală față de InfluxDB?

Există multe diferențe fundamentale. În ceea ce privește performanța și consumul de memorie, InfluxDB în teste arată un consum de memorie de 10 ori mai mare pentru seriile temporale cu cardinalitate ridicată, când ai o mulțime de ele, de exemplu, milioane. De exemplu, VictoriaMetrics consumă 1 GB pe milion de rânduri active, în timp ce InfluxDB consumă 10 GB. Și asta e o mare diferență.

A doua diferență fundamentală este că InfluxDB are limbaje de interogare ciudate - Flux și InfluxQL. Ele nu sunt foarte convenabile pentru a lucra cu serii de timp în comparație cu PromQL, care este susținut de VictoriaMetrics. PromQL este un limbaj de interogare de la Prometheus.

Și încă o diferență este că InfluxDB are un model de date ușor ciudat, în care fiecare linie poate stoca mai multe câmpuri cu un set diferit de etichete. Aceste linii sunt împărțite în continuare în diferite tabele. Aceste complicații suplimentare complică munca ulterioară cu această bază de date. Este greu de susținut și de înțeles.

În VictoriaMetrics totul este mult mai simplu. Acolo, fiecare serie temporală este o valoare-cheie. Valoarea este un set de puncte - (timestamp, value), iar cheia este setul label=value. Nu există nicio separare între câmpuri și măsurători. Vă permite să selectați orice date și apoi să combinați, să adăugați, să scădeți, să înmulțiți, să împărțiți, spre deosebire de InfluxDB, unde calculele între diferite rânduri încă nu sunt implementate din câte știu eu. Chiar dacă sunt implementate, este dificil, trebuie să scrii mult cod.

Am o intrebare clarificatoare. Am înțeles bine că a existat un fel de problemă despre care ați vorbit, că acest index inversat nu se potrivește în memorie, deci există partiționare acolo?

În primul rând, am arătat o implementare naivă a unui index inversat pe o hartă Go standard. Această implementare nu este potrivită pentru baze de date deoarece acest index inversat nu este salvat pe disc, iar baza de date trebuie să salveze pe disc, astfel încât aceste date să rămână disponibile la repornire. În această implementare, când reporniți aplicația, indexul dvs. inversat va dispărea. Și veți pierde accesul la toate datele deoarece nu le veți putea găsi.

Buna ziua! Multumesc pentru raport! Numele meu este Pavel. Sunt din Wildberries. Am câteva întrebări pentru tine. Întrebarea unu. Credeți că dacă ați fi ales un alt principiu la construirea arhitecturii aplicației dvs. și ați fi partiționat datele de-a lungul timpului, atunci poate că ați fi putut să intersectați datele atunci când căutați, doar pe baza faptului că o partiție conține date pentru una perioadă de timp, adică într-un interval de timp și nu ar trebui să vă faceți griji pentru faptul că piesele dvs. sunt împrăștiate diferit? Întrebarea numărul 2 - deoarece implementați un algoritm similar cu set de biți și orice altceva, atunci poate ați încercat să utilizați instrucțiunile procesorului? Poate ai încercat astfel de optimizări?

La al doilea iti raspund imediat. Încă nu am ajuns în acel punct. Dar dacă va fi nevoie, vom ajunge acolo. Și primul, care a fost întrebarea?

Ai discutat două scenarii. Și au spus că au ales-o pe a doua cu o implementare mai complexă. Și nu l-au preferat pe primul, unde datele sunt împărțite în funcție de timp.

Da. În primul caz, volumul total al indexului ar fi mai mare, deoarece în fiecare partiție ar trebui să stocăm date duplicat pentru acele serii temporale care continuă prin toate aceste partiții. Și dacă rata de pierdere a seriilor dvs. de timp este mică, adică aceleași serii sunt utilizate în mod constant, atunci în primul caz am pierde mult mai mult din cantitatea de spațiu pe disc ocupată în comparație cu al doilea caz.

Și așa - da, partiționarea timpului este o opțiune bună. Prometheus îl folosește. Dar Prometeu are un alt dezavantaj. Când îmbină aceste date, trebuie să păstreze în memorie metainformațiile pentru toate etichetele și seriile temporale. Prin urmare, dacă datele pe care le îmbină sunt mari, atunci consumul de memorie crește foarte mult în timpul îmbinării, spre deosebire de VictoriaMetrics. La îmbinare, VictoriaMetrics nu consumă deloc memorie; sunt consumați doar câțiva kiloocteți, indiferent de dimensiunea pieselor de date îmbinate.

Algoritmul pe care îl utilizați folosește memoria. Acesta marchează etichete de serie temporală care conțin valori. Și în acest fel verificați prezența asociată într-o matrice de date și în alta. Și înțelegi dacă intersectarea a avut loc sau nu. De obicei, bazele de date implementează cursori și iteratoare care stochează conținutul lor curent și rulează prin datele sortate datorită complexității simple a acestor operațiuni.

De ce nu folosim cursoare pentru a parcurge datele?

Da.

Stocăm rânduri sortate în LevelDB sau mergeset. Putem muta cursorul și găsim intersecția. De ce nu-l folosim? Pentru că e lent. Pentru că cursoarele înseamnă că trebuie să apelați o funcție pentru fiecare linie. Un apel de funcție este de 5 nanosecunde. Și dacă aveți 100 de linii, atunci se dovedește că petrecem o jumătate de secundă doar apelând funcția.

Există așa ceva, da. Și ultima mea întrebare. Întrebarea poate suna puțin ciudată. De ce nu este posibil să citiți toate agregatele necesare în momentul în care sosesc datele și să le salvați în forma cerută? De ce să salvați volume uriașe în unele sisteme precum VictoriaMetrics, ClickHouse etc. și apoi să petreceți mult timp pe ele?

Voi da un exemplu pentru a fi mai clar. Să spunem cum funcționează un mic vitezometru de jucărie? Înregistrează distanța pe care ați parcurs-o, adăugând-o tot timpul la o valoare, iar a doua - o dată. Și împarte. Și obține viteza medie. Poți face cam același lucru. Adaugă toate faptele necesare din mers.

Bine, înțeleg întrebarea. Exemplul tău are locul lui. Dacă știți de ce agregate aveți nevoie, atunci aceasta este cea mai bună implementare. Dar problema este că oamenii salvează aceste valori, unele date în ClickHouse și încă nu știu cum le vor agrega și filtra în viitor, așa că trebuie să salveze toate datele brute. Dar dacă știți că trebuie să calculați ceva în medie, atunci de ce să nu îl calculați în loc să stocați o grămadă de valori brute acolo? Dar asta doar dacă știi exact de ce ai nevoie.

Apropo, bazele de date pentru stocarea seriilor temporale acceptă numărarea agregatelor. De exemplu, Prometheus susține reguli de înregistrare. Adică, acest lucru se poate face dacă știți de ce unități veți avea nevoie. VictoriaMetrics nu are încă acest lucru, dar este de obicei precedat de Prometheus, în care acest lucru se poate face în regulile de recodificare.

De exemplu, într-o lucrare anterioară, a fost necesar să se contorizeze numărul de evenimente dintr-o fereastră glisantă în ultima oră. Problema este că a trebuit să creez o implementare personalizată în Go, adică un serviciu pentru numărarea acestui lucru. Acest serviciu a fost în cele din urmă non-trivial, deoarece este dificil de calculat. Implementarea poate fi simplă dacă trebuie să numărați unele agregate la intervale de timp fixe. Dacă doriți să numărați evenimentele într-o fereastră glisantă, atunci nu este atât de simplu pe cât pare. Cred că acest lucru nu a fost încă implementat în ClickHouse sau în bazele de date de serie temporală, deoarece este greu de implementat.

Și încă o întrebare. Vorbeam doar despre medie și mi-am amintit că a existat odată un astfel de lucru precum Grafitul cu un backend Carbon. Și a știut să subțire datele vechi, adică să lase un punct pe minut, un punct pe oră etc. În principiu, acest lucru este destul de convenabil dacă avem nevoie de date brute, relativ vorbind, pentru o lună și orice altceva poate fi subtie . Dar Prometheus și VictoriaMetrics nu acceptă această funcționalitate. Se plănuiește să o susțină? Dacă nu, de ce nu?

Multumesc pentru intrebare. Utilizatorii noștri pun această întrebare periodic. Ei întreabă când vom adăuga suport pentru downsampling. Există mai multe probleme aici. În primul rând, fiecare utilizator înțelege downsampling ceva diferit: cineva vrea să obțină orice punct arbitrar pe un interval dat, cineva vrea valori maxime, minime, medii. Dacă multe sisteme scriu date în baza de date, atunci nu le puteți combina pe toate. Este posibil ca fiecare sistem să necesite o subțiere diferită. Și acest lucru este greu de implementat.

Și al doilea lucru este că VictoriaMetrics, precum ClickHouse, este optimizat pentru a lucra pe cantități mari de date brute, astfel încât să poată arunca cu lopata un miliard de linii în mai puțin de o secundă dacă aveți multe nuclee în sistem. Scanarea punctelor din seria temporală în VictoriaMetrics – 50 de puncte pe secundă per nucleu. Și această performanță se extinde la nucleele existente. Adică, dacă ai 000 de nuclee, de exemplu, vei scana un miliard de puncte pe secundă. Și această proprietate a VictoriaMetrics și ClickHouse reduce nevoia de subsamling.

O altă caracteristică este că VictoriaMetrics comprimă efectiv aceste date. Compresia în medie în producție este de la 0,4 la 0,8 octeți pe punct. Fiecare punct este un marcaj de timp + valoare. Și este comprimat în mai puțin de un octet în medie.

Serghei. Am o întrebare. Care este cuantumul minim al timpului de înregistrare?

O milisecundă. Am avut recent o conversație cu alți dezvoltatori de baze de date în serie de timp. Intervalul lor minim de timp este de o secundă. Și în Graphite, de exemplu, este și o secundă. În OpenTSDB este, de asemenea, o secundă. InfluxDB are precizie de nanosecundă. În VictoriaMetrics este de o milisecundă, pentru că în Prometheus este de o milisecundă. Și VictoriaMetrics a fost dezvoltat inițial ca stocare la distanță pentru Prometheus. Dar acum poate salva date din alte sisteme.

Persoana cu care am vorbit spune că are acuratețe de la secundă la secundă - este suficient pentru ei, deoarece depinde de tipul de date care sunt stocate în baza de date a seriilor temporale. Dacă acestea sunt date DevOps sau date din infrastructură, unde le colectați la intervale de 30 de secunde, pe minut, atunci precizia secundă este suficientă, nu aveți nevoie de nimic mai puțin. Și dacă colectați aceste date din sistemele de tranzacționare de înaltă frecvență, atunci aveți nevoie de precizie în nanosecunde.

Precizia în milisecundă în VictoriaMetrics este potrivită și pentru cazul DevOps și poate fi potrivită pentru majoritatea cazurilor pe care le-am menționat la începutul raportului. Singurul lucru pentru care ar putea să nu fie potrivit este sistemele de tranzacționare de înaltă frecvență.

Mulțumesc! Si inca o intrebare. Ce este compatibilitatea în PromQL?

Compatibilitate inversă completă. VictoriaMetrics acceptă pe deplin PromQL. În plus, adaugă funcționalități avansate suplimentare în PromQL, care se numește MetricsQL. Există o discuție pe YouTube despre această funcționalitate extinsă. Am vorbit la reuniunea de monitorizare din primăvară la Sankt Petersburg.

Canal de telegramă VictoriaMetrics.

Numai utilizatorii înregistrați pot participa la sondaj. Loghează-te, Vă rog.

Ce vă împiedică să treceți la VictoriaMetrics ca stocare pe termen lung pentru Prometheus? (Scrieți în comentarii, îl voi adăuga la sondaj))

  • 71,4%Nu folosesc Prometheus5

  • 28,6%Nu știam despre VictoriaMetrics2

Au votat 7 utilizatori. 12 utilizatori s-au abținut.

Sursa: www.habr.com

Adauga un comentariu