Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Jeg foreslår, at du læser udskriften af ​​den sene 2019-rapport af Alexander Valyalkin "Go optimizations in VictoriaMetrics"

VictoriaMetrics — et hurtigt og skalerbart DBMS til lagring og behandling af data i form af en tidsserie (recorden danner tid og et sæt værdier svarende til dette tidspunkt, f.eks. opnået gennem periodisk polling af sensorers status eller indsamling af målinger).

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Her er et link til videoen af ​​denne rapport - https://youtu.be/MZ5P21j_HLE

Dias

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Fortæl os om dig selv. Jeg er Alexander Valyalkin. Her min GitHub-konto. Jeg brænder for Go og ydeevneoptimering. Jeg skrev en masse nyttige og knap så nyttige biblioteker. De starter med enten fasteller med quick præfiks.

Jeg arbejder i øjeblikket på VictoriaMetrics. Hvad er det, og hvad laver jeg der? Jeg vil tale om dette i denne præsentation.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Oplægget af rapporten er som følger:

  • Først vil jeg fortælle dig, hvad VictoriaMetrics er.
  • Så vil jeg fortælle dig, hvad tidsserier er.
  • Så vil jeg fortælle dig, hvordan en tidsseriedatabase fungerer.
  • Dernæst vil jeg fortælle dig om databasearkitekturen: hvad den består af.
  • Og lad os så gå videre til de optimeringer, som VictoriaMetrics har. Dette er en optimering for det inverterede indeks og en optimering for bitset-implementeringen i Go.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Er der nogen blandt publikum, der ved, hvad VictoriaMetrics er? Wow, mange mennesker ved det allerede. Det er en god nyhed. For dem, der ikke ved det, er dette en tidsseriedatabase. Den er baseret på ClickHouse-arkitekturen, på nogle detaljer om ClickHouse-implementeringen. For eksempel på som: MergeTree, parallel beregning på alle tilgængelige processorkerner og ydelsesoptimering ved at arbejde på datablokke, der placeres i processorcachen.

VictoriaMetrics giver bedre datakomprimering end andre tidsseriedatabaser.

Den skaleres lodret – det vil sige, at du kan tilføje flere processorer, mere RAM på én computer. VictoriaMetrics vil med succes udnytte disse tilgængelige ressourcer og forbedre den lineære produktivitet.

VictoriaMetrics skalerer også vandret - det vil sige, du kan tilføje yderligere noder til VictoriaMetrics-klyngen, og dens ydeevne vil stige næsten lineært.

Som du gættede, er VictoriaMetrics en hurtig database, fordi jeg ikke kan skrive andre. Og det er skrevet i Go, så jeg taler om det på dette møde.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Hvem ved, hvad en tidsserie er? Han kender også mange mennesker. En tidsserie er en serie af par (timestamp, значение), hvor disse par er sorteret efter tid. Værdien er et flydende kommatal – float64.

Hver tidsserie er unikt identificeret med en nøgle. Hvad består denne nøgle af? Den består af et ikke-tomt sæt nøgleværdi-par.

Her er et eksempel på en tidsserie. Nøglen til denne serie er en liste over par: __name__="cpu_usage" er navnet på metrikken, instance="my-server" - dette er den computer, hvorpå denne metrik er indsamlet, datacenter="us-east" - dette er datacentret, hvor denne computer er placeret.

Vi endte med et tidsserienavn bestående af tre nøgleværdi-par. Denne tast svarer til en liste over par (timestamp, value). t1, t3, t3, ..., tN - disse er tidsstempler, 10, 20, 12, ..., 15 — de tilsvarende værdier. Dette er cpu-forbruget på et givet tidspunkt for en given række.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Hvor kan tidsserier bruges? Er der nogen der har nogen idé?

  • I DevOps kan du måle CPU, RAM, netværk, rps, antal fejl osv.
  • IoT – vi kan måle temperatur, tryk, geokoordinater og noget andet.
  • Også finans – vi kan overvåge priser for alle mulige aktier og valutaer.
  • Derudover kan tidsserier bruges til overvågning af produktionsprocesser på fabrikker. Vi har brugere, der bruger VictoriaMetrics til at overvåge vindmøller, for robotter.
  • Tidsserier er også nyttige til at indsamle information fra sensorer på forskellige enheder. For eksempel til en motor; til måling af dæktryk; til måling af hastighed, afstand; til måling af benzinforbrug mv.
  • Tidsserier kan også bruges til at overvåge fly. Hvert fly har en sort boks, der samler tidsserier for forskellige parametre for flyets helbred. Tidsserier bruges også i rumfartsindustrien.
  • Sundhedspleje er blodtryk, puls mv.

Der kan være flere applikationer, som jeg har glemt, men jeg håber, du forstår, at tidsserier aktivt bruges i den moderne verden. Og mængden af ​​deres brug vokser hvert år.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Hvorfor har du brug for en tidsseriedatabase? Hvorfor kan du ikke bruge en almindelig relationsdatabase til at gemme tidsserier?

Fordi tidsserier normalt indeholder en stor mængde information, som er svær at gemme og behandle i konventionelle databaser. Derfor dukkede der specialiserede databaser for tidsserier op. Disse baser gemmer effektivt point (timestamp, value) med den givne nøgle. De giver en API til at læse lagrede data efter nøgle, af et enkelt nøgle-værdi-par eller af flere nøgle-værdi-par eller ved regexp. For eksempel vil du finde CPU-belastningen af ​​alle dine tjenester i et datacenter i Amerika, så skal du bruge denne pseudo-forespørgsel.

