Tutvustan teile 2019. aasta lÔpu raporti "Go optimizations in VictoriaMetrics" tÔlgendust, mille autor on Aleksandr Valjalkin.
â kiire ja skaleeritav andmebaasi haldusĂŒsteem, mis on mĂ”eldud ajarealiste andmete salvestamiseks ja töötlemiseks (salvestus moodustab aja ja vastavad sellele ajale vÀÀrtuste kogumi, nĂ€iteks anduritelt saadud perioodiliselt mÔÔdetud olekute vĂ”i meetrite kogumise kaudu).

Siin on link selle raporti videole â

RÀÀgin natuke endast. Olen Aleksandr Valjalkin. Siin on . Mind huvitab Go ja jÔudluse optimeerimine. Olen kirjutanud palju kasulikke ja vÀhem pruugi raamatukogusid. Need algavad kas fast, vÔi quick eesliidelt.
Hetkel töötan VictoriaMetrics'i kallal. Mis see on ja mida ma seal teen? RÀÀgin sellest presentatsioonis.

Raporti ĂŒlevaade on jĂ€rgmine:
- Alustuseks rÀÀgin, mis on VictoriaMetrics.
- SeejÀrel selgitan, mis on ajaread.
- K seejÀrel rÀÀgin, kuidas ajarealiste andmete andmebaas töötab.
- Edasi rÀÀgin andmebaasi arhitektuurist: millest see koosneb.
- Ja seejÀrel liigume edasi optimeerimistele, mis on olemas VictoriaMetrics'is. Need on pööratud indeksi optimeerimine ja bitset-realisatsiooni optimeerimine Go-s.

Kas keegi teab, mis on VictoriaMetrics? Oh, juba paljud inimesed teavad. See on hea uudis. Neile, kes ei tea â see on ajarealiste andmete andmebaas. See pĂ”hineb ClickHouse arhitektuuril, teatud ClickHouse'i rakenduse detailidel. NĂ€iteks nagu: MergeTree, paralleelsed arvutused kĂ”igil saadaval olevatel protsessorikasutajatel ja jĂ”udluse optimeerimine töötamata andmeblokkide osas, mis paigutatakse protsessorikapslisse.
VictoriaMetrics pakub paremat andmete tihendamist vÔrreldes teiste ajarealiste andmete andmebaasidega.
See skaleerub vertikaalselt â st vĂ”ite lisada rohkem protsessoreid, rohkem RAM-i ĂŒhte arvutisse. VictoriaMetrics suudab tĂ”husalt kasutada neid ressursse ja kasvatada lineaarselt jĂ”udlust.
Samuti skaleerub VictoriaMetrics horisontaalselt â st vĂ”ite lisada tĂ€iendavaid sĂ”lmi VictoriaMetrics'i klastrisse ning selle jĂ”udlus suureneb peaaegu lineaarselt.
Kuidas arvata vÔisite, VictoriaMetrics on kiire andmebaas, sest ma ei oska kirjutada muid. Ja see on kirjutatud Go-s, seetÔttu rÀÀgin ma sellest kohtumisel.

Kes teab, mis on ajaread? Ka paljusid inimesi teab. Ajaread on paaride seeria (timestamp, vÀÀrtus), kus paarid on jĂ€rjestatud aja jĂ€rgi. VÀÀrtus on ujuv-punktiga number â float64.
Iga ajareale tuvastatakse ainulaadne vĂ”ti. Mis see vĂ”ti sisaldab? See koosneb mitte-tĂŒhjast vĂ”tme-vÀÀrtuse paaride kogumist.
Siin on nĂ€ide ajareast. Selle rea vĂ”tmena on paaride nimekiri: __name__="cpu_usage" â see on mÔÔdiku nimi, instance="my-server" â see on arvuti, kus see mÔÔdik on kogutud, datacenter="us-east" â see on andmekeskus, kus see arvuti asub.
Meil on ajareanimi, mis koosneb kolmest vĂ”tme-vÀÀrtuse paarist. Sellele vĂ”tmele vastab paaride nimekiri (timestamp, value). t1, t3, t3, ..., tN â need on ajamĂ€rgid, 10, 20, 12, ..., 15 â vastavad vÀÀrtused. See on cpu-kasutus antud ajahetkel selle rea jaoks.

Kus vÔivad ajaread olla kasulikud? Kas kellelgi on ideid?
- DevOps-is saab mÔÔta CPU, RAM, vÔrgu, rps, vigade arvu jne koormust.
- IoT â saame mÔÔta temperatuuri, rĂ”hku, geo-kordinaate ja veel midagi.
- Samuti rahanduses â saame jĂ€lgida aktsiate ja valuutade hindu.
- Lisaks sellele vÔivad ajaread olla kasulikud töötlemisprotsesside jÀlgimiseks tehasetes. Meil on kasutajaid, kes kasutavad VictoriaMetrics tuulegeneraatorite ja robotite jÀlgimiseks.
- Ajaread on ka kasulikud teatud seadmete andurite info kogumiseks. NĂ€iteks mootori jaoks; rehvide rĂ”hu mÔÔtmiseks; kiirus, kaugus; kĂŒtusekulu mÔÔtmiseks jne.
- Ajaread vÔivad samuti olla kasulikud lennukite jÀlgimiseks. Igas lennukis on must kast, mis kogub ajareade erinevate lennuki tervise parameetrite kohta. Ajaread on samuti kasutusel lennundustööstuses.
- Tervishoid â see on vererĂ”hk, pulss jne.
VÔib-olla on veel rakendusi, millest ma unustasin, kuid loodan, et te mÔistate, et ajaread on kaasaegses maailmas aktiivselt kasutusel. Ja nende kasutamine suureneb iga aastaga.

