Jag vill gÀrna dela med mig av en sammanfattning av Alexandr Valjalkins föredrag frÄn slutet av 2019 "Go-optimeringar i VictoriaMetrics"
â en snabb och skalbar databas för lagring och behandling av tidsseriedata (en post bestĂ„r av tid och en uppsĂ€ttning vĂ€rden som motsvarar den tiden, till exempel insamlade genom periodisk avlĂ€sning av sensorer eller insamling av mĂ€tvĂ€rden).

HĂ€r Ă€r lĂ€nken till videon av detta föredrag â

LÄt mig berÀtta lite om mig sjÀlv. Jag heter Alexandr Valjalkin. HÀr Àr . Jag Àr intresserad av Go och prestandaoptimering. Jag har skrivit mÄnga olika anvÀndbara och mindre anvÀndbara bibliotek. De börjar antingen med fast, eller med quick prefix.
För nÀrvarande arbetar jag med VictoriaMetrics. Vad Àr det och vad gör jag dÀr? Det kommer jag att berÀtta om i denna presentation.

Föredragets plan ser ut sÄ hÀr:
- Först kommer jag att berÀtta vad VictoriaMetrics Àr.
- Sedan kommer jag att förklara vad tidsserier Àr.
- DÀrefter kommer jag att förklara hur en tidsseriedatabas fungerar.
- Vidare kommer jag att berÀtta om databasens arkitektur: hur den Àr uppbyggd.
- Och tillslut kommer vi att diskutera optimeringarna som finns i VictoriaMetrics. Det handlar om optimering av den inverterade indexen och optimering för bitset-implementationen pÄ Go.

Vet nĂ„gon i publiken vad VictoriaMetrics Ă€r? Oj, mĂ„nga kĂ€nner redan till den. Det Ă€r goda nyheter. För dem som inte vet â det Ă€r en databas för tidsserier. Den Ă€r baserad pĂ„ ClickHouse-arkitekturen, med vissa detaljer frĂ„n ClickHouse-implementationen. Till exempel aspekter som: MergeTree, parallell berĂ€kning pĂ„ alla tillgĂ€ngliga processor kĂ€rnor och prestandaoptimering genom att arbeta med datablokkar som placeras i processor-cache.
VictoriaMetrics erbjuder bÀttre datakompression jÀmfört med andra databaser för tidsserier.
Den kan skalas vertikalt â det vill sĂ€ga att du kan lĂ€gga till fler processorer, mer RAM-minne pĂ„ en maskin. VictoriaMetrics kommer att utnyttja dessa tillgĂ€ngliga resurser effektivt och öka den linjĂ€ra prestandan.
VictoriaMetrics kan Ă€ven skalas horisontellt â det vill sĂ€ga att du kan lĂ€gga till fler noder i VictoriaMetrics-klustret, och dess prestanda kommer att öka nĂ€stan linjĂ€rt.
Som ni kanske har gissat Àr VictoriaMetrics en snabb databas, eftersom jag inte kan skriva om andra. Den Àr skriven i Go, sÄ dÀrför pratar jag om den pÄ detta meet-up.

Vem vet vad en tidsserie Ă€r? MĂ„nga mĂ€nniskor kĂ€nner ocksĂ„ till det. En tidsserie Ă€r en sekvens av par (timestamp, vĂ€rde), dĂ€r dessa par Ă€r sorterade efter tid. VĂ€rdet utgörs av ett flyttal â float64.
Varje tidsserie identifieras unikt av en nyckel. Vad bestÄr denna nyckel av? Den bestÄr av en icke-tom uppsÀttning av par nyckel-vÀrde.
HĂ€r Ă€r ett exempel pĂ„ en tidsserie. Nyckeln till denna serie Ă€r en lista av par: __name__="cpu_usage" â det Ă€r namnet pĂ„ metriket, instance="my-server" â det Ă€r datorn dĂ€r denna metrik samlades in, datacenter="us-east" â det Ă€r datacentret dĂ€r denna dator finns.
Vi har fĂ„tt namnet pĂ„ tidsserien, bestĂ„ende av tre par nyckel-vĂ€rde. Denna nyckel motsvarar en lista av par (timestamp, value). t1, t2, t3, ..., tN â dessa Ă€r timestamps, 10, 20, 12, ..., 15 â med motsvarande vĂ€rden. Detta Ă€r cpu-anvĂ€ndning just nu för den hĂ€r serien.

Var kan tidsserier anvÀndas? Har nÄgon nÄgra idéer?
- Inom DevOps kan man mÀta belastningen pÄ CPU, RAM, nÀtverk, rps, antal fel, etc.
- IoT â vi kan mĂ€ta temperatur, tryck, geografiska koordinater och mer.
- Ăven inom finans â vi kan övervaka priser pĂ„ olika aktier och valutor.
- Dessutom kan tidsserier anvÀndas för att övervaka produktionsprocesser pÄ fabriker. Vi har anvÀndare som anvÀnder VictoriaMetrics för att övervaka vindkraftverk och robotar.
- Tidsserier Àr ocksÄ anvÀndbara för att samla in information frÄn sensorer pÄ olika enheter. Till exempel för en motor; för att mÀta trycket i dÀck; för att mÀta hastighet, avstÄnd; för att mÀta brÀnsleförbrukning, etc.
- Tidsserier kan ocksÄ anvÀndas för att övervaka flygplan. Varje flygplan har en svart lÄda som samlar in tidsserier om olika hÀlsoparametrar för flygplanet. Tidsserier anvÀnds ocksÄ inom rymdindustrin.
- HĂ€lsovĂ„rd â det handlar om blodtryck, puls, etc.
Kanske finns det fler tillÀmpningar som jag har glömt, men jag hoppas att ni förstÄr att tidsserier anvÀnds aktivt i den moderna vÀrlden. Och mÀngden av deras anvÀndning vÀxer för varje Är.