Typisk leverer tidsseriedatabaser specialiserede forespørgselssprog, fordi tidsserie-SQL ikke er særlig velegnet. Selvom der findes databaser, der understøtter SQL, er det ikke særlig velegnet. Forespørgselssprog som f.eks PromQL, InfluxQL, Flux, Q. Jeg håber, at nogen har hørt mindst et af disse sprog. Mange mennesker har sikkert hørt om PromQL. Dette er Prometheus-forespørgselssproget.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Sådan ser en moderne tidsseriedatabasearkitektur ud ved at bruge VictoriaMetrics som eksempel.

Den består af to dele. Dette er lagring for det inverterede indeks og lagring for tidsserieværdier. Disse depoter er adskilt.

Når en ny post ankommer i databasen, får vi først adgang til det inverterede indeks for at finde tidsserieidentifikationen for et givet sæt label=value for en given metrik. Vi finder denne identifikator og gemmer værdien i datalageret.

Når der kommer en anmodning om at hente data fra TSDB, går vi først til det inverterede indeks. Lad os få alt timeseries_ids rekorder, der matcher dette sæt label=value. Og så får vi alle de nødvendige data fra datavarehuset, indekseret af timeseries_ids.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Lad os se på et eksempel på, hvordan en tidsseriedatabase behandler en indgående udvalgt forespørgsel.

  • Først og fremmest får hun alt timeseries_ids fra et omvendt indeks, der indeholder de givne par label=value, eller opfylde et givet regulært udtryk.
  • Derefter henter den alle datapunkter fra datalageret med et givet tidsinterval for de fundne timeseries_ids.
  • Herefter udfører databasen nogle beregninger på disse datapunkter, efter brugerens anmodning. Og derefter returnerer den svaret.

I denne præsentation vil jeg fortælle dig om den første del. Dette er en søgning timeseries_ids ved omvendt indeks. Du kan se om anden del og tredje del senere VictoriaMetrics kilder, eller vent til jeg forbereder andre rapporter :)

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Lad os gå videre til det omvendte indeks. Mange tror måske, at dette er enkelt. Hvem ved, hvad et omvendt indeks er, og hvordan det fungerer? Åh, ikke så mange mennesker længere. Lad os prøve at forstå, hvad det er.

Det er faktisk simpelt. Det er simpelthen en ordbog, der kortlægger en nøgle til en værdi. Hvad er en nøgle? Dette par label=valueHvor label и value - det er linjer. Og værdierne er et sæt timeseries_ids, som inkluderer det givne par label=value.

Inverteret indeks giver dig mulighed for hurtigt at finde alt timeseries_ids, som har givet label=value.

Det giver dig også mulighed for hurtigt at finde timeseries_ids tidsserier for flere par label=value, eller for par label=regexp. Hvordan sker dette? Ved at finde skæringspunktet mellem sættet timeseries_ids for hvert par label=value.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Lad os se på forskellige implementeringer af det omvendte indeks. Lad os starte med den enkleste naive implementering. Hun ser sådan ud.

Funktion getMetricIDs får en liste over strenge. Hver linje indeholder label=value. Denne funktion returnerer en liste metricIDs.

Hvordan det virker? Her har vi en global variabel kaldet invertedIndex. Dette er en almindelig ordbog (map), som vil kortlægge strengen til slice ints. Linjen indeholder label=value.

Funktionsimplementering: få metricIDs for det første label=value, så gennemgår vi alt det andet label=value, vi forstår det metricIDs for dem. Og ring til funktionen intersectInts, som vil blive diskuteret nedenfor. Og denne funktion returnerer skæringspunktet mellem disse lister.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Som du kan se, er implementering af et inverteret indeks ikke særlig kompliceret. Men dette er en naiv implementering. Hvilke ulemper har det? Den største ulempe ved den naive implementering er, at et sådant omvendt indeks er gemt i RAM. Efter genstart af applikationen mister vi dette indeks. Der er ingen lagring af dette indeks på disk. Et sådant omvendt indeks er usandsynligt egnet til en database.

Den anden ulempe er også relateret til hukommelsen. Det omvendte indeks skal passe ind i RAM. Hvis det overstiger størrelsen af ​​RAM, så vil vi naturligvis få - ud af hukommelsesfejl. Og programmet virker ikke.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Dette problem kan løses ved hjælp af færdige løsninger som f.eks NiveauDBEller KlipperDB.

Kort sagt, vi har brug for en database, der giver os mulighed for at udføre tre operationer hurtigt.

  • Den første operation er optagelse ключ-значение til denne database. Hun gør det meget hurtigt, hvor ключ-значение er vilkårlige strenge.
  • Den anden operation er en hurtig søgning efter en værdi ved hjælp af en given tast.
  • Og den tredje operation er en hurtig søgning efter alle værdier med et givet præfiks.

LevelDB og RocksDB - disse databaser er udviklet af Google og Facebook. Først kom LevelDB. Så tog fyrene fra Facebook LevelDB og begyndte at forbedre det, de lavede RocksDB. Nu virker næsten alle interne databaser på RocksDB inde på Facebook, inklusive dem, der er blevet overført til RocksDB og MySQL. De navngav ham MyRocks.

