Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Ik stel foar dat jo it transkripsje lêze fan it lette 2019-rapport fan Alexander Valyalkin "Go optimizations in VictoriaMetrics"

VictoriaMetrics - in rappe en skalberbere DBMS foar it opslaan en ferwurkjen fan gegevens yn 'e foarm fan in tiidsearje (it rekord foarmet tiid en in set wearden dy't oerienkomme mei dizze tiid, bygelyks, krigen troch periodike polling fan' e status fan sensoren of sammeljen fan metriken).

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Hjir is in keppeling nei de fideo fan dit rapport - https://youtu.be/MZ5P21j_HLE

Slides

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Fertel ús wat oer dysels. Ik bin Alexander Valyalkin. Hjir myn GitHub-akkount. Ik bin hertstochtlik oer Go en prestaasjesoptimalisaasje. Ik skreau in protte nuttige en net sa brûkbere biblioteken. Se begjinne mei beide fast, of mei quick foarheaksel.

Ik wurkje op it stuit oan VictoriaMetrics. Wat is it en wat doch ik dêr? Ik sil it oer dit yn dizze presintaasje.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

De gearfetting fan it rapport is as folget:

  • Earst sil ik jo fertelle wat VictoriaMetrics is.
  • Dan sil ik jo fertelle wat tiidrekken binne.
  • Dan sil ik jo fertelle hoe't in tiidrige databank wurket.
  • Dêrnei sil ik jo fertelle oer de databankarsjitektuer: wêrút it bestiet.
  • En lit ús dan trochgean nei de optimalisaasjes dy't VictoriaMetrics hat. Dit is in optimalisaasje foar de omkearde yndeks en in optimalisaasje foar de ymplemintaasje fan bitset yn Go.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Wit immen yn it publyk wat VictoriaMetrics is? Wow, in protte minsken witte it al. It is goed nijs. Foar dyjingen dy't it net witte, is dit in databank foar tiidsearjes. It is basearre op de ClickHouse-arsjitektuer, op guon details fan 'e ClickHouse-ymplemintaasje. Bygelyks, op lykas: MergeTree, parallelle berekkening op alle beskikbere prosessor kearnen en prestaasjes optimalisaasje troch te wurkjen oan gegevens blokken dy't pleatst yn de prosessor cache.

VictoriaMetrics jout bettere gegevens kompresje as oare tiid rige databases.

It skaalfertikaal - dat is, jo kinne mear processors tafoegje, mear RAM op ien kompjûter. VictoriaMetrics sil dizze beskikbere boarnen mei súkses brûke en lineêre produktiviteit ferbetterje.

VictoriaMetrics skaalet ek horizontaal - dat is, jo kinne ekstra knopen tafoegje oan it VictoriaMetrics-kluster, en har prestaasjes sille hast lineêr ferheegje.

As jo ​​​​riede, is VictoriaMetrics in rappe databank, om't ik oaren net skriuwe kin. En it is skreaun yn Go, dus ik praat der oer op dizze gearkomste.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Wa wit wat in tiidrige is? Hy ken ek in protte minsken. In tiidrige is in rige fan pearen (timestamp, значение), dêr't dizze pearen wurde sortearre op tiid. De wearde is in driuwend punt nûmer - float64.

Eltse tiid rige wurdt unyk identifisearre troch in kaai. Wêr bestiet dizze kaai út? It bestiet út in net-lege set fan kaai-wearde-pearen.

Hjir is in foarbyld fan in tiidrige. De kaai fan dizze searje is in list mei pearen: __name__="cpu_usage" is de namme fan 'e metrike, instance="my-server" - dit is de kompjûter wêrop dizze metrik wurdt sammele, datacenter="us-east" - dit is it datasintrum wêr't dizze kompjûter leit.

Wy einige mei in tiidrige namme besteande út trije kaai-wearde pearen. Dizze kaai komt oerien mei in list fan pearen (timestamp, value). t1, t3, t3, ..., tN - dit binne tiidstempels, 10, 20, 12, ..., 15 - de oerienkommende wearden. Dit is it cpu-gebrûk op in opjûne tiid foar in opjûne searje.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Wêr kinne tiidrekken brûkt wurde? Hat immen in idee?

  • Yn DevOps kinne jo CPU, RAM, netwurk, rps, oantal flaters mjitte, ensfh.
  • IoT - wy kinne temperatuer, druk, geo-koordinaten en wat oars mjitte.
  • Ek finânsjes - wy kinne prizen kontrolearje foar alle soarten oandielen en faluta.
  • Derneist kinne tiidsearjes brûkt wurde by it kontrolearjen fan produksjeprosessen yn fabriken. Wy hawwe brûkers dy't VictoriaMetrics brûke om wynturbines te kontrolearjen, foar robots.
  • Tiidsearjes binne ek nuttich foar it sammeljen fan ynformaasje fan sensoren fan ferskate apparaten. Bygelyks foar in motor; foar it mjitten fan tire druk; foar it mjitten fan snelheid, ôfstân; foar it mjitten fan benzineferbrûk, ensfh.
  • Tiidsearjes kinne ek brûkt wurde om tafersjoch te hâlden. Elk fleantúch hat in swarte doaze dy't tiidsearjes sammelet foar ferskate parameters fan 'e sûnens fan it fleantúch. Tiidsearjes wurde ek brûkt yn 'e loftfeartyndustry.
  • Sûnenssoarch is bloeddruk, pols, ensfh.

D'r kinne mear applikaasjes wêze wêr't ik oer fergeat, mar ik hoopje dat jo begripe dat tiidreeksen aktyf wurde brûkt yn 'e moderne wrâld. En it folume fan har gebrûk groeit elk jier.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Wêrom hawwe jo nedich in tiid rige databank? Wêrom kinne jo net brûke in reguliere relational databank te bewarjen tiid rige?