Vad behövs en databas för tidsserier? Varför kan man inte anvÀnda en vanlig relationsdatabas för att lagra tidsserier?
För att tidsserier vanligtvis innehÄller en stor mÀngd information som Àr svÄr att lagra och bearbeta i vanliga databaser. DÀrför har det uppstÄtt specialiserade databaser för tidsserier. Dessa databaser lagrar effektivt punkter (timestamp, value) med en given nyckel. De erbjuder ett API för att lÀsa de sparade uppgifterna efter nyckel, efter ett par nyckel-vÀrde, flera sÄdana par eller efter regexp. Till exempel, om du vill hitta CPU-belastningen för alla dina tjÀnster i datacentret i Amerika, sÄ behövs en sÄdan pseudofrÄga.
Vanligtvis har databaser för tidsserier specialiserade frĂ„gesprĂ„k, eftersom SQL inte passar sĂ€rskilt bra för tidsserier. Ăven om det finns databaser som stödjer SQL sĂ„ passar det inte sĂ€rskilt bra. Mer lĂ€mpliga frĂ„gesprĂ„k Ă€r , , , . Jag hoppas att nĂ„gon har hört talas om minst ett av dessa sprĂ„k. Om PromQL har nog mĂ„nga hört talas om.

SÄ hÀr ser arkitekturen för en modern databas för tidsserier ut, med VictoriaMetrics som exempel.
Den bestÄr av tvÄ delar. Det Àr ett lagringsutrymme för den inverterade indexen och ett lagringsutrymme för vÀrdena i tidsserier. Dessa lagringsutrymmen Àr separerade.
NÀr en ny post kommer in i databasen, börjar vi med att konsultera den inverterade indexen för att hitta identifieraren för tidsserien baserat pÄ ett givet set label=value för denna metrik. Vi hittar denna identifierare och sparar vÀrdet i data lagret.
NÀr en frÄga om att hÀmta data frÄn TSDB kommer in, gÄr vi först till den inverterade indexen. Vi hÀmtar alla timeseries_ids poster som motsvarar det angivna uppsÀttningen. label=valueOch sedan hÀmtar vi all nödvÀndig data frÄn data lagret, indexerat efter timeseries_ids.

LÄt oss titta pÄ ett exempel pÄ hur en databas för tidsserier behandlar en inkommande select-frÄga.
- Först hÀmtar den alla
timeseries_idsfrÄn den inverterade indexen som innehÄller de angivna parenlabel=value, eller som uppfyller det angivna reguljÀra uttrycket. - Sedan hÀmtar den alla data points frÄn lagringsutrymmet för den angivna tidsperioden för de hittade
timeseries_ids. - Efter detta utför databasen vissa berÀkningar pÄ dessa datapunkter enligt anvÀndarens begÀran. Och efter det returnerar den ett svar.
I denna presentation kommer jag att berĂ€tta om den första delen. Det handlar om sökning timeseries_ids i det inverterade indexet. Den andra och tredje delen kan ni se senare , eller vĂ€nta tills jag förbereder fler föredrag đ

LÄt oss gÄ vidare till det inverterade indexet. MÄnga kan tycka att detta Àr enkelt. Vem vet vad ett inverterat index Àr och hur det fungerar? à h, det Àr inte sÄ mÄnga lÀngre. LÄt oss försöka förstÄ vad det handlar om.
I sjĂ€lva verket Ă€r allt enkelt. Det Ă€r bara en ordbok som mappar nyckeln till vĂ€rdet. Vad Ă€r en nyckel? Det Ă€r paret label=value, dĂ€r label och value â det Ă€r strĂ€ngar. Och vĂ€rdena Ă€r en uppsĂ€ttning timeseries_ids, som inkluderar det angivna paret label=value.
Det inverterade indexet möjliggör snabb sökning av alla timeseries_ids, som har de angivna label=value.
Och det möjliggör ocksÄ snabb sökning av timeseries_ids tidsserier för flera par label=value, eller för paren label=regexp. Hur fungerar det? Genom att hitta snittet av mÀngden timeseries_ids för varje par label=value.

LÄt oss titta pÄ olika implementeringar av det inverterade indexet. Vi börjar med den enklaste naiva implementeringen. Den ser ut sÄ hÀr.
Funktionen getMetricIDs hÀmtar en lista med strÀngar. Varje strÀng innehÄller label=value. Denna funktion returnerar en lista metricIDs.
Hur fungerar det? HÀr har vi en global variabel som heter invertedIndex. Det Àr en vanlig ordbok (map), som mappar strÀngen till en slice av heltal. StrÀngen innehÄller label=value.
Implementeringen av funktionen: vi fÄr tag pÄ metricIDs för den första label=value, sedan gÄr vi igenom alla andra label=value, tar tag pÄ metricIDs för dem. Och vi kallar funktionen intersectInts, som kommer att diskuteras senare. Och denna funktion returnerar snittet av dessa listor.

Som ni ser Àr implementeringen av det inverterade indexet inte sÀrskilt komplicerad. Men det Àr en naiv implementation. Vilka nackdelar har den? Huvud nackdelen med den naiva implementeringen Àr att detta inverterade index lagras i arbetsminnet. Efter omstart av applikationen förlorar vi detta index. Det finns ingen lagring av detta index pÄ disken. För en databas Àr det inverterade indexet svÄrt att anvÀnda.
Den andra nackdelen handlar ocksĂ„ om minne. Den inverterade indexen mĂ„ste placeras i RAM. Om den överstiger storleken pĂ„ RAM, fĂ„r vi uppenbarligen â out of memory error. Och programmet kommer inte att fungera.