Miks on ajareadeks vajalik andmebaas? Miks ei saa ajareadeks kasutada tavalist relatsioonilist andmebaasi?
Kuna ajajoonetes on tavaliselt suur hulk teavet, mille sĂ€ilitamine ja töötlemine tavapĂ€rastes andmebaasides on keeruline. SeetĂ”ttu on vĂ€lja töötatud spetsialiseeritud ajajoonte andmebaasid. Need andmebaasid sĂ€ilitavad punkte tĂ”husalt, (timestamp, value) antud vĂ”tme pĂ”hjal. Nad pakuvad API-d salvestatud andmete lugemiseks vĂ”tme jĂ€rgi, kas ĂŒhe vĂ”tme-vÀÀrtuse paari, mitme sellise paari vĂ”i regexp jĂ€rgi. NĂ€iteks, kui soovite leida kĂ”igi oma teenuste CPU koormuse andmed Ameerika andmekeskuses, peate kasutama sellist pseudo-pĂ€ringut.
Tavaliselt esindavad ajajoonte andmebaasid spetsialiseeritud pÀringute keeli, kuna SQL ei sobi ajajoonte jaoks vÀga hÀsti. Kuigi on andmebaase, mis toetavad SQL-i, ei ole see siiski eriti sobiv. Rohkem sobivad sellised pÀringu keeled nagu , , , . Loodan, et keegi on neist keeltest vÀhemalt midagi kuulnud. O PromQL on ilmselt paljud kuulnud. See on Prometheuse pÀringu keel.

Nii nÀeb vÀlja kaasaegse ajajoonte andmebaasi arhitektuur VictoriaMetrics'i nÀitel.
See koosneb kahest osast. Need on pöördindeksi salvestus ja ajajoonte vÀÀrtuste salvestus. Need salvestused on eraldatud.
Kui andmebaasi tuleb uus kirje, pöördume kÔigepealt pöördindeksi poole, et leida ajajoonte identifikaator antud mÀÀratud kogumi label=value selle mÔÔtiku jaoks. Leiame selle identifikaatori ja salvestame vÀÀrtuse andmesalvestusse.
Kui tuleb pÀring andmete valimiseks TSDB-st, vaatame kÔigepealt pöördindeksi. Saame kÔik timeseries_ids kirjed, mis vastavad antud mÀÀratud kogule. label=valueJa seejÀrel saame kÔik vajalikud andmed andmesalvestusest, mille on indekseeritud timeseries_ids.

Vaadakem nÀidet, kuidas ajajoonte andmebaas töötleb sisenevat select-pÀringut.
- Esimese asjana saab ta kÔik
timeseries_idspöördindeksist, mis sisaldavad mÀÀratud paarelabel=value, vÔi vastavad mÀÀratud regulaaravaldisele. - SeejÀrel saadab ta kÔik andmepunktid andmesalvestusest antud ajavahemikus leidudest
timeseries_ids. - PÀrast seda teeb andmebaas kasutaja pÀringu jÀrgi nende andmepunktide pÔhjal arvutusi. Ja seejÀrel tagastab vastuse.
Selles esitluses rÀÀgin ma teile esimesest osast. See on otsimine timeseries_ids pööratud indeksis. Teise ja kolmanda osa vĂ”ite hiljem vaadata , vĂ”i oodata, kuni ma valmistan ette teisi ettekandeid đ

Alustame pööratud indeksist. Paljudele vÔib tunduda, et see on lihtne. Kes teab, mis on pööratud indeks ja kuidas see töötab? Oh, inimesi, kes teavad, ei olegi nii palju. Proovime mÔista, mis see on.
Tegelikult on kĂ”ik lihtne. See on lihtsalt sĂ”nastik, mis seob vĂ”tme vÀÀrtusega. Mis on vĂ”ti? See paar label=value, kus label ja value on stringid. Ja vÀÀrtused â on kogum timeseries_ids, kuhu kuulub antud paar label=value.
Pööratud indeks vÔimaldab kiiresti leida kÔik timeseries_ids, millel on antud label=value.
Samuti vĂ”imaldab see kiiresti leida timeseries_ids aja rea jaoks mitme paari label=value, vĂ”i paaride jaoks label=regexp. Kuidas see toimub? TĂ”mbamise kaudu hulkade ĂŒhisosa timeseries_ids iga paari jaoks label=value.

Vaadakem erinevaid pööratud indeksi teostusi. Alustame kÔige lihtsama naiivse teostusega. See nÀeb vÀlja selline.
Funktsioon getMetricIDs saab loendi stringidest. Iga string sisaldab label=value. See funktsioon tagastab loendi metricIDs.
Kuidas see töötab? Siin on meil globaalne muutuja, mis nimetatakse invertedIndex. See on tavaline sÔnastik (map), mis seondab stringi int slice'iga. String sisaldab label=value.
Funktsiooni teostamine: saame metricIDs esimese jaoks label=value, seejĂ€rel kĂ€ime ĂŒle kĂ”ikide teiste label=value, saame metricIDs nende jaoks. Ja kutsume vĂ€lja funktsiooni intersectInts, millest rÀÀgitakse jĂ€rgmises osas. See funktsioon tagastab nende loendite ĂŒhisosa.

Kuidas nÀete, pööratud indeksi teostamine ei ole vÀga keeruline. Kuid see on naivne teostus. Millised on selle puudused? Peamine puudus naiivses teostuses on see, et selline pööratud indeks hoitakse meie mÀlus. PÀrast rakenduse taaskÀivitamist kaotame selle indeksi. Ei ole indeksi salvestamist kettale. Andmebaasi jaoks ei sobi selline pööratud indeks tÔenÀoliselt.
Teine puudus on samuti seotud mĂ€luga. Pööratud indeks peab mahtuma operatiivmĂ€lu. Kui see ĂŒletab operatiivmĂ€lu suuruse, siis on ilmne, et saame â out of memory error. Ja programm ei tööta.

