Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Us suggereixo que llegiu la transcripció de l'informe de finals de 2019 d'Alexander Valyalkin "Go optimisations in VictoriaMetrics"

VictoriaMetrics — un SGBD ràpid i escalable per emmagatzemar i processar dades en forma de sèrie temporal (el registre forma temps i un conjunt de valors corresponents a aquest temps, per exemple, obtinguts mitjançant sondejos periòdics de l'estat dels sensors o recollida de mètriques).

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Aquí teniu un enllaç al vídeo d'aquest reportatge: https://youtu.be/MZ5P21j_HLE

Diapositives

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Parla'ns sobre tu. Sóc Alexander Valyalkin. Aquí el meu compte de GitHub. M'apassiona Go i l'optimització del rendiment. Vaig escriure moltes biblioteques útils i poc útils. Comencen amb qualsevol fast, o amb quick prefix.

Actualment estic treballant a VictoriaMetrics. Què és i què hi faig? En parlaré en aquesta presentació.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

L'esquema de l'informe és el següent:

  • Primer, us explicaré què és VictoriaMetrics.
  • Aleshores us explicaré quines són les sèries temporals.
  • Aleshores us explicaré com funciona una base de dades de sèries temporals.
  • A continuació, us parlaré de l'arquitectura de la base de dades: en què consisteix.
  • I després passem a les optimitzacions que té VictoriaMetrics. Aquesta és una optimització per a l'índex invertit i una optimització per a la implementació del conjunt de bits a Go.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Algú de l'audiència sap què és VictoriaMetrics? Vaja, molta gent ja ho sap. És una bona notícia. Per a aquells que no ho sàpiguen, aquesta és una base de dades de sèries temporals. Es basa en l'arquitectura ClickHouse, en alguns detalls de la implementació de ClickHouse. Per exemple, com ara: MergeTree, càlcul paral·lel de tots els nuclis de processador disponibles i optimització del rendiment treballant en blocs de dades que es col·loquen a la memòria cau del processador.

VictoriaMetrics proporciona una millor compressió de dades que altres bases de dades de sèries temporals.

S'escala verticalment, és a dir, podeu afegir més processadors, més memòria RAM en un ordinador. VictoriaMetrics utilitzarà amb èxit aquests recursos disponibles i millorarà la productivitat lineal.

VictoriaMetrics també escala horitzontalment, és a dir, podeu afegir nodes addicionals al clúster VictoriaMetrics i el seu rendiment augmentarà gairebé linealment.

Com heu endevinat, VictoriaMetrics és una base de dades ràpida, perquè no puc escriure altres. I està escrit a Go, així que en parlo en aquesta trobada.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Qui sap què és una sèrie temporal? També coneix molta gent. Una sèrie temporal és una sèrie de parelles (timestamp, значение), on aquestes parelles estan ordenades per temps. El valor és un nombre de coma flotant: float64.

Cada sèrie temporal s'identifica de manera única mitjançant una clau. En què consisteix aquesta clau? Consisteix en un conjunt no buit de parells clau-valor.

Aquí teniu un exemple de sèrie temporal. La clau d'aquesta sèrie és una llista de parelles: __name__="cpu_usage" és el nom de la mètrica, instance="my-server" - aquest és l'ordinador on es recull aquesta mètrica, datacenter="us-east" - aquest és el centre de dades on es troba aquest ordinador.

Hem acabat amb un nom de sèrie temporal que consta de tres parells clau-valor. Aquesta clau correspon a una llista de parelles (timestamp, value). t1, t3, t3, ..., tN - aquestes són marques de temps, 10, 20, 12, ..., 15 - els valors corresponents. Aquest és l'ús de la CPU en un moment determinat per a una sèrie determinada.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

On es poden utilitzar les sèries temporals? Algú té alguna idea?

  • A DevOps, podeu mesurar CPU, RAM, xarxa, rps, nombre d'errors, etc.
  • IoT: podem mesurar la temperatura, la pressió, les coordenades geogràfiques i alguna cosa més.
  • També financer: podem controlar els preus de tot tipus d'accions i divises.
  • A més, les sèries temporals es poden utilitzar en el seguiment dels processos de producció a les fàbriques. Tenim usuaris que utilitzen VictoriaMetrics per controlar aerogeneradors, per a robots.
  • Les sèries temporals també són útils per recollir informació dels sensors de diversos dispositius. Per exemple, per a un motor; per mesurar la pressió dels pneumàtics; per mesurar la velocitat, la distància; per mesurar el consum de gasolina, etc.
  • Les sèries temporals també es poden utilitzar per controlar avions. Cada avió té una caixa negra que recull sèries temporals per a diversos paràmetres de la salut de l'avió. Les sèries temporals també s'utilitzen a la indústria aeroespacial.
  • La salut és la pressió arterial, el pols, etc.

Potser hi ha més aplicacions que m'he oblidat, però espero que entengueu que les sèries de temps s'utilitzen activament al món modern. I el volum del seu ús creix cada any.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Per què necessiteu una base de dades de sèries temporals? Per què no podeu utilitzar una base de dades relacional normal per emmagatzemar sèries temporals?

Perquè les sèries temporals solen contenir una gran quantitat d'informació, que és difícil d'emmagatzemar i processar a les bases de dades convencionals. Per tant, van aparèixer bases de dades especialitzades per a sèries temporals. Aquestes bases emmagatzemen punts de manera efectiva (timestamp, value) amb la clau donada. Proporcionen una API per llegir les dades emmagatzemades per clau, per un sol parell clau-valor, per diversos parells clau-valor, o per expressió regular. Per exemple, voleu trobar la càrrega de la CPU de tots els vostres serveis en un centre de dades a Amèrica, llavors heu d'utilitzar aquesta pseudo-consulta.

Normalment, les bases de dades de sèries temporals ofereixen llenguatges de consulta especialitzats perquè l'SQL de sèries temporals no és molt adequat. Tot i que hi ha bases de dades que admeten SQL, no és molt adequat. Llenguatges de consulta com ara PromQL, InfluxQL, Flux, Q. Espero que algú hagi escoltat almenys un d'aquests idiomes. Probablement molta gent ha sentit parlar de PromQL. Aquest és el llenguatge de consulta de Prometheus.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Així és l'arquitectura moderna de bases de dades de sèries temporals utilitzant VictoriaMetrics com a exemple.

Consta de dues parts. Aquest és l'emmagatzematge de l'índex invertit i l'emmagatzematge dels valors de sèries temporals. Aquests dipòsits estan separats.

Quan arriba un registre nou a la base de dades, primer accedim a l'índex invertit per trobar l'identificador de sèrie temporal per a un conjunt determinat. label=value per a una mètrica determinada. Trobem aquest identificador i desem el valor al magatzem de dades.

Quan arriba una sol·licitud per recuperar dades de TSDB, primer anem a l'índex invertit. Agafem-ho tot timeseries_ids registres que coincideixen amb aquest conjunt label=value. I després obtenim totes les dades necessàries del magatzem de dades, indexades per timeseries_ids.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Vegem un exemple de com una base de dades de sèries temporals processa una consulta de selecció entrant.

  • Primer de tot ella ho aconsegueix tot timeseries_ids a partir d'un índex invertit que conté els parells donats label=value, o satisfer una expressió regular determinada.
  • A continuació, recupera tots els punts de dades de l'emmagatzematge de dades en un interval de temps determinat per als trobats timeseries_ids.
  • Després d'això, la base de dades realitza alguns càlculs sobre aquests punts de dades, segons la sol·licitud de l'usuari. I després d'això retorna la resposta.

En aquesta presentació us explicaré la primera part. Això és una recerca timeseries_ids per índex invertit. Podeu veure la segona part i la tercera part més endavant Fonts de VictoriaMetrics, o espera fins que prepari altres informes :)

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Passem a l'índex invertit. Molts poden pensar que això és senzill. Qui sap què és un índex invertit i com funciona? Oh, ja no tanta gent. Intentem entendre què és.