Detta problem kan lösas med hjÀlp av fÀrdiga lösningar som , eller .
Kort sagt behöver vi en databas som snabbt kan utföra tre operationer.
- Den första operationen Àr att skriva
nyckel-vÀrdetill denna databas. Detta görs vÀldigt snabbt, dÀrnyckel-vÀrdeÀr godtyckliga strÀngar. - Den andra operationen Àr en snabb sökning av vÀrdet för en given nyckel.
- Och den tredje operationen Àr en snabb sökning av alla vÀrden efter ett givet prefix.
LevelDB och RocksDB â dessa databaser har utvecklats av Google och Facebook. Först kom LevelDB. Sedan tog Facebook-teamet LevelDB och började förbĂ€ttra den och skapade RocksDB. Nu anvĂ€nder nĂ€stan alla interna databaser pĂ„ Facebook RocksDB, dessutom har de Ă€ven migrerat MySQL till RocksDB. De kallade det .
Den inverterade indexen kan implementeras med hjĂ€lp av LevelDB. Hur gör vi detta? Vi sparar som nyckel label=value. Och som vĂ€rde â identifieraren för tidsserien dĂ€r paret finns label=value.
Om vi har mÄnga tidsserier med detta par label=value, kommer det att finnas mÄnga rader i denna databas med samma nyckel men olika timeseries_ids. För att fÄ en lista över alla timeseries_ids, som börjar med detta label=prefix, gör vi en range scan, för vilket denna databas Àr optimerad. Det vill sÀga, vi vÀljer alla rader som börjar med label=prefix och fÄr de nödvÀndiga timeseries_ids.

HÀr Àr en ungefÀrlig implementation hur den skulle se ut i Go. Vi har en inverterad index. Det Àr LevelDB.
Funktionen Àr densamma som för den naiva implementationen. Den upprepar nÀstan rad för rad den naiva implementationen. Den enda skillnaden Àr att istÀllet för att referera till map refererar vi till den inverterade indexen. Vi hÀmtar alla vÀrden för den första label=value. Sedan gÄr vi igenom alla kvarstÄende par label=value och hÀmtar motsvarande uppsÀttningar av metricIDs för dem. DÀrefter hittar vi snittet.

Det verkar bra, men det finns nackdelar med denna lösning. VictoriaMetrics implementerade först den inverterade indexen baserad pÄ LevelDB. Men till slut var de tvungna att överge den.
Varför? Eftersom LevelDB Àr lÄngsammare Àn den naiva implementationen. I den naiva implementationen fÄr vi direkt hela skivmÄtt för en given nyckel. metricIDs. Denna mycket snabba operation - hela slice Àr redo att anvÀndas.
I LevelDB mÄste man vid varje anrop av funktionen GetValues gÄ igenom alla rader som börjar med label=value. Och för varje rad hÀmta vÀrdet timeseries_ids. Av sÄdana timeseries_ids samla slice av dessa timeseries_ids. Det Àr uppenbart att detta Àr mycket lÄngsammare Àn att helt enkelt komma Ät en vanlig map genom nyckel.
Den andra nackdelen Ă€r att LevelDB Ă€r skrivet i C. Anrop till C-funktioner frĂ„n Go Ă€r inte sĂ€rskilt snabbt. Det tar hundratals nanosekunder. Det Ă€r inte sĂ„ snabbt, eftersom jĂ€mfört med ett vanligt funktionsanrop, skrivet i Go, som tar 1-5 nanosekunder, blir prestandaskillnaden tiotals gĂ„nger. För VictoriaMetrics var detta en dödlig nackdel đ

DÀrför har jag skrivit en egen implementation av det inverterade indexet. Jag kallade den .
Mergeset Àr baserat pÄ datakonstruktionen MergeTree. Denna datakonstruktion Àr lÄnad frÄn ClickHouse. Uppenbarligen bör mergeset optimeras för snabb sökning timeseries_ids efter en given nyckel. Mergeset Àr helt skrivet i Go. Du kan titta pÄ . Implementeringen av mergeset ligger i mappen . Du kan försöka ta reda pÄ vad som hÀnder dÀr.
API:et för mergeset liknar mycket LevelDB och RocksDB. Det vill sÀga, det tillÄter att snabbt spara nya postningar och snabbt hÀmta postningar efter en given prefix.

Vi kommer att prata om nackdelarna med mergeset senare. Just nu ska vi diskutera vilka problem som uppstod med VictoriaMetrics i produktion vid implementeringen av det inverterade indexet.
Varför uppstod de?
Den första anledningen Àr den höga churn rate. PÄ svenska betyder detta - ofta byte av tidsserier. Detta intrÀffar nÀr en tidsserie slutar och en ny börjar, eller mÄnga nya tidsserier börjar. Och detta hÀnder ofta.
Den andra anledningen Àr det stora antalet tidsserier. I början, nÀr övervakningen blev populÀr, var antalet tidsserier litet. Till exempel, för varje dator mÄste CPU, minne, nÀtverk och disk övervakas. 4 tidsserier per dator. Om du har, sÀg, 100 datorer och 400 tidsserier. Det Àr mycket lite.
Med tiden har mÀnniskor kommit pÄ att man kan mÀta mer detaljerad information. Till exempel, att mÀta belastningen inte för hela processorn, utan separat för varje processor kÀrna. Om du har 40 processor kÀrnor, fÄr du dÀrmed 40 gÄnger fler tidsserier för att mÀta processorbelastningen.
Men det Àr inte allt. Varje processor kÀrna kan ha flera tillstÄnd som idle, nÀr den Àr inaktiv. Dessutom finns det arbete i user space, arbete i kernel space och andra tillstÄnd. Och varje sÄdant tillstÄnd kan ocksÄ mÀtas som en separat tidsserie. Detta ökar antalet serier ytterligare 7-8 gÄnger.
FrÄn en enda metrik har vi fÄtt 40 x 8 = 320 metrikker bara för en dator. Multiplicerat med 100 ger det oss 32 000 istÀllet för 400.
Sedan kom Kubernetes. Och det förvÀrrade saken ytterligare, eftersom mÄnga olika tjÀnster kan hostas i Kubernetes. Varje tjÀnst i Kubernetes bestÄr av mÄnga pods. Allt detta mÄste övervakas. Dessutom har vi stÀndiga deployment av nya versioner av dina tjÀnster. För varje ny version mÄste nya tidsserier skapas. Slutresultatet Àr att antalet tidsserier vÀxer exponentiellt och vi stÄr inför problemet med ett stort antal tidsserier, som kallas high-cardinality. VictoriaMetrics hanterar detta framgÄngsrikt jÀmfört med andra tidsserie-databaser.