Et inverteret indeks kan implementeres ved hjælp af LevelDB. Hvordan gør man det? Vi gemmer som nøgle label=value. Og værdien er identifikatoren for den tidsserie, hvor parret er til stede label=value.

Hvis vi har mange tidsserier med et givet par label=value, så vil der være mange rækker i denne database med samme nøgle og forskellige timeseries_ids. For at få en liste over alle timeseries_ids, som starter med dette label=prefix, laver vi en rækkeviddescanning, som denne database er optimeret til. Det vil sige, at vi vælger alle linjer, der begynder med label=prefix og få det nødvendige timeseries_ids.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Her er et eksempel på implementering af, hvordan det ville se ud i Go. Vi har et omvendt indeks. Dette er LevelDB.

Funktionen er den samme som for den naive implementering. Det gentager den naive implementering næsten linje for linje. Den eneste pointe er, at i stedet for at vende sig til map vi får adgang til det omvendte indeks. Vi får alle værdierne til det første label=value. Så gennemgår vi alle de resterende par label=value og få de tilsvarende sæt metricID'er for dem. Så finder vi krydset.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Alt ser ud til at være i orden, men der er ulemper ved denne løsning. VictoriaMetrics implementerede oprindeligt et omvendt indeks baseret på LevelDB. Men til sidst måtte jeg opgive det.

Hvorfor? Fordi LevelDB er langsommere end den naive implementering. I en naiv implementering, givet en given nøgle, henter vi straks hele udsnittet metricIDs. Dette er en meget hurtig operation - hele skiven er klar til brug.

I LevelDB, hver gang en funktion kaldes GetValues du skal gennemgå alle de linjer, der starter med label=value. Og få værdien for hver linje timeseries_ids. Af sådan timeseries_ids saml en skive af disse timeseries_ids. Det er klart, at dette er meget langsommere end blot at få adgang til et almindeligt kort med nøgle.

Den anden ulempe er, at LevelDB er skrevet i C. At kalde C-funktioner fra Go er ikke særlig hurtigt. Det tager hundredvis af nanosekunder. Dette er ikke særlig hurtigt, for sammenlignet med et almindeligt funktionskald skrevet in go, som tager 1-5 nanosekunder, er forskellen i ydeevne titusvis af gange. For VictoriaMetrics var dette en fatal fejl :)

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Så jeg skrev min egen implementering af det omvendte indeks. Og han ringede til hende mergeset.

Mergeset er baseret på MergeTree-datastrukturen. Denne datastruktur er lånt fra ClickHouse. Naturligvis skal mergeset være optimeret til hurtig søgning timeseries_ids i henhold til den givne nøgle. Mergeset er skrevet helt i Go. Du kan se VictoriaMetrics kilder på GitHub. Implementeringen af ​​mergeset er i mappen /lib/mergeset. Du kan prøve at finde ud af, hvad der foregår der.

Mergeset API er meget lig LevelDB og RocksDB. Det vil sige, at det giver dig mulighed for hurtigt at gemme nye poster der og hurtigt vælge poster med et givet præfiks.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Vi vil tale om ulemperne ved mergeset senere. Lad os nu tale om, hvilke problemer der opstod med VictoriaMetrics i produktionen, når vi implementerede et omvendt indeks.

Hvorfor opstod de?

Den første årsag er den høje churn rate. Oversat til russisk er dette en hyppig ændring i tidsserier. Det er, når en tidsserie slutter, og en ny serie begynder, eller mange nye tidsserier begynder. Og dette sker ofte.

Den anden grund er det store antal tidsserier. I begyndelsen, da overvågningen blev populær, var antallet af tidsserier lille. For eksempel skal du for hver computer overvåge CPU, hukommelse, netværk og diskbelastning. 4 tidsserier pr. computer. Lad os sige, at du har 100 computere og 400 tidsserier. Dette er meget lidt.

Med tiden fandt folk ud af, at de kunne måle mere detaljeret information. Mål f.eks. belastningen ikke af hele processoren, men separat af hver processorkerne. Hvis du har 40 processorkerner, så har du 40 gange flere tidsserier til at måle processorbelastning.

Men det er ikke alt. Hver processorkerne kan have flere tilstande, såsom inaktiv, når den er inaktiv. Og også arbejde i brugerrum, arbejde i kernerum og andre tilstande. Og hver sådan tilstand kan også måles som en separat tidsserie. Dette øger desuden antallet af rækker med 7-8 gange.

Fra en metrik fik vi 40 x 8 = 320 metrics for kun én computer. Gang med 100, vi får 32 i stedet for 000.

Så kom Kubernetes. Og det blev værre, fordi Kubernetes kan hoste mange forskellige tjenester. Hver tjeneste i Kubernetes består af mange pods. Og alt dette skal overvåges. Derudover har vi en konstant udrulning af nye versioner af dine tjenester. For hver ny version skal der oprettes nye tidsserier. Som følge heraf vokser antallet af tidsserier eksponentielt, og vi står over for problemet med et stort antal tidsserier, som kaldes højkardinalitet. VictoriaMetrics klarer det med succes sammenlignet med andre tidsseriedatabaser.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Lad os se nærmere på høj churn rate. Hvad forårsager en høj churn rate i produktionen? Fordi nogle betydninger af etiketter og tags ændrer sig konstant.

