Predlagam, da preberete prepis poročila Aleksandra Valjalkina konec leta 2019 »Optimizacije Go v VictoriaMetrics«
Tukaj je povezava do videoposnetka tega poročila -
Povej nam o sebi. Jaz sem Alexander Valyalkin. Tukaj fast
ali z quick
predpono.
Trenutno delam na VictoriaMetrics. Kaj je in kaj počnem tam? O tem bom govoril v tej predstavitvi.
Osnutek poročila je naslednji:
- Najprej vam bom povedal, kaj je VictoriaMetrics.
- Potem vam bom povedal, kaj so časovne vrste.
- Nato vam bom povedal, kako deluje podatkovna zbirka časovnih vrst.
- Nato vam bom povedal o arhitekturi baze podatkov: iz česa je sestavljena.
- In potem preidimo na optimizacije, ki jih ima VictoriaMetrics. To je optimizacija za invertni indeks in optimizacija za implementacijo bitset v Go.
Ali kdo v občinstvu ve, kaj je VictoriaMetrics? Vau, veliko ljudi že ve. To je dobra novica. Za tiste, ki ne vedo, je to baza časovnih vrst. Temelji na arhitekturi ClickHouse, na nekaterih podrobnostih implementacije ClickHouse. Na primer, kot so: MergeTree, vzporedni izračun na vseh razpoložljivih procesorskih jedrih in optimizacija zmogljivosti z delom na podatkovnih blokih, ki so v predpomnilniku procesorja.
VictoriaMetrics zagotavlja boljše stiskanje podatkov kot druge baze podatkov časovnih vrst.
Prilagodi se navpično - to pomeni, da lahko dodate več procesorjev, več RAM-a na en računalnik. VictoriaMetrics bo uspešno uporabila te razpoložljive vire in izboljšala linearno produktivnost.
VictoriaMetrics se skalira tudi vodoravno – to pomeni, da lahko dodate dodatna vozlišča v gručo VictoriaMetrics in njegova zmogljivost se bo povečala skoraj linearno.
Kot ste uganili, je VictoriaMetrics hitra zbirka podatkov, ker ne morem pisati drugih. In to je napisano v Go, zato o tem govorim na tem srečanju.
Kdo ve, kaj je časovna vrsta? Pozna tudi veliko ljudi. Časovni niz je niz parov (timestamp, значение)
, kjer so ti pari razvrščeni po času. Vrednost je število s plavajočo vejico – float64.
Vsaka časovna vrsta je edinstveno identificirana s ključem. Iz česa je sestavljen ta ključ? Sestavljen je iz neprazne množice parov ključ-vrednost.
Tukaj je primer časovne serije. Ključ te serije je seznam parov: __name__="cpu_usage"
je ime metrike, instance="my-server"
- to je računalnik, na katerem se zbira ta metrika, datacenter="us-east"
- to je podatkovni center, kjer se nahaja ta računalnik.
Na koncu smo dobili ime časovne serije, sestavljeno iz treh parov ključ-vrednost. Ta ključ ustreza seznamu parov (timestamp, value)
. t1, t3, t3, ..., tN
- to so časovni žigi, 10, 20, 12, ..., 15
— ustrezne vrednosti. To je poraba procesorja v danem času za dano serijo.
Kje je mogoče uporabiti časovne vrste? Ima kdo kakšno idejo?
- V DevOps lahko merite CPE, RAM, omrežje, rps, število napak itd.
- IoT - merimo lahko temperaturo, pritisk, geo koordinate in še kaj.
- Tudi finance – spremljamo lahko cene za vse vrste delnic in valut.
- Poleg tega se časovne vrste lahko uporabljajo pri spremljanju proizvodnih procesov v tovarnah. Imamo uporabnike, ki uporabljajo VictoriaMetrics za spremljanje vetrnih turbin za robote.
- Časovne vrste so uporabne tudi za zbiranje informacij iz senzorjev različnih naprav. Na primer za motor; za merjenje tlaka v pnevmatikah; za merjenje hitrosti, razdalje; za merjenje porabe bencina itd.
- Časovne vrste se lahko uporabljajo tudi za spremljanje letal. Vsako letalo ima črno skrinjico, ki zbira časovne vrste za različne parametre zdravstvenega stanja letala. Časovne vrste se uporabljajo tudi v vesoljski industriji.
- Zdravstvo je krvni tlak, pulz itd.
Morda obstaja več aplikacij, na katere sem pozabil, vendar upam, da razumete, da se časovne vrste aktivno uporabljajo v sodobnem svetu. In obseg njihove uporabe vsako leto narašča.
Zakaj potrebujete bazo časovnih vrst? Zakaj ne morete uporabiti navadne relacijske baze podatkov za shranjevanje časovnih vrst?
Kajti časovne vrste običajno vsebujejo veliko količino informacij, ki jih je težko shraniti in obdelati v običajnih bazah podatkov. Zato so se pojavile specializirane zbirke podatkov za časovne vrste. Te baze učinkovito shranjujejo točke (timestamp, value)
z danim ključem. Zagotavljajo API za branje shranjenih podatkov po ključu, po enem paru ključ-vrednost ali po več parih ključ-vrednost ali po regularnem izrazu. Na primer, če želite ugotoviti obremenitev procesorja vseh vaših storitev v podatkovnem centru v Ameriki, potem morate uporabiti to psevdo poizvedbo.
Podatkovne zbirke časovnih vrst običajno zagotavljajo specializirane poizvedovalne jezike, ker SQL časovnih vrst ni zelo primeren. Čeprav obstajajo baze podatkov, ki podpirajo SQL, ni zelo primeren. Poizvedovalni jeziki, kot je npr
Tako je videti sodobna arhitektura baze podatkov časovnih vrst na primeru VictoriaMetrics.
Sestavljen je iz dveh delov. To je shramba za invertni indeks in shramba za vrednosti časovnih vrst. Ti repozitoriji so ločeni.
Ko v bazo podatkov prispe nov zapis, najprej dostopamo do obrnjenega indeksa, da poiščemo identifikator časovne vrste za dani niz label=value
za dano metriko. Ta identifikator poiščemo in vrednost shranimo v shrambo podatkov.
Ko pride zahteva za pridobitev podatkov iz TSDB, gremo najprej na obrnjeni indeks. Dobimo vse timeseries_ids
zapisov, ki ustrezajo temu nizu label=value
. In potem dobimo vse potrebne podatke iz podatkovnega skladišča, indeksirane s timeseries_ids
.
Oglejmo si primer, kako baza podatkov časovnih vrst obdela dohodno poizvedbo za izbiro.
- Najprej dobi vse
timeseries_ids
iz obrnjenega indeksa, ki vsebuje podane parelabel=value
, ali zadovoljijo dani regularni izraz. - Nato pridobi vse podatkovne točke iz pomnilnika podatkov v danem časovnem intervalu za najdene
timeseries_ids
. - Po tem baza podatkov izvede nekaj izračunov na teh podatkovnih točkah glede na zahtevo uporabnika. In po tem vrne odgovor.
V tej predstavitvi vam bom povedal o prvem delu. To je iskanje timeseries_ids
z obrnjenim indeksom. Drugi del in tretji del si lahko ogledate kasneje
Preidimo na obrnjeni indeks. Mnogi morda mislijo, da je to preprosto. Kdo ve, kaj je invertni indeks in kako deluje? Oh, ni več toliko ljudi. Poskusimo razumeti, kaj je to.
Pravzaprav je preprosto. To je preprosto slovar, ki preslika ključ v vrednost. Kaj je ključ? Ta par label=value
Če label
и value
- to so vrstice. In vrednosti so nabor timeseries_ids
, ki vključuje dani par label=value
.
Obrnjen indeks vam omogoča, da hitro najdete vse timeseries_ids
, ki so dali label=value
.
Omogoča tudi hitro iskanje timeseries_ids
časovne vrste za več parov label=value
, ali za pare label=regexp
. Kako se to zgodi? Z iskanjem presečišča množice timeseries_ids
za vsak par label=value
.
Oglejmo si različne izvedbe obrnjenega indeksa. Začnimo z najpreprostejšo naivno izvedbo. Izgleda takole.
Funkcija getMetricIDs
dobi seznam nizov. Vsaka vrstica vsebuje label=value
. Ta funkcija vrne seznam metricIDs
.
Kako deluje? Tukaj imamo globalno spremenljivko, imenovano invertedIndex
. To je običajni slovar (map
), ki bo preslikal niz v rezino int. Vrstica vsebuje label=value
.
Izvedba funkcije: get metricIDs
za prvo label=value
, potem gremo skozi vse ostalo label=value
, razumemo metricIDs
za njih. In pokličite funkcijo intersectInts
, o katerem bo govora v nadaljevanju. In ta funkcija vrne presečišče teh seznamov.
Kot lahko vidite, implementacija obrnjenega indeksa ni zelo zapletena. Ampak to je naivna izvedba. Kakšne slabosti ima? Glavna pomanjkljivost naivne izvedbe je, da je tako obrnjen indeks shranjen v RAM-u. Po ponovnem zagonu aplikacije izgubimo ta indeks. Ta indeks se ne shrani na disk. Takšen obrnjen indeks verjetno ne bo primeren za bazo podatkov.
Druga pomanjkljivost je povezana tudi s spominom. Obrnjeni indeks se mora prilegati RAM-u. Če preseže velikost RAM-a, potem bomo očitno dobili napako - zmanjkalo pomnilnika. In program ne bo deloval.
Ta problem je mogoče rešiti z že pripravljenimi rešitvami, kot je npr
Skratka, potrebujemo bazo podatkov, ki nam omogoča hitro izvedbo treh operacij.
- Prva operacija je snemanje
ключ-значение
v to bazo podatkov. To počne zelo hitro, kjerключ-значение
so poljubni nizi. - Druga operacija je hitro iskanje vrednosti z danim ključem.
- In tretja operacija je hitro iskanje vseh vrednosti po dani predponi.
LevelDB in RocksDB - ti bazi podatkov sta razvila Google in Facebook. Najprej je prišel LevelDB. Potem so fantje iz Facebooka vzeli LevelDB in ga začeli izboljševati, naredili so RocksDB. Zdaj skoraj vse notranje baze podatkov delujejo na RocksDB znotraj Facebooka, vključno s tistimi, ki so bile prenesene v RocksDB in MySQL. Poimenovali so ga
Obrnjen indeks je mogoče implementirati z uporabo LevelDB. Kako narediti? Shranjujemo kot ključ label=value
. In vrednost je identifikator časovne serije, kjer je par prisoten label=value
.
Če imamo veliko časovnih vrst z danim parom label=value
, potem bo v tej zbirki podatkov veliko vrstic z enakim in različnim ključem timeseries_ids
. Da bi dobili seznam vseh timeseries_ids
, ki se začnejo s tem label=prefix
, opravimo pregled obsega, za katerega je ta zbirka podatkov optimizirana. To pomeni, da izberemo vse vrstice, ki se začnejo z label=prefix
in dobite potrebno timeseries_ids
.
Tukaj je vzorčna izvedba, kako bi to izgledalo v Go. Imamo obrnjen indeks. To je LevelDB.
Funkcija je enaka kot pri naivni izvedbi. Skoraj vrstico za vrstico ponavlja naivno izvedbo. Edina točka je, da namesto da bi se obrnil na map
dostopamo do obrnjenega indeksa. Dobimo vse vrednosti za prvo label=value
. Nato gremo skozi vse preostale pare label=value
in zanje pridobite ustrezne nize metričnih ID-jev. Nato najdemo križišče.
Zdi se, da je vse v redu, vendar ima ta rešitev pomanjkljivosti. VictoriaMetrics je sprva implementiral invertni indeks, ki temelji na LevelDB. Toda na koncu sem moral opustiti.
Zakaj? Ker je LevelDB počasnejši od naivne izvedbe. V naivni izvedbi z danim ključem takoj pridobimo celotno rezino metricIDs
. To je zelo hiter postopek - celotna rezina je pripravljena za uporabo.
V LevelDB vsakič, ko se pokliče funkcija GetValues
iti morate skozi vse vrstice, ki se začnejo z label=value
. In dobite vrednost za vsako vrstico timeseries_ids
. Takih timeseries_ids
zberite rezino teh timeseries_ids
. Očitno je to veliko počasneje kot preprost dostop do običajnega zemljevida s ključem.
Druga pomanjkljivost je, da je LevelDB napisan v C. Klicanje funkcij C iz Go ni zelo hitro. Traja na stotine nanosekund. To ni zelo hitro, ker je v primerjavi z običajnim klicem funkcije, napisanim v go, ki traja 1–5 nanosekund, razlika v zmogljivosti desetkratna. Za VictoriaMetrics je bila to usodna napaka :)
Zato sem napisal lastno implementacijo obrnjenega indeksa. In poklical jo je
Mergeset temelji na podatkovni strukturi MergeTree. Ta struktura podatkov je izposojena pri ClickHouse. Očitno bi moral biti mergeset optimiziran za hitro iskanje timeseries_ids
po danem ključu. Mergeset je v celoti napisan v Go. Lahko vidiš
API mergeset je zelo podoben LevelDB in RocksDB. To pomeni, da vam omogoča, da tam hitro shranite nove zapise in hitro izberete zapise po dani predponi.
O slabostih mergeseta bomo govorili kasneje. Zdaj pa se pogovorimo o tem, kakšne težave so se pojavile pri VictoriaMetrics v proizvodnji pri izvajanju obrnjenega indeksa.
Zakaj so nastali?
Prvi razlog je visoka stopnja odliva. Prevedeno v ruščino, je to pogosta sprememba časovnih vrst. Takrat se časovna vrsta konča in začne nova serija ali pa se začnejo številne nove časovne vrste. In to se pogosto zgodi.
Drugi razlog je veliko število časovnih vrst. Na začetku, ko je spremljanje postajalo vse bolj priljubljeno, je bilo število časovnih vrst majhno. Na primer, za vsak računalnik morate spremljati CPU, pomnilnik, omrežje in obremenitev diska. 4 časovne serije na računalnik. Recimo, da imate 100 računalnikov in 400 časovnih vrst. To je zelo malo.
Sčasoma so ljudje ugotovili, da lahko merijo bolj natančne informacije. Na primer, izmerite obremenitev ne celotnega procesorja, temveč ločeno vsakega jedra procesorja. Če imate 40 procesorskih jeder, potem imate 40-krat več časovnih vrst za merjenje obremenitve procesorja.
A to še ni vse. Vsako procesorsko jedro ima lahko več stanj, na primer stanje mirovanja, ko je nedejavno. In tudi delo v uporabniškem prostoru, delo v prostoru jedra in drugih stanjih. In vsako takšno stanje je mogoče izmeriti tudi kot ločeno časovno vrsto. S tem se dodatno poveča število vrstic za 7-8 krat.
Iz ene metrike smo dobili 40 x 8 = 320 metrik za samo en računalnik. Če pomnožimo s 100, dobimo 32 namesto 000.
Potem je prišel Kubernetes. Še huje je postalo, ker lahko Kubernetes gosti veliko različnih storitev. Vsaka storitev v Kubernetesu je sestavljena iz številnih sklopov. In vse to je treba spremljati. Poleg tega imamo stalno uvajanje novih različic vaših storitev. Za vsako novo različico je treba ustvariti nove časovne vrste. Posledično število časovnih vrst eksponentno narašča in soočeni smo s problemom velikega števila časovnih vrst, kar imenujemo visoka kardinalnost. VictoriaMetrics se s tem uspešno spopada v primerjavi z drugimi bazami časovnih vrst.
Oglejmo si podrobneje visoko stopnjo odliva. Kaj povzroča visoko stopnjo osipa v proizvodnji? Ker se nekateri pomeni oznak in oznak nenehno spreminjajo.
Na primer, vzemite Kubernetes, ki ima koncept deployment
, tj. ko je uvedena nova različica vaše aplikacije. Iz neznanega razloga so se razvijalci Kubernetesa odločili, da nalepki dodajo ID uvedbe.
Kaj je to vodilo? Poleg tega se z vsako novo uvedbo vse stare časovne vrste prekinejo in namesto njih se začnejo nove časovne serije z novo vrednostjo oznake deployment_id
. Takšnih vrstic je lahko več sto tisoč in celo milijone.
Pri vsem tem je pomembno, da skupno število časovnih vrst raste, vendar ostaja število časovnih vrst, ki so trenutno aktivne in prejemajo podatke, konstantno. To stanje se imenuje visoka stopnja odliva.
Glavna težava visoke stopnje osipa je zagotoviti konstantno hitrost iskanja za vse časovne vrste za dani nabor oznak v določenem časovnem intervalu. Običajno je to časovni interval za zadnjo uro ali zadnji dan.
Kako rešiti ta problem? Tukaj je prva možnost. To je razdelitev obrnjenega indeksa na neodvisne dele skozi čas. To pomeni, da mine nekaj časovnega intervala, zaključimo delo s trenutnim obrnjenim indeksom. In ustvarite nov obrnjen indeks. Mine še en časovni interval, ustvarimo še enega in še enega.
In pri vzorčenju iz teh obrnjenih indeksov najdemo niz obrnjenih indeksov, ki spadajo v dani interval. In v skladu s tem od tam izberemo ID časovne serije.
To prihrani sredstva, ker nam ni treba gledati delov, ki ne spadajo v dani interval. To pomeni, da običajno, če izberemo podatke za zadnjo uro, potem za prejšnje časovne intervale preskočimo poizvedbe.
Obstaja še ena možnost za rešitev te težave. To je za shranjevanje za vsak dan ločenega seznama ID-jev časovnih vrst, ki so se zgodile na ta dan.
Prednost te rešitve pred prejšnjo rešitvijo je, da ne podvajamo informacij o časovnih vrstah, ki s časom ne izginejo. Stalno so prisotni in se ne spreminjajo.
Pomanjkljivost je, da je takšno rešitev težje implementirati in težje odpravljati napake. In VictoriaMetrics je izbrala to rešitev. Tako se je zgodovinsko zgodilo. Tudi ta rešitev se dobro obnese v primerjavi s prejšnjo. Ker ta rešitev ni bila implementirana zaradi dejstva, da je treba v vsaki particiji podvojiti podatke za časovne vrste, ki se ne spreminjajo, torej ki s časom ne izginejo. VictoriaMetrics je bil v prvi vrsti optimiziran za porabo prostora na disku, prejšnja izvedba pa je porabo prostora na disku poslabšala. Toda ta izvedba je bolj primerna za zmanjšanje porabe prostora na disku, zato je bila izbrana.
Moral sem se boriti z njo. Težava je bila v tem, da morate pri tej izvedbi še vedno izbrati veliko večje število timeseries_ids
za podatke kot takrat, ko je obrnjeni indeks časovno particioniran.
Kako smo rešili ta problem? Rešili smo ga na izviren način - tako, da smo namesto enega identifikatorja v vsak vnos obrnjenega indeksa shranili več identifikatorjev časovnih vrst. Se pravi, imamo ključ label=value
, ki se pojavlja v vsaki časovni seriji. In zdaj jih prihranimo nekaj timeseries_ids
v enem vnosu.
Tukaj je primer. Prej smo imeli N vnosov, zdaj pa imamo en vnos, katerega predpona je enaka vsem ostalim. Za prejšnji vnos vrednost vsebuje vse ID-je časovnih vrst.
To je omogočilo povečanje hitrosti skeniranja takšnega obrnjenega indeksa do 10-krat. Omogočil nam je tudi zmanjšanje porabe pomnilnika za predpomnilnik, ker zdaj shranjujemo niz label=value
samo enkrat v predpomnilnik skupaj N-krat. In ta vrstica je lahko velika, če v oznake in nalepke shranite dolge vrstice, ki jih Kubernetes rad potisne tja.
Druga možnost za pospešitev iskanja po obrnjenem indeksu je razrezovanje. Ustvarjanje več obrnjenih indeksov namesto enega in razdelitev podatkov med njimi po ključu. To je komplet key=value
paro. To pomeni, da dobimo več neodvisnih invertiranih indeksov, po katerih lahko vzporedno poizvedujemo na več procesorjih. Prejšnje izvedbe so dovoljevale samo delovanje v enoprocesorskem načinu, torej skeniranje podatkov v samo enem jedru. Ta rešitev vam omogoča skeniranje podatkov v več jedrih hkrati, kot to rad počne ClickHouse. To nameravamo uresničiti.
Zdaj pa se vrnimo k našim ovcam - k funkciji presečišča timeseries_ids
. Razmislimo, kakšne izvedbe lahko obstajajo. Ta funkcija vam omogoča iskanje timeseries_ids
za dani niz label=value
.
Prva možnost je naivna izvedba. Dve ugnezdeni zanki. Tukaj dobimo vnos funkcije intersectInts
dve rezini - a
и b
. Na izhodu nam mora vrniti presečišče teh rezin.
Naivna izvedba izgleda takole. Ponavljamo vse vrednosti iz rezine a
, znotraj te zanke gremo skozi vse vrednosti rezine b
. In jih primerjamo. Če se ujemata, potem smo našli presečišče. In ga shranite result
.
Kakšne so slabosti? Kvadratna kompleksnost je njegova glavna pomanjkljivost. Na primer, če so vaše mere rezine a
и b
en milijon naenkrat, potem vam ta funkcija nikoli ne bo vrnila odgovora. Ker bo moral narediti bilijon iteracij, kar je veliko tudi za sodobne računalnike.
Druga izvedba temelji na zemljevidu. Ustvarjamo zemljevid. V ta zemljevid smo postavili vse vrednosti iz rezine a
. Nato gremo skozi rezino v ločeni zanki b
. In preverimo, ali je ta vrednost iz rezine b
v zemljevidu. Če obstaja, ga dodajte rezultatu.
Kakšne so prednosti? Prednost je, da obstaja samo linearna kompleksnost. To pomeni, da se bo funkcija za večje rezine izvajala veliko hitreje. Za rezino velikosti milijona se bo ta funkcija izvedla v 2 milijonih ponovitev, v nasprotju z bilijoni ponovitev prejšnje funkcije.
Slaba stran je, da ta funkcija za ustvarjanje tega zemljevida potrebuje več pomnilnika.
Druga pomanjkljivost so veliki stroški zgoščevanja. Ta pomanjkljivost ni zelo očitna. In tudi za nas to ni bilo zelo očitno, zato je bila sprva v VictoriaMetrics implementacija presečišča prek zemljevida. Toda nato je profiliranje pokazalo, da se čas glavnega procesorja porabi za pisanje zemljevida in preverjanje prisotnosti vrednosti na tem zemljevidu.
Zakaj se na teh mestih zapravlja čas procesorja? Ker Go izvede operacijo zgoščevanja teh vrstic. To pomeni, da izračuna zgoščeno vrednost ključa, da bi nato do njega dostopal pri danem indeksu v HashMap. Operacija izračuna zgoščene vrednosti se zaključi v desetinah nanosekund. To je za VictoriaMetrics počasno.
Odločil sem se implementirati bitset, optimiziran posebej za ta primer. Tako je zdaj videti presečišče dveh rezin. Tukaj ustvarimo bitset. Vanj dodamo elemente iz prve rezine. Nato preverimo prisotnost teh elementov v drugi rezini. In jih dodajte rezultatu. To pomeni, da se skoraj ne razlikuje od prejšnjega primera. Edina stvar tukaj je, da smo zamenjali dostop do zemljevida s funkcijami po meri add
и has
.
Na prvi pogled se zdi, da bi to moralo delovati počasneje, če je bil prej tam uporabljen standardni zemljevid, potem pa se kličejo nekatere druge funkcije, vendar profiliranje pokaže, da ta stvar deluje 10-krat hitreje kot standardni zemljevid v primeru VictoriaMetrics.
Poleg tega uporablja veliko manj pomnilnika v primerjavi z izvedbo zemljevida. Ker tukaj shranjujemo bite namesto osembajtnih vrednosti.
Slabost te izvedbe je, da ni tako očitna, ni trivialna.
Druga pomanjkljivost, ki je mnogi morda ne opazijo, je, da ta izvedba v nekaterih primerih morda ne bo delovala dobro. To pomeni, da je optimiziran za poseben primer, za ta primer presečišča ID-jev časovnih vrst VictoriaMetrics. To ne pomeni, da je primeren za vse primere. Če je uporabljen nepravilno, ne bomo dobili povečanja zmogljivosti, ampak napako zmanjkanja pomnilnika in upočasnitev delovanja.
Oglejmo si izvedbo te strukture. Če želite pogledati, se nahaja v virih VictoriaMetrics, v mapi timeseries_id
je 64-bitna vrednost, kjer je prvih 32 bitov v bistvu stalnih in se spreminja samo zadnjih 32 bitov.
Ta podatkovna struktura ni shranjena na disku, ampak deluje samo v pomnilniku.
Tukaj je njegov API. Ni zelo zapleteno. API je prilagojen posebej za določen primer uporabe VictoriaMetrics. To pomeni, da tukaj ni nepotrebnih funkcij. Tukaj so funkcije, ki jih izrecno uporablja VictoriaMetrics.
Obstajajo funkcije add
, ki dodaja nove vrednosti. Obstaja funkcija has
, ki preverja nove vrednosti. In obstaja funkcija del
, ki odstrani vrednosti. Obstaja pomočna funkcija len
, ki vrne velikost nabora. funkcija clone
veliko klonira. In funkcija appendto
pretvori ta niz v rezino timeseries_ids
.
Takole izgleda implementacija te podatkovne strukture. set ima dva elementa:
-
ItemsCount
je pomožno polje za hitro vrnitev števila elementov v nizu. Brez tega pomožnega polja bi bilo mogoče, vendar ga je bilo treba dodati tukaj, ker VictoriaMetrics v svojih algoritmih pogosto povprašuje po dolžini bitseta. -
Drugo polje je
buckets
. To je delček strukturebucket32
. Vsaka struktura shranjujehi
polje. To je zgornjih 32 bitov. In dve rezini -b16his
иbuckets
z dnebucket16
strukture.
Tukaj je shranjenih 16 zgornjih bitov drugega dela 64-bitne strukture. In tukaj so bitni nizi shranjeni za spodnjih 16 bitov vsakega bajta.
Bucket64
je sestavljen iz niza uint64
. Dolžina se izračuna z uporabo teh konstant. V enem bucket16
lahko shranite največ 2^16=65536
bit. Če to delite z 8, potem je 8 kilobajtov. Če še enkrat delite z 8, je 1000 uint64
pomen. To je Bucket16
– to je naša 8-kilobajtna struktura.
Poglejmo, kako je implementirana ena od metod te strukture za dodajanje nove vrednosti.
Vse se začne z uint64
pomeni. Izračunamo zgornjih 32 bitov, izračunamo spodnjih 32 bitov. Pojdimo skozi vse buckets
. Zgornjih 32 bitov v vsakem vedru primerjamo z dodano vrednostjo. In če se ujemata, potem pokličemo funkcijo add
v strukturi b32 buckets
. In tam dodajte spodnjih 32 bitov. In če se je vrnilo true
, potem to pomeni, da smo tam dodali takšno vrednost in je nismo imeli. Če se vrne false
, potem je tak pomen že obstajal. Nato povečamo število elementov v strukturi.
Če nismo našli tistega, ki ga potrebujete bucket
z zahtevano hi-vrednostjo, nato pokličemo funkcijo addAlloc
, ki bo izdelala novo bucket
, ki ga doda v strukturo vedra.
To je izvajanje funkcije b32.add
. Podobno je prejšnji izvedbi. Izračunamo najpomembnejših 16 bitov, najmanj pomembnih 16 bitov.
Nato gremo skozi vseh zgornjih 16 bitov. Najdemo ujemanja. In če obstaja ujemanje, pokličemo metodo dodajanja, ki jo bomo obravnavali na naslednji strani bucket16
.
In tukaj je najnižja raven, ki jo je treba čim bolj optimizirati. Računamo za uint64
vrednost id v bitu rezine in tudi bitmask
. To je maska za dano 64-bitno vrednost, s katero lahko preverite prisotnost tega bita ali jo nastavite. Preverimo, ali je ta bit nastavljen, ga nastavimo in vrnemo prisotnost. To je naša izvedba, ki nam je omogočila, da smo delovanje sekajočih se ID-jev časovnih vrst pohitrili za 10-krat v primerjavi s konvencionalnimi zemljevidi.
Poleg te optimizacije ima VictoriaMetrics še veliko drugih optimizacij. Večina teh optimizacij je bila dodanih z razlogom, vendar po profiliranju kode v proizvodnji.
To je glavno pravilo optimizacije – ne dodajajte optimizacije ob predpostavki, da bo tukaj ozko grlo, ker se lahko izkaže, da tam ozkega grla ne bo. Optimizacija običajno poslabša kakovost kode. Zato se splača optimizirati šele po profiliranju in po možnosti v produkciji, da so to pravi podatki. Če koga zanima, si lahko ogleda izvorno kodo VictoriaMetrics in razišče druge optimizacije, ki so tam.
Imam vprašanje glede bitset-a. Zelo podoben vektorski bool implementaciji C++, optimiziran bitset. Ste implementacijo prevzeli od tam?
Ne, ne od tam. Pri implementaciji tega bitseta me je vodilo poznavanje strukture teh časovnih vrst id, ki se uporabljajo v VictoriaMetrics. In njihova struktura je takšna, da je zgornjih 32 bitov v bistvu konstantnih. Spodnjih 32 bitov se lahko spremeni. Nižji kot je bit, pogosteje se lahko spreminja. Zato je ta izvedba posebej optimizirana za to podatkovno strukturo. Implementacija C++ je, kolikor vem, optimizirana za splošni primer. Če optimizirate za splošen primer, to pomeni, da za konkreten primer ne bo najbolj optimalno.
Svetujem vam tudi ogled poročila Alekseja Milovida. Pred približno mesecem dni je govoril o optimizaciji v ClickHouse za določene specializacije. Pove le, da je v splošnem implementacija C++ ali kakšna druga implementacija prilagojena tako, da v povprečju dobro deluje v bolnišnici. Morda deluje slabše kot implementacija, specifična za znanje, kot je naša, kjer vemo, da je zgornjih 32 bitov večinoma konstantnih.
Imam drugo vprašanje. Kakšna je temeljna razlika od InfluxDB?
Obstaja veliko temeljnih razlik. Kar zadeva zmogljivost in porabo pomnilnika, InfluxDB v testih pokaže 10-krat večjo porabo pomnilnika za časovne serije z visoko kardinalnostjo, ko jih imate veliko, na primer na milijone. Na primer, VictoriaMetrics porabi 1 GB na milijon aktivnih vrstic, medtem ko InfluxDB porabi 10 GB. In to je velika razlika.
Druga temeljna razlika je, da ima InfluxDB čudne jezike poizvedb - Flux in InfluxQL. Niso zelo priročni za delo s časovnimi serijami v primerjavi z
In še ena razlika je ta, da ima InfluxDB nekoliko nenavaden podatkovni model, kjer lahko vsaka vrstica shrani več polj z drugačnim nizom oznak. Te vrstice so nadalje razdeljene v različne tabele. Ti dodatni zapleti otežujejo nadaljnje delo s to bazo podatkov. Težko je podpirati in razumeti.
V VictoriaMetrics je vse veliko preprostejše. Tam je vsaka časovna serija ključna vrednost. Vrednost je niz točk - (timestamp, value)
, ključ pa je komplet label=value
. Med polji in meritvami ni ločnice. Omogoča vam, da izberete poljubne podatke in nato združite, seštejete, odštejete, pomnožite, delite, za razliko od InfluxDB, kjer izračuni med različnimi vrsticami še vedno niso izvedeni, kolikor vem. Tudi če so implementirani, je težko, napisati je treba veliko kode.
Imam pojasnjevalno vprašanje. Ali sem prav razumel, da je prišlo do nekakšne težave, o kateri ste govorili, da se ta obrnjen indeks ne prilega pomnilniku, zato je tam particioniranje?
Najprej sem pokazal naivno izvedbo obrnjenega indeksa na standardnem zemljevidu Go. Ta izvedba ni primerna za baze podatkov, ker ta obrnjeni indeks ni shranjen na disk, baza podatkov pa se mora shraniti na disk, tako da ti podatki ostanejo na voljo po ponovnem zagonu. V tej izvedbi, ko znova zaženete aplikacijo, bo vaš obrnjeni indeks izginil. In izgubili boste dostop do vseh podatkov, ker jih ne boste mogli najti.
Zdravo! Hvala za poročilo! Moje ime je Pavel. Jaz sem iz Wildberries. Imam nekaj vprašanj za vas. Prvo vprašanje. Ali menite, da bi, če bi izbrali drugačen princip pri gradnji arhitekture vaše aplikacije in razdelili podatke skozi čas, morda lahko presekali podatke pri iskanju samo na podlagi dejstva, da ena particija vsebuje podatke za eno časovnem obdobju , torej v enem časovnem intervalu in vam ne bi bilo treba skrbeti, da so vaši deli različno razpršeni? Vprašanje številka 2 - ker implementirate podoben algoritem z bitsetom in vsem ostalim, ste morda poskusili uporabiti navodila procesorja? Ste morda že poskusili takšne optimizacije?
Na drugo odgovorim takoj. Nismo še prišli do te točke. A če bo treba, pridemo tja. In prvo, kaj je bilo vprašanje?
Razpravljali ste o dveh scenarijih. In rekli so, da so izbrali drugega, ki ima bolj zapleteno izvedbo. In ni jim bil ljubši prvi, kjer so podatki časovno razdeljeni.
ja V prvem primeru bi bil skupni obseg indeksa večji, ker bi morali v vsaki particiji shraniti podvojene podatke za tiste časovne serije, ki se nadaljujejo skozi vse te particije. In če je stopnja opuščanja vaše časovne serije majhna, tj. nenehno se uporabljajo iste serije, potem bi v prvem primeru izgubili veliko več pri količini zasedenega prostora na disku v primerjavi z drugim primerom.
In tako – ja, časovna delitev je dobra možnost. Prometej ga uporablja. Toda Prometej ima še eno pomanjkljivost. Pri združevanju teh kosov podatkov mora v pomnilniku hraniti meta informacije za vse oznake in časovne serije. Če so torej deli podatkov, ki jih združuje, veliki, se poraba pomnilnika med združevanjem zelo poveča, za razliko od VictoriaMetrics. Pri združevanju VictoriaMetrics sploh ne porablja pomnilnika, porabi se le nekaj kilobajtov, ne glede na velikost spojenih podatkov.
Algoritem, ki ga uporabljate, uporablja pomnilnik. Označuje oznake časovnih vrst, ki vsebujejo vrednosti. In na ta način preverite prisotnost parov v enem in drugem podatkovnem nizu. In razumete, ali je do preseka prišlo ali ne. Običajno baze podatkov izvajajo kazalce in iteratorje, ki shranjujejo svojo trenutno vsebino in tečejo skozi razvrščene podatke zaradi preproste zapletenosti teh operacij.
Zakaj ne uporabljamo kazalcev za premikanje po podatkih?
Da.
Razvrščene vrstice shranjujemo v LevelDB ali mergeset. Kazalec lahko premaknemo in poiščemo križišče. Zakaj ga ne uporabimo? Ker je počasen. Ker kazalci pomenijo, da morate za vsako vrstico poklicati funkcijo. Klic funkcije je 5 nanosekund. In če imate 100 vrstic, potem se izkaže, da porabimo pol sekunde samo za klicanje funkcije.
Obstaja kaj takega, ja. In moje zadnje vprašanje. Vprašanje se morda sliši nekoliko čudno. Zakaj ni mogoče prebrati vseh potrebnih agregatov v trenutku, ko podatki prispejo in jih shraniti v zahtevani obliki? Zakaj bi prihranili ogromne količine v nekaterih sistemih, kot so VictoriaMetrics, ClickHouse itd., in potem zanje porabili veliko časa?
Bom dal primer, da bo bolj jasno. Recimo, kako deluje mali merilnik hitrosti igrače? Beleži razdaljo, ki ste jo prepotovali, in jo ves čas dodaja eni vrednosti, drugi pa času. In deli. In dobi povprečno hitrost. Lahko storite približno enako. Sproti seštejte vsa potrebna dejstva.
V redu, razumem vprašanje. Vaš primer ima svoje mesto. Če veste, katere agregate potrebujete, potem je to najboljša izvedba. Toda težava je v tem, da ljudje te metrike, nekatere podatke shranjujejo v ClickHouse in še ne vedo, kako jih bodo v prihodnosti združevali in filtrirali, zato morajo shraniti vse neobdelane podatke. Če pa veste, da morate nekaj izračunati v povprečju, zakaj potem tega ne bi izračunali, namesto da bi tam shranili kup neobdelanih vrednosti? A to je le, če natančno veste, kaj potrebujete.
Mimogrede, baze podatkov za shranjevanje časovnih vrst podpirajo štetje agregatov. Na primer, Prometheus podpira
Na primer, v prejšnji službi sem moral prešteti število dogodkov v drsečem oknu v zadnji uri. Problem je v tem, da sem moral v Go narediti custom implementacijo, torej storitev za štetje te stvari. Ta storitev je bila na koncu nepomembna, saj jo je težko izračunati. Implementacija je lahko preprosta, če morate nekaj agregatov prešteti v določenih časovnih intervalih. Če želite šteti dogodke v drsečem oknu, potem ni tako preprosto, kot se zdi. Mislim, da to še ni implementirano v ClickHouse ali v baze podatkov časovnih vrst, ker je težko implementirati.
In še eno vprašanje. Ravno smo govorili o povprečenju in spomnil sem se, da je nekoč obstajal Graphite z ogljikovim zaledjem. In znal je redčiti stare podatke, se pravi pustiti eno točko na minuto, eno točko na uro itd. Načeloma je to kar priročno, če rabimo surove podatke, relativno za en mesec, vse ostalo pa lahko biti razredčen . Toda Prometheus in VictoriaMetrics ne podpirata te funkcije. Ali je predvidena podpora? Če ne, zakaj ne?
Hvala za vprašanje. Naši uporabniki občasno postavljajo to vprašanje. Sprašujejo, kdaj bomo dodali podporo za znižanje vzorčenja. Tukaj je več težav. Prvič, vsak uporabnik razume downsampling
nekaj drugega: nekdo želi dobiti poljubno točko na danem intervalu, nekdo želi maksimalne, minimalne, povprečne vrednosti. Če veliko sistemov zapisuje podatke v vašo zbirko podatkov, potem ne morete združiti vseh skupaj. Lahko se zgodi, da vsak sistem zahteva drugačno redčenje. In to je težko izvedljivo.
In druga stvar je, da je VictoriaMetrics, tako kot ClickHouse, optimiziran za delo z velikimi količinami neobdelanih podatkov, tako da lahko v manj kot sekundi odstrani milijardo vrstic, če imate v sistemu veliko jeder. Skeniranje točk časovne serije v VictoriaMetrics – 50 točk na sekundo na jedro. In ta zmogljivost se prilagaja obstoječim jedrom. Se pravi, če imate na primer 000 jeder, boste skenirali milijardo točk na sekundo. In ta lastnost VictoriaMetrics in ClickHouse zmanjšuje potrebo po zniževanju vzorcev.
Druga značilnost je, da VictoriaMetrics te podatke učinkovito stisne. Stiskanje v produkciji je v povprečju od 0,4 do 0,8 bajtov na točko. Vsaka točka je časovni žig + vrednost. In v povprečju je stisnjen v manj kot en bajt.
Sergej. Imam vprašanje. Kolikšen je minimalni kvantum časa snemanja?
Ena milisekunda. Nedavno smo se pogovarjali z drugimi razvijalci baz podatkov časovnih vrst. Njihov minimalni časovni odrez je ena sekunda. In na primer v Graphite je tudi ena sekunda. V OpenTSDB je tudi ena sekunda. InfluxDB ima nanosekundno natančnost. V VictoriaMetrics je ena milisekunda, ker je v Prometheusu ena milisekunda. In VictoriaMetrics je bil prvotno razvit kot oddaljena shramba za Prometheus. Zdaj pa lahko shranjuje podatke iz drugih sistemov.
Oseba, s katero sem govoril, pravi, da imajo natančnost od sekunde do sekunde – to jim zadošča, ker je odvisno od vrste podatkov, ki so shranjeni v zbirki časovnih vrst. Če so to podatki DevOps ali podatki iz infrastrukture, kjer jih zbirate v intervalih 30 sekund, na minuto, je sekundna natančnost dovolj, nič manj ne potrebujete. In če te podatke zbirate iz visokofrekvenčnih trgovalnih sistemov, potem potrebujete nanosekundno natančnost.
Milisekundna natančnost v VictoriaMetrics je primerna tudi za primer DevOps in je lahko primerna za večino primerov, ki sem jih omenil na začetku poročila. Edina stvar, za katero morda ni primeren, so sistemi visokofrekvenčnega trgovanja.
Hvala vam! In še eno vprašanje. Kaj je združljivost v PromQL?
Popolna združljivost za nazaj. VictoriaMetrics v celoti podpira PromQL. Poleg tega dodaja dodatno napredno funkcionalnost v PromQL, ki se imenuje
Telegram kanal
V anketi lahko sodelujejo samo registrirani uporabniki.
Kaj vam preprečuje, da bi preklopili na VictoriaMetrics kot svojo dolgoročno shrambo za Prometheus? (Napiši v komentarje, dodam v anketo))
-
71,4%Prometheus5 ne uporabljam
-
28,6%Nisem vedel za VictoriaMetrics2
Glasovalo je 7 uporabnikov. 12 uporabnikov se je vzdržalo.
Vir: www.habr.com