Om't tiidrige meastentiids in grutte hoemannichte ynformaasje befetsje, dy't lestich is om te bewarjen en te ferwurkjen yn konvinsjonele databases. Dêrom ferskynden spesjale databases foar tiidsearjes. Dizze bases bewarje punten effektyf (timestamp, value) mei de opjûne kaai. Se leverje in API foar it lêzen fan bewarre gegevens troch kaai, troch in inkele kaai-wearde-pear, of troch meardere kaai-wearde-pearen, of troch regexp. Jo wolle bygelyks de CPU-lading fan al jo tsjinsten fine yn in datasintrum yn Amearika, dan moatte jo dizze pseudo-query brûke.

Typysk biede databases foar tiidsearjes spesjalisearre query-talen, om't tiidreeks SQL net heul geskikt is. Hoewol d'r databases binne dy't SQL stypje, is it net heul geskikt. Query talen lykas PromQL, InfluxQL, flow, Q. Ik hoopje dat immen op syn minst ien fan dizze talen heard hat. In protte minsken hawwe wierskynlik heard oer PromQL. Dit is de Prometheus-fraachtaal.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Dit is hoe't in moderne tiidserie-database-arsjitektuer liket mei it brûken fan VictoriaMetrics as foarbyld.

It bestiet út twa dielen. Dit is opslach foar de omkearde yndeks en opslach foar tiidrige wearden. Dizze repositories binne skieden.

As in nij rekord yn 'e databank komt, krije wy earst tagong ta de omkearde yndeks om de tiidrige identifier te finen foar in opjûne set label=value foar in opjûne metryske. Wy fine dizze identifier en bewarje de wearde yn 'e gegevenswinkel.

As in fersyk komt om gegevens fan TSDB op te heljen, geane wy ​​earst nei de omkearde yndeks. Litte wy alles krije timeseries_ids records dy't oerienkomme mei dizze set label=value. En dan krije wy alle nedige gegevens út it data warehouse, yndeksearre troch timeseries_ids.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Litte wy nei in foarbyld sjen fan hoe't in tiidreeksdatabank in ynkommende seleksjefraach ferwurket.

  • Earst krijt se alles timeseries_ids fan in omkearde yndeks dy't de opjûne pearen befetsje label=value, of foldwaan oan in opjûne reguliere útdrukking.
  • Dan hellet it alle gegevenspunten út 'e gegevensopslach op in opjûne tiidynterval foar de fûnen timeseries_ids.
  • Hjirnei fiert de databank guon berekkeningen op dizze gegevenspunten, neffens it fersyk fan de brûker. En dêrnei komt it antwurd werom.

Yn dizze presintaasje sil ik jo fertelle oer it earste diel. Dit is in syktocht timeseries_ids troch inverted index. Oer it twadde diel en it tredde diel kinne jo letter besjen VictoriaMetrics boarnen, of wachtsje oant ik oare rapporten tariede :)

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Litte wy nei de omkearde yndeks gean. In protte kinne tinke dat dit ienfâldich is. Wa wit wat in omkearde yndeks is en hoe't it wurket? Och, net safolle minsken mear. Litte wy besykje te begripen wat it is.

It is eins ienfâldich. It is gewoan in wurdboek dat in kaai foar in wearde yn kaart bringt. Wat is in kaai? Dit pear label=valuewêr label и value - dit binne rigels. En de wearden binne in set timeseries_ids, dy't it opjûne pear omfettet label=value.

Inverted yndeks kinne jo fluch fine alles timeseries_ids, dy't jûn hawwe label=value.

It lit jo ek fluch fine timeseries_ids tiidrige foar ferskate pearen label=value, of foar pearen label=regexp. Hoe komt dit? Troch it krúspunt fan 'e set te finen timeseries_ids foar elk pear label=value.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Litte wy nei ferskate ymplemintaasjes fan 'e omkearde yndeks sjen. Litte wy begjinne mei de ienfâldichste naïve ymplemintaasje. Se sjocht der sa út.

function getMetricIDs krijt in list mei snaren. Elke rigel befettet label=value. Dizze funksje jout in list werom metricIDs.

Hoe't it wurket? Hjir hawwe wy in globale fariabele neamd invertedIndex. Dit is in gewoan wurdboek (map), dy't de tekenrige yn kaart bringt nei slice ints. De line befettet label=value.

Funksje ymplemintaasje: krije metricIDs foar de earste label=value, dan geane wy ​​troch al it oare label=value, wy krije it metricIDs foar harren. En neam de funksje intersectInts, dy't hjirûnder besprutsen wurde. En dizze funksje jout de krusing fan dizze listen werom.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Sa't jo sjen kinne, it útfieren fan in omkearde yndeks is net hiel yngewikkeld. Mar dit is in naïve ymplemintaasje. Hokker neidielen hat it? It wichtichste neidiel fan de naïve ymplemintaasje is dat sa'n omkearde yndeks wurdt opslein yn RAM. Nei it opnij starte fan 'e applikaasje ferlieze wy dizze yndeks. D'r is gjin opslaan fan dizze yndeks op skiif. Sa'n omkearde yndeks is nei alle gedachten net geskikt foar in databank.

It twadde nadeel is ek yn ferbân mei ûnthâld. De omkearde yndeks moat passe yn RAM. As it grutter is as de grutte fan RAM, dan sille wy fansels krije - út ûnthâld flater. En it programma sil net wurkje.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Dit probleem kin wurde oplost mei help fan klearmakke oplossingen lykas LevelDB, of RocksDB.

Koartsein, wy hawwe in databank nedich wêrmei wy trije operaasjes fluch kinne dwaan.

  • De earste operaasje is opname ключ-значение nei dizze databank. Se docht dit hiel fluch, wêr ключ-значение binne willekeurige snaren.
  • De twadde operaasje is in fluch sykjen nei in wearde mei in opjûne kaai.
  • En de tredde operaasje is in fluch sykjen nei alle wearden troch in opjûn foarheaksel.

LevelDB en RocksDB - dizze databases waarden ûntwikkele troch Google en Facebook. Earst kaam LevelDB. Doe namen de jongens fan Facebook LevelDB en begon it te ferbetterjen, se makken RocksDB. No wurkje hast alle ynterne databases op RocksDB binnen Facebook, ynklusyf dyjingen dy't binne oerbrocht nei RocksDB en MySQL. Se neamden him MyRocks.