Tag for eksempel Kubernetes, som har konceptet deployment, dvs. når en ny version af din applikation udrulles. Af en eller anden grund besluttede Kubernetes-udviklerne at tilføje implementerings-id'et til etiketten.

Hvad førte dette til? Desuden afbrydes alle de gamle tidsserier med hver ny implementering, og i stedet for dem begynder nye tidsserier med en ny etiketværdi deployment_id. Der kan være hundredtusinder og endda millioner af sådanne rækker.

Det vigtige ved alt dette er, at det samlede antal tidsserier vokser, men antallet af tidsserier, der i øjeblikket er aktive og modtager data, forbliver konstant. Denne tilstand kaldes høj churn rate.

Hovedproblemet med høj churn rate er at sikre en konstant søgehastighed for alle tidsserier for et givet sæt etiketter over et bestemt tidsinterval. Dette er typisk tidsintervallet for den sidste time eller den sidste dag.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Hvordan løser man dette problem? Her er den første mulighed. Dette er for at opdele det omvendte indeks i uafhængige dele over tid. Det vil sige, at der går noget tidsinterval, vi afslutter arbejdet med det aktuelle inverterede indeks. Og opret et nyt omvendt indeks. Endnu et tidsinterval går, vi skaber endnu et og endnu et.

Og når vi prøver fra disse inverterede indekser, finder vi et sæt af inverterede indekser, der falder inden for det givne interval. Og derfor vælger vi tidsseriens id derfra.

Dette sparer ressourcer, fordi vi ikke skal se på dele, der ikke falder inden for det givne interval. Det vil sige, at hvis vi normalt vælger data for den sidste time, så springer vi anmodninger over for tidligere tidsintervaller.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Der er en anden mulighed for at løse dette problem. Dette er for at gemme for hver dag en separat liste over id'er af tidsserier, der fandt sted den dag.

Fordelen ved denne løsning i forhold til den tidligere løsning er, at vi ikke duplikerer tidsserieinformation, som ikke forsvinder over tid. De er konstant til stede og ændrer sig ikke.

Ulempen er, at en sådan løsning er sværere at implementere og sværere at debugge. Og VictoriaMetrics valgte denne løsning. Sådan skete det historisk. Denne løsning fungerer også godt sammenlignet med den forrige. Fordi denne løsning ikke blev implementeret på grund af det faktum, at det er nødvendigt at duplikere data i hver partition for tidsserier, der ikke ændrer sig, dvs. som ikke forsvinder over tid. VictoriaMetrics var primært optimeret til diskpladsforbrug, og den tidligere implementering gjorde diskpladsforbruget værre. Men denne implementering er bedre egnet til at minimere diskpladsforbrug, så den blev valgt.

Jeg var nødt til at kæmpe mod hende. Kampen var, at du i denne implementering stadig skal vælge et meget større antal timeseries_ids for data, end når det inverterede indeks er tidsopdelt.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Hvordan løste vi dette problem? Vi løste det på en original måde - ved at gemme flere tidsserieidentifikatorer i hver inverteret indeksindgang i stedet for én identifikator. Det vil sige, at vi har en nøgle label=value, som forekommer i hver tidsserie. Og nu sparer vi flere timeseries_ids i én indgang.

Her er et eksempel. Tidligere havde vi N poster, men nu har vi én post, hvis præfiks er det samme som alle de andre. For den forrige indtastning indeholder værdien alle tidsserie-id'er.

Dette gjorde det muligt at øge scanningshastigheden for et sådant omvendt indeks op til 10 gange. Og det gav os mulighed for at reducere hukommelsesforbruget til cachen, for nu gemmer vi strengen label=value kun én gang i cachen sammen N gange. Og denne linje kan være stor, hvis du gemmer lange linjer i dine tags og etiketter, som Kubernetes kan lide at skubbe der.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

En anden mulighed for at fremskynde søgning på et omvendt indeks er sharding. Oprettelse af flere inverterede indekser i stedet for ét og deling af data mellem dem med nøgle. Dette er et sæt key=value damp. Det vil sige, at vi får flere uafhængige inverterede indekser, som vi kan forespørge parallelt på flere processorer. Tidligere implementeringer tillod kun drift i enkeltprocessortilstand, dvs. scanning af data på kun én kerne. Denne løsning giver dig mulighed for at scanne data på flere kerner på én gang, som ClickHouse kan lide at gøre. Det er det, vi planlægger at implementere.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Lad os nu vende tilbage til vores får - til krydsningsfunktionen timeseries_ids. Lad os overveje, hvilke implementeringer der kan være tale om. Denne funktion giver dig mulighed for at finde timeseries_ids for et givet sæt label=value.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Den første mulighed er en naiv implementering. To indlejrede løkker. Her får vi funktionen input intersectInts to skiver - a и b. Ved udgangen skulle det vende tilbage til os skæringspunktet mellem disse skiver.