En realitat és senzill. És simplement un diccionari que assigna una clau a un valor. Què és una clau? Aquesta parella label=valueOn label и value - Aquestes són línies. I els valors són un conjunt timeseries_ids, que inclou la parella donada label=value.

L'índex invertit us permet trobar-ho tot ràpidament timeseries_ids, que han donat label=value.

També us permet trobar ràpidament timeseries_ids sèries temporals per a diverses parelles label=value, o per a parelles label=regexp. Com passa això? En trobar la intersecció del conjunt timeseries_ids per a cada parella label=value.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Vegem diverses implementacions de l'índex invertit. Comencem amb la implementació ingènua més senzilla. Ella es veu així.

Funció getMetricIDs obté una llista de cadenes. Cada línia conté label=value. Aquesta funció retorna una llista metricIDs.

Com funciona? Aquí tenim una variable global anomenada invertedIndex. Aquest és un diccionari normal (map), que mapejarà la cadena per tallar int. La línia conté label=value.

Implementació de la funció: obtenir metricIDs per la primera label=value, després passem per tota la resta label=value, ho aconseguim metricIDs per ells. I crida a la funció intersectInts, que es comentarà a continuació. I aquesta funció retorna la intersecció d'aquestes llistes.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Com podeu veure, implementar un índex invertit no és gaire complicat. Però aquesta és una implementació ingènua. Quins inconvenients té? El principal desavantatge de la implementació ingènua és que aquest índex invertit s'emmagatzema a la memòria RAM. Després de reiniciar l'aplicació perdem aquest índex. No hi ha cap desa d'aquest índex al disc. És poc probable que aquest índex invertit sigui adequat per a una base de dades.

El segon inconvenient també està relacionat amb la memòria. L'índex invertit ha d'encaixar a la memòria RAM. Si supera la mida de la memòria RAM, òbviament sortirem de l'error de memòria. I el programa no funcionarà.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Aquest problema es pot resoldre mitjançant solucions ja fetes com ara LevelDBO RocksDB.

En definitiva, necessitem una base de dades que ens permeti fer tres operacions ràpidament.

  • La primera operació és la gravació ключ-значение a aquesta base de dades. Ho fa molt ràpidament, on ключ-значение són cadenes arbitràries.
  • La segona operació és una cerca ràpida d'un valor mitjançant una clau determinada.
  • I la tercera operació és una cerca ràpida de tots els valors mitjançant un prefix determinat.