In omkearde yndeks kin ymplementearre wurde mei LevelDB. Hoe it te dwaan? Wy bewarje as in kaai label=value. En de wearde is de identifier fan 'e tiidrige wêr't it pear oanwêzich is label=value.

As wy hawwe in protte tiid rige mei in opjûne pear label=value, dan sille d'r in protte rigen yn dizze databank wêze mei deselde kaai en oars timeseries_ids. Om in list fan allegear te krijen timeseries_ids, dy't mei dizze begjinne label=prefix, dogge wy in berik scan wêrfoar dizze databank is optimalisearre. Dat is, wy selektearje alle rigels dy't begjinne mei label=prefix en krije de nedige timeseries_ids.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Hjir is in foarbyld ymplemintaasje fan hoe't it der útsjen soe yn Go. Wy hawwe in omkearde yndeks. Dit is LevelDB.

De funksje is itselde as foar de naïve ymplemintaasje. It werhellet de naïve ymplemintaasje hast rigel foar rigel. It ienige punt is dat ynstee fan te draaien nei map wy tagong ta de omkearde yndeks. Wy krije alle wearden foar de earste label=value. Dan geane wy ​​troch alle oerbleaune pearen label=value en krije de oerienkommende sets fan metricIDs foar harren. Dan fine wy ​​it krúspunt.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Alles liket goed te wêzen, mar d'r binne neidielen oan dizze oplossing. VictoriaMetrics ymplementearre yn earste ynstânsje in omkearde yndeks basearre op LevelDB. Mar op it lêst moast ik it opjaan.

Wêrom? Om't LevelDB stadiger is as de naïve ymplemintaasje. Yn in naïve ymplemintaasje, jûn in opjûne kaai, helje wy fuortendaliks de hiele slice metricIDs. Dit is in heul rappe operaasje - it heule stik is klear foar gebrûk.

Yn LevelDB wurdt elke kear in funksje oanroppen GetValues jo moatte gean troch alle rigels dy't begjinne mei label=value. En krije de wearde foar elke rigel timeseries_ids. Fan sokke timeseries_ids sammelje in stikje fan dizze timeseries_ids. Fansels is dit folle stadiger dan gewoan tagong ta in gewoane kaart mei kaai.

It twadde nadeel is dat LevelDB is skreaun yn C. C-funksjes oproppe fan Go is net heul fluch. It duorret hûnderten nanosekonden. Dit is net heul fluch, om't yn ferliking mei in gewoane funksje-oprop skreaun yn 'e go, dy't 1-5 nanosekonden nimt, is it ferskil yn prestaasjes tsientallen kearen. Foar VictoriaMetrics wie dit in fatale flater :)

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Dat ik skreau myn eigen ymplemintaasje fan 'e omkearde yndeks. En hy neamde har gearfoegje.

Mergeset is basearre op de MergeTree-gegevensstruktuer. Dizze gegevensstruktuer is liend fan ClickHouse. Fansels moat mergeset wurde optimalisearre foar fluch sykjen timeseries_ids neffens de opjûne kaai. Mergeset is folslein skreaun yn Go. Do kinst sjen VictoriaMetrics boarnen op GitHub. De ymplemintaasje fan mergeset is yn 'e map /lib/mergeset. Jo kinne besykje út te finen wat der bart.

De gearfoeging API is heul gelyk oan LevelDB en RocksDB. Dat is, it lit jo dêr fluch nije records opslaan en records fluch selektearje troch in opjûn foarheaksel.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Wy sille letter oer de neidielen fan fusearje prate. No litte wy prate oer hokker problemen ûntstienen mei VictoriaMetrics yn produksje by it útfieren fan in omkearde yndeks.

Wêrom binne se ûntstien?

De earste reden is de hege churn rate. Oerset yn Russysk, dit is in faak feroaring yn tiid rige. Dit is as in tiidrige einiget en in nije searje begjint, of in protte nije tiidsearjes begjinne. En dit bart faak.

De twadde reden is it grutte oantal tiidrekken. Yn it begjin, doe't monitoaring populêr waard, wie it oantal tiidsearjes lyts. Bygelyks, foar elke kompjûter moatte jo CPU, ûnthâld, netwurk en skiiflading kontrolearje. 4 tiid rige per kompjûter. Litte wy sizze dat jo 100 kompjûters en 400 tiidsearjes hawwe. Dit is hiel lyts.

Yn 'e rin fan' e tiid fûnen minsken út dat se mear korrelige ynformaasje kinne mjitte. Meitsje bygelyks de lading net fan 'e hiele prosessor, mar apart fan elke prosessorkearn. As jo ​​​​40 prosessorkearnen hawwe, dan hawwe jo 40 kear mear tiidsearjes om prosessorlading te mjitten.

Mar dat is net alles. Eltse prosessor kearn kin hawwe ferskate steaten, lykas idle, as it is idle. En wurkje ek yn brûkersromte, wurkje yn kernelromte en oare steaten. En elke sa'n steat kin ek mjitten wurde as in aparte tiidrige. Dit fergruttet ek it oantal rigen mei 7-8 kear.

Fan ien metrik krigen wy 40 x 8 = 320 metriken foar mar ien kompjûter. Fermannichfâldigje mei 100, wy krije 32 ynstee fan 000.

Doe kaam Kubernetes by. En it waard slimmer om't Kubernetes in protte ferskillende tsjinsten kin hostje. Elke tsjinst yn Kubernetes bestiet út in protte pods. En dit alles moat wurde kontrolearre. Derneist hawwe wy in konstante ynset fan nije ferzjes fan jo tsjinsten. Foar elke nije ferzje moatte nije tiidsearjes oanmakke wurde. Dêrtroch groeit it tal tiidsearjes eksponentieel en steane wy ​​foar it probleem fan in grut tal tiidrekken, dat heechkardinaliteit neamd wurdt. VictoriaMetrics omgiet it mei súkses fergelike mei oare databases foar tiidsearjes.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Litte wy in tichterby besjen op hege churn rate. Wat feroarsaket in hege churn rate yn produksje? Om't guon betsjuttingen fan labels en tags konstant feroarje.