En naiv implementering ser sådan ud. Vi itererer over alle værdier fra slice a, inde i denne løkke gennemgår vi alle værdierne af slice b. Og vi sammenligner dem. Hvis de matcher, så har vi fundet et vejkryds. Og gem det ind result.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Hvad er ulemperne? Kvadratisk kompleksitet er dens største ulempe. For eksempel hvis dine dimensioner er skive a и b en million ad gangen, så vil denne funktion aldrig returnere et svar til dig. Fordi det skal lave en trillion iterationer, hvilket er meget selv for moderne computere.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Den anden implementering er baseret på kort. Vi laver kort. Vi sætter alle værdierne fra slice ind i dette kort a. Derefter går vi gennem skive i en separat løkke b. Og vi tjekker, om denne værdi er fra skive b i kort. Hvis det findes, så føj det til resultatet.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Hvad er fordelene? Fordelen er, at der kun er lineær kompleksitet. Det vil sige, at funktionen vil køre meget hurtigere for større udsnit. For en million-størrelse udsnit vil denne funktion udføres i 2 millioner iterationer i modsætning til trillioner iterationer af den tidligere funktion.

Ulempen er, at denne funktion kræver mere hukommelse for at skabe dette kort.

Den anden ulempe er den store overhead til hashing. Denne ulempe er ikke særlig indlysende. Og for os var det heller ikke særlig indlysende, så først i VictoriaMetrics var implementeringen af ​​krydset gennem et kort. Men så viste profilering, at hovedprocessortiden bruges på at skrive til kortet og kontrollere, om der er en værdi i dette kort.

Hvorfor spildes CPU-tid disse steder? Fordi Go udfører en hashing-operation på disse linjer. Det vil sige, at den beregner nøglens hash for derefter at få adgang til den ved et givet indeks i HashMap. Hash-beregningsoperationen er afsluttet på snesevis af nanosekunder. Dette er langsomt for VictoriaMetrics.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Jeg besluttede at implementere et bitsæt, der er optimeret specifikt til denne sag. Sådan ser skæringspunktet mellem to skiver ud nu. Her laver vi et bitsæt. Vi tilføjer elementer fra den første skive til den. Derefter kontrollerer vi tilstedeværelsen af ​​disse elementer i den anden skive. Og føj dem til resultatet. Det vil sige, at det næsten ikke er anderledes end det foregående eksempel. Det eneste her er, at vi har erstattet adgang til kort med brugerdefinerede funktioner add и has.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Umiddelbart ser det ud til, at dette skulle virke langsommere, hvis der tidligere blev brugt et standardkort der, og så kaldes nogle andre funktioner, men profilering viser, at denne ting fungerer 10 gange hurtigere end standardkortet i tilfældet med VictoriaMetrics.

Derudover bruger den meget mindre hukommelse sammenlignet med kortimplementeringen. Fordi vi gemmer bits her i stedet for otte-byte værdier.

Ulempen ved denne implementering er, at den ikke er så indlysende, ikke triviel.

En anden ulempe, som mange måske ikke bemærker, er, at denne implementering muligvis ikke fungerer godt i nogle tilfælde. Det vil sige, at det er optimeret til et specifikt tilfælde, for dette tilfælde af skæring af VictoriaMetrics tidsserie-id'er. Det betyder ikke, at det er egnet til alle tilfælde. Hvis det bruges forkert, vil vi ikke få en ydelsesforøgelse, men en fejl i hukommelsen og en nedgang i ydeevnen.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Lad os overveje implementeringen af ​​denne struktur. Hvis du vil kigge, er det placeret i VictoriaMetrics-kilderne, i mappen lib/uint64set. Den er optimeret specifikt til VictoriaMetrics sagen, hvor timeseries_id er en 64-bit værdi, hvor de første 32 bit stort set er konstante og kun de sidste 32 bit ændres.

Denne datastruktur er ikke gemt på disken, den fungerer kun i hukommelsen.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Her er dens API. Det er ikke særlig kompliceret. API'et er skræddersyet specifikt til et specifikt eksempel på brug af VictoriaMetrics. Det vil sige, at der ikke er unødvendige funktioner her. Her er de funktioner, der udtrykkeligt bruges af VictoriaMetrics.

Der er funktioner add, som tilføjer nye værdier. Der er en funktion has, som tjekker for nye værdier. Og der er en funktion del, som fjerner værdier. Der er en hjælpefunktion len, som returnerer størrelsen af ​​sættet. Fungere clone kloner meget. Og funktion appendto konverterer dette sæt til udsnit timeseries_ids.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Sådan ser implementeringen af ​​denne datastruktur ud. sæt har to elementer:

  • ItemsCount er et hjælpefelt til hurtigt at returnere antallet af elementer i et sæt. Det ville være muligt at undvære dette hjælpefelt, men det skulle tilføjes her, fordi VictoriaMetrics ofte forespørger på bitsætlængden i sine algoritmer.

  • Det andet felt er buckets. Dette er udsnit fra strukturen bucket32. Hver struktur gemmer hi Mark. Disse er de øverste 32 bits. Og to skiver - b16his и buckets af bucket16 strukturer.

De øverste 16 bits af den anden del af 64-bit strukturen er gemt her. Og her gemmes bitsæt for de nederste 16 bits af hver byte.

Bucket64 består af et array uint64. Længden beregnes ved hjælp af disse konstanter. I en bucket16 maksimalt kan gemmes 2^16=65536 lidt. Hvis du dividerer dette med 8, så er det 8 kilobyte. Hvis du dividerer med 8 igen, er det 1000 uint64 betyder. Det er Bucket16 – dette er vores 8-kilobyte struktur.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Lad os se på, hvordan en af ​​metoderne i denne struktur til at tilføje en ny værdi implementeres.