LevelDB i RocksDB: aquestes bases de dades van ser desenvolupades per Google i Facebook. Primer va arribar LevelDB. Llavors els nois de Facebook van agafar LevelDB i van començar a millorar-lo, van fer RocksDB. Ara gairebé totes les bases de dades internes funcionen a RocksDB dins de Facebook, incloses les que s'han transferit a RocksDB i MySQL. Li van posar nom MyRocks.

Un índex invertit es pot implementar mitjançant LevelDB. Com fer-ho? Guardem com a clau label=value. I el valor és l'identificador de la sèrie temporal on la parella està present label=value.

Si tenim moltes sèries temporals amb una parella determinada label=value, llavors hi haurà moltes files en aquesta base de dades amb la mateixa clau i diferents timeseries_ids. Per obtenir una llista de tots timeseries_ids, que comencen amb això label=prefix, fem una exploració d'intervals per a la qual aquesta base de dades està optimitzada. És a dir, seleccionem totes les línies que comencen per label=prefix i aconseguir el necessari timeseries_ids.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Aquí teniu una implementació de mostra de com seria a Go. Tenim un índex invertit. Això és LevelDB.

La funció és la mateixa que per a la implementació ingènua. Repeteix la implementació ingènua gairebé línia per línia. L'únic punt és que en comptes de recórrer a map accedim a l'índex invertit. Obtenim tots els valors del primer label=value. Després repassem totes les parelles restants label=value i obteniu els conjunts corresponents de metricIDs per a ells. Aleshores trobem la intersecció.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Sembla que tot va bé, però aquesta solució té inconvenients. VictoriaMetrics va implementar inicialment un índex invertit basat en LevelDB. Però al final vaig haver de renunciar-hi.

Per què? Perquè LevelDB és més lenta que la implementació ingènua. En una implementació ingènua, donada una clau determinada, recuperem immediatament tota la porció metricIDs. Aquesta és una operació molt ràpida: tota la llesca està llesta per al seu ús.

A LevelDB, cada vegada que es crida una funció GetValues heu de passar per totes les línies que comencen per label=value. I obteniu el valor de cada línia timeseries_ids. De tal timeseries_ids recollir un tros d'aquests timeseries_ids. Òbviament, això és molt més lent que simplement accedir a un mapa normal per clau.

El segon inconvenient és que LevelDB està escrit en C. Cridar funcions C des de Go no és molt ràpid. Es necessiten centenars de nanosegons. Això no és molt ràpid, perquè en comparació amb una trucada de funció normal escrita a go, que triga entre 1 i 5 nanosegons, la diferència de rendiment és de desenes de vegades. Per a VictoriaMetrics va ser un defecte fatal :)

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Així que vaig escriure la meva pròpia implementació de l'índex invertit. I la va cridar mergeset.

Mergeset es basa en l'estructura de dades MergeTree. Aquesta estructura de dades s'ha manllevat de ClickHouse. Òbviament, mergeset s'hauria d'optimitzar per a una cerca ràpida timeseries_ids segons la clau donada. Mergeset està escrit completament a Go. Pots veure Fonts de VictoriaMetrics a GitHub. La implementació de mergeset es troba a la carpeta /lib/mergeset. Pots intentar esbrinar què hi passa.

L'API mergeset és molt similar a LevelDB i RocksDB. És a dir, us permet desar-hi ràpidament nous registres i seleccionar-los ràpidament per un prefix determinat.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Més endavant parlarem dels inconvenients de mergeset. Ara parlem de quins problemes van sorgir amb VictoriaMetrics en producció en implementar un índex invertit.

Per què van sorgir?

La primera raó és l'elevada taxa de rotació. Traduït al rus, aquest és un canvi freqüent en les sèries temporals. És quan acaba una sèrie temporal i comença una nova sèrie, o comencen moltes sèries temporals noves. I això passa sovint.

La segona raó és el gran nombre de sèries temporals. Al principi, quan el monitoratge guanyava popularitat, el nombre de sèries temporals era petit. Per exemple, per a cada ordinador cal controlar la CPU, la memòria, la xarxa i la càrrega del disc. 4 sèries temporals per ordinador. Suposem que teniu 100 ordinadors i 400 sèries temporals. Això és molt poc.

Amb el temps, la gent es va adonar que podia mesurar informació més granular. Per exemple, mesura la càrrega no de tot el processador, sinó per separat de cada nucli de processador. Si teniu 40 nuclis de processador, teniu 40 vegades més sèries temporals per mesurar la càrrega del processador.

Però això no és tot. Cada nucli de processador pot tenir diversos estats, com ara inactiu, quan està inactiu. I també treballar a l'espai d'usuari, treballar a l'espai del nucli i altres estats. I cadascun d'aquests estats també es pot mesurar com una sèrie temporal separada. Això augmenta, a més, el nombre de files entre 7 i 8 vegades.

D'una mètrica hem obtingut 40 x 8 = 320 mètriques per a un sol ordinador. Multiplicant per 100, obtenim 32 en lloc de 000.