Nim bygelyks Kubernetes, dy't it konsept hat deployment, dus as in nije ferzje fan jo applikaasje wurdt útrôle. Om ien of oare reden besleaten de Kubernetes-ûntwikkelders de ynset-id ta te foegjen oan it label.

Wat hat dit liede ta? Boppedat wurde by elke nije ynset alle âlde tiidsearjes ûnderbrutsen, en ynstee dêrfan begjinne nije tiidsearjes mei in nije labelwearde deployment_id. D'r kinne hûnderttûzenen en sels miljoenen fan sokke rigen wêze.

It wichtige ding oer dit alles is dat it totale oantal tiidsearjes groeit, mar it oantal tiidsearjes dy't op it stuit aktyf binne en gegevens ûntfange, bliuwt konstant. Dizze steat wurdt hege churn rate neamd.

It wichtichste probleem fan hege churn rate is te garandearjen in konstante sykhastighet foar alle tiid rige foar in opjûne set fan labels oer in bepaalde tiid ynterfal. Typysk is dit it tiidynterval foar it lêste oere of de lêste dei.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Hoe kinne jo dit probleem oplosse? Hjir is de earste opsje. Dit is om de omkearde yndeks oer de tiid te dielen yn ûnôfhinklike dielen. Dat is, wat tiid ynterval giet foarby, wy klear wurkje mei de hjoeddeiske omkearde yndeks. En meitsje in nije omkearde yndeks. In oare tiid ynterval foarby, wy meitsje in oar en in oar.

En by sampling fan dizze omkearde yndeksen, fine wy ​​in set fan omkearde yndeksen dy't binnen it opjûne ynterval falle. En dêrom selektearje wy dêrwei de id fan 'e tiidsearje.

Dit besparret boarnen om't wy net hoege te sjen nei dielen dy't net binnen it opjûne ynterval falle. Dat is, normaal, as wy gegevens selektearje foar it lêste oere, dan slaan wy fersiken oer foar eardere tiidintervallen.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

D'r is in oare opsje om dit probleem op te lossen. Dit is om foar elke dei in aparte list op te slaan mei id's fan tiidsearjes dy't op dy dei barde.

It foardiel fan dizze oplossing boppe de foarige oplossing is dat wy gjin tiidrige ynformaasje duplisearje dy't net ferdwynt oer de tiid. Se binne konstant oanwêzich en feroarje net.

It neidiel is dat sa'n oplossing dreger is om te realisearjen en dreger te debuggen. En VictoriaMetrics keas foar dizze oplossing. Dit is hoe't it histoarysk barde. Dizze oplossing docht ek goed yn ferliking mei de foarige. Om't dizze oplossing net ymplementearre waard fanwege it feit dat it nedich is om gegevens yn elke partysje te duplisearjen foar tiidsearjes dy't net feroarje, d.w.s. dy't net ferdwine oer de tiid. VictoriaMetrics waard foaral optimalisearre foar skiifromtekonsumpsje, en de foarige ymplemintaasje makke skiifromteferbrûk slimmer. Mar dizze ymplemintaasje is better geskikt foar minimalisearje skiif romte konsumpsje, dus it waard keazen.

Ik moast har fjochtsje. De striid wie dat jo yn dizze ymplemintaasje noch in folle grutter oantal kieze moatte timeseries_ids foar gegevens as wannear't de omkearde yndeks tiidferdield is.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Hoe hawwe wy dit probleem oplost? Wy hawwe it op in orizjinele manier oplost - troch ferskate identifier foar tiidsearjes te bewarjen yn elke omkearde yndeksyngong ynstee fan ien identifier. Dat is, wy hawwe in kaai label=value, dy't yn elke tiidrige foarkomt. En no bewarje wy ferskate timeseries_ids yn ien yngong.

Hjir is in foarbyld. Earder hienen wy N ynstjoerings, mar no hawwe wy ien yngong wêrfan it foarheaksel itselde is as alle oaren. Foar de foarige yngong befettet de wearde alle tiidrige id's.

Dit makke it mooglik om te fergrutsjen de skennen snelheid fan sa'n omkearde yndeks oant 10 kear. En it koe ús ûnthâldferbrûk foar de cache ferminderje, om't wy no de snaar opslaan label=value mar ien kear yn 'e cache tegearre N kear. En dizze line kin grut wêze as jo lange rigels opslaan yn jo tags en etiketten, dy't Kubernetes dêr graach skowe.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

In oare opsje foar it rapperjen fan sykjen op in omkearde yndeks is sharding. It oanmeitsjen fan ferskate omkearde yndeksen yn stee fan ien en sharding gegevens tusken harren troch kaai. Dit is in set key=value steam. Dat is, wy krije ferskate ûnôfhinklike omkearde yndeksen, dy't wy parallel kinne freegje op ferskate processors. Foarige ymplemintaasjes tastien allinnich operaasje yn ien-prosessor modus, dat wol sizze, skennen gegevens op mar ien kearn. Dizze oplossing lit jo gegevens op ferskate kearnen tagelyk scannen, lykas ClickHouse graach dwaan. Dit is wat wy fan plan binne út te fieren.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

No litte wy weromgean nei ús skiep - nei de krusingfunksje timeseries_ids. Litte wy beskôgje hokker ymplemintaasjes d'r kinne wêze. Dizze funksje kinne jo fine timeseries_ids foar in opjûne set label=value.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

De earste opsje is in naïve ymplemintaasje. Twa ynsletten loops. Hjir krije wy de funksje ynfier intersectInts twa plakjes - a и b. By de útfier, it moat werom nei ús de krusing fan dizze plakjes.