Det hele starter med uint64 betydninger. Vi beregner de øverste 32 bit, vi beregner de nederste 32 bits. Lad os gennemgå alt buckets. Vi sammenligner de øverste 32 bits i hver spand med den værdi, der tilføjes. Og hvis de matcher, så kalder vi funktionen add i struktur b32 buckets. Og tilføj de nederste 32 bits der. Og hvis det vendte tilbage true, så betyder det, at vi tilføjede sådan en værdi der, og vi havde ikke sådan en værdi. Hvis den vender tilbage false, så eksisterede en sådan betydning allerede. Så øger vi antallet af elementer i strukturen.

Hvis vi ikke har fundet den du skal bruge bucket med den påkrævede hi-værdi, så kalder vi funktionen addAlloc, som vil producere en ny bucket, tilføjer det til spandstrukturen.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Dette er implementeringen af ​​funktionen b32.add. Det ligner den tidligere implementering. Vi beregner de mest signifikante 16 bits, de mindst signifikante 16 bits.

Så gennemgår vi alle de øverste 16 bits. Vi finder matcher. Og hvis der er et match, kalder vi add-metoden, som vi vil overveje på næste side for bucket16.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Og her er det laveste niveau, som bør optimeres mest muligt. Vi beregner for uint64 id-værdi i skivebit og også bitmask. Dette er en maske for en given 64-bit værdi, som kan bruges til at kontrollere tilstedeværelsen af ​​denne bit eller indstille den. Vi tjekker, om denne bit er indstillet, og indstiller den, og returnerer tilstedeværelse. Dette er vores implementering, som gjorde det muligt for os at fremskynde driften af ​​krydsende id'er af tidsserier med 10 gange sammenlignet med konventionelle kort.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Ud over denne optimering har VictoriaMetrics mange andre optimeringer. De fleste af disse optimeringer blev tilføjet af en grund, men efter profilering af koden i produktionen.

Dette er hovedreglen for optimering - tilføj ikke optimering, hvis du antager, at der vil være en flaskehals her, for det kan vise sig, at der ikke vil være en flaskehals der. Optimering forringer normalt kodens kvalitet. Derfor er det værd at optimere først efter profilering og gerne i produktionen, så der er tale om rigtige data. Hvis nogen er interesseret, kan du se på VictoriaMetrics-kildekoden og udforske andre optimeringer, der er der.

Gå til optimeringer i VictoriaMetrics. Alexander Valyalkin

Jeg har et spørgsmål om bitset. Meget lig C++ vektor bool implementeringen, optimeret bitset. Tog du implementeringen derfra?

Nej, ikke derfra. Da jeg implementerede dette bitsæt, blev jeg styret af viden om strukturen af ​​disse ids-tidsserier, som bruges i VictoriaMetrics. Og deres struktur er sådan, at de øverste 32 bits grundlæggende er konstante. De nederste 32 bit kan ændres. Jo lavere bit, jo oftere kan det ændre sig. Derfor er denne implementering specifikt optimeret til denne datastruktur. C++-implementeringen er, så vidt jeg ved, optimeret til det generelle tilfælde. Hvis man optimerer til den generelle sag, betyder det, at det ikke vil være det mest optimale for en konkret sag.

Jeg råder dig også til at se rapporten fra Alexey Milovid. For omkring en måned siden talte han om optimering i ClickHouse til specifikke specialiseringer. Han siger bare, at i det generelle tilfælde er en C++ implementering eller en anden implementering skræddersyet til at fungere godt i gennemsnit på et hospital. Det kan fungere dårligere end en vidensspecifik implementering som vores, hvor vi ved, at de øverste 32 bits for det meste er konstante.

Jeg har et andet spørgsmål. Hvad er den grundlæggende forskel fra InfluxDB?

Der er mange grundlæggende forskelle. Med hensyn til ydeevne og hukommelsesforbrug viser InfluxDB i test 10 gange mere hukommelsesforbrug for tidsserier med høj kardinalitet, når man har mange af dem, for eksempel millioner. For eksempel bruger VictoriaMetrics 1 GB pr. million aktive rækker, mens InfluxDB bruger 10 GB. Og det er en stor forskel.

Den anden grundlæggende forskel er, at InfluxDB har mærkelige forespørgselssprog - Flux og InfluxQL. De er ikke særlig bekvemme at arbejde med tidsserier i forhold til PromQL, som er understøttet af VictoriaMetrics. PromQL er et forespørgselssprog fra Prometheus.

Og endnu en forskel er, at InfluxDB har en lidt mærkelig datamodel, hvor hver linje kan gemme flere felter med forskellige tags. Disse linjer er yderligere opdelt i forskellige tabeller. Disse yderligere komplikationer komplicerer efterfølgende arbejde med denne database. Det er svært at støtte og forstå.

I VictoriaMetrics er alt meget enklere. Der er hver tidsserie en nøgleværdi. Værdien er et sæt af punkter - (timestamp, value), og nøglen er sættet label=value. Der er ingen adskillelse mellem felter og mål. Det giver dig mulighed for at vælge alle data og derefter kombinere, addere, subtrahere, gange, dividere, i modsætning til InfluxDB, hvor beregninger mellem forskellige rækker stadig ikke er implementeret, så vidt jeg ved. Selvom de er implementeret, er det svært, du skal skrive meget kode.