Després va venir Kubernetes. I va empitjorar perquè Kubernetes pot allotjar molts serveis diferents. Cada servei de Kubernetes consta de molts pods. I tot això s'ha de controlar. A més, tenim un desplegament constant de noves versions dels teus serveis. Per a cada nova versió, s'han de crear noves sèries temporals. Com a resultat, el nombre de sèries temporals creix de manera exponencial i ens trobem davant del problema d'un gran nombre de sèries temporals, que s'anomena alta cardinalitat. VictoriaMetrics ho fa amb èxit en comparació amb altres bases de dades de sèries temporals.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Fem una ullada més de prop a l'alta taxa de rotació. Què causa una alta taxa de rotació en la producció? Perquè alguns significats de les etiquetes i etiquetes estan canviant constantment.

Per exemple, prenem Kubernetes, que té el concepte deployment, és a dir, quan es desplega una nova versió de l'aplicació. Per alguna raó, els desenvolupadors de Kubernetes van decidir afegir l'identificador de desplegament a l'etiqueta.

A què va portar això? A més, amb cada nou desplegament, totes les sèries temporals antigues s'interrompen i, en comptes d'elles, les noves sèries temporals comencen amb un valor d'etiqueta nou deployment_id. Hi pot haver centenars de milers i fins i tot milions d'aquestes files.

L'important de tot això és que el nombre total de sèries temporals creix, però el nombre de sèries temporals que actualment estan actives i reben dades es manté constant. Aquest estat s'anomena taxa de rotació alta.

El principal problema de l'alta taxa de rotació és garantir una velocitat de cerca constant per a totes les sèries de temps per a un determinat conjunt d'etiquetes durant un interval de temps determinat. Normalment, aquest és l'interval de temps de l'última hora o l'últim dia.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Com resoldre aquest problema? Aquí teniu la primera opció. Es tracta de dividir l'índex invertit en parts independents al llarg del temps. És a dir, que passa algun interval de temps, acabem de treballar amb l'índex invertit actual. I creeu un nou índex invertit. Passa un altre interval de temps, en creem un altre i un altre.

I en el mostreig d'aquests índexs invertits, trobem un conjunt d'índexs invertits que es troben dins de l'interval donat. I, en conseqüència, seleccionem l'identificador de la sèrie temporal a partir d'aquí.

Això estalvia recursos perquè no hem de mirar les peces que no entren dins de l'interval donat. És a dir, normalment, si seleccionem dades de l'última hora, per als intervals de temps anteriors saltem les consultes.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Hi ha una altra opció per resoldre aquest problema. Això és per emmagatzemar per a cada dia una llista separada d'identificadors de sèries temporals que es van produir aquell dia.

L'avantatge d'aquesta solució respecte a la solució anterior és que no dupliquem informació de sèries temporals que no desapareix amb el temps. Estan presents constantment i no canvien.

El desavantatge és que aquesta solució és més difícil d'implementar i més difícil de depurar. I VictoriaMetrics va triar aquesta solució. Així va passar històricament. Aquesta solució també funciona bé en comparació amb l'anterior. Perquè aquesta solució no es va implementar pel fet que és necessari duplicar dades a cada partició per a sèries temporals que no canvien, és a dir, que no desapareixen amb el temps. VictoriaMetrics es va optimitzar principalment per al consum d'espai en disc, i la implementació anterior va empitjorar el consum d'espai en disc. Però aquesta implementació és més adequada per minimitzar el consum d'espai en disc, per la qual cosa es va triar.

Vaig haver de lluitar contra ella. La lluita va ser que en aquesta implementació encara cal triar un nombre molt més gran timeseries_ids per a dades que quan l'índex invertit està dividit en temps.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Com hem resolt aquest problema? Ho hem resolt d'una manera original: emmagatzemant diversos identificadors de sèries temporals a cada entrada d'índex invertit en lloc d'un identificador. És a dir, tenim una clau label=value, que es produeix en totes les sèries temporals. I ara en guardem uns quants timeseries_ids en una entrada.

Aquí teniu un exemple. Abans teníem N entrades, però ara tenim una entrada el prefix de la qual és el mateix que totes les altres. Per a l'entrada anterior, el valor conté tots els identificadors de sèries temporals.

Això va permetre augmentar la velocitat d'escaneig d'aquest índex invertit fins a 10 vegades. I ens va permetre reduir el consum de memòria per a la memòria cau, perquè ara emmagatzemem la cadena label=value només una vegada a la memòria cau junts N vegades. I aquesta línia pot ser gran si emmagatzemeu línies llargues a les vostres etiquetes i etiquetes, que a Kubernetes li agrada posar-hi.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Una altra opció per accelerar la cerca en un índex invertit és la fragmentació. Creació de diversos índexs invertits en lloc d'un i repartiment de dades entre ells per clau. Aquest és un conjunt key=value vapor. És a dir, obtenim diversos índexs invertits independents, que podem consultar en paral·lel en diversos processadors. Les implementacions anteriors només permetien operar en mode d'un sol processador, és a dir, escanejar dades en un sol nucli. Aquesta solució us permet escanejar dades de diversos nuclis alhora, com li agrada fer a ClickHouse. Això és el que tenim previst implementar.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Ara tornem a les nostres ovelles: a la funció d'intersecció timeseries_ids. Considerem quines implementacions hi pot haver. Aquesta funció us permet trobar timeseries_ids per a un conjunt determinat label=value.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