In naïve ymplemintaasje sjocht der sa út. Wy iterearje oer alle wearden fan slice a, binnen dizze lus geane wy ​​troch alle wearden fan slice b. En wy ferlykje se. As se oerienkomme, dan hawwe wy in krusing fûn. En bewarje it yn result.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Wat binne de neidielen? Kwadratyske kompleksiteit is har wichtichste nadeel. Bygelyks, as jo ôfmjittings slice binne a и b ien miljoen tagelyk, dan sil dizze funksje jo noait in antwurd jaan. Om't it ien triljoen iteraasjes moat meitsje, wat sels foar moderne kompjûters in protte is.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

De twadde útfiering is basearre op kaart. Wy meitsje kaart. Wy sette alle wearden fan slice yn dizze kaart a. Dan geane wy ​​troch de slice yn in aparte loop b. En wy kontrolearje oft dizze wearde is fan slice b yn map. As it bestiet, foegje it dan ta oan it resultaat.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Wat binne de foardielen? It foardiel is dat der allinnich lineêre kompleksiteit. Dat is, de funksje sil folle rapper útfiere foar gruttere plakken. Foar in plak fan miljoen grutte sil dizze funksje yn 2 miljoen iteraasjes útfiere, yn tsjinstelling ta de trillion iteraasjes fan 'e foarige funksje.

It neidiel is dat dizze funksje mear ûnthâld fereasket om dizze kaart te meitsjen.

It twadde nadeel is de grutte overhead foar hashing. Dit nadeel is net hiel dúdlik. En foar ús wie it ek net heul dúdlik, dus earst yn VictoriaMetrics wie de ymplemintaasje fan krusing fia in kaart. Mar doe profilearring die bliken dat de wichtichste prosessor tiid wurdt bestege oan skriuwen nei de kaart en kontrolearjen foar de oanwêzigens fan in wearde yn dizze kaart.

Wêrom wurdt CPU-tiid fergriemd op dizze plakken? Omdat Go fiert in hashing operaasje op dizze rigels. Dat is, it berekkent de hash fan 'e kaai om dan tagong te krijen by in opjûne yndeks yn' e HashMap. De hash-berekkeningsoperaasje wurdt foltôge yn tsientallen nanosekonden. Dit is stadich foar VictoriaMetrics.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Ik besleat om in bitset te ymplementearjen spesifyk foar dit gefal optimalisearre. Sa sjocht de krusing fan twa plakken no der út. Hjir meitsje wy in bitset. Wy foegje dêr eleminten fan 'e earste slach oan. Dan kontrolearje wy de oanwêzigens fan dizze eleminten yn 'e twadde slach. En foegje se ta oan it resultaat. Dat is, it is hast net oars as it foarige foarbyld. It ienige ding hjir is dat wy ferfongen tagong ta kaart mei oanpaste funksjes add и has.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Op it earste each liket it derop dat dit stadiger wurkje moat, as dêr earder in standertkaart brûkt waard, en dan wurde guon oare funksjes neamd, mar profilearring lit sjen dat dit ding 10 kear flugger wurket as de standertkaart yn it gefal fan VictoriaMetrics.

Dêrneist brûkt it folle minder ûnthâld yn ferliking mei de kaart ymplemintaasje. Om't wy hjir bits opslaan ynstee fan acht-byte wearden.

It neidiel fan dizze ymplemintaasje is dat it net sa fanselssprekkend is, net triviaal.

In oar nadeel dat in protte miskien net merke is dat dizze ymplemintaasje yn guon gefallen miskien net goed wurket. Dat is, it is optimalisearre foar in spesifyk gefal, foar dit gefal fan krusing fan VictoriaMetrics tiidrige ids. Dit betsjut net dat it geskikt is foar alle gefallen. As it ferkeard brûkt wurdt, krije wy gjin prestaasjesferheging, mar in flater sûnder ûnthâld en in fertraging yn prestaasjes.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Lit ús beskôgje de útfiering fan dizze struktuer. As jo ​​wolle sjen, leit it yn 'e VictoriaMetrics-boarnen, yn' e map lib/uint64set. It is optimalisearre spesifyk foar de VictoriaMetrics saak, wêr timeseries_id is in 64-bit wearde, dêr't de earste 32 bits binne yn prinsipe konstante en allinnich de lêste 32 bits feroarje.

Dizze gegevensstruktuer wurdt net opslein op skiif, it wurket allinnich yn it ûnthâld.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Hjir is syn API. It is net hiel yngewikkeld. De API is spesifyk ôfstimd op in spesifyk foarbyld fan it brûken fan VictoriaMetrics. Dat is, d'r binne hjir gjin ûnnedige funksjes. Hjir binne de funksjes dy't eksplisyt wurde brûkt troch VictoriaMetrics.

Der binne funksjes add, dy't nije wearden tafoeget. Der is in funksje has, dy't kontrolearret foar nije wearden. En der is in funksje del, dy't wearden ferwideret. Der is in helperfunksje len, dy't de grutte fan 'e set werombringt. Funksje clone clones in protte. En funksje appendto konvertearret dizze set nei slice timeseries_ids.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Dit is hoe't de ymplemintaasje fan dizze gegevensstruktuer derút sjocht. set hat twa eleminten:

  • ItemsCount is in helper fjild om fluch werom it oantal eleminten yn in set. It soe mooglik wêze om sûnder dit helpfjild te dwaan, mar it moast hjir tafoege wurde om't VictoriaMetrics faak de bitset-lingte yn har algoritmen freget.

  • It twadde fjild is buckets. Dit is in stik út 'e struktuer bucket32. Eltse struktuer winkels hi fjild. Dit binne de boppeste 32 bits. En twa plakjes - b16his и buckets fan bucket16 struktueren.

De top 16 bits fan it twadde diel fan 'e 64-bit struktuer wurde hjir opslein. En hjir wurde bitsets opslein foar de legere 16 bits fan elke byte.