Jeg har et opklarende spørgsmål. Forstod jeg rigtigt, at der var en eller anden form for problem, som du talte om, at dette omvendte indeks ikke passer ind i hukommelsen, så der er partitionering der?

Først viste jeg en naiv implementering af et omvendt indeks på et standard Go-kort. Denne implementering er ikke egnet til databaser, fordi dette omvendte indeks ikke gemmes på disk, og databasen skal gemme på disk, så disse data forbliver tilgængelige ved genstart. I denne implementering, når du genstarter applikationen, vil dit omvendte indeks forsvinde. Og du mister adgangen til alle data, fordi du ikke vil kunne finde dem.

Hej! Tak for rapporten! Mit navn er Pavel. Jeg er fra Wildberries. Jeg har et par spørgsmål til dig. Spørgsmål et. Tror du, at hvis du havde valgt et andet princip ved opbygningen af ​​din applikations arkitektur og partitioneret dataene over tid, så ville du måske have været i stand til at krydse data ved søgning, kun baseret på det faktum, at en partition indeholder data for en tidsrum , det vil sige i et tidsinterval, og du behøver ikke at bekymre dig om, at dine brikker er spredt anderledes? Spørgsmål nummer 2 - siden du implementerer en lignende algoritme med bitset og alt muligt andet, så har du måske prøvet at bruge processorinstruktioner? Måske har du prøvet sådanne optimeringer?

Jeg svarer på den anden med det samme. Vi er ikke nået dertil endnu. Men hvis det er nødvendigt, kommer vi dertil. Og det første, hvad var spørgsmålet?

Du diskuterede to scenarier. Og de sagde, at de valgte den anden med en mere kompleks implementering. Og de foretrak ikke den første, hvor dataene er opdelt efter tid.

Ja. I det første tilfælde ville det samlede volumen af ​​indekset være større, fordi vi i hver partition skulle gemme duplikerede data for de tidsserier, der fortsætter gennem alle disse partitioner. Og hvis din tidsserieafgang er lille, dvs. de samme serier bruges konstant, så ville vi i det første tilfælde miste meget mere i mængden af ​​optaget diskplads sammenlignet med det andet tilfælde.

Og så - ja, tidspartitionering er en god mulighed. Prometheus bruger det. Men Prometheus har en anden ulempe. Når disse stykker af data flettes, skal den opbevare metaoplysninger for alle etiketter og tidsserier i hukommelsen. Derfor, hvis de stykker data, som den flettes, er store, så stiger hukommelsesforbruget meget under fletningen, i modsætning til VictoriaMetrics. Ved fletning bruger VictoriaMetrics slet ikke hukommelse; kun et par kilobyte forbruges, uanset størrelsen på de flettede datastykker.

Algoritmen du bruger bruger hukommelse. Det markerer tidsserietags, der indeholder værdier. Og på denne måde tjekker du for parret tilstedeværelse i ét dataarray og i et andet. Og du forstår, om skæringspunktet fandt sted eller ej. Typisk implementerer databaser markører og iteratorer, der gemmer deres nuværende indhold og kører gennem de sorterede data på grund af den simple kompleksitet af disse operationer.

Hvorfor bruger vi ikke markører til at krydse data?

Ja.

Vi gemmer sorterede rækker i LevelDB eller mergeset. Vi kan flytte markøren og finde krydset. Hvorfor bruger vi det ikke? Fordi det er langsomt. Fordi markører betyder, at du skal kalde en funktion for hver linje. Et funktionskald er 5 nanosekunder. Og har du 100 linjer, så viser det sig, at vi bruger et halvt sekund på bare at kalde funktionen.

Der er sådan noget, ja. Og mit sidste spørgsmål. Spørgsmålet lyder måske lidt mærkeligt. Hvorfor er det ikke muligt at læse alle de nødvendige aggregater i det øjeblik, dataene ankommer og gemme dem i den påkrævede form? Hvorfor spare store mængder i nogle systemer som VictoriaMetrics, ClickHouse osv., og så bruge meget tid på dem?

Jeg vil give et eksempel for at gøre det klarere. Lad os sige, hvordan fungerer et lille legetøjs speedometer? Den registrerer den afstand, du har tilbagelagt, hele tiden tilføjer den til én værdi, og den anden - gang. Og deler sig. Og får gennemsnitsfart. Du kan gøre omtrent det samme. Læg alle de nødvendige fakta sammen med det samme.

Okay, jeg forstår spørgsmålet. Dit eksempel har sin plads. Hvis du ved, hvilke aggregater du har brug for, så er dette den bedste implementering. Men problemet er, at folk gemmer disse metrics, nogle data i ClickHouse, og de ved endnu ikke, hvordan de vil samle og filtrere dem i fremtiden, så de er nødt til at gemme alle de rå data. Men hvis du ved, at du skal beregne noget i gennemsnit, hvorfor så ikke beregne det i stedet for at gemme en masse råværdier der? Men det er kun, hvis du ved præcis, hvad du har brug for.