La primera opció és una implementació ingènua. Dos bucles imbricats. Aquí obtenim l'entrada de la funció intersectInts dues llesques - a и b. A la sortida, ens hauria de tornar la intersecció d'aquestes rodanxes.

Una implementació ingènua té aquest aspecte. Iterem sobre tots els valors de la llesca a, dins d'aquest bucle passem per tots els valors de slice b. I els comparem. Si coincideixen, hem trobat una intersecció. I guarda-ho result.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Quins són els inconvenients? La complexitat quadràtica és el seu principal inconvenient. Per exemple, si les vostres dimensions són slice a и b un milió alhora, aquesta funció mai us retornarà una resposta. Perquè haurà de fer un bilió d'iteracions, que és molt fins i tot per als ordinadors moderns.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

La segona implementació es basa en un mapa. Creem un mapa. Posem tots els valors de slice en aquest mapa a. A continuació, passem per rodanxes en un bucle separat b. I comprovem si aquest valor és de slice b al mapa. Si existeix, afegiu-lo al resultat.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Quins són els beneficis? L'avantatge és que només hi ha complexitat lineal. És a dir, la funció s'executarà molt més ràpid per a llesques més grans. Per a una porció de mida d'un milió, aquesta funció s'executarà en 2 milions d'iteracions, a diferència dels bilions d'iteracions de la funció anterior.

L'inconvenient és que aquesta funció requereix més memòria per crear aquest mapa.

El segon inconvenient és la gran sobrecàrrega per hash. Aquest inconvenient no és gaire evident. I per a nosaltres tampoc era molt evident, així que al principi a VictoriaMetrics la implementació de la intersecció es feia a través d'un mapa. Però aleshores l'elaboració de perfils va mostrar que el temps del processador principal es dedica a escriure al mapa i comprovar la presència d'un valor en aquest mapa.

Per què es perd el temps de la CPU en aquests llocs? Perquè Go realitza una operació hash en aquestes línies. És a dir, calcula el hash de la clau per després accedir-hi a un índex determinat al HashMap. L'operació de càlcul hash es completa en desenes de nanosegons. Això és lent per a VictoriaMetrics.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Vaig decidir implementar un conjunt de bits optimitzat específicament per a aquest cas. Així és com es veu ara la intersecció de dues rodanxes. Aquí creem un bitset. Hi afegim elements de la primera llesca. A continuació, comprovem la presència d'aquests elements en el segon tall. I afegiu-los al resultat. És a dir, gairebé no és diferent de l'exemple anterior. L'única cosa aquí és que hem substituït l'accés al mapa per funcions personalitzades add и has.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

A primer cop d'ull, sembla que això hauria de funcionar més lentament, si abans s'utilitzava un mapa estàndard allà, i després s'anomenen altres funcions, però el perfil mostra que això funciona 10 vegades més ràpid que el mapa estàndard en el cas de VictoriaMetrics.

A més, utilitza molta menys memòria en comparació amb la implementació del mapa. Perquè aquí emmagatzemem bits en lloc de valors de vuit bytes.

El desavantatge d'aquesta implementació és que no és tan obvi, ni trivial.

Un altre inconvenient que molts potser no noten és que aquesta implementació pot no funcionar bé en alguns casos. És a dir, està optimitzat per a un cas concret, per a aquest cas d'intersecció d'identificadors de sèries temporals de VictoriaMetrics. Això no vol dir que sigui adequat per a tots els casos. Si s'utilitza de manera incorrecta, no obtindrem un augment de rendiment, sinó un error de memòria sense memòria i una desacceleració del rendiment.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Considerem la implementació d'aquesta estructura. Si voleu mirar, es troba a les fonts de VictoriaMetrics, a la carpeta lib/uint64set. Està optimitzat específicament per al cas VictoriaMetrics, on timeseries_id és un valor de 64 bits, on els primers 32 bits són bàsicament constants i només canvien els últims 32 bits.

Aquesta estructura de dades no s'emmagatzema al disc, només funciona a la memòria.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Aquí teniu la seva API. No és gaire complicat. L'API s'adapta específicament a un exemple específic d'ús de VictoriaMetrics. És a dir, aquí no hi ha funcions innecessàries. Aquí hi ha les funcions que s'utilitzen explícitament per VictoriaMetrics.

Hi ha funcions add, que afegeix nous valors. Hi ha una funció has, que comprova nous valors. I hi ha una funció del, que elimina valors. Hi ha una funció d'ajuda len, que retorna la mida del conjunt. Funció clone clona molt. I funció appendto converteix aquest conjunt en slice timeseries_ids.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Així és com es veu la implementació d'aquesta estructura de dades. El conjunt té dos elements:

  • ItemsCount és un camp auxiliar per retornar ràpidament el nombre d'elements d'un conjunt. Seria possible prescindir d'aquest camp auxiliar, però s'havia d'afegir aquí perquè VictoriaMetrics sovint consulta la longitud del conjunt de bits als seus algorismes.

  • El segon camp és buckets. Aquesta és una porció de l'estructura bucket32. Cada estructura emmagatzema hi camp. Aquests són els 32 bits superiors. I dues llesques - b16his и buckets d' bucket16 estructures.