Bucket64 bestiet út in array uint64. De lingte wurdt berekkene mei dizze konstanten. Yn ien bucket16 maksimum kin wurde opslein 2^16=65536 bit. As jo ​​dit diele troch 8, dan is it 8 kilobytes. As jo ​​wer diele troch 8, is it 1000 uint64 betsjutting. Dat is Bucket16 - dit is ús struktuer fan 8 kilobyte.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Litte wy sjen hoe't ien fan 'e metoaden fan dizze struktuer foar it tafoegjen fan in nije wearde wurdt ymplementearre.

It begjint allegear mei uint64 betsjuttings. Wy berekkenje de boppeste 32 bits, wy berekkenje de legere 32 bits. Lit ús troch alles gean buckets. Wy fergelykje de top 32 bits yn elke emmer mei de tafoege wearde. En as se oerienkomme, dan neame wy de funksje add yn struktuer b32 buckets. En foegje dêr de legere 32 bits ta. En as it weromkomt true, dan betsjut dit dat wy dêr sa'n wearde tafoege hawwe en sa'n wearde hawwe wy net. As it weromkomt false, dan bestie sa'n betsjutting al. Dan ferheegje wy it oantal eleminten yn 'e struktuer.

As wy dejinge dy't jo nedich hawwe net hawwe fûn bucket mei de fereaske hi-wearde, dan neame wy de funksje addAlloc, dy't in nije produsearje sil bucket, it tafoegjen oan 'e bakstruktuer.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Dit is de útfiering fan 'e funksje b32.add. It is fergelykber mei de foarige útfiering. Wy berekkenje de meast wichtige 16 bits, de minst wichtige 16 bits.

Dan geane wy ​​troch alle boppeste 16 bits. Wy fine wedstriden. En as der in wedstriid, wy neame de add metoade, dat wy sille beskôgje op de folgjende side foar bucket16.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

En hjir is it leechste nivo, dat moat wurde optimalisearre safolle mooglik. Wy berekkenje foar uint64 id wearde yn slice bit en ek bitmask. Dit is in masker foar in opjûne 64-bit wearde, dat kin brûkt wurde om te kontrolearjen de oanwêzigens fan dit bit, of set it. Wy kontrolearje om te sjen oft dit bit is ynsteld en set it, en weromkomme oanwêzigens. Dit is ús ymplemintaasje, wêrtroch't wy de wurking fan krusende id's fan tiidsearjes mei 10 kear kinne fersnelle yn ferliking mei konvinsjonele kaarten.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Neist dizze optimalisaasje hat VictoriaMetrics in protte oare optimalisaasjes. De measte fan dizze optimalisaasjes waarden tafoege foar in reden, mar nei profilearring fan de koade yn produksje.

Dit is de haadregel fan optimalisaasje - foegje gjin optimalisaasje ta as jo der fan útgean dat d'r hjir in knelpunt sil wêze, om't it kin blike dat d'r gjin knelpunt sil wêze. Optimalisaasje ferleget normaal de kwaliteit fan 'e koade. Dêrom is it wurdich om allinich te optimalisearjen nei profilearjen en leafst yn produksje, sadat dit echte gegevens is. As immen ynteressearre is, kinne jo de boarnekoade VictoriaMetrics besjen en oare optimalisaasjes ferkenne dy't der binne.

Gean optimalisaasjes yn VictoriaMetrics. Alexander Valyalkin

Ik haw in fraach oer bitset. Hiel gelyk oan de C ++ vector bool ymplemintaasje, optimalisearre bitset. Hawwe jo de ymplemintaasje dêrwei nommen?

Nee, dêr net fan. By it ymplementearjen fan dizze bitset waard ik liede troch kennis fan 'e struktuer fan dizze ids-timeseries, dy't wurde brûkt yn VictoriaMetrics. En har struktuer is sa dat de boppeste 32 bits yn prinsipe konstant binne. De legere 32 bits binne ûnder foarbehâld fan feroaring. Hoe leger it bit, hoe faker it kin feroarje. Dêrom is dizze ymplemintaasje spesifyk optimalisearre foar dizze gegevensstruktuer. De C ++ ymplemintaasje, sa fier as ik wit, is optimalisearre foar it algemiene gefal. As jo ​​optimisearje foar it algemiene gefal, betsjut dit dat it net it meast optimale sil wêze foar in spesifyk gefal.

Ik advisearje jo ek om it rapport fan Alexey Milovid te besjen. Sawat in moanne lyn spruts hy oer optimisaasje yn ClickHouse foar spesifike spesjalisaasjes. Hy seit gewoan dat yn it algemien gefal in C++ ymplemintaasje of in oare ymplemintaasje oanpast is om gemiddeld goed te wurkjen yn in sikehûs. It kin minder prestearje dan in kennisspesifike ymplemintaasje lykas ús, wêr't wy witte dat de top 32 bits meast konstant binne.

Ik haw in twadde fraach. Wat is it fûnemintele ferskil fan InfluxDB?

D'r binne in protte fûnemintele ferskillen. Yn termen fan prestaasjes en ûnthâld konsumpsje, InfluxDB yn tests toant 10 kear mear ûnthâld konsumpsje foar hege cardinality tiid rige, as jo hawwe in protte fan harren, Bygelyks, miljoenen. Bygelyks, VictoriaMetrics konsumearret 1 GB per miljoen aktive rigen, wylst InfluxDB 10 GB konsumearret. En dat is in grut ferskil.

It twadde fûnemintele ferskil is dat InfluxDB frjemde query-talen hat - Flux en InfluxQL. Se binne net hiel handich foar wurkjen mei tiid rige ferlike mei PromQL, dat wurdt stipe troch VictoriaMetrics. PromQL is in query-taal fan Prometheus.

En noch ien ferskil is dat InfluxDB in wat frjemd gegevensmodel hat, wêrby't elke rigel ferskate fjilden kin opslaan mei in oare set fan tags. Dizze rigels wurde fierder ferdield yn ferskate tabellen. Dizze ekstra komplikaasjes komplisearje folgjende wurk mei dizze databank. It is lestich om te stypjen en te begripen.