I øvrigt understøtter databaser til lagring af tidsserier optælling af aggregater. For eksempel understøtter Prometheus regler for optagelse. Det vil sige, at dette kan lade sig gøre, hvis du ved, hvilke enheder du skal bruge. VictoriaMetrics har ikke dette endnu, men det indledes normalt af Prometheus, hvor dette kan gøres i omkodningsreglerne.

For eksempel havde jeg i mit tidligere job brug for at tælle antallet af begivenheder i et glidende vindue i løbet af den sidste time. Problemet er, at jeg var nødt til at lave en tilpasset implementering i Go, altså en service til at tælle denne ting. Denne service var i sidste ende ikke-triviel, fordi den er svær at beregne. Implementeringen kan være enkel, hvis du skal tælle nogle aggregater med faste tidsintervaller. Hvis du vil tælle begivenheder i et glidende vindue, så er det ikke så enkelt, som det ser ud til. Jeg tror, ​​at dette endnu ikke er implementeret i ClickHouse eller i tidsseriedatabaser, fordi det er svært at implementere.

Og endnu et spørgsmål. Vi talte bare om gennemsnit, og jeg huskede, at der engang var sådan noget som Graphite med en Carbon-backend. Og han vidste, hvordan man udtyndede gamle data, det vil sige at efterlade et point i minuttet, et point i timen osv. I princippet er det ret praktisk, hvis vi relativt set har brug for rådata i en måned, og alt andet kan blive tyndet ud. Men Prometheus og VictoriaMetrics understøtter ikke denne funktionalitet. Er det planlagt at støtte det? Hvis ikke, hvorfor ikke?

Tak for spørgsmålet. Vores brugere stiller dette spørgsmål med jævne mellemrum. De spørger, hvornår vi vil tilføje støtte til downsampling. Der er flere problemer her. For det første forstår enhver bruger downsampling noget andet: nogen ønsker at få et hvilket som helst vilkårligt punkt på et givet interval, nogen ønsker maksimum, minimum, gennemsnitsværdier. Hvis mange systemer skriver data til din database, så kan du ikke klumpe det hele sammen. Det kan være, at hvert system kræver forskellig udtynding. Og det er svært at gennemføre.

Og den anden ting er, at VictoriaMetrics ligesom ClickHouse er optimeret til at arbejde med store mængder rådata, så det kan skovle en milliard linjer på mindre end et sekund, hvis du har mange kerner i dit system. Scanning af tidsseriepunkter i VictoriaMetrics – 50 point pr. sekund pr. kerne. Og denne ydeevne skalerer til eksisterende kerner. Det vil sige, at hvis du for eksempel har 000 kerner, vil du scanne en milliard point i sekundet. Og denne egenskab hos VictoriaMetrics og ClickHouse reducerer behovet for downsamling.

En anden funktion er, at VictoriaMetrics effektivt komprimerer disse data. Kompression i gennemsnit i produktionen er fra 0,4 til 0,8 bytes pr. punkt. Hvert punkt er et tidsstempel + værdi. Og det er komprimeret til mindre end én byte i gennemsnit.

Sergey. Jeg har et spørgsmål. Hvad er den mindste optagetidskvante?

Et millisekund. Vi havde for nylig en samtale med andre udviklere af tidsseriedatabaser. Deres minimumstid er et sekund. Og i Graphite er det for eksempel også et sekund. I OpenTSDB er det også et sekund. InfluxDB har nanosekund præcision. I VictoriaMetrics er det et millisekund, for i Prometheus er det et millisekund. Og VictoriaMetrics blev oprindeligt udviklet som fjernlager til Prometheus. Men nu kan den gemme data fra andre systemer.

Den person, jeg talte med, siger, at de har anden-til-sekund nøjagtighed - det er nok for dem, fordi det afhænger af den type data, der bliver gemt i tidsseriedatabasen. Hvis dette er DevOps-data eller data fra infrastruktur, hvor du indsamler det med intervaller på 30 sekunder, i minuttet, så er sekunds nøjagtighed nok, du behøver ikke noget mindre. Og hvis du indsamler disse data fra højfrekvente handelssystemer, har du brug for nanosekunds nøjagtighed.

Millisekundens nøjagtighed i VictoriaMetrics er også velegnet til DevOps-sagen og kan være egnet til de fleste af de tilfælde, som jeg nævnte i begyndelsen af ​​rapporten. Det eneste, som det måske ikke er egnet til, er højfrekvente handelssystemer.

Tak skal du have! Og endnu et spørgsmål. Hvad er kompatibilitet i PromQL?

Fuld bagudkompatibilitet. VictoriaMetrics understøtter fuldt ud PromQL. Derudover tilføjer det yderligere avanceret funktionalitet i PromQL, som kaldes MetricsQL. Der er en snak på YouTube om denne udvidede funktionalitet. Jeg talte ved Monitoring Meetup i foråret i St. Petersborg.

Telegram kanal VictoriaMetrics.

Kun registrerede brugere kan deltage i undersøgelsen. Log ind, Vær venlig.

Hvad forhindrer dig i at skifte til VictoriaMetrics som din langtidslagring til Prometheus? (Skriv i kommentarerne, jeg tilføjer det til afstemningen))

  • 71,4 %Jeg bruger ikke Prometheus5

  • 28,6 %Kendte ikke til VictoriaMetrics2

7 brugere stemte. 12 brugere undlod at stemme.

Kilde: www.habr.com

Tilføj en kommentar