LÄt oss titta nÀrmare pÄ high churn rate. Vad orsakar high churn rate i produktion? Eftersom vissa vÀrden pÄ etiketter och taggar stÀndigt förÀndras.
Till exempel, lÄt oss ta Kubernetes, dÀr begreppet deployment, dvs. nÀr en ny version av din applikation rullas ut. Utvecklarna av Kubernetes har av nÄgon anledning valt att lÀgga till deployment-id i etiketten.
Vad ledde detta till? Att vid varje ny deployment bryts alla gamla tidsserier, och istÀllet börjar nya tidsserier med det nya etikettvÀrdet deployment_id. SÄdana serier kan vara hundratusentals eller till och med miljoner.
En viktig aspekt av allt detta Àr att det totala antalet tidsserier vÀxer, men antalet tidsserier som för nÀrvarande Àr aktiva, som fÄr data, förblir konstant. Detta tillstÄnd kallas high churn rate.
Huvudproblemet med hög churn rate Àr att sÀkerstÀlla en konstant sökhastighet för alla tidsserier över en given uppsÀttning etiketter under en viss tidsperiod. Vanligtvis handlar det om tidsperioden den senaste timmen eller den senaste dagen.

Hur kan vi lösa detta problem? HÀr Àr den första lösningen. Vi delar den inverterade indexen i oberoende delar baserat pÄ tid. Det vill sÀga, nÀr en tidsperiod har passerat, avslutar vi arbetet med den nuvarande inverterade indexen. Och vi skapar en ny inverterad index. NÀr ytterligare en tidsperiod passerar, skapar vi en till och en till.
Och vid urvalet frÄn dessa inverterade index hittar vi en uppsÀttning inverterade index som faller inom den angivna tidsperioden. Och, i enlighet dÀrmed, vÀljer vi id:n för tidsserierna dÀrifrÄn.
Detta sparar resurser eftersom vi inte behöver granska delar som inte faller inom den angivna tidsperioden. Det vill sÀga, om vi vanligtvis vÀljer data frÄn den senaste timmen, sÄ hoppar vi över förfrÄgningar frÄn tidigare tidsperioder.

Det finns ytterligare en lösning pÄ detta problem. Det Àr att för varje dag lagra en separat lista med id:n för tidsserier som förekommit den dagen.
Fördelen med denna lösning jÀmfört med den tidigare lösningen Àr att vi inte duplicerar information om tidsserier som inte försvinner över tid. De Àr konstant nÀrvarande och förÀndras inte.
Nackdelen Àr att en sÄdan lösning Àr mer komplicerad att implementera och svÄrare att debugga. Och VictoriaMetrics har valt denna lösning. Det har historiskt sett blivit sÄ. Denna lösning visar sig ocksÄ fungera ganska bra, jÀmfört med den tidigare. Eftersom denna lösning inte implementerades pÄ grund av att det krÀver att vi duplicerar data i varje partition för tidsserier som inte förÀndras, det vill sÀga de som inte försvinner över tid. VictoriaMetrics har i första hand optimerats för diskutrymmesanvÀndning, och den tidigare implementationen försÀmrade diskutrymmesanvÀndningen. Men denna implementation passar bÀttre för att minimera diskutrymmesanvÀndningen, vilket Àr varför den valdes.
Vi fick kÀmpa emot den. Kampen bestod i att i denna implementation mÄste vi fortfarande vÀlja ett betydligt större antal timeseries_ids för data Àn nÀr den inverterade indexen Àr delad efter tid.

Hur löste vi detta problem? Vi löste det pĂ„ ett unikt sĂ€tt â genom att spara flera identifierare för tidsserier i varje post i den inverterade indexen istĂ€llet för en enda identifierare. Det vill sĂ€ga, vi har en nyckel label=value, som förekommer i varje tidsserie. Och nu sparar vi flera timeseries_ids i en post.
HÀr Àr ett exempel. Tidigare hade vi N poster, nu har vi en post vars prefix Àr detsamma som alla andra. Den tidigare posten innehöll alla id:n för tidsserier.
Detta har ökat hastigheten för att skanna en sÄdan inverterad index med upp till 10 gÄnger. Och det har minskat minnesförbrukningen för cachen, eftersom vi nu sparar strÀngen label=value bara en gÄng i cachen istÀllet för N gÄnger. Och denna strÀng kan vara stor, om du har lÄnga strÀngar i taggar och etiketter som Kubernetes gÀrna skjuter in dÀr.

Ett annat alternativ för att snabba upp sökningarna i den inverterade indexen Àr sharding. Att skapa flera inverterade index istÀllet för ett och shardera data mellan dem efter nyckel. Det Àr ett set av key=value par. Det vill sÀga, vi fÄr flera oberoende inverterade index som vi kan frÄga parallellt pÄ flera processorer. Tidigare implementationer tillÀt endast att arbeta i en enskild processor, det vill sÀga att skanna data endast pÄ ett kÀrna. Denna lösning möjliggör att skanna data pÄ flera kÀrnor samtidigt, vilket ClickHouse gÀrna gör. Det Àr vad vi planerar att implementera.

Och nu tillbaka till vĂ„ra fĂ„r â till funktionen för intersection timeseries_ids. LĂ„t oss övervĂ€ga vilka implementationer som kan finnas. Denna funktion gör det möjligt att hitta timeseries_ids för ett givet set label=value.

Det första alternativet â det Ă€r den naĂŻva implementationen. TvĂ„ nĂ€stlade loopar. HĂ€r fĂ„r vi tvĂ„ slices som indata till funktionen intersectInts â a och b. UtgĂ„ngen ska ge oss intersection av dessa slices.
Den naïva implementationen ser ut sÄ hÀr. Vi gÄr igenom alla vÀrden frÄn slice a, inom denna loop gÄr vi igenom alla vÀrden frÄn slice b. Och vi jÀmför dem. Om de matchar, betyder det att vi har hittat intersection. Och vi sparar det i result.