Yn VictoriaMetrics is alles folle ienfâldiger. Dêr is elke tiidrige in kaai-wearde. De wearde is in set fan punten - (timestamp, value), en de kaai is de set label=value. Der is gjin skieding tusken fjilden en mjittingen. It lit jo alle gegevens selektearje en dan kombinearje, tafoegje, subtractearje, fermannichfâldigje, diele, yn tsjinstelling ta InfluxDB wêr't berekkeningen tusken ferskate rigen noch altyd net ymplementearre binne foar safier't ik wit. Sels as se ymplementearre binne, is it lestich, jo moatte in protte koade skriuwe.

Ik haw in ferdúdlikjende fraach. Haw ik goed begrepen dat d'r in soarte fan probleem wie wêr't jo it oer hawwe, dat dizze omkearde yndeks net yn it ûnthâld past, dus is d'r partitionearring dêr?

Earst toande ik in naïve ymplemintaasje fan in omkearde yndeks op in standert Go-kaart. Dizze ymplemintaasje is net geskikt foar databases, om't dizze omkearde yndeks net op skiif bewarre wurdt, en de databank moat op skiif bewarje, sadat dizze gegevens beskikber bliuwe by it opnij starte. Yn dizze ymplemintaasje, as jo de applikaasje opnij starte, sil jo omkearde yndeks ferdwine. En jo sille tagong krije ta alle gegevens, om't jo it net kinne fine.

Hallo! Tank foar it ferslach! Myn namme is Pavel. Ik kom út Wildberries. Ik haw in pear fragen foar jo. Fraach ien. Tinke jo dat as jo in oar prinsipe keazen hiene by it bouwen fan 'e arsjitektuer fan jo applikaasje en de gegevens yn' e rin fan 'e tiid partitioneare, dan miskien jo gegevens by it sykjen kinne snije, allinich basearre op it feit dat ien partition gegevens foar ien befettet perioade fan tiid , dat is, yn ien tiidsinterval en jo hoege jo gjin soargen te meitsjen oer it feit dat jo stikken oars ferspraat binne? Fraach nûmer 2 - om't jo in ferlykber algoritme ymplementearje mei bitset en al it oare, hawwe jo miskien besocht prosessorynstruksjes te brûken? Miskien hawwe jo sokke optimalisaasjes besocht?

Ik sil daliks antwurdzje op de twadde. Op dat punt binne wy ​​noch net kommen. Mar as it nedich is, komme wy dêr. En de earste, wat wie de fraach?

Jo hawwe twa senario's besprutsen. En se seine dat se de twadde keazen hawwe mei in kompleksere ymplemintaasje. En se hawwe de earste net leaver, wêr't de gegevens binne ferdield troch tiid.

Ja. Yn it earste gefal soe it totale folume fan 'e yndeks grutter wêze, om't wy yn elke partysje dûbele gegevens moatte opslaan foar dy tiidsearjes dy't trochgean troch al dizze partysjes. En as jo tiidrige churn rate is lyts, dat wol sizze deselde rige wurde hieltyd brûkt, dan yn it earste gefal soene wy ​​ferlieze folle mear yn de hoemannichte skiif romte beset yn ferliking mei de twadde gefal.

En dus - ja, tiidferdieling is in goede opsje. Prometheus brûkt it. Mar Prometheus hat in oar nadeel. By it gearfoegjen fan dizze stikken gegevens, moat it meta-ynformaasje yn it ûnthâld hâlde foar alle labels en timeseries. Dêrom, as de stikken gegevens dy't it fusearret grut binne, dan nimt it ûnthâldferbrûk tige ta by it fusearjen, yn tsjinstelling ta VictoriaMetrics. By it fusearjen konsumearret VictoriaMetrics hielendal gjin ûnthâld; mar in pear kilobytes wurde konsumeare, nettsjinsteande de grutte fan 'e gearfoege stikken gegevens.

It algoritme dat jo brûke brûkt ûnthâld. It markearret timeseries tags dy't befetsje wearden. En op dizze manier kontrolearje jo op keppele oanwêzigens yn ien gegevensarray en yn in oare. En jo begripe oft krusing barde of net. Typysk ymplementearje databases cursors en iterators dy't har hjoeddeistige ynhâld opslaan en troch de sorteare gegevens rinne fanwegen de ienfâldige kompleksiteit fan dizze operaasjes.

Wêrom brûke wy rinnerkes net om gegevens troch te gean?

Ja.

Wy bewarje sorteare rigen yn LevelDB of mergeset. Wy kinne de rinnerke ferpleatse en de krusing fine. Wêrom brûke wy it net? Want it is stadich. Om't cursors betsjutte dat jo in funksje foar elke rigel neame moatte. In funksje-oprop is 5 nanosekonden. En as jo 100 rigels hawwe, dan docht bliken dat wy in heale sekonde gewoan de funksje neame.

Der is sa'n ding, ja. En myn lêste fraach. De fraach klinkt miskien in bytsje nuver. Wêrom is it net mooglik om alle nedige aggregaten te lêzen op it momint dat de gegevens oankomme en se yn 'e fereaske foarm opslaan? Wêrom enoarme folumes besparje yn guon systemen lykas VictoriaMetrics, ClickHouse, ensfh., En dan in protte tiid oan besteegje?

Ik sil in foarbyld jaan om it dúdliker te meitsjen. Litte wy sizze hoe wurket in lytse boartersguodsnelheidsmeter? It registrearret de ôfstân dy't jo hawwe reizge, hieltyd tafoegjen oan ien wearde, en de twadde - kear. En dielt. En krijt gemiddelde snelheid. Jo kinne dwaan oer itselde ding. Foegje alle nedige feiten op 'e flecht op.

Okee, ik begryp de fraach. Jo foarbyld hat syn plak. As jo ​​​​witte hokker aggregaten jo nedich binne, dan is dit de bêste ymplemintaasje. Mar it probleem is dat minsken dizze metriken bewarje, guon gegevens yn ClickHouse en se witte noch net hoe't se se yn 'e takomst sille aggregearje en filterje, sadat se alle rauwe gegevens moatte bewarje. Mar as jo witte dat jo wat gemiddeld moatte berekkenje, wêrom dan net berekkenje yn plak fan dêr in boskje rauwe wearden op te slaan? Mar dit is allinich as jo krekt witte wat jo nedich binne.