Aquí s'emmagatzemen els 16 bits superiors de la segona part de l'estructura de 64 bits. I aquí s'emmagatzemen conjunts de bits per als 16 bits inferiors de cada byte.

Bucket64 consisteix en una matriu uint64. La longitud es calcula utilitzant aquestes constants. En un bucket16 màxim es pot emmagatzemar 2^16=65536 bit. Si divideix això per 8, són 8 kilobytes. Si tornes a dividir per 8, és 1000 uint64 significat. Això és Bucket16 - aquesta és la nostra estructura de 8 kilobytes.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Vegem com s'implementa un dels mètodes d'aquesta estructura per afegir un nou valor.

Tot comença amb uint64 significats. Calculem els 32 bits superiors, calculem els 32 bits inferiors. Passem per tot buckets. Comparem els 32 bits principals de cada cub amb el valor afegit. I si coincideixen, llavors anomenem la funció add a l'estructura b32 buckets. I afegiu-hi els 32 bits inferiors. I si tornava true, llavors això vol dir que hi vam afegir aquest valor i no el teníem. Si torna false, llavors aquest significat ja existia. Després augmentem el nombre d'elements de l'estructura.

Si no hem trobat el que necessites bucket amb l'hi-valor requerit, llavors anomenem la funció addAlloc, que en produirà un de nou bucket, afegint-lo a l'estructura del cub.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Aquesta és la implementació de la funció b32.add. És similar a la implementació anterior. Calculem els 16 bits més significatius, els 16 bits menys significatius.

A continuació, repassem tots els 16 bits superiors. Trobem coincidències. I si hi ha una coincidència, anomenem el mètode add, que tindrem en compte a la pàgina següent bucket16.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

I aquí hi ha el nivell més baix, que s'hauria d'optimitzar al màxim. Calculem per uint64 valor d'identificació en bit de porció i també bitmask. Aquesta és una màscara per a un valor determinat de 64 bits, que es pot utilitzar per comprovar la presència d'aquest bit o configurar-lo. Comprovem si aquest bit està configurat i el configurem, i tornem la presència. Aquesta és la nostra implementació, que ens va permetre accelerar 10 vegades l'operació d'identificadors d'intersecció de sèries temporals en comparació amb els mapes convencionals.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

A més d'aquesta optimització, VictoriaMetrics té moltes altres optimitzacions. La majoria d'aquestes optimitzacions es van afegir per un motiu, però després de perfilar el codi en producció.

Aquesta és la regla principal d'optimització: no afegiu optimització suposant que hi haurà un coll d'ampolla aquí, perquè pot resultar que no hi haurà cap coll d'ampolla. L'optimització sol degradar la qualitat del codi. Per tant, val la pena optimitzar només després del perfil i preferiblement en producció, de manera que es tracta de dades reals. Si algú està interessat, pot mirar el codi font de VictoriaMetrics i explorar altres optimitzacions que hi ha.

Aneu a optimitzacions a VictoriaMetrics. Alexander Valyalkin

Tinc una pregunta sobre bitset. Molt semblant a la implementació bool vectorial C++, conjunt de bits optimitzat. Vau agafar la implementació des d'allà?

No, no d'allà. Quan vaig implementar aquest conjunt de bits, em vaig guiar pel coneixement de l'estructura d'aquestes sèries temporals d'identificadors, que s'utilitzen a VictoriaMetrics. I la seva estructura és tal que els 32 bits superiors són bàsicament constants. Els 32 bits inferiors estan subjectes a canvis. Com més baix sigui el bit, més sovint pot canviar. Per tant, aquesta implementació està optimitzada específicament per a aquesta estructura de dades. La implementació de C++, pel que jo sé, està optimitzada per al cas general. Si optimitzeu per al cas general, això vol dir que no serà el més òptim per a un cas concret.

També us aconsello que mireu el reportatge d'Alexei Milovid. Fa aproximadament un mes, va parlar de l'optimització a ClickHouse per a especialitzacions específiques. Només diu que, en el cas general, una implementació de C++ o alguna altra implementació està adaptada per funcionar bé de mitjana en un hospital. Pot ser que funcioni pitjor que una implementació específica de coneixement com la nostra, on sabem que els 32 bits principals són majoritàriament constants.

Tinc una segona pregunta. Quina és la diferència fonamental amb InfluxDB?

Hi ha moltes diferències fonamentals. Pel que fa al rendiment i el consum de memòria, InfluxDB a les proves mostra 10 vegades més consum de memòria per a sèries temporals d'alta cardinalitat, quan en tens moltes, per exemple, milions. Per exemple, VictoriaMetrics consumeix 1 GB per milió de files actives, mentre que InfluxDB consumeix 10 GB. I això és una gran diferència.

La segona diferència fonamental és que InfluxDB té llenguatges de consulta estranys: Flux i InfluxQL. No són molt convenients per treballar amb sèries temporals en comparació amb PromQL, que compta amb el suport de VictoriaMetrics. PromQL és un llenguatge de consulta de Prometheus.

I una diferència més és que InfluxDB té un model de dades una mica estrany, on cada línia pot emmagatzemar diversos camps amb un conjunt diferent d'etiquetes. Aquestes línies es divideixen a més en diverses taules. Aquestes complicacions addicionals compliquen el treball posterior amb aquesta base de dades. És difícil de donar suport i comprendre.