Vilka nackdelar finns det? Den kvadratiska komplexiteten â detta Ă€r dess frĂ€msta nackdel. Till exempel, om du har storlekar pĂ„ slice a och b Om du har en miljon, kommer den hĂ€r funktionen aldrig att ge dig ett svar. Eftersom den mĂ„ste göra en biljon iterationer, vilket Ă€r mycket Ă€ven för moderna datorer.

Den andra implementationen baseras pĂ„ map. Vi skapar en map. Vi lĂ€gger alla vĂ€rden frĂ„n slice i denna map. aSedan gĂ„r vi igenom slice med en separat loop. bOch vi kontrollerar â finns det detta vĂ€rde frĂ„n slice b i mappen. Om det finns, lĂ€gger vi till det i resultatet.

Vilka fördelar finns det? Fördelen Àr att hÀr Àr det endast linjÀr komplexitet. Det vill sÀga, funktionen kommer att utföras mycket snabbare för stora storlekar av slices. För en slice av storlek en miljon kommer denna funktion att utföras pÄ 2 miljoner iterationer, till skillnad frÄn en biljon iterationer som i den tidigare funktionen.
Nackdelen Àr att denna funktion krÀver mer minne för att skapa denna map.
Den andra nackdelen Àr den stora overheaden för hashing. Denna nackdel Àr inte sÀrskilt uppenbar. Och för oss var den inte heller sÀrskilt uppenbar, dÀrför var den initiala implementationen av intersection i VictoriaMetrics genom map. Men sedan visade profileringsresultaten att den största processor tiden gick Ät till att skriva till mappen och kontrollera om det finns ett vÀrde i denna map.
Varför gÄr processor tiden Ät pÄ dessa stÀllen? För att i dessa rader utför Go en hash operation. Det vill sÀga att den berÀknar hash av nyckeln för att sedan kunna komma Ät det angivna indexet i HashMap. HashberÀkningen utförs pÄ tiotals nanosekunder. Det Àr lÄngsamt för VictoriaMetrics.

Jag beslutade att implementera en bitset, sÀrskilt optimerad för detta fall. SÄ hÀr ser nu intersection av tvÄ slices ut. HÀr skapar vi en bitset. Vi lÀgger till elementen frÄn den första slice. Sedan kontrollerar vi nÀrvaron av dessa element i den andra slice. Och vi lÀgger till dem i resultatet. Det vill sÀga, det skiljer sig knappt frÄn det tidigare exemplet. Det enda som vi har bytt hÀr Àr att vi har ersatt anropet till mappen med specialfunktioner. add och has.

Vid första anblicken verkar det som om detta borde fungera lÄngsammare, om standardmappen anvÀndes tidigare och hÀr anropas det ytterligare funktioner, men profileringsresultaten visar att denna sak fungerar 10 gÄnger snabbare Àn standardmappen för fallet med VictoriaMetrics.
Dessutom anvÀnder den mycket mindre minne jÀmfört med implementationen med map. För vi lagrar bitar hÀr istÀllet för Ättabyte vÀrden.
En nackdel med denna implementation Àr att den inte Àr sÄ uppenbar, inte trivial.
En annan nackdel som mĂ„nga kanske inte mĂ€rker â Ă€r att denna implementation kan fungera dĂ„ligt i vissa fall. Det vill sĂ€ga, den Ă€r optimerad för ett specifikt fall, för detta fall av överlappande ids av tidsserier i VictoriaMetrics. Det betyder inte att den passar alla situationer. Om den anvĂ€nds felaktigt, kan vi fĂ„ inte bara sĂ€mre prestanda utan ocksĂ„ minnesfel och en minskning av prestanda.

LÄt oss titta pÄ implementationen av denna struktur. Om du vill se den, sÄ finns den i kÀllkoden till VictoriaMetrics, i mappen . Den Àr optimerad just för fallet VictoriaMetrics, dÀr timeseries_id Àr ett 64-bitarsvÀrde, dÀr de första 32 bitarna Àr konstant och endast de sista 32 bitarna Àndras.
Denna datastruktur lagras inte pÄ disken, den fungerar endast i minnet.

HÀr Àr dess API. Det Àr inte sÀrskilt komplicerat. API:t Àr anpassat exakt för det specifika exempel som anvÀnds av VictoriaMetrics. Det vill sÀga, det finns inga onödiga funktioner hÀr. HÀr finns funktioner som VictoriaMetrics tydligt anvÀnder.
Det finns funktioner add, som lÀgger till nya vÀrden. Det finns en funktion has, som kontrollerar nya vÀrden. Och det finns en funktion del, som tar bort vÀrden. Det finns ocksÄ en hjÀlpfunktion len, som returnerar storleken pÄ mÀngden. Funktionen clone klonar mÀngden. Och funktionen appendto omvandlar denna set till en slice timeseries_ids.

SÄ hÀr ser implementationen av denna datastruktur ut. I set finns det tvÄ element:
ItemsCountâ detta Ă€r ett hjĂ€lpfĂ€lt, för att snabbt returnera antalet element i set. Man skulle kunna klara sig utan det hĂ€r hjĂ€lpfĂ€ltet, men det lades till hĂ€r eftersom VictoriaMetrics ofta frĂ„gar om lĂ€ngden pĂ„ bitset i sina algoritmer.Det andra fĂ€ltet Ă€r
buckets. Detta Ă€r en slice av strukturenbucket32. I varje struktur lagrashifĂ€lt. Detta Ă€r de översta 32 bitarna. Och tvĂ„ slices âb16hisochbucketsfrĂ„nbucket16strukturer.
HÀr lagras de översta 16 bitarna av den andra delen av den 64-bitars strukturen. Och hÀr lagras bitsets för de lÀgre 16 bitarna av varje byte.
Bucket64 bestĂ„r av en array uint64. LĂ€ngden berĂ€knas med hjĂ€lp av dessa konstanter. I en bucket16 kan maximalt lagras 2^16=65536 bitar. Om vi delar detta med 8, fĂ„r vi 8 kilobyte. Om vi delar en gĂ„ng till med 8, sĂ„ Ă€r det 1000 uint64 vĂ€rden. Det vill sĂ€ga, Bucket16 â Ă€r en 8-kilobyte struktur.