Trouwens, databases foar it opslaan fan tiidsearjes stypje it tellen fan aggregaten. Bygelyks, Prometheus stipet opname regels. Dat is, dit kin dien wurde as jo witte hokker ienheden jo nedich hawwe. VictoriaMetrics hat dit noch net, mar it wurdt meastentiids foarôfgien troch Prometheus, wêryn't dit kin dien wurde yn 'e werkodearjen regels.

Bygelyks, yn myn foarige baan moast ik it oantal eveneminten telle yn in sliding finster oer de lêste oere. It probleem is dat ik in oanpaste ymplemintaasje meitsje moast yn Go, dus in tsjinst foar it tellen fan dit ding. Dizze tsjinst wie úteinlik net-trivial, om't it dreech is om te berekkenjen. De ymplemintaasje kin ienfâldich wêze as jo wat aggregaten moatte telle op fêste tiidintervallen. As jo ​​​​eveneminten yn in sliding finster wolle telle, dan is it net sa ienfâldich as it liket. Ik tink dat dit noch net is ymplementearre yn ClickHouse of yn timeseries databases, om't it lestich is om te ymplementearjen.

En noch ien fraach. Wy hiene it gewoan oer gemiddelde, en ik tocht dat der ienris sa'n ding wie as Graphite mei in Carbon-backend. En hy wist âlde gegevens út te dûnjen, dat is, ien punt yn 'e minút, ien punt yn' e oere, ensfh. Yn prinsipe is dit frij handich as wy relatyf sprutsen rûge gegevens nedich binne foar in moanne, en al it oare kin útdûn wurde. Mar Prometheus en VictoriaMetrics stypje dizze funksjonaliteit net. Is it pland om it te stypjen? Sa net, wêrom net?

Tank foar de fraach. Us brûkers stelle dizze fraach periodyk. Se freegje wannear't wy stipe sille tafoegje foar downsampling. D'r binne ferskate problemen hjir. As earste begrypt elke brûker downsampling wat oars: immen wol te krijen eltse willekeurige punt op in opjûne ynterval, immen wol maksimum, minimum, gemiddelde wearden. As in protte systemen gegevens skriuwe nei jo databank, dan kinne jo it net allegear byinoar sammelje. It kin wêze dat elk systeem ferskillende tinning fereasket. En dit is dreech om te fieren.

En it twadde ding is dat VictoriaMetrics, lykas ClickHouse, is optimalisearre foar it wurkjen mei grutte folumes fan rûge gegevens, sadat it in miljard rigels yn minder dan in sekonde kin skodzje as jo in protte kearnen yn jo systeem hawwe. Scannen fan tiidrige punten yn VictoriaMetrics - 50 punten per sekonde per kearn. En dizze prestaasje skalen oan besteande kearnen. Dat is, as jo bygelyks 000 kearnen hawwe, sille jo in miljard punten per sekonde scannen. En dit eigendom fan VictoriaMetrics en ClickHouse fermindert it ferlet fan downsamling.

In oar skaaimerk is dat VictoriaMetrics dizze gegevens effektyf komprimearret. Kompresje gemiddeld yn produksje is fan 0,4 oant 0,8 bytes per punt. Elk punt is in tiidstempel + wearde. En it is gemiddeld yn minder dan ien byte komprimearre.

Sergey. Ik haw in fraach. Wat is de minimale opname tiid kwantum?

Ien millisekonde. Wy hawwe koartlyn in petear hân mei oare ûntwikkelders fan tiidseriedatabases. Har minimum tiid slice is ien sekonde. En yn Graphite, bygelyks, is it ek ien sekonde. Yn OpenTSDB is it ek ien sekonde. InfluxDB hat nanosekonde presyzje. Yn VictoriaMetrics is it ien millisekonde, want yn Prometheus is it ien millisekonde. En VictoriaMetrics waard oarspronklik ûntwikkele as opslach op ôfstân foar Prometheus. Mar no kin it gegevens fan oare systemen bewarje.

De persoan mei wa't ik spriek, seit dat se twadde-op-sekonde krektens hawwe - dat is genôch foar har, om't it hinget fan it type gegevens dat wurdt opslein yn 'e tiidreeksdatabase. As dit DevOps-gegevens is as gegevens fan ynfrastruktuer, wêr't jo se sammelje mei yntervallen fan 30 sekonden, per minuut, dan is twadde krektens genôch, jo hawwe neat minder nedich. En as jo dizze gegevens sammelje fan hannelssystemen mei hege frekwinsje, dan hawwe jo nanosekonde krektens nedich.

Millisekonde krektens yn VictoriaMetrics is ek geskikt foar de DevOps-saak, en kin geskikt wêze foar de measte gefallen dy't ik oan it begjin fan it rapport neamde. It ienige ding dêr't it miskien net geskikt is, is hege frekwinsje hannelssystemen.

Dankewol! En in oare fraach. Wat is kompatibiliteit yn PromQL?

Folsleine efterútkompatibiliteit. VictoriaMetrics stipet PromQL folslein. Derneist foeget it ekstra avansearre funksjonaliteit ta yn PromQL, dy't neamd wurdt MetricsQL. D'r is in petear op YouTube oer dizze útwreide funksjonaliteit. Ik spruts op de Monitoring Meetup yn 'e maitiid yn Sint Petersburg.

Telegramkanaal VictoriaMetrics.

Allinnich registrearre brûkers kinne meidwaan oan 'e enkête. Ynlogge, asjebleaft.

Wat hâldt jo fan oerstap nei VictoriaMetrics as jo lange-termyn opslach foar Prometheus? (Skriuw yn 'e kommentaren, ik sil it tafoegje oan' e poll))

  • 71,4%Ik brûk gjin Prometheus5

  • 28,6%Wist net oer VictoriaMetrics2

7 brûkers stimden. 12 brûkers ûntholden har.

Boarne: www.habr.com

Add a comment