Seda probleemi saab lahendada valmis lahendustega, nagu , vÔi .
LĂŒhidalt öeldes, vajame andmebaasi, mis vĂ”imaldab kiiresti teha kolme toimingut.
- Esimene toiming on salvestamine
vÔtme-vÀÀrtuse paarsellesse andmebaasi. Seda teeb ta vÀga kiiresti, kusvÔtme-vÀÀrtuse paar- need on vÔivad olla suvalised stringid. - Teine toiming on kiire vÀÀrtuse otsimine antud vÔtme alusel.
- Ja kolmas toiming on kiire kÔigi vÀÀrtuste leidmine antud prefiksi alusel.
LevelDB ja RocksDB â need andmebaasid on vĂ€lja töötatud Google'is ja Facebookis. Esiteks ilmus LevelDB. Siis vĂ”tsid Facebooki inimesed LevelDB ja hakkasid seda tĂ€iustama, luues RocksDB. Praegu töötab Facebooki sees peaaegu kĂ”ikides andmebaasides RocksDB, sealhulgas nad on sellel ĂŒle viinud ka MySQL-i. Nad nimetasid seda .
Pööratud indeksi saab rakendada LevelDB abil. Kuidas seda teha? Me salvestame vĂ”tmena label=value. Ja vÀÀrtusena â ajaseeria identifikaatori, kus see paar esineb. label=value.
Kui meil on palju ajaseeriaid selle paariga label=value, siis on sellel andmebaasis palju ridu sama vÔtme ja erinevatega. timeseries_idsSelleks, et saada nimekiri kÔigist timeseries_ids, mis algavad antud label=prefix, teeme ulatusliku skaneerimise, mille jaoks antud andmebaas on optimeeritud. Ehk valime kÔik read, mis algavad label=prefix ja saame vajalikud timeseries_ids.

Siin on umbkaudne teostus, kuidas see Go-s vÀlja nÀeks. Meil on pööratud indeks. See on LevelDB.
Funktsioon on sama, mis naiivses teostuses. See kordab peaaegu rida-realt naiivset teostust. Ainus moment on see, et tavalise map asemel pöördume pööratud indeksisse. Saame kĂ”ik vÀÀrtused esimese jaoks label=value. Siis kĂ€ime lĂ€bi kĂ”ik ĂŒlejÀÀnud paarid label=value ja saame nende jaoks vastavad metricID-d. SeejĂ€rel leiame ristumise.

Tundub, et kÔik on hÀsti, kuid selles lahenduses on puudusi. VictoriaMetrics rakendas alguses pööratud indeksi LevelDB-l pÔhinevalt. Kuid lÔpuks tuli sellest loobuda.
Miks? Sest LevelDB on aeglasem kui naivne teostus. Naivses teostuses saame antud vĂ”tme alusel kohe kogu viilu metricIDs. See on vĂ€ga kiire toiming â kogu viilu saab kasutada.
Kuid LevelDB-s tuleb iga kord, kui kutsume vÀlja funktsiooni GetValues lÀbida kÔik read, mis algavad label=value. Ja iga read tuleb vÀlja vÔtta vÀÀrtus timeseries_ids. Sellest timeseries_ids koostatakse nende timeseries_ids. Ilmselgelt on see palju aeglasem kui lihtsalt pöördumine tavalise kaardi poole vÔtme alusel.
Teine puudus on see, et LevelDB on kirjutatud C-s. C-funktsioonide vĂ€ljakutsumine Go-st ei ole vĂ€ga kiire. See vĂ”tab sadu nanosekundeid. See pole vĂ€ga kiire, sest vĂ”rreldes tavapĂ€rase funktsiooni vĂ€ljakutsumisega, mis on kirjutatud Go-s ja mis kestab 1-5 nanosekundit, on jĂ”udluse erinevus kĂŒmneid kordi. VictoriaMetrics'i jaoks oli see saatuslik puudus đ

SeetÔttu kirjutasin ma omaenda inversiooni indeksi rakenduse. Ja nimetasime selle .
Mergeset pÔhineb MergeTree andmestruktuuril. See andmestruktuur on laenatud ClickHouse'ist. Selgelt peab mergeset olema optimeeritud kiireks otsimiseks timeseries_ids antud vÔtme jÀrgi. Mergeset on tÀielikult kirjutatud Go-s. Saate vaadata . Mergeseti rakendus asub kaustas . VÔite proovida aru saada, mis seal toimub.
Mergeseti API on vÀga sarnane LevelDB ja RocksDB-ga. See tÀhendab, et see vÔimaldab kiiresti salvestada uusi kirjeid ja kiiresti valida kirjeid antud prefiksi jÀrgi.

Mergeseti puudustest rÀÀgime hiljem. Praegu rÀÀgime probleemidest, mis tekkisid VictoriaMetrics'il tootmises inversiooni indeksi rakendamise kÀigus.
Kuidas need tekkisid?
Esimene pÔhjus on suur churn rate. TÔlkes eesti keelde - see on ajaliselt rikka sagedane vahetus. See on juhul, kui ajakava lÔpeb ja algab uus, vÔi algavad palju uusi ajakavasid. Seda juhtub sageli.
Teine pÔhjus on suur hulk ajakavasid. Alguses, kui jÀlgimine hakkas populaarsust koguma, oli ajakavasid vÀhe. NÀiteks, iga arvuti jaoks tuleb jÀlgida protsessori, mÀluse, vÔrgu ja ketta kasutamist. 4 ajakava iga arvuti kohta. Oletame, et teil on 100 arvutit, see teeb kokku 400 ajakava. See on vÀga vÀhe.
Aja jooksul mĂ”istsid inimesed, et on vĂ”imalik mÔÔta ĂŒksikasjalikumat teavet. NĂ€iteks mÔÔta mitte ainult kogu protsessori, vaid ka iga protsessorituuma koormust eraldi. Kui teil on 40 protsessorituuma, siis vastavalt sellele teil on 40 korda rohkem ajakavasid protsessori koormuse mÔÔtmiseks.
Kuid see pole veel kÔik. Iga protsessorituuma puhul vÔivad olla mitmed olekud, nagu idle, kui see ei tee midagi. Lisaks on olemas töö user space'is, töö kernel space'is ja muid olekuid. Iga sellist olekut on samuti vÔimalik mÔÔta eraldi ajajoonena. See suurendab ridade arvu 7-8 korda.
Meie uurimisest saime ĂŒhe mÔÔdiku pĂ”hjal 40 x 8 = 320 mÔÔdikut ainult ĂŒhe arvuti kohta. Kui korrutame selle 100-ga, saame 32 000 asemel 400.
Hiljem ilmus Kubernetes. Ja see halvenes veelgi, kuna Kuberneteses vÔidakse hostida palju erinevaid teenuseid. Iga teenus Kuberneteses koosneb paljusid pod'idest. KÔike seda tuleb jÀlgida. Lisaks on meil pidev uute versioonide juurutamine teie teenuste jaoks. Iga uue versiooni jaoks tuleb luua uued ajajooned. LÔppkokkuvÔttes kasvab ajajoonte arv eksponentsiaalselt ja seisame silmitsi suure hulga ajajoonte probleemiga, mida nimetatakse high-cardinality. VictoriaMetrics suudab selle probleemiga tÔhusalt toime tulla vÔrreldes teiste ajajoonte andmebaasidega.

Vaatame lÀhemalt high churn rate'i. Mis pÔhjustab high churn rate'i tootmises? Sest mÔned sildid ja tÀhised muutuvad pidevalt.
NÀiteks vÔtame Kubernetes, kus on mÔisted deployment, st kui teie rakenduse uus versioon rakendatakse. Kubernetes'e arendajad otsustasid mingil pÔhjusel lisada deployment'i id sildi.
Kuhu see viis? See tÀhendab, et iga uue deployment'i puhul katkestatakse kÔik vanad ajajooned ja nende asemel algavad uued ajajooned uue sildi vÀÀrtusega deployment_id. Selliseid ridasid vÔib olla sadu tuhandeid ja isegi miljoneid.
Kogu selle asja oluline eripĂ€ra on see, et ajajoonte koguarv kasvab, kuid ajajoonte arv, mis praegu on aktiivsed, kuhu andmed voolavad, jÀÀb konstantseks. Seda olekut nimetatakse â high churn rate.
High churn rate'i peamine probleem on tagada pidev otsingukiirus kĂ”igi ajajoonte seas, mis vastavad mÀÀratud siltide kogumile teatud ajavahemiku jooksul. TĂŒĂŒpiliselt on see ajavahemik viimase tunni vĂ”i viimase pĂ€eva jooksul.

Kuidas seda probleemi lahendada? Siin on esimene variant. See on pöördindeksi jagamine sĂ”ltumatuteks osadeks ajaperioodi kaupa. T. e. kui on möödunud mingi ajavahemik, lĂ”petame kĂ€esoleva pöördindeksiga töötamise ja loome uue pöördindeksi. Möödub veel ĂŒks ajavahemik, loome veel ĂŒhe ja veel ĂŒhe.
Ja nende pöördindeksite valimisel leiame need pöördindeksid, mis jÀÀvad antud ajavahemikku. Ja seega valime sealt ajaliste jÀrjestuste ID-d.
See vÔimaldab ressursse sÀÀsta, kuna me ei pea vaatama osi, mis ei mahu antud ajavahemikku. T. e. tavaliselt, kui me valime andmed viimase tunni jooksul, siis eelnevate ajavahemike jaoks jÀtame pÀringud vahele.

On olemas veel ĂŒks lahendus sellele probleemile. See on iga pĂ€eva jaoks eraldi ajajĂ€rjestuste ID-de nimekirja hoidmine, mis sellel pĂ€eval esinesid.
Selle lahenduse eelis vÔrreldes eelneva lahendusega on selles, et me ei dubleeri teavet ajajÀrjestuste kohta, mis ei kao aja jooksul. Need on pidevalt olemas ja ei muutu.
Puuduseks on see, et selline lahendus on keerulisem rakendada ja debugeerida. Ja VictoriaMetrics valis selle lahenduse. See on ajalooliselt nii kujunenud. See lahendus nĂ€itab end samuti ĂŒsna hĂ€sti, vĂ”rreldes eelneva lahendusega, kuna see lahendus ei olnud ellu viidud seetĂ”ttu, et tuli dubleerida andmed igas jagunemises ajajĂ€rjestuste jaoks, mis ei muutu, t. e. mis ei kao aja jooksul. VictoriaMetrics oli esmajoones optimeeritud diskiruumi tarbimise osas ja eelmine rakendus halvendas diskiruumi tarbimist. Kuid see rakendus sobib paremini diskiruumi tarbimise minimeerimiseks, seega see valiti.
Pidime selle vastu vÔitlema. VÔitlus seisnes selles, et antud rakenduses tuleb siiski valida palju suurem hulk timeseries_ids andmete jaoks, kui siis, kui pöördindeks on jagatud ajaperioodi kaupa.

Kuidas me selle probleemi lahendasime? Me lahendasime selle originaalsel viisil â salvestades iga pöördindeksi kirje sisse mitmeid ajajĂ€rjestuste identifikaatoreid ĂŒhe identifikaatori asemel. T. e. meil on vĂ”ti label=value, mis esineb igas ajareas. Ja nĂŒĂŒd salvestame mĂ”ned timeseries_ids ĂŒhes kirjes.
Siin on nĂ€ide. Varem oli meil N kirjet, aga nĂŒĂŒd on meil ĂŒks kirjutis, mille prefiks on sama kui kĂ”igil teistel. Eelmise kirje vÀÀrtus sisaldab kĂ”iki ajareade id-sid.
See vĂ”imaldas suurendada sellise pöördindeksi skaneerimise kiirus kuni 10 korda. Ja vĂ€hendada mĂ€lu tarbimist vahemĂ€es, kuna nĂŒĂŒd salvestame rea label=value ainult ĂŒks kord vahemĂ€esse koos N korraga. Ja see rida vĂ”ib olla suur, kui teie siltides ja mĂ€rgendites on pikad read, mille paneb sinna Kubernetes.

Veel ĂŒks vĂ”imalus pöördindeksi otsimise kiirusel kiirendamiseks on sharding. Luues mitu pöördindeksit ĂŒhe asemel ja jagades andmed nende vahel vĂ”tme jĂ€rgi. See on komplekt key=value paare. St. meil on mitu sĂ”ltumatut pöördindeksit, mida saame kĂŒsida paralleelselt mitmel protsessoril. Eelivad rakendused vĂ”imaldasid töötada ainult ĂŒhe protsessori reĆŸiimis, st skaneerida andmeid ainult ĂŒhel tuumal. See lahendus vĂ”imaldab skaneerida andmeid kohe mitmel tuumal, nagu ClickHouse seda armastab teha. Seda plaanime rakendada.

Ja nĂŒĂŒd naaseme meie seadmete juurde â lĂ”ikamise funktsioonile timeseries_ids. Vaatame, millised vĂ”ivad olla rakendused. See funktsioon vĂ”imaldab leida timeseries_ids antud komplekti jaoks label=value.

Esimene variant â see on naive rakendus. Kaks sisemist tsĂŒklit. Saame funktsioonile sisendi intersectInts kaks slice'i â a ja b. VĂ€ljundina peaks see meile tagastama nende slice'ide lĂ”ike.
Naive rakendus nĂ€eb vĂ€lja nii. LĂ€bime kĂ”ik vÀÀrtused slice'ist a, selle tsĂŒkli sees kontrollime kĂ”iki vÀÀrtusi slice'ist b. Ja vĂ”rdleme neid. Kui nad klapivad, siis oleme leidnud lĂ”ike. Ja salvestame selle result.

Millised on puudused? Kvadratiivne keerukus â see on tema peamine puudus. NĂ€iteks, kui teie slice'i suurused a ja b on miljon, siis see funktsioon ei naudi kunagi vastust. Sest tal tuleb teha ĂŒks triljon iteratsiooni, mis on isegi tĂ€napĂ€evastele arvutitele vĂ€ga palju.

Teine rakendus pĂ”hineb kaardil. Loome kaardi. Paneme sellesse kaardisse kĂ”ik vÀÀrtused slice'ist a. Siis liigume eraldi tsĂŒkliga slice'i b. Ja kontrollime â kas see vÀÀrtus on slice'ist b map. Kui see on olemas, lisame selle tulemustesse.

Millised on eelised? Eelis seisneb selles, et siin on ainult lineaarne keerukus. See tĂ€hendab, et funktsioon tĂ€idab ĂŒlesande palju kiiremini suurte slices'i suuruste korral. Miljoni suuruse slice'i jaoks tĂ€idab see funktsioon 2 miljonit iteratsiooni, erinevalt triljonist iteratsioonist, nagu eelnevas funktsioonis.
Aga puuduseks on see, et see funktsioon vajab rohkem mÀlu selle map'i loomiseks.
Teine puudus â see on suur overhead hash'i tegemisel. See puudus ei ole sugugi ilmselge. Ka meie jaoks ei olnud see alguses vĂ€ga ilmne, seetĂ”ttu oli VictoriaMetrics'i rakendamine intersection'i kaudu map. Kuid seejĂ€rel profilib profili jĂ€lgimise kĂ€igus nĂ€itas, et peamine protsessori aeg kulub map'i kirjutamisele ja selle map'is oleva vÀÀrtuse kontrollimisele.
Miks kulub protsessorite aeg just nendes kohtades? Sest nendes ridades teostab Go hashâimise toimingut. See tĂ€hendab, et see arvutab vĂ”tme hashâi, et seejĂ€rel pöörduda antud indeksi poole HashMap'is. Hashâi arvutamise operatsioon kestab kĂŒmneid nanosekundeid. See on VictoriaMetrics'i jaoks aeglane.

Otsustasin rakendada bitset'i, mis on spetsiaalselt selle juhtumi jaoks optimeeritud. Nii nĂ€eb nĂŒĂŒd kahte slices'i ristumist. Siin loome bitset'i. Lisame sellesse esimesest slices'ist elemendid. SeejĂ€rel kontrollime nende elementide olemasolu teises slices'is. Ja lisame need tulemustesse. Peaaegu ei erine eelmisest nĂ€itest. Ainus, et oleme siin asendanud juurdepÀÀsu map'ile kohandatud funktsioonidega. add ja has.

Esmapilgul nĂ€ib, et see peaks töötama aeglasemalt, kui varem kasutati standardset map'i, nĂŒĂŒd kutsutakse vĂ€lja mĂ”ned funktsioonid, kuid profileerimine nĂ€itab, et see asi töötab 10 korda kiiremini kui standardne map VictoriaMetrics'i jaoks.
Lisaks kasutab see vÔrreldes map'i rakendusega mÀrgatavalt vÀhem mÀlu. Sest me salvestame siin bitid 8-baidiste vÀÀrtuste asemel.
Selle rakendamise puudus on see, et see ei ole nii ilmne, mitte triviaalne.
Veel inimesed ei pruugi mĂ€rkida, et sellel lahendusel on ĂŒks puudus: see vĂ”ib mĂ”nes olukorras halvasti töötada. See tĂ€hendab, et see on optimeeritud konkreetse juhtumi, VictoriaMetrics'i ajavoo id-de ristumiste jaoks. See ei tĂ€henda, et see sobib kĂ”igile juhtumitele. Kui seda vale kasutada, siis saame mitte jĂ”udluse suurenemise, vaid mĂ€lu ĂŒletĂ€itumise vea ja jĂ”udluse vĂ€henemise.

Vaadakem selle struktuuri rakendust. Kui soovite vaadata, siis see asub VictoriaMetrics'i lÀhtekoodis kaustas . See on optimeeritud just VictoriaMetrics'i jaoks, kus timeseries_id on 64-bitine vÀÀrtus, kus esimesed 32 bitti on peaaegu pidevad ja ainult viimased 32 bitti muutuvad.
See andmestruktuur ei salvestata kettale, see töötab ainult mÀlu sees.

Siin on selle API. See ei ole vĂ€ga keeruline. API on kohandatud just VictoriaMetrics'i konkreetse kasutusnĂ€ite jĂ€rgi. See tĂ€hendab, et siin ei ole ĂŒleliigseid funktsioone. Siin on funktsioonid, mida VictoriaMetrics selgelt kasutab.
On funktsioon add, mis lisab uusi vÀÀrtusi. On funktsioon has, mis kontrollib uusi vÀÀrtusi. Ja on funktsioon del, mis eemaldab vÀÀrtuseid. On abifunktsioon len, mis tagastab hulga suuruse. Funktsioon clone kloneerib hulga. Ja funktsioon appendto muudab selle komplekti viiluks. timeseries_ids.

Nii nÀeb see andmestruktuuri rakendus vÀlja. Set'is on kaks elementi:
ItemsCountâ see on abivĂ€li, et kiiresti tagastada elementide arvu set'is. Ilma selle abivĂ€ljata oleks saanud hakkama, kuid see tuli siia lisada, sest VictoriaMetrics kĂŒsib oma algoritmides sageli bitset'i pikkust.Teine vĂ€li on
buckets. See on viil struktureeritudbucket32. Igas struktuuris on salvestatudhivĂ€li. See on ĂŒlemised 32 bitti. Ja kaks viilu âb16hisjabucketsAPI-sbucket16struktuurid.
Siin on salvestatud teise osa 64-bitise struktuuri ĂŒlemised 16 bitti. Ja siin on salvestatud bitset'id iga byte'i madalamate 16 bitti jaoks.
Bucket64 koosneb massiivist uint64. Pikkus arvutatakse nende muutujate abil. Ăhes bucket16 vĂ”ib maksimaalselt hoida 2^16=65536 bitti. Kui jagada see 8-ga, siis on see 8 kilobaiti. Kui jagada veel 8-ga, siis on see 1000 uint64 vÀÀrtuses. See tĂ€hendab, et Bucket16 â see on 8-kilobaitine struktuur.

Vaadakem, kuidas on rakendatud ĂŒks selle struktuuri meetod uut vÀÀrtust lisada.
KĂ”ik algab uint64 vÀÀrtused. Arvutame ĂŒlemised 32 bitti, arvutame alumised 32 bitti. LĂ€bime kĂ”ik buckets. VĂ”rdleme iga bucket'i ĂŒlemisi 32 bitti lisatavaga. Ja kui need kattuvad, kutsume vĂ€lja funktsiooni add struktuuris b32 buckets. Ja lisame sinna alumised 32 bitti. Ja kui see tagastas true, siis tĂ€hendab, et oleme sellise vÀÀrtuse sinna lisanud ja meil ei olnud sellist vÀÀrtust. Kui see tagastab false, siis selline vÀÀrtus oli juba olemas. Siis suurendame struktuuri elementide arvu.
Kui me ei leidnud vajalikku bucket'i vajaliku hi-vÀÀrtusega, siis kutsume vÀlja funktsiooni addAlloc, mis eraldab uue bucket'i, lisades selle bucket-struktuuri.

See on funktsiooni b32.add. See sarnaneb eelneva teostusega. Arvutame vanemad 16 bitti, nooremad 16 bitti.
Siis kĂ€ime lĂ€bi kĂ”ik ĂŒlemised 16 bitti. Leiame kattuvused. Ja kattumise korral kutsume vĂ€lja meetodi add, mida kĂ€sitleme jĂ€rgmisel lehel bucket16.

Ja see on kÔige madalam tase, mis peaks olema maksimaalselt optimeeritud. Arvutame uint64 id vÀÀrtuse slice bit, samuti bitmask. See on mask selle 64-bitise vÀÀrtuse jaoks, millega saab kontrollida selle bitti olemasolu vÔi selle seadmist. Kontrollime, kas see bitt on seatud ja seadistame selle, ning tagastame olemasolu. Selline on meie teostus, mis vÔimaldas kiirendada ids ajaseeriate ristumiste operatsiooni 10 korda vÔrreldes tavapÀraste maps'idega.

VictoriaMetrics'is on peale selle optimeerimise palju teisi optimeerimisi. Enamik neist optimeerimistest ei ole lisatud lihtsalt niisama, vaid pÀrast koodi profiliseerimist tootmises.
See on optimeerimise peamine reegel - Àra lisa optimeerimist, oletades, et siin on kitsaskoht, sest vÔib juhtuda, et seal ei olegi kitsaskohta. Optimeerimine halvendab tavaliselt koodi kvaliteeti. SeetÔttu on mÔistlik optimeerida ainult pÀrast profiliseerimist ja eelistatult tootmises, et need oleksid reaalsed andmed. Kui interese, siis vÔite vaadata VictoriaMetrics'i allikaid ja uurida teisi seal olevaid optimeerimisi.

Mul on kĂŒsimus bitset'i kohta. VĂ€ga sarnane C++ vector bool'i teostusele, optimeeritud bitset. Kas te vĂ”tsite sealt teostuse?
Ei, mitte sealt. Selle bitset'i elluviimisel tuginesin nende ids timeseries struktuuri teadmistele, mida kasutatakse VictoriaMetrics'is. Nende struktuur on selline, et ĂŒlemised 32 bitti on enamasti pĂŒsivad. Alumised 32 bitti vĂ”ivad muutuda. Mida madalam on bitt, seda tihedamini vĂ”ib see muutuda. SeetĂ”ttu on see elluviimine optimeeritud just selle andmestruktuuri jaoks. C++ elluviimine, niipalju kui ma tean, on optimeeritud ĂŒldise juhtumi jaoks. Kui teha optimeerimist ĂŒldise juhtumi jaoks, siis see tĂ€hendab, et see ei ole konkreetse juhtumi jaoks kĂ”ige optimaalsem.
Soovitan teil veel vaadata Alexei Milovid'i ettekannet. Ta rÀÀkis kuskil kuu aega tagasi ClickHouse optimeerimisest konkreetsete spetsialiseerumiste jaoks. Ta rÀÀgib, et ĂŒldjuhul on C++ elluviimine vĂ”i mĂ”ni muu elluviimine suunatud hea töö tagamisele keskmiselt. See vĂ”ib töötada halvemini kui spetsialiseeritud elluviimine konkreetsete teadmiste jaoks, nagu meil, kui me teame, et ĂŒlemised 32 bitti on enamasti pĂŒsivad.
Mul on teine kĂŒsimus. Mis on kardinaalne erinevus InfluxDB-st?
Kardinaalseid erinevusi on palju. Kui rÀÀkida jÔudlusest ja mÀlu tarbimisest, siis InfluxDB nÀitab testides 10 korda suuremat mÀlu tarbimist kÔrge kardinaalsusega ajarealistes, kui neid on palju, nÀiteks miljonites. NÀiteks VictoriaMetrics tarbib 1 GB miljoni aktiivse rea kohta, samas kui InfluxDB tarbib 10 Gb. Ja see on suur erinevus.
Teine kardinaalne erinevus on see, et InfluxDB-l on kummalised pĂ€ringukeeled â Flux ja InfluxQL. Need ei ole ajarealistega töötamiseks eriti mugavad vĂ”rreldes , mida toetatakse VictoriaMetrics'is. PromQL on pĂ€ringukeel Prometheuses.
Ja veel ĂŒks erinevus â InfluxDB-l on veidi kummaline andmemudel, kus igas real vĂ”ib olla mitu field'i erineva sildistuse kogumiga. Need read jagunevad veel erinevateks tabeliteks. Need lisakompleksused raskendavad sellele andmebaasile jĂ€rgnevate toimingute tegemist. Seda on keeruline hallata ja mĂ”ista.
VictoriaMetrics'is on kĂ”ik kindlasti lihtsam. Seal esindab iga ajareaal vĂ”tme-vÀÀrtuse paari. VÀÀrtus on punktide komplekt â (timestamp, value), vĂ”tme aga on kogum label=value. Ei ole mingit eraldatust fields ja measurements vahel. See vĂ”imaldab teil valida ĂŒkskĂ”ik milliseid andmeid ja seejĂ€rel neid kombineerida, liita, lahutada, korrutada ja jagada, erinevalt InfluxDB-st, kus erinevate ridade vahelisi arvutusi pole seni niipalju teadaolevalt rakendatud. Isegi kui nad on rakendatud, on see keeruline ja selleks tuleb kirjutada palju koodi.
Mul on tĂ€iendav kĂŒsimus. Kas mĂ”istsin Ă”igesti, et oli mingi probleem, millest rÀÀkisite, et see pööratud indeks ei mahu mĂ€llu, seetĂ”ttu toimub seal partitsioneerimine?
Alguses nĂ€itasin ma naiivset rakendust pööratud indeksist standardse Go mapâiga. Selline rakendus ei sobi andmebaaside jaoks, sest see pööratud indeks ei salvestata kettale, aga andmebaasid peavad andmeid salvestama, et need oleks taas kĂ€ivitatuna kergesti kĂ€ttesaadavad. Antud rakenduses kaob teie pööratud indeks rakenduse taaskĂ€ivitamisel. Ja kaotate juurdepÀÀsu kĂ”ikidele andmetele, kuna te ei suuda neid leida.
Tere! AitĂ€h ettekande eest! Minu nimi on Pavel. Olen ettevĂ”ttest Wildberries. Mul on teile mĂ”ned kĂŒsimused. Esimene kĂŒsimus. Kuidas arvate, et kui valiksite oma rakenduse arhitektuuri ehitamisel teise printsiibi ja partitsioneeriksite andmed ajas, siis vĂ”iks-olla suudaksite te andmete ristumisi otsingul teha, tuginedes ainult sellele, et ĂŒhes partitsioonis on andmed ĂŒhe ajavahemiku kohta, st ĂŒhe aja vahe kohta ja te ei peaks muretsema selle ĂŒle, et teil on tĂŒkid erinevalt laiali jaotatud? Teine kĂŒsimus â kuna te rakendate sarnast algoritmi bitsetiga ja kĂ”igega muuga, kas olete proovinud kasutada protsessori kĂ€ske? VĂ”ib-olla olete proovinud selliseid optimeerimisi?
Teisele kĂŒsimusele vastan kohe. Me ei ole sinna veel jĂ”udnud. Aga kui on vaja, siis jĂ”uame. Aga esimese, mis oli kĂŒsimus?
Te arutasite kahte stsenaariumi. Ja ĂŒtlesite, et valisite teise keerukama rakenduse. Ja ei eelistanud esimest, kus andmed on ajas partitsioneeritud.
Jah. Esimeses variandis oleks indeksi kogumaht suurem, kuna igas partitsioonis peaksime hoidma andmete koopiaid nende ajaseeriate jaoks, mis kestavad lÀbi kÔikide nende partitsioonide. Ja kui teie ajaseeriate churn rate on madal, st pidevalt kasutatakse samu seeriaid, siis esimese variandi puhul kaotaksime oluliselt rohkem kettaruumi vÔrreldes teise variandiga.
Aga jah, ajapĂ”hine partitsioneerimine on hea variant. Seda kasutab ka Prometheus. Kuid Prometheusel on teine puudus. Kui need andmeosad ĂŒhendatakse, peab ta hoidma mĂ€lus kĂ”iki labelâi ja ajaseeriate metaandmeid. SeetĂ”ttu, kui andmeosad, mida ta ĂŒhendab, on suured, kasvab mĂ€llu minev tarbimine oluliselt, erinevalt VictoriaMetricsist. VictoriaMetrics ei tarbi ĂŒhendamisel ĂŒldse mĂ€lu, seal tarbitakse vaid paar kilobaiti, sĂ”ltumata ĂŒhendatud andmeosade suurusest.
Teie kasutatav algoritm tarbib mĂ€lu. Seal tĂ€histatakse ajaseeriate silti, millel on vÀÀrtused. Nii kontrollite, kas ĂŒhes andme massiivis on paariline olemas ja teisest. Ja saate aru â kas toimus ristumine vĂ”i mitte. TĂŒĂŒpiliselt rakendavad andmebaasid kursoreid, iteraatoreid, mis hoiavad oma praegust olekut ja liiguvad jĂ€rjestatud andmete kaudu, tagades, et nende toimingute keerukus on lihtne.
Miks me ei kasuta andmete ristumise jaoks kursoreid?
Jah.
Meie LevelDB-s vĂ”i mergeset'is ongi just jĂ€rjekorras olevad read. Me saame kursori abil lĂ€bida ja leida ristumise. Aga miks ei kasuta? Sest â see on aeglane. Sest kursorid eeldavad, et igale reale tuleb funktsioon kutsuda. Funktsiooni kutsumine on 5 nanosekundit. Ja kui teil on 100 000 000 rida, siis kulutame pelgalt funktsiooni kutsumise peale pool sekundit.
Selline asi on, jah. Ja mul on viimane kĂŒsimus. KĂŒsimus, mis vĂ”ib-olla tunduda veidi imelik. Miks ei saa andmete sisestamise hetkel kĂ”iki vajalikke agregaatide arvestusi teha ja neid vajalikus formaadis salvestada? Miks hoida tohutul hulgal andmeid sĂŒsteemidesse nagu VictoriaMetrics, ClickHouse jne, et hiljem neile vĂ€ga palju aega kulutada?
To clarify, let me give an example. How does a small toy speedometer work? It records the distance you have traveled, continuously adding it to one variable, while in another it tracks time. Then it divides to get the average speed. You can do something similar by collecting all necessary facts on the fly.
Alright, I understand the question. Your example has its merits. If you know which aggregates you need, itâs the best implementation. The problem is that people save these metrics, some data in ClickHouse, and they donât yet know how theyâll aggregate and filter them in the future, so they end up saving all the raw data. But if you know you need to calculate something average, why not calculate it instead of storing a bunch of raw values? This is only if you know exactly what you need.
By the way, databases for storing time series support aggregate counting. For instance, Prometheus supports . This can be done if you know what aggregates you will need. This feature is not available in VictoriaMetrics yet, but Prometheus is usually placed in front of it, where this can be done with recording rules.
For example, at my previous job, we needed to count the number of events in a sliding window over the last hour. The problem was that we had to create a custom implementation in Go, a service for counting this. Ultimately, this service was non-trivial because it's challenging to calculate. The implementation can be simple if you need to count aggregates at fixed time intervals. However, if you want to count events in a sliding window, it's not as easy as it seems. I believe this hasn't been implemented in ClickHouse or time series databases yet because it's complex to do.
And one more question. We were just discussing averaging, and I remembered that there used to be a tool called Graphite with a Carbon backend. It could prune old data, retaining one point per minute, one point per hour, and so on. It's quite convenient if we need raw data, say for a month, while everything else can be pruned. But Prometheus and VictoriaMetrics do not support this functionality. Will it be supported in the future? If not, why?
AitĂ€h kĂŒsimuse eest. Meie kasutajad esitavad seda aeg-ajalt. Nad kĂŒsivad, millal me lisame downsampling'i toe. Siin on mitu probleemi. Esiteks, iga kasutaja mĂ”istab seda erinevalt. allapoole nĂ€itamine Keegi soovib saada suvalist punkti mÀÀratud intervallis, keegi soovib maksimaalseid, minimaalseid, keskmisi vÀÀrtuseid. Kui teie andmebaasi salvestavad andmeid paljud sĂŒsteemid, siis ei saa neid ĂŒhte patta panna. VĂ”ib juhtuda, et iga sĂŒsteemi jaoks tuleb kasutada erinevat downsampling'ut. Ja see on keeruline ellu viia.
Teiseks on see, et VictoriaMetrics, nagu ka ClickHouse, on optimeeritud töötama suurte tooredate kogustega, seetĂ”ttu suudab see töödelda miljardit rida vĂ€hem kui sekundiga, kui teil on palju tuumasid teie sĂŒsteemis. Aja rea punkte skaneerimine VictoriaMetrics'is â 50 000 000 punkti sekundis ĂŒhes tuumas. Ja see jĂ”udlus skaleerub olemasolevatele tuumadele. Ehk siis kui teil on nĂ€iteks 20 tuuma, siis saate miljard punkti sekundis skaneerida. Ja see omadus vĂ€hendab VictoriaMetrics'i ja ClickHouse'i vajadust downsampling'i jĂ€rele.
Veel ĂŒks omadus on see, et VictoriaMetrics tihendab andmeid tĂ”husalt. Tihendamine keskmiselt production'is on 0,4 kuni 0,8 baiti punkti kohta. Iga punkt â see on ajatemperatuur + vÀÀrtus. Ja see tihendatakse keskmiselt vĂ€hem kui ĂŒhe baidi alla.
Sergei. Mul on kĂŒsimus. Mis on minimaalne salvestamise ajavahemik?
Ăks millisekund. Hiljuti oli meil vestlus teiste ajareadade andmebaaside arendajatega. Nende minimaalne ajavahemik on ĂŒks sekund. NĂ€iteks Graphites on see ka ĂŒks sekund. OpenTSDB-s on samuti ĂŒks sekund. InfluxDB-s on nanosekundiline tĂ€psus. VictoriaMetrics'is on see ĂŒks millisekund, kuna Prometheus'el on ĂŒks millisekund. Ja VictoriaMetrics loodi algselt Prometheuse kaugandmete salvestamiseks. Kuid nĂŒĂŒd suudab see salvestada andmeid ka muudest sĂŒsteemidest.
Inimene, kellega ma rÀÀkisin, ĂŒtleb, et neil on sekundiline tĂ€psus â sellest piisab, kuna see sĂ”ltub andmete tĂŒĂŒbist, mida ajareadadesse salvestatakse. Kui need on DevOps andmed vĂ”i andmed infrastruktuurist, kus te kogute neid 30 sekundi, minuti kaupa, siis seal piisab sekundilisest tĂ€psusest, vĂ€hem ei ole vajalik. Kuid kui te kogute andmeid kĂ”rge sagedusega kauplemissĂŒsteemidest, siis on vajalik nanosekundiline tĂ€psus.
Millisekundi tĂ€psus VictoriaMetrics sobib nii DevOps juhtumitele, kui ka enamikule juhtumitest, millest ma aruandesse alguses rÀÀkisin. Ainus, mille jaoks see ei pruugi sobida, on kĂ”rge sagedusega kaubandussĂŒsteemid.
AitĂ€h! Ja veel ĂŒks kĂŒsimus. Milline on ĂŒhilduvus PromQL-iga?
TĂ€ielik tagasikĂ€idatav ĂŒhilduvus. VictoriaMetrics toetab tĂ€ielikult PromQL-i. Peale selle lisab see ka tĂ€iendavaid funktsioone PromQL-ile, mida nimetatakse . Selle tĂ€iendava funktsiooni kohta on YouTube'is ettekandeid. Olen sellest rÀÀkinud Monitoring Meetup'il kevadel Peterburis.
Telegrami kanal .
Ainult registreeritud kasutajad saavad kĂŒsitluses osaleda. , palun.
Mis takistab teid ĂŒleminekul VictoriaMetricsile kui pikaajalisele salvestuslahendusele Prometheusele? (Kirjutage kommentaaridesse, lisaan kĂŒsitlusse))
71,4%Ei kasuta Prometheus5
28,6%Ei teadnud VictoriaMetrics2 kohta
7 kasutajat hÀÀletas. 12 kasutajat viibis.
Allikas: habr.com