LÄt oss titta pÄ hur en av metoderna för denna struktur för att lÀgga till ett nytt vÀrde Àr implementerad.
Allt börjar med uint64 vÀrdet. Vi berÀknar de övre 32 bitarna, berÀknar de nedre 32 bitarna. Vi gÄr igenom alla buckets. Vi jÀmför de övre 32 bitarna i varje bucket med det tillagda vÀrdet. Och om de matchar, anropar vi funktionen add i strukturen b32 buckets. Och lÀgger till de nedre 32 bitarna dÀr. Om detta returnerade true, betyder det att vi har lagt till ett sÄdant vÀrde dÀr och att vi inte hade ett sÄdant vÀrde tidigare. Om det returnerar false, sÄ fanns ett sÄdant vÀrde redan. Sedan ökar vi antalet element i strukturen.
Om vi inte hittade den nödvÀndiga bucket med det nödvÀndiga hi-vÀrdet, dÄ anropar vi funktionen addAlloc, som tilldelar en ny bucket, genom att lÀgga till den i bucket-strukturen.

Detta Àr implementeringen av funktionen b32.add. Den liknar den tidigare implementationen. Vi berÀknar de översta 16 bitarna, de nedersta 16 bitarna.
Sedan gÄr vi igenom alla övre 16 bitar. Vi hittar matchningar. Och vid en match anropar vi metoden add, som vi kommer att gÄ igenom pÄ nÀsta sida för bucket16.

Och hÀr Àr den nedersta nivÄn, som mÄste vara sÄ optimal som möjligt. Vi berÀknar för uint64 id-vÀrdet i slice bit, samt bitmask. Detta Àr en mask för detta 64-bitars vÀrde, genom vilken vi kan kontrollera om denna bit finns, eller sÀtta den. Vi kontrollerar om denna bit Àr instÀlld och sÀtter den, och returnerar dess tillstÄnd. SÄ hÀr har vi en implementation som har gjort det möjligt att pÄskynda operationen för att korsa ids för tidsserier med 10 gÄnger jÀmfört med vanliga maps.

I VictoriaMetrics finns det mÄnga andra optimeringar förutom denna. De flesta av dessa optimeringar har lagts till av en anledning, efter profilering av koden i produktion.
Detta Ă€r den grundlĂ€ggande regeln för optimering â lĂ€gg inte till en optimering, antar att det kommer att vara en flaskhals hĂ€r, för det kan visa sig att det inte finns nĂ„gon flaskhals dĂ€r. Optimering försĂ€mrar vanligtvis kodens kvalitet. DĂ€rför bör man optimera endast efter profilering och helst i produktion, sĂ„ att det handlar om verkliga data. Om du Ă€r intresserad kan du titta pĂ„ kĂ€llkoden för VictoriaMetrics och studera andra optimeringar som finns dĂ€r.