A VictoriaMetrics tot és molt més senzill. Allà, cada sèrie temporal és un valor-clau. El valor és un conjunt de punts - (timestamp, value), i la clau és el conjunt label=value. No hi ha separació entre camps i mesures. Us permet seleccionar qualsevol dada i després combinar, sumar, restar, multiplicar, dividir, a diferència d'InfluxDB on els càlculs entre diferents files encara no s'implementen pel que jo sé. Encara que estiguin implementats, és difícil, cal escriure molt de codi.

Tinc una pregunta aclaridora. He entès bé que hi va haver algun tipus de problema del qual vau parlar, que aquest índex invertit no encaixa a la memòria, per tant hi ha particions?

Primer, vaig mostrar una implementació ingènua d'un índex invertit en un mapa Go estàndard. Aquesta implementació no és adequada per a bases de dades perquè aquest índex invertit no es desa al disc i la base de dades s'ha de desar al disc perquè aquestes dades romanguin disponibles en reiniciar-se. En aquesta implementació, quan reinicieu l'aplicació, el vostre índex invertit desapareixerà. I perdràs l'accés a totes les dades perquè no les podràs trobar.

Hola! Gràcies pel reportatge! Em dic Pavel. Sóc de Wildberries. Tinc unes quantes preguntes per a tu. Pregunta 2. Creus que si haguessis escollit un principi diferent a l'hora de crear l'arquitectura de la teva aplicació i particionar les dades al llarg del temps, potser hauries pogut tallar dades en cercar, basant-te només en el fet que una partició conté dades per a una? període de temps, és a dir, en un interval de temps i no hauríeu de preocupar-vos del fet que les vostres peces estiguin disperses de manera diferent? Pregunta número XNUMX: com que esteu implementant un algorisme similar amb bitset i tota la resta, potser heu provat d'utilitzar instruccions del processador? Potser heu provat aquestes optimitzacions?

Contesto el segon de seguida. Encara no hem arribat a aquest punt. Però si cal, hi arribarem. I el primer, quina era la pregunta?

Vas discutir dos escenaris. I van dir que van triar el segon amb una implementació més complexa. I no preferien el primer, on les dades estan particionades per temps.

Sí. En el primer cas, el volum total de l'índex seria més gran, perquè en cada partició hauríem d'emmagatzemar dades duplicades per a aquelles sèries temporals que continuen per totes aquestes particions. I si la vostra taxa de rotació de sèries temporals és petita, és a dir, s'utilitzen constantment les mateixes sèries, en el primer cas perdríem molt més en la quantitat d'espai de disc ocupat en comparació amb el segon cas.

I així, sí, la partició del temps és una bona opció. Prometeu l'utilitza. Però Prometeu té un altre inconvenient. Quan es fusiona aquestes dades, ha de mantenir a la memòria la metainformació de totes les etiquetes i sèries temporals. Per tant, si les dades que fusiona són grans, el consum de memòria augmenta molt durant la fusió, a diferència de VictoriaMetrics. Quan es fusiona, VictoriaMetrics no consumeix memòria en absolut; només es consumeixen un parell de kilobytes, independentment de la mida de les dades combinades.

L'algorisme que utilitzeu utilitza memòria. Marca etiquetes de sèries temporals que contenen valors. I d'aquesta manera comproveu la presència aparellada en una matriu de dades i en una altra. I enteneu si la intersecció es va produir o no. Normalment, les bases de dades implementen cursors i iteradors que emmagatzemen el seu contingut actual i passen per les dades ordenades a causa de la senzilla complexitat d'aquestes operacions.

Per què no fem servir els cursors per recórrer les dades?

Emmagatzemem files ordenades a LevelDB o mergeset. Podem moure el cursor i trobar la intersecció. Per què no l'utilitzem? Perquè és lent. Perquè els cursors signifiquen que cal cridar una funció per a cada línia. Una crida de funció és de 5 nanosegons. I si teniu 100 de línies, resulta que ens passem mig segon només cridant la funció.

Hi ha una cosa així, sí. I la meva última pregunta. La pregunta pot semblar una mica estranya. Per què no és possible llegir tots els agregats necessaris en el moment en què arriben les dades i guardar-los en el format requerit? Per què estalviar grans volums en alguns sistemes com VictoriaMetrics, ClickHouse, etc., i després dedicar-hi molt de temps?

Posaré un exemple per aclarir-ho. Diguem com funciona un petit velocímetre de joguina? Enregistra la distància que heu recorregut, tot el temps afegint-la a un valor i el segon temps. I divideix. I aconsegueix una velocitat mitjana. Pots fer el mateix. Suma tots els fets necessaris sobre la marxa.

D'acord, entenc la pregunta. El teu exemple té el seu lloc. Si sabeu quins agregats necessiteu, aquesta és la millor implementació. Però el problema és que la gent guarda aquestes mètriques, algunes dades a ClickHouse i encara no saben com les agregaran i filtraran en el futur, de manera que han de desar totes les dades en brut. Però si sabeu que necessiteu calcular alguna cosa de mitjana, per què no calcular-la en lloc d'emmagatzemar-hi un munt de valors en brut? Però això només és si saps exactament el que necessites.

Per cert, les bases de dades per emmagatzemar sèries temporals admeten el recompte d'agregats. Per exemple, Prometeu dóna suport regles de gravació. És a dir, això es pot fer si sabeu quines unitats necessitareu. VictoriaMetrics encara no ho té, però sol anar precedit per Prometeu, en el qual es pot fer a les regles de codificació.

Per exemple, en el meu treball anterior necessitava comptar el nombre d'esdeveniments en una finestra lliscant durant l'última hora. El problema és que vaig haver de fer una implementació personalitzada a Go, és a dir, un servei per comptar aquesta cosa. En definitiva, aquest servei no va ser trivial, perquè és difícil de calcular. La implementació pot ser senzilla si necessiteu comptar alguns agregats a intervals de temps fixos. Si voleu comptar esdeveniments en una finestra lliscant, no és tan senzill com sembla. Crec que això encara no s'ha implementat a ClickHouse ni a les bases de dades de sèries temporals, perquè és difícil d'implementar.

I una pregunta més. Només estàvem parlant de la mitjana, i vaig recordar que alguna vegada hi havia una cosa com el grafit amb un backend de carboni. I va saber reduir dades antigues, és a dir, deixar un punt per minut, un punt per hora, etc. En principi, això és força convenient si necessitem dades en brut, relativament parlant, durant un mes, i tota la resta estar aprimat. Però Prometheus i VictoriaMetrics no admeten aquesta funcionalitat. Està previst donar-hi suport? Si no, per què no?

Gràcies per la pregunta. Els nostres usuaris fan aquesta pregunta periòdicament. Ens pregunten quan afegirem suport per a la baixada de mostres. Aquí hi ha diversos problemes. En primer lloc, cada usuari ho entén downsampling alguna cosa diferent: algú vol obtenir qualsevol punt arbitrari en un interval determinat, algú vol valors màxims, mínims i mitjans. Si molts sistemes escriuen dades a la vostra base de dades, no podreu agrupar-les totes. Pot ser que cada sistema requereixi un aclarit diferent. I això és difícil d'implementar.

I la segona cosa és que VictoriaMetrics, com ClickHouse, està optimitzat per treballar amb grans quantitats de dades en brut, de manera que pot treure mil milions de línies en menys d'un segon si teniu molts nuclis al vostre sistema. Escaneig de punts de sèries temporals a VictoriaMetrics: 50 de punts per segon per nucli. I aquest rendiment s'escala als nuclis existents. És a dir, si tens 000 nuclis, per exemple, escanejaràs mil milions de punts per segon. I aquesta propietat de VictoriaMetrics i ClickHouse redueix la necessitat de reducció de mostres.

Una altra característica és que VictoriaMetrics comprimeix aquestes dades de manera efectiva. La compressió mitjana en producció és de 0,4 a 0,8 bytes per punt. Cada punt és una marca de temps + valor. I es comprimeix en menys d'un byte de mitjana.

Sergey. Tinc una pregunta. Quin és el quàntic de temps d'enregistrament mínim?

Un mil·lisegon. Recentment hem tingut una conversa amb altres desenvolupadors de bases de dades de sèries temporals. El seu temps mínim és d'un segon. I en Graphite, per exemple, també és un segon. A OpenTSDB també és d'un segon. InfluxDB té una precisió de nanosegons. A VictoriaMetrics és d'un mil·lisegon, perquè a Prometheus és d'un mil·lisegon. I VictoriaMetrics es va desenvolupar originalment com a emmagatzematge remot per a Prometheus. Però ara pot desar dades d'altres sistemes.

La persona amb qui vaig parlar diu que tenen una precisió de segon a segon; això és suficient perquè depèn del tipus de dades que s'emmagatzemen a la base de dades de sèries temporals. Si es tracta de dades de DevOps o de dades de la infraestructura, on les reculles a intervals de 30 segons per minut, aleshores la precisió d'un segon és suficient, no necessites res menys. I si recopileu aquestes dades de sistemes de comerç d'alta freqüència, necessiteu una precisió de nanosegons.

La precisió de mil·lisegons a VictoriaMetrics també és adequada per al cas DevOps i pot ser adequada per a la majoria dels casos que he esmentat al principi de l'informe. L'únic per al qual pot no ser adequat són els sistemes de comerç d'alta freqüència.

Gràcies! I una altra pregunta. Què és la compatibilitat a PromQL?

Compatibilitat enrere total. VictoriaMetrics és totalment compatible amb PromQL. A més, afegeix una funcionalitat avançada addicional a PromQL, que s'anomena MetricsQL. Hi ha una xerrada a YouTube sobre aquesta funcionalitat ampliada. Vaig parlar a la reunió de seguiment a la primavera a Sant Petersburg.

Canal de Telegram VictoriaMetrics.

Només els usuaris registrats poden participar en l'enquesta. Inicia sessiósi us plau.

Què us impedeix canviar a VictoriaMetrics com a emmagatzematge a llarg termini per a Prometheus? (Escriu als comentaris, l'afegiré a l'enquesta))

  • 71,4%No faig servir Prometeu5

  • 28,6%No sabia sobre VictoriaMetrics2

Han votat 7 usuaris. 12 usuaris es van abstenir.

Font: www.habr.com

Afegeix comentari