Jag har en frÄga om bitset. Mycket liknar implementeringen av C++ vector, optimerad bitset. Tog ni implementeringen dÀrifrÄn?
Nej, det Àr inte dÀrifrÄn. Vid implementeringen av detta bitset utgick jag frÄn kunskapen om strukturen hos dessa ids tidsserier, som anvÀnds i VictoriaMetrics. Deras struktur Àr sÄdan att de övre 32 bitarna vanligtvis Àr konstanta. De undre 32 bitarna kan förÀndras. Ju lÀgre bit, desto oftare kan den förÀndras. DÀrför Àr denna implementering specifikt optimerad för denna databasstruktur. C++-implementeringen, sÄ vitt jag vet, Àr optimerad för allmÀnna fall. Att göra en optimering för allmÀnna fall innebÀr att den inte kommer att vara den mest optimala för det specifika fallet.
Jag rekommenderar att du ocksÄ kollar pÄ föredraget av Alexej Milovid. Han pratade för ungefÀr en mÄnad sedan om optimeringar i ClickHouse för specifika specialiseringar. Han nÀmner just att i allmÀnhet Àr C++-implementeringen eller nÄgon annan implementation anpassad för att fungera bra i genomsnitt. Den kan fungera sÀmre Àn en specialiserad implementering för specifik kunskap, sÄsom vÄr, nÀr vi vet att de övre 32 bitarna huvudsakligen Àr konstanta.
Jag har en andra frÄga. Vad Àr den grundlÀggande skillnaden frÄn InfluxDB?
Det finns mÄnga grundlÀggande skillnader. NÀr det gÀller prestanda och minnesanvÀndning visar InfluxDB i tester 10 gÄnger högre minnesanvÀndning för hög kardinalitet tidsserier, nÀr ni har mÄnga av dem, till exempel miljoner. Till exempel anvÀnder VictoriaMetrics 1 GB för en miljon aktiva serier, medan InfluxDB samtidigt anvÀnder 10 GB. Och det Àr en stor skillnad.
Den andra grundlĂ€ggande skillnaden Ă€r att InfluxDB har konstiga frĂ„gesprĂ„k â Flux och InfluxQL. De Ă€r inte sĂ„ bekvĂ€ma för att arbeta med tidsserier i jĂ€mförelse med , som stöds i VictoriaMetrics. PromQL Ă€r frĂ„gesprĂ„ket frĂ„n Prometheus.
En annan skillnad Àr att InfluxDB har en lite mÀrklig datamodell, dÀr varje rad kan innehÄlla flera fÀlt med olika uppsÀttningar av taggar. Dessa rader delas ocksÄ in i olika tabeller. Dessa ytterligare komplikationer försvÄrar det fortsatta arbetet med databasen. Det Àr svÄrt att underhÄlla och förstÄ.
I VictoriaMetrics Ă€r allt mycket enklare. DĂ€r representerar varje tidsserie ett nyckel-vĂ€rde-par. VĂ€rdet Ă€r en uppsĂ€ttning punkter â (timestamp, value), och nyckeln Ă€r en uppsĂ€ttning label=valueDet finns ingen uppdelning i fields och measurements. Detta gör att du kan vĂ€lja vilken data som helst och sedan kombinera, addera, subtrahera, multiplicera och dividera, till skillnad frĂ„n InfluxDB dĂ€r berĂ€kningar mellan olika rader fortfarande inte Ă€r implementerade, sĂ„vitt jag vet. Ăven om de skulle vara det, Ă€r det komplicerat och krĂ€ver massor av kod.
Jag har en uppföljande frÄga. Förstod jag rÀtt att det fanns nÄgot problem som ni nÀmnde, att denna inverterade index inte fÄr plats i minnet, och dÀrför gÄr partitioneringen till?
I början visade jag en naiv implementation av ett inverterat index pÄ en standard Go-karta. En sÄdan implementation passar inte för databaser, eftersom detta inverterade index inte sparas pÄ disken, och en databas mÄste spara pÄ disken sÄ att dessa data förblir tillgÀngliga vid omstart. I den hÀr implementationen försvinner det inverterade indexet vid omstart av applikationen. Och du kommer att förlora tillgÄngen till all data, eftersom du inte kommer att kunna hitta den.
Hej! Tack för presentationen! Jag heter Pavel. Jag kommer frÄn företaget Wildberries. Jag har nÄgra frÄgor till dig. Första frÄgan. Vad tror du, om du hade valt en annan princip för att bygga arkitekturen av din applikation och partitionerat data efter tid, skulle du dÄ kanske kunna göra datakorsningar vid sökning, baserat pÄ att data i en partition ligger för en viss tidsperiod, dvs. för ett visst tidsintervall, och du skulle inte behöva bekymra dig om att du har bitar som Àr spridda pÄ olika sÀtt? FrÄga nummer 2 - eftersom ni implementerar en sÄdan algoritm med bitset och allt annat, har ni kanske provat att anvÀnda processorinstruktioner? Kanske har ni provat sÄdana optimeringar?
Jag svarar direkt pÄ den andra. Vi har Ànnu inte kommit dit. Men om det behövs, gör vi det. Och den första, vad var frÄgan?
Ni diskuterade tvÄ scenarier. Och sa att ni valde den andra med en mer komplicerad implementation. Och ni föredrog inte den första dÀr data Àr partitionerade efter tid.
Ja. I det första fallet skulle den totala mÀngden av indexet vara större, eftersom vi i varje partition skulle behöva lagra dubbletter av data för de tidsserier som strÀcker sig över dessa alla partitioner. Och om er churn rate för tidsserierna Àr lÄg, dvs. om stÀndigt samma serier anvÀnds, sÄ skulle vi i det första fallet förlora mycket mer utrymme jÀmfört med det andra fallet.
Och ja, tidspartitionering Àr ett bra alternativ. Det anvÀnds av Prometheus. Men Prometheus har en annan nackdel. Vid sammanslagning av dessa datadelar behöver den hÄlla metadata om alla labels och tidsserier i minnet. DÀrför, om de datadelar som den slÄr ihop Àr stora, ökar minnesanvÀndningen kraftigt under sammanslagningen, till skillnad frÄn VictoriaMetrics. Vid sammanslagningen förbrukar VictoriaMetrics överhuvud taget inget minne, dÀr anvÀnds bara nÄgra kilobyte, oavsett storleken pÄ de sammanfogade datadelarna.
Algoritmen som ni anvĂ€nder, den anvĂ€nder minne. I minnet markeras tidsseriemetrik dĂ€r det finns vĂ€rden. PĂ„ sĂ„ sĂ€tt kontrollerar ni om det finns parvis existens i en datamĂ€ngd och i en annan. Och förstĂ„r â har en intersection skett eller inte. Vanligtvis implementeras det i databaser med hjĂ€lp av pekare, iteratorer som lagrar sitt nuvarande tillstĂ„nd och löper genom sorterad data vilket ger en enkel komplexitet för dessa operationer.
Varför anvÀnder vi inte pekare för att korsa data?
Ja.
I vÄr LevelDB eller i mergeset lagras just sorterade rader. Vi kan anvÀnda en pekare för att gÄ igenom och hitta korsningen. Men varför anvÀnder vi inte det? För att det Àr lÄngsamt. För att pekare innebÀr att för varje rad mÄste en funktion anropas. Anrop av en funktion tar 5 nanosekunder. Och om ni har 100 000 000 rader, sÄ tar det en halv sekund bara för funktionsanropet.
SÄdana problem finns, ja. Och min sista frÄga. FrÄgan kanske lÄter lite konstig. Varför kan vi inte berÀkna alla nödvÀndiga aggregat vid datainmatning och spara dem i önskat format? Varför spara stora mÀngder i system som VictoriaMetrics, ClickHouse osv., för att sedan spendera sÄ mycket tid pÄ dem?
LĂ„t mig ge ett exempel för att göra det tydligare. Anta att hur en liten leksakshastighetsmĂ€tare fungerar? Den registrerar avstĂ„ndet du har kört, hela tiden adderande det till en figur, och i en annan â tiden. Och delar. Och fĂ„r den genomsnittliga hastigheten. Man kan göra nĂ„got liknande. Samla ihop alla nödvĂ€ndiga fakta.
Okej, jag förstÄr frÄgan. Ditt exempel har sin plats. Om du vet vilka aggregationer du behöver, sÄ Àr det den bÀsta implementeringen. Men problemet Àr att folk sparar dessa metrikor, vissa data i ClickHouse och de vet Ànnu inte hur de kommer att aggregera och filtrera dem i framtiden, sÄ de mÄste spara alla rÄdata. Men om du vet att du behöver rÀkna nÄgot genomsnittligt, varför rÀknar du inte det istÀllet för att spara en massa rÄa vÀrden dÀr? Men det Àr bara om du exakt vet vad du behöver.
Förresten, databaser för lagring av tidsserier stödjer berÀkning av aggregationer. Till exempel, Prometheus stödjer . Det vill sÀga, detta kan göras om du vet vilka aggregationer du kommer att behöva. I VictoriaMetrics finns det inte Àn, men vanligtvis placeras Prometheus framför den, dÀr detta kan göras i inspelningsreglerna.
Till exempel, pÄ mitt tidigare jobb behövde vi rÀkna antal hÀndelser i ett glidande fönster under den senaste timmen. Problemet Àr att jag var tvungen att göra en anpassad implementation i Go, det vill sÀga en tjÀnst för att rÀkna denna sak. Denna tjÀnst blev slutligen ganska komplex eftersom det Àr svÄrt att rÀkna. Implementeringen kan vara enkel om du behöver rÀkna nÄgra aggregationer vid fasta tidsintervall. Men om du vill rÀkna hÀndelser i ett glidande fönster, Àr det inte sÄ enkelt som det verkar. Jag tror att det fortfarande inte har implementerats i ClickHouse eller i tidsserie-databaser, eftersom det Àr svÄrt att implementera.
Och en sista frÄga. Vi har just pratat om att rÀkna genomsnitt, och jag kom ihÄg att det en gÄng fanns nÄgot som Graphite med backend Carbon. Den kunde tunna ut gamla data, det vill sÀga lÀmna en punkt per minut, en punkt per timme, osv. I princip Àr det ganska bekvÀmt om vi behöver rÄdata, sÄ att sÀga, under en mÄnad, och allt annat kan tunas ut. Men Prometheus, VictoriaMetrics stödjer inte denna funktionalitet. Planerar ni att stödja det? Om inte, varför?
Tack för din frÄga. VÄra anvÀndare stÀller den ibland. De undrar nÀr vi kommer att lÀgga till stöd för nedskÀrning (downsampling). HÀr finns det flera problem. För det första, vad varje anvÀndare menar med nedskÀrning Àr olika: nÄgon vill fÄ en godtycklig punkt inom ett angivet intervall, nÄgon annan vill ha maximala, minimala, eller genomsnittliga vÀrden. Om mÄnga system skriver data till din databas kan du inte bara behandla dem som om de vore samma. Det kan visa sig att varje system behöver anvÀnda olika nedskÀrningar. Och det Àr komplicerat att implementera.
För det andra, VictoriaMetrics, precis som ClickHouse, Ă€r optimerad för att arbeta med stora mĂ€ngder rĂ„data, sĂ„ den kan bearbeta en miljard rader pĂ„ mindre Ă€n en sekund om du har mĂ„nga kĂ€rnor i ditt system. Skanning av tidsserie-punkter i VictoriaMetrics â 50 000 000 punkter per sekund per kĂ€rna. Och denna prestanda skalas med tillgĂ€ngliga kĂ€rnor. Om du har 20 kĂ€rnor, till exempel, sĂ„ blir det en miljard punkter per sekund. Och denna egenskap hos VictoriaMetrics och ClickHouse minskar behovet av nedskĂ€rning.
En annan egenskap Àr att VictoriaMetrics effektivt komprimerar dessa data. Kompressionen ligger i genomsnitt pÄ 0,4 till 0,8 byte per punkt i produktion. Varje punkt bestÄr av en tidsstÀmpel + ett vÀrde. Och den komprimeras till mindre Àn en byte i genomsnitt.
Sergej. Jag har en frÄga. Vad Àr den minimi tidskvant för registrering?
En millisekund. Nyligen hade vi ett samtal med andra utvecklare av tidsserie-databaser. Hos dem Ă€r den minimi tidskvanten en sekund. I Graphite, till exempel, Ă€r det ocksĂ„ en sekund. I OpenTSDB Ă€r det ocksĂ„ en sekund. I InfluxDB â nanosekundprecision. I VictoriaMetrics Ă€r det en millisekund, eftersom det i Prometheus Ă€r en millisekund. Och VictoriaMetrics utvecklades ursprungligen som fjĂ€rrlagring för Prometheus. Men nu kan den ocksĂ„ spara data frĂ„n andra system.
MĂ€nniskan jag pratade med sĂ€ger att de har sekundprecision â det rĂ€cker för dem, eftersom det beror pĂ„ vilken typ av data som lagras i tidsseriedatabasen. Om det Ă€r DevOps-data eller data frĂ„n infrastruktur, dĂ€r du samlar in dem med intervaller pĂ„ 30 sekunder eller en minut, sĂ„ rĂ€cker sekundprecision. Mindre behövs inte. Men om du samlar in dessa data frĂ„n högfrekventa handelssystem, dĂ„ behövs nanosekundprecision.
Millisekundnog noga i precision i VictoriaMetrics passar bÄde för DevOps-fall och kan passa de flesta av de fall jag nÀmnde i början av presentationen. Det enda den kanske inte Àr lÀmplig för Àr högfrekventa handelssystem.
Tack! Och en frÄga till. Vad Àr kompatibiliteten i PromQL?
Full bakÄtkompatibilitet. VictoriaMetrics stödjer helt och hÄllet PromQL. Utöver detta lÀgger den till ytterligare utökad funktionalitet pÄ PromQL, vilket kallas . AngÄende denna utökade funktionalitet finns det en presentation pÄ YouTube. Jag pratade pÄ Monitoring Meetup i vÄras i Petersburg.
Telegram-kanal .
Endast registrerade anvÀndare kan delta i omröstningen. , tack.
Vad hindrar dig frÄn att byta till VictoriaMetrics som en lÄngsiktig lagring för Prometheus? (Skriv i kommentarerna, jag lÀgger till det i enkÀten))
71,4%AnvÀnder inte Prometheus5
28,6%Visste inte om VictoriaMetrics2
7 anvÀndare röstade. 12 anvÀndare avstod.
KĂ€lla: habr.com
