Je vous propose de découvrir la transcription de la présentation d'Alexandre Valialkin à la fin de l'année 2019 intitulée "Optimisations Go dans VictoriaMetrics"
â une base de donnĂ©es rapide et Ă©volutive pour stocker et traiter des donnĂ©es sous forme de sĂ©ries temporelles (l'enregistrement forme le temps et un ensemble de valeurs correspondantes Ă ce moment, par exemple, obtenues par un sondage pĂ©riodique de l'Ă©tat des capteurs ou la collecte de mĂ©triques).

Voici le lien vers la vidĂ©o de cette prĂ©sentation â

Permettez-moi de me présenter. Je suis Alexandre Valialkin. Voici . Je suis passionné par Go et l'optimisation des performances. J'ai écrit de nombreuses bibliothÚques utiles et moins utiles. Elles commencent soit par fast, ou par quick comme préfixe.
Actuellement, je travaille sur VictoriaMetrics. Qu'est-ce que c'est et quel est mon rÎle là -dedans ? Je vais en parler dans cette présentation.

Le plan de la présentation est le suivant :
- Dans un premier temps, je vais vous expliquer ce qu'est VictoriaMetrics.
- Ensuite, je vais vous parler des séries temporelles.
- Puis je vais expliquer comment fonctionne la base de données des séries temporelles.
- Ensuite, je parlerai de l'architecture de la base de données : de quoi elle est composée.
- Et enfin, nous aborderons les optimisations présentes dans VictoriaMetrics. Cela inclut l'optimisation de l'index inversé et l'optimisation de l'implémentation du bitset en Go.

Est-ce que quelqu'un dans le public connaĂźt VictoriaMetrics ? Incroyable, dĂ©jĂ beaucoup de gens savent. C'est une bonne nouvelle. Pour ceux qui ne savent pas, c'est une base de donnĂ©es pour les sĂ©ries temporelles. Elle est basĂ©e sur l'architecture de ClickHouse, sur certains dĂ©tails d'implĂ©mentation de ClickHouse. Par exemple, sur des Ă©lĂ©ments tels que : MergeTree, le calcul parallĂšle sur tous les cĆurs de processeur disponibles et l'optimisation des performances grĂące Ă la gestion de blocs de donnĂ©es qui sont placĂ©s dans le cache du processeur.
VictoriaMetrics offre la meilleure compression des données par rapport aux autres bases de données pour les séries temporelles.
Elle se scale verticalement, c'est-à -dire que vous pouvez ajouter davantage de processeurs, plus de mémoire vive sur une seule machine. VictoriaMetrics utilisera efficacement ces ressources disponibles et améliorera la performance linéaire.
VictoriaMetrics se scale Ă©galement horizontalement, c'est-Ă -dire que vous pouvez ajouter des nĆuds supplĂ©mentaires au cluster VictoriaMetrics, et sa performance augmentera presque linĂ©airement.
Comme vous l'avez deviné, VictoriaMetrics est une base de données rapide, car je ne peux pas écrire sur d'autres. Et elle est développée en Go, c'est pourquoi j'en parle lors de ce meetup.

Qui sait ce qu'est une sĂ©rie temporelle ? Beaucoup de gens le savent aussi. Une sĂ©rie temporelle est une sĂ©rie de paires (timestamp, valeur), oĂč ces paires sont triĂ©es par temps. La valeur est un nombre Ă virgule flottante â float64.
Chaque série temporelle est identifiée de maniÚre unique par une clé. De quoi se compose cette clé ? Elle se compose d'un ensemble non vide de paires clé-valeur.
Voici un exemple de sĂ©rie temporelle. La clĂ© de cette sĂ©rie est une liste de paires : __name__="cpu_usage" â c'est le nom de la mĂ©trique, instance="my-server" â c'est l'ordinateur sur lequel cette mĂ©trique a Ă©tĂ© collectĂ©e, datacenter="us-east" â c'est le centre de donnĂ©es oĂč se trouve cet ordinateur.
Nous avons obtenu un nom de sĂ©rie temporelle composĂ© de trois paires clĂ©-valeur. Cette clĂ© correspond Ă une liste de paires (timestamp, value). t1, t3, t3, ..., tN â ce sont des timestamps, 10, 20, 12, ..., 15 â avec les valeurs correspondantes. C'est l'utilisation du CPU Ă ce moment donnĂ© pour cette sĂ©rie.

OĂč peuvent ĂȘtre utilisĂ©es les sĂ©ries temporelles ? Quelqu'un a-t-il des idĂ©es ?
- Dans DevOps, nous pouvons mesurer les lectures de charge CPU, RAM, réseau, rps, nombre d'erreurs, etc.
- IoT â nous pouvons mesurer la tempĂ©rature, la pression, les coordonnĂ©es gĂ©ographiques et d'autres Ă©lĂ©ments.
- Ăgalement pour les finances â nous pouvons surveiller les prix des actions et des devises.
- De plus, les sĂ©ries temporelles peuvent ĂȘtre utilisĂ©es pour surveiller les processus de production dans les usines. Nous avons des utilisateurs qui utilisent VictoriaMetrics pour surveiller les Ă©oliennes, pour les robots.
- Les séries temporelles sont également utiles pour collecter des informations à partir de capteurs de divers dispositifs. Par exemple, pour les moteurs ; pour mesurer la pression dans les pneus ; pour mesurer la vitesse, la distance ; pour mesurer la consommation d'essence, etc.
- Les sĂ©ries temporelles peuvent Ă©galement ĂȘtre utilisĂ©es pour surveiller les avions. Chaque avion a une boĂźte noire qui collecte des sĂ©ries temporelles sur diffĂ©rents paramĂštres de santĂ© de l'avion. Les sĂ©ries temporelles sont Ă©galement utilisĂ©es dans l'industrie aĂ©rospatiale.
- La santĂ© â c'est la pression sanguine, le pouls, etc.
Il peut encore y avoir d'autres applications dont j'ai oublié de parler, mais j'espÚre que vous avez compris que les séries temporelles sont activement utilisées dans le monde moderne. Et leur utilisation augmente chaque année.

à quoi sert une base de données pour les séries temporelles ? Pourquoi ne pas utiliser une base de données relationnelle classique pour stocker des séries temporelles ?
Parce que dans les séries temporelles, il y a généralement une grande quantité d'informations qui sont difficiles à stocker et à traiter dans des bases de données classiques. C'est pourquoi des bases de données spécialisées pour les séries temporelles ont vu le jour. Ces bases stockent efficacement les points. (timestamp, value) avec une clé donnée. Elles fournissent une API pour lire les données stockées par clé, soit une paire clé-valeur, soit plusieurs de ces paires, soit par regexp. Par exemple, si vous souhaitez trouver la charge du processeur de tous vos services dans un centre de données en Amérique, vous devez utiliser cette sorte de pseudointerrogation.
En gĂ©nĂ©ral, les bases de donnĂ©es pour sĂ©ries temporelles proposent des langages de requĂȘte spĂ©cialisĂ©s, car SQL n'est pas trĂšs adaptĂ© aux sĂ©ries temporelles. Bien qu'il existe des bases de donnĂ©es qui supportent SQL, ce dernier ne convient pas vraiment. Des langages de requĂȘte comme , , , sont mieux adaptĂ©s. J'espĂšre que quelqu'un a dĂ©jĂ entendu parler d'au moins l'un de ces langages. De nombreux gens ont sĂ»rement entendu parler de PromQL. C'est le langage de requĂȘtes de Prometheus.

Voici à quoi ressemble l'architecture d'une base de données moderne pour séries temporelles, avec VictoriaMetrics comme exemple.
Elle se compose de deux parties. Il y a un stockage pour l'index inversé et un stockage pour les valeurs des séries temporelles. Ces stockages sont séparés.
Lorsque qu'un nouvel enregistrement arrive dans la base de données, nous consultons d'abord l'index inversé pour trouver l'identifiant de la série temporelle correspondant à un ensemble donné label=value pour cette métrique. Nous trouvons cet identifiant et stockons la valeur dans le stockage de données.
Lorsque qu'une requĂȘte d'extraction de donnĂ©es est faite au TSDB, nous consultons d'abord l'index inversĂ©. Nous obtenons tous les timeseries_ids d'enregistrements, qui correspondent Ă l'ensemble donnĂ©. label=valuePuis nous rĂ©cupĂ©rons toutes les donnĂ©es nĂ©cessaires du stockage de donnĂ©es, indexĂ©es par timeseries_ids.

ConsidĂ©rons un exemple oĂč la base de donnĂ©es pour sĂ©ries temporelles traite une requĂȘte SELECT entrante.
- Dans un premier temps, elle récupÚre tous les
timeseries_idsde l'index inversé, qui contiennent les paires donnéeslabel=value, ou qui satisfont à une expression réguliÚre donnée. - Puis elle extrait tous les points de données du stockage de données sur l'intervalle de temps donné pour les trouvés.
timeseries_ids. - AprĂšs cela, la base de donnĂ©es effectue certains calculs sur ces points de donnĂ©es, selon la requĂȘte de l'utilisateur. Et ensuite, elle retourne la rĂ©ponse.
Dans cette prĂ©sentation, je vais vous parler de la premiĂšre partie. C'est la recherche. timeseries_ids selon l'index inversĂ©. Vous pouvez ensuite consulter la deuxiĂšme et la troisiĂšme partie. , ou attendre que je prĂ©pare d'autres prĂ©sentations đ

Commençons par l'index inversé. Beaucoup peuvent penser que c'est simple. Qui sait ce qu'est un index inversé et comment il fonctionne ? Oh, il n'y a déjà plus tant de gens. Essayons de comprendre ce que c'est.
En rĂ©alitĂ©, c'est assez simple. C'est juste un dictionnaire qui fait correspondre une clĂ© Ă une valeur. Qu'est-ce qu'une clĂ© ? Cette paire label=value, oĂč label et value â ce sont des chaĂźnes. Et les valeurs sont un ensemble timeseries_ids, qui contient la paire donnĂ©e. label=value.
L'index inversé permet de trouver rapidement tous timeseries_ids, qui possÚdent les label=value.
Il permet également de trouver rapidement des timeseries_ids séries temporelles pour plusieurs paires label=value, ou pour des paires label=regexp. Comment cela fonctionne-t-il ? Grùce à la recherche de l'intersection d'un ensemble timeseries_ids pour chaque paire. label=value.

Examinons différentes implémentations de l'index inversé. Commençons par l'implémentation naïve la plus simple. Elle ressemble à ceci.
Fonction getMetricIDs obtient une liste de chaĂźnes. Chaque chaĂźne contient label=value. Cette fonction renvoie une liste de metricIDs..
Comment cela fonctionne-t-il ? Ici, nous avons une variable globale appelée invertedIndex.C'est un simple dictionnaire (carte), qui associe une chaßne à un slice d'entiers. La chaßne contient label=value.
Implémentation de la fonction : nous obtenons metricIDs. pour le premier label=value, puis nous parcourons tous les autres label=value, nous obtenons metricIDs. pour eux. Et nous appelons la fonction intersectInts,qui sera expliquée plus loin. Cette fonction renvoie l'intersection de ces listes.

Comme vous pouvez le voir, l'implémentation de l'index inversé n'est pas trÚs compliquée. Mais c'est une implémentation naïve. Quels en sont les inconvénients ? Le principal inconvénient de l'implémentation naïve est que cet index inversé est stocké en mémoire vive. AprÚs le redémarrage de l'application, nous perdons cet index. Il n'y a pas de sauvegarde de cet index sur disque. Pour une base de données, un tel index inversé n'est probablement pas adapté.
Le deuxiÚme inconvénient est également lié à la mémoire. L'index inversé doit tenir dans la mémoire vive. S'il dépasse la taille de la mémoire, il est évident que nous obtiendrons une erreur de type sorti de mémoire. Et le programme ne fonctionnera pas.

Ce problĂšme peut ĂȘtre rĂ©solu grĂące Ă des solutions prĂȘtes Ă l'emploi telles que , ou bien .
En résumé, nous avons besoin d'une base de données qui permet d'effectuer rapidement trois opérations.
- La premiÚre opération consiste à écrire
clĂ©-valeurdans cette base. Elle le fait trĂšs rapidement, oĂčclĂ©-valeurce sont des chaĂźnes alĂ©atoires. - La deuxiĂšme opĂ©ration consiste Ă rechercher rapidement une valeur par clĂ©.
- Et la troisiÚme opération consiste à rechercher rapidement toutes les valeurs par un préfixe donné.
LevelDB et RocksDB â ces bases ont Ă©tĂ© dĂ©veloppĂ©es chez Google et Facebook. LevelDB est apparu en premier. Ensuite, les gars de Facebook ont pris LevelDB et l'ont amĂ©liorĂ©, crĂ©ant RocksDB. Actuellement, presque toutes les bases de donnĂ©es internes de Facebook fonctionnent sur RocksDB, y compris MySQL, qui a Ă©tĂ© transfĂ©rĂ© sur RocksDB. Ils l'ont appelĂ© .
Un index inversĂ© peut ĂȘtre rĂ©alisĂ© avec LevelDB. Comment faire cela ? Nous le sauvegardons en tant que clĂ© label=value. Et en tant que valeur â l'identifiant de la sĂ©rie temporelle oĂč la paire est prĂ©sente. label=value.
Si nous avons plusieurs sĂ©ries temporelles avec cette paire label=value, il y aura beaucoup de lignes dans cette base de donnĂ©es avec la mĂȘme clĂ© et diffĂ©rentes timeseries_ids. Pour obtenir la liste de tous les timeseries_ids, qui commencent par un certain label=prefix, nous effectuons un scan de plage, pour lequel cette base de donnĂ©es est optimisĂ©e. C'est-Ă -dire que nous sĂ©lectionnons toutes les lignes qui commencent par label=prefix et obtenons les timeseries_ids.

Voici une implémentation approximative de ce à quoi cela ressemblerait en Go. Nous avons un index inversé. C'est LevelDB.
La fonction est la mĂȘme que pour l'implĂ©mentation naĂŻve. Elle rĂ©pĂšte presque ligne par ligne l'implĂ©mentation naĂŻve. Le seul point, c'est qu'au lieu d'accĂ©der Ă carte , nous accĂ©dons Ă l'index inversĂ©. Nous rĂ©cupĂ©rons toutes les valeurs pour le premier label=value. Ensuite, nous passons en revue toutes les paires restantes label=value et obtenons les ensembles correspondants de metricIDs pour elles. Puis nous trouvons l'intersection.

Tout semble bien, mais cette solution a ses inconvénients. VictoriaMetrics a d'abord implémenté l'index inversé basé sur LevelDB. Mais au final, ils ont dû y renoncer.
Pourquoi ? Parce que LevelDB est plus lent que l'implĂ©mentation naĂŻve. Dans l'implĂ©mentation naĂŻve, pour une clĂ© donnĂ©e, nous accĂ©dons directement Ă tout le slice metricIDs.. C'est une opĂ©ration trĂšs rapide â tout le slice est prĂȘt Ă ĂȘtre utilisĂ©.
Dans LevelDB, à chaque appel de la fonction GetValues , il faut parcourir toutes les lignes qui commencent par label=value. Et pour chaque ligne, extraire la valeur timeseries_ids. De ces timeseries_ids , recueillir un slice de ces timeseries_ids. Il est évident que c'est beaucoup plus lent que d'accéder directement à une map normale par clé.
Le deuxiĂšme inconvĂ©nient est que LevelDB est Ă©crit en C. L'appel des fonctions C depuis Go n'est pas trĂšs rapide. Cela prend des centaines de nanosecondes. Ce n'est pas trĂšs rapide, car par rapport Ă un appel de fonction Ă©crit en Go, qui prend 1 Ă 5 nanosecondes, l'Ă©cart de performance est de plusieurs ordres de grandeur. Pour VictoriaMetrics, cela a Ă©tĂ© un inconvĂ©nient fatal đ

C'est pourquoi j'ai écrit ma propre implémentation d'un index inversé. Je l'ai appelée .
Mergeset est basĂ© sur la structure de donnĂ©es MergeTree. Cette structure de donnĂ©es est empruntĂ©e Ă ClickHouse. Il est Ă©vident que mergeset doit ĂȘtre optimisĂ© pour une recherche rapide timeseries_ids par clĂ©. Mergeset est entiĂšrement Ă©crit en Go. Vous pouvez consulter . L'implĂ©mentation de mergeset se trouve dans le dossier . Vous pouvez essayer de comprendre ce qui s'y passe.
L'API de mergeset est trÚs similaire à celle de LevelDB et RocksDB. C'est-à -dire qu'elle permet de sauvegarder rapidement de nouvelles entrées et de sélectionner rapidement des entrées par un préfixe donné.

Nous parlerons plus tard des défauts de mergeset. Pour l'instant, discutons des problÚmes rencontrés avec VictoriaMetrics en production lors de l'implémentation de l'index inversé.
Pourquoi sont-ils survenus ?
La premiÚre raison est le taux de renouvellement élevé. En d'autres termes, il s'agit d'un changement fréquent des séries temporelles. C'est lorsque une série temporelle se termine et qu'une nouvelle série commence, ou que de nombreuses nouvelles séries temporelles commencent. Et cela se produit fréquemment.
La deuxiÚme raison est le grand nombre de séries temporelles. Au début, lorsque la surveillance gagnait en popularité, le nombre de séries temporelles était faible. Par exemple, pour chaque ordinateur, il faut surveiller l'utilisation du processeur, de la mémoire, du réseau et du disque. 4 séries temporelles par ordinateur. Supposons que vous ayez 100 ordinateurs et 400 séries temporelles. C'est trÚs peu.
Avec le temps, les gens ont rĂ©alisĂ© qu'il Ă©tait possible de mesurer des informations plus dĂ©taillĂ©es. Par exemple, mesurer l'utilisation non pas de l'ensemble du processeur, mais de chaque cĆur de processeur sĂ©parĂ©ment. Si vous avez 40 cĆurs de processeur, alors vous obtenez 40 fois plus de sĂ©ries temporelles pour mesurer l'utilisation du processeur.
Mais ce n'est pas tout. Chaque cĆur de processeur peut avoir plusieurs Ă©tats, tels que idle, lorsqu'il est inactif. Il peut Ă©galement fonctionner en espace utilisateur, en espace noyau et dans d'autres Ă©tats. Chacun de ces Ă©tats peut Ă©galement ĂȘtre mesurĂ© comme une sĂ©rie temporelle distincte. Cela augmente encore le nombre de sĂ©ries de 7 Ă 8 fois.
Nous avons obtenu 40 x 8 = 320 métriques rien que pour un ordinateur. En multipliant par 100, nous obtenons 32 000 au lieu de 400.
Puis est arrivĂ© Kubernetes. Cela a encore aggravĂ© la situation, car de nombreux services diffĂ©rents peuvent ĂȘtre hĂ©bergĂ©s dans Kubernetes. Chaque service dans Kubernetes est composĂ© de nombreux pods. Tout cela nĂ©cessite une surveillance. De plus, nous avons un dĂ©ploiement constant de nouvelles versions de vos services. Pour chaque nouvelle version, nous devons crĂ©er de nouvelles sĂ©ries temporelles. Au final, le nombre de sĂ©ries temporelles augmente de maniĂšre exponentielle et nous faisons face Ă un problĂšme de nombreuses sĂ©ries temporelles, qui est appelĂ© high-cardinality. VictoriaMetrics gĂšre cela avec succĂšs par rapport Ă d'autres bases de donnĂ©es de sĂ©ries temporelles.

Examinons de plus prÚs le high churn rate. Qu'est-ce qui cause un high churn rate en production ? Parce que certaines valeurs d'étiquettes et de tags changent constamment.
Par exemple, prenons Kubernetes, qui a le concept de déploiement, c'est-à -dire lorsque la nouvelle version de votre application est déployée. Les développeurs de Kubernetes ont décidé d'ajouter l'ID du déploiement dans l'étiquette.
Quelle en est la conséquence ? Que lors de chaque nouveau déploiement, toutes nos anciennes séries temporelles sont interrompues, et à la place de celles-ci, de nouvelles séries temporelles commencent avec une nouvelle valeur d'étiquette deployment_id. Il peut y avoir des centaines de milliers, voire des millions de telles séries.
Une caractĂ©ristique importante de tout cela est que le nombre total de sĂ©ries temporelles augmente, mais le nombre de sĂ©ries temporelles qui sont actuellement actives, dont les donnĂ©es arrivent, reste constant. Cet Ă©tat est appelĂ© â high churn rate.
Le principal problÚme du high churn rate est d'assurer une vitesse de recherche constante pour toutes les séries temporelles sur la base d'un ensemble donné d'étiquettes pendant un certain intervalle de temps. Généralement, il s'agit d'un intervalle de temps au cours de la derniÚre heure ou de la derniÚre journée.

Comment résoudre ce problÚme ? Voici une premiÚre option. Il s'agit de diviser l'index inversé en parties indépendantes par période. C'est-à -dire qu'aprÚs un certain intervalle de temps, nous terminons notre travail avec l'index inversé actuel et créons un nouvel index inversé. Un autre intervalle de temps passe, et nous créons encore un autre index inversé.
Et lors de la sélection parmi ces index inversés, nous trouvons un ensemble d'index inversés qui tombent dans l'intervalle spécifié. Nous sélectionnons donc les identifiants des séries temporelles à partir de là .
Cela permet d'Ă©conomiser des ressources, car nous n'avons pas besoin d'examiner des parties qui ne tombent pas dans l'intervalle spĂ©cifiĂ©. C'est-Ă -dire gĂ©nĂ©ralement, si nous sĂ©lectionnons des donnĂ©es de la derniĂšre heure, nous ignorons les requĂȘtes des intervalles de temps prĂ©cĂ©dents.

Il existe une autre option pour résoudre ce problÚme. Il s'agit de stocker pour chaque jour une liste distincte des identifiants des séries temporelles rencontrées ce jour-là .
L'avantage de cette solution par rapport à la précédente est que nous ne doublons pas les informations sur les séries temporelles qui ne disparaissent pas avec le temps. Elles sont toujours présentes et ne changent pas.
L'inconvĂ©nient est que cette solution est plus complexe Ă mettre en Ćuvre et plus difficile Ă dĂ©boguer. Et VictoriaMetrics a choisi cette solution. Cela s'est installĂ© historiquement. Cette solution s'est Ă©galement bien comportĂ©e par rapport Ă la prĂ©cĂ©dente. Car cette solution n'a pas Ă©tĂ© mise en Ćuvre Ă cause de la nĂ©cessitĂ© de dupliquer les donnĂ©es dans chaque partition pour les sĂ©ries temporelles qui ne changent pas, c'est-Ă -dire qui ne disparaissent pas avec le temps. VictoriaMetrics a d'abord Ă©tĂ© optimisĂ©e pour la consommation d'espace disque, et l'implĂ©mentation prĂ©cĂ©dente a dĂ©gradĂ© la consommation d'espace disque. Mais cette implĂ©mentation est mieux adaptĂ©e Ă la minimisation de la consommation d'espace disque, c'est pourquoi elle a Ă©tĂ© choisie.
Il a fallu lutter contre ça. La lutte consistait à devoir sélectionner un nombre beaucoup plus important timeseries_ids de données, que lorsque l'index inversé est divisé par période.

Comment avons-nous rĂ©solu ce problĂšme ? Nous l'avons rĂ©solu de maniĂšre originale â en sauvegardant plusieurs identifiants de sĂ©ries temporelles dans chaque enregistrement de l'index inversĂ© au lieu d'un seul identifiant. C'est-Ă -dire que nous avons une clĂ© label=value, qui est prĂ©sent dans chaque sĂ©rie temporelle. Et maintenant, nous conservons plusieurs timeseries_ids dans un seul enregistrement.
Voici un exemple. Avant, nous avions N enregistrements, et maintenant, nous avons un enregistrement dont le prĂ©fixe est le mĂȘme que celui de tous les autres. L'enregistrement prĂ©cĂ©dent contenait toutes les identifiants de sĂ©ries temporelles.
Cela a permis d'augmenter la vitesse de consultation de cet index inversĂ© jusqu'Ă 10 fois. Et a permis de diminuer la consommation de mĂ©moire pour le cache, car maintenant nous stockons la chaĂźne label=value une seule fois dans le cache au lieu de N fois. Et cette chaĂźne peut ĂȘtre grande si vos tags et labels contiennent de longues chaĂźnes que Kubernetes aime y intĂ©grer.

Une autre option pour accĂ©lĂ©rer la recherche dans un index inversĂ© est le sharding. CrĂ©er plusieurs index inversĂ©s au lieu d'un seul et shard les donnĂ©es entre eux par clĂ©. C'est un ensemble clĂ©=valeur de paires. C'est-Ă -dire que nous obtenons plusieurs index inversĂ©s indĂ©pendants que nous pouvons interroger en parallĂšle sur plusieurs processeurs. Les implĂ©mentations prĂ©cĂ©dentes ne permettaient de fonctionner qu'en mode monocĆur, c'est-Ă -dire de scanner les donnĂ©es sur un seul cĆur. Cette solution permet de scanner les donnĂ©es simultanĂ©ment sur plusieurs cĆurs, comme ClickHouse aime le faire. C'est ce que nous prĂ©voyons de mettre en Ćuvre.

Et maintenant, revenons Ă nos moutons â Ă la fonction d'intersection timeseries_ids. Voyons quelles pourraient ĂȘtre les rĂ©alisations. Cette fonction permet de trouver timeseries_ids pour un ensemble donnĂ© label=value.

La premiĂšre option est l'implĂ©mentation naĂŻve. Deux boucles imbriquĂ©es. Nous recevons en entrĂ©e de la fonction intersectInts, deux slices â a et b. En sortie, elle doit nous renvoyer l'intersection de ces slices.
L'implémentation naïve ressemble à cela. Nous passons en revue toutes les valeurs du slice a, à l'intérieur de cette boucle, nous parcourons toutes les valeurs du slice b. Et nous les comparons. S'ils correspondent, cela signifie que nous avons trouvé une intersection. Et nous l'enregistrons dans result.

Quels sont les inconvĂ©nients ? La complexitĂ© quadratique â c'est son principal inconvĂ©nient. Par exemple, si vous avez des tailles de slice a et b d'un million, cette fonction ne vous renverra jamais une rĂ©ponse. Parce qu'il lui faudrait effectuer un trillion d'itĂ©rations, ce qui est Ă©norme mĂȘme pour des ordinateurs modernes.

La deuxiÚme réalisation est basée sur une map. Nous créons une map. Nous y insérons toutes les valeurs du slice a. Ensuite, nous parcourons le slice b. Et nous vérifions s'il y a cette valeur du slice. b dans map. S'il existe, nous l'ajoutons au résultat.

Quels sont les avantages ? L'avantage est qu'il s'agit uniquement d'une complexité linéaire. C'est-à -dire que la fonction s'exécute beaucoup plus rapidement pour de grandes tailles de slices. Pour une taille de slice d'un million, cette fonction s'exécutera en 2 millions d'itérations, contrairement à un trillion d'itérations, comme dans la fonction précédente.
L'inconvénient est que cette fonction nécessite plus de mémoire pour créer cette map.
Le deuxiÚme inconvénient est le grand overhead lors du hachage. Cet inconvénient n'est pas trÚs évident. Et cela n'a pas été trÚs évident pour nous non plus, donc au début, l'implémentation de l'intersection dans VictoriaMetrics était via map. Mais ensuite, le profilage a montré que le temps processeur principal était dépensé à écrire dans la map et à vérifier la présence d'une valeur dans cette map.
Pourquoi le temps processeur est-il dépensé ici ? Parce que dans ces lignes, Go effectue une opération de hachage. C'est-à -dire qu'il calcule le hash de la clé pour ensuite y accéder par l'indice donné dans le HashMap. L'opération de calcul du hash est réalisée en quelques dizaines de nanosecondes. C'est lent pour VictoriaMetrics.

J'ai décidé d'implémenter un bitset, optimisé spécialement pour ce cas. Voici comment se présente maintenant l'intersection de deux slices. Ici, nous créons un bitset. Nous y ajoutons des éléments du premier slice. Ensuite, nous vérifions la présence de ces éléments dans le second slice. Et nous les ajoutons au résultat. C'est-à -dire que cela ne diffÚre presque pas de l'exemple précédent. La seule chose que nous avons remplacée ici, c'est l'accÚs à la map par des fonctions personnalisées. add et has.

Ă premiĂšre vue, cela semble devoir ĂȘtre plus lent, si auparavant une map standard Ă©tait utilisĂ©e et ici des fonctions supplĂ©mentaires sont appelĂ©es, mais le profilage montre que cela fonctionne 10 fois plus vite que la map standard dans le contexte de VictoriaMetrics.
De plus, cela utilise beaucoup moins de mémoire par rapport à l'implémentation de la map. Parce que nous stockons ici des bits au lieu de valeurs de huit octets.
L'inconvénient de cette implémentation est qu'elle n'est pas aussi évidente, pas triviale.
Un autre inconvénient que beaucoup pourraient ne pas remarquer est que cette implémentation peut mal fonctionner dans certains cas. C'est-à -dire qu'elle est optimisée pour un cas particulier, celui de l'intersection des identifiants de séries temporelles de VictoriaMetrics. Cela ne veut pas dire qu'elle convient à tous les cas. Si elle est mal utilisée, nous n'obtiendrons pas d'augmentation des performances, mais une erreur de mémoire insuffisante et un ralentissement de la performance.

Examinons l'implĂ©mentation de cette structure. Si vous souhaitez la consulter, elle se trouve dans les sources de VictoriaMetrics, dans le dossier . Elle est optimisĂ©e pour le cas de VictoriaMetrics, oĂč timeseries_id reprĂ©sente une valeur de 64 bits, oĂč les 32 premiers bits sont principalement constants et seuls les 32 derniers bits changent.
Cette structure de données n'est pas stockée sur le disque, elle fonctionne uniquement en mémoire.

Voici son API. Elle n'est pas trÚs compliquée. L'API est ajustée spécifiquement pour un exemple d'utilisation de VictoriaMetrics. C'est-à -dire qu'il n'y a pas de fonctions superflues. Ici, les fonctions sont celles qui sont explicitement utilisées par VictoriaMetrics.
Il y a des fonctions add, qui ajoutent de nouvelles valeurs. Il y a une fonction has, qui vérifie les nouvelles valeurs. Et il y a une fonction del, qui supprime des valeurs. Il y a aussi une fonction auxiliaire len, qui retourne la taille de l'ensemble. La fonction clone clone l'ensemble. Et la fonction appendto transforme cet ensemble en tranche timeseries_ids.

Voici à quoi ressemble l'implémentation de cette structure de données. Dans l'ensemble, il y a deux éléments :
ItemsCountâ c'est un champ auxiliaire, pour renvoyer rapidement le nombre d'Ă©lĂ©ments dans l'ensemble. On aurait pu se passer de ce champ auxiliaire, mais il a dĂ» ĂȘtre ajoutĂ© ici, car VictoriaMetrics interroge souvent dans ses algorithmes la longueur du bitset.Le deuxiĂšme champ est
buckets. C'est une tranche de la structurebucket32. Chaque structure contienthiun champ. Ce sont les 32 bits supĂ©rieurs. Et deux tranches âb16hisetbucketsdebucket16de structures.
Ici sont stockés les 16 bits supérieurs de la deuxiÚme partie de la structure 64 bits. Et ici sont stockés les bitsets pour les 16 bits inférieurs de chaque octet.
Bucket64 consiste en un tableau uint64. La longueur est calculĂ©e Ă l'aide de ces constantes. Dans un bucket16 maximum peut ĂȘtre stockĂ© 2^16=65536 bits. Si l'on divise cela par 8, cela fait 8 ko. Si l'on divise encore par 8, cela fait 1000 uint64 valeurs. C'est-Ă -dire que Bucket16 est une structure de 8 ko.

Examinons comment l'une des méthodes de cette structure pour ajouter une nouvelle valeur est implémentée.
Tout commence par uint64 valeurs. Nous calculons les 32 bits supérieurs, nous calculons les 32 bits inférieurs. Nous parcourons tous les buckets. Nous comparons les 32 bits supérieurs dans chaque bucket avec la valeur ajoutée. Et si elles correspondent, nous appelons la fonction add dans la structure b32 buckets. Et nous y ajoutons les 32 bits inférieurs. Et si cela a retourné true, cela signifie que nous avons ajouté cette valeur et que nous n'avions pas cette valeur auparavant. Si elle retourne faux, cela signifie que cette valeur était déjà là . Nous augmentons ensuite le nombre d'éléments dans la structure.
Si nous n'avons pas trouvé le bucket avec la valeur hi-essentielle correspondante, nous appelons la fonction addAlloc, qui alloue un nouvel bucket, en l'ajoutant à la structure de bucket.

C'est l'implémentation de la fonction b32.add. Elle ressemble à l'implémentation précédente. Nous calculons les 16 bits supérieurs, les 16 bits inférieurs.
Ensuite, nous parcourons tous les 16 bits supérieurs. Nous trouvons des correspondances. Et en cas de correspondance, nous appelons la méthode add, que nous examinerons à la page suivante pour bucket16.

Et voici le niveau le plus bas, qui doit ĂȘtre maximisĂ© pour l'optimisation. Nous calculons pour uint64 la valeur id dans slice bit, ainsi que bitmask. C'est un masque pour cette valeur 64 bits, avec lequel nous pouvons vĂ©rifier la prĂ©sence de ce bit, ou le dĂ©finir. Nous vĂ©rifions si ce bit est dĂ©jĂ dĂ©fini, nous le dĂ©finissons, et nous retournons sa prĂ©sence. Voici une telle implĂ©mentation que nous avons, qui a permis d'accĂ©lĂ©rer l'opĂ©ration d'intersection d'ids de sĂ©ries temporelles de 10 fois par rapport aux maps ordinaires.

Dans VictoriaMetrics, en plus de cette optimisation, il y a beaucoup d'autres optimisations. La majorité de ces optimisations ont été ajoutées non pas par hasard, mais aprÚs profilage du code en production.
C'est la rÚgle principale de l'optimisation - ne pas ajouter d'optimisation en supposant qu'il y aura un goulet d'étranglement ici, car il peut s'avérer qu'il n'y en a pas. L'optimisation dégrade généralement la qualité du code. Par conséquent, il vaut mieux optimiser seulement aprÚs profilage et idéalement en production, afin que ce soient des données réelles. Pour ceux qui sont intéressés, vous pouvez consulter les sources de VictoriaMetrics et étudier d'autres optimisations qui s'y trouvent.

J'ai une question sur le bitset. Cela ressemble beaucoup à l'implémentation du vector bool en C++, un bitset optimisé. Avez-vous pris l'implémentation de là -bas?
Non, ce n'est pas ça. Lors de la mise en Ćuvre de ce bitset, je me suis basĂ© sur la connaissance de la structure de ces ids timeseries, utilisĂ©s dans VictoriaMetrics. Leur structure est telle que les 32 bits supĂ©rieurs sont principalement constants. Les 32 bits infĂ©rieurs peuvent varier. Plus le bit est bas, plus il peut changer frĂ©quemment. Par consĂ©quent, cette mise en Ćuvre est prĂ©cisĂ©ment optimisĂ©e pour cette structure de donnĂ©es. L'implĂ©mentation C++, autant que je sache, est optimisĂ©e pour le cas gĂ©nĂ©ral. Si l'on fait une optimisation pour le cas gĂ©nĂ©ral, cela signifie qu'elle ne sera pas optimale pour un cas spĂ©cifique.
Je vous conseille Ă©galement de consulter la prĂ©sentation d'Alexey Milovidov. Il a parlĂ© des optimisations dans ClickHouse pour des spĂ©cialisations spĂ©cifiques il y a environ un mois. Il explique justement que dans le cas gĂ©nĂ©ral, l'implĂ©mentation C++ ou toute autre implĂ©mentation est conçue pour bien fonctionner dans l'ensemble. Elle peut fonctionner moins bien qu'une implĂ©mentation spĂ©cialisĂ©e en fonction de connaissances spĂ©cifiques, comme dans notre cas, oĂč nous savons que les 32 bits supĂ©rieurs sont principalement constants.
J'ai une deuxiÚme question. Quelle est la différence fondamentale avec InfluxDB ?
Il y a beaucoup de différences fondamentales. En termes de performance et de consommation de mémoire, InfluxDB montre dans les tests une consommation de mémoire dix fois plus élevée pour des séries temporelles à haute cardinalité, lorsque vous en avez beaucoup, par exemple des millions. Par exemple, VictoriaMetrics consomme 1 Go pour un million de séries actives, tandis qu'InfluxDB consomme 10 Go. Et c'est une grande différence.
La deuxiĂšme diffĂ©rence fondamentale est qu'InfluxDB a des langages de requĂȘte Ă©tranges â Flux et InfluxQL. Ils ne sont pas trĂšs pratiques pour travailler avec des sĂ©ries temporelles par rapport Ă , qui est pris en charge dans VictoriaMetrics. PromQL est le langage de requĂȘtes de Prometheus.
Et une autre diffĂ©rence est que InfluxDB a un modĂšle de donnĂ©es un peu Ă©trange, oĂč chaque ligne peut contenir plusieurs champs avec diffĂ©rentes ensembles d'Ă©tiquettes. Ces lignes sont Ă©galement divisĂ©es en diffĂ©rentes tables. Ces complications supplĂ©mentaires rendent le travail ultĂ©rieur avec cette base plus difficile. Il est difficile de la maintenir et de la comprendre.
Dans VictoriaMetrics, tout est beaucoup plus simple. Chaque sĂ©rie temporelle reprĂ©sente une clĂ©-valeur. La valeur est un ensemble de points â (timestamp, value), et la clĂ© est un ensemble label=value. Il n'y a pas de sĂ©paration entre les fields et les measurements. Cela vous permet de sĂ©lectionner n'importe quelles donnĂ©es, puis de les combiner, d'additionner, de soustraire, de multiplier et de diviser, contrairement Ă InfluxDB, oĂč les calculs entre diffĂ©rentes sĂ©ries ne sont toujours pas rĂ©alisĂ©s, autant que je sache. MĂȘme s'ils l'Ă©taient, c'est compliquĂ©, il faudrait Ă©crire beaucoup de code.
J'ai une question de clarification. Ai-je bien compris qu'il y avait un problÚme dont vous parliez, à savoir que cet index inversé ne tient pas en mémoire, c'est pourquoi le partitionnement a lieu ?
Au début, j'ai présenté une implémentation naïve de l'index inversé utilisant la carte standard de Go. Cette implémentation n'est pas adaptée aux bases de données, car cet index inversé n'est pas enregistré sur le disque, et une base de données doit conserver ces données sur le disque pour qu'elles restent accessibles aprÚs un redémarrage. Dans cette implémentation, aprÚs un redémarrage de l'application, vous perdrez l'index inversé. Et vous perdrez l'accÚs à toutes les données, car vous ne pourrez pas les retrouver.
Bonjour ! Merci pour votre prĂ©sentation ! Je m'appelle Pavel. Je viens de la sociĂ©tĂ© Wildberries. J'ai plusieurs questions Ă vous poser. PremiĂšre question. Que pensez-vous, si vous aviez choisi un autre principe pour construire l'architecture de votre application et partitionnĂ© les donnĂ©es par temps, il est possible que vous ayez pu faire des intersections de donnĂ©es lors des recherches, en vous basant uniquement sur le fait qu'une partition contient des donnĂ©es pour une pĂ©riode de temps donnĂ©e, c'est-Ă -dire pour un seul intervalle de temps et que vous n'auriez pas eu Ă vous soucier des morceaux dispersĂ©s ? Question numĂ©ro 2 - puisque vous implĂ©mentez un algorithme similaire avec un bitset et tout le reste, peut-ĂȘtre que vous avez essayĂ© d'utiliser des instructions du processeur ? Peut-ĂȘtre avez-vous tentĂ© de faire de telles optimisations ?
Je vais répondre au deuxiÚme tout de suite. Nous n'en sommes pas encore là . Mais si besoin, nous y arriverons. Et pour le premier, quelle était la question ?
Vous avez discutĂ© de deux scĂ©narios. Et vous avez dit que vous aviez choisi le second avec une implĂ©mentation plus complexe. Et que vous ne prĂ©fĂ©riez pas le premier, oĂč les donnĂ©es sont partitionnĂ©es par temps.
Oui. Dans le premier cas, le volume total de l'index serait plus Ă©levĂ©, car nous devrions stocker des doublons de donnĂ©es pour les sĂ©ries temporelles qui se poursuivent Ă travers toutes ces partitions. Et si vous avez un taux de dĂ©sabonnement des sĂ©ries temporelles faible, c'est-Ă -dire que les mĂȘmes sĂ©ries sont constamment utilisĂ©es, dans le premier cas, nous perdrions beaucoup plus en termes d'espace disque occupĂ© par rapport au deuxiĂšme cas.
En effet, le partitionnement temporel est une bonne option. Prometheus l'utilise. Mais Prometheus a un autre inconvénient. Lors de la fusion de ces morceaux de données, il doit garder en mémoire les métadonnées de tous les labels et séries temporelles. Donc, si les morceaux de données qu'il fusionne sont volumineux, la consommation de mémoire augmente considérablement, contrairement à VictoriaMetrics. Lors de la fusion, VictoriaMetrics ne consomme pratiquement pas de mémoire, quelques kilooctets sont utilisés, indépendamment de la taille des morceaux de données fusionnés.
L'algorithme que vous utilisez utilise de la mémoire. Il marque les labels des séries temporelles sur lesquels il y a des valeurs. Ainsi, vous vérifiez la présence paire dans un tableau de données et dans un autre. Et vous comprenez s'il y a eu intersection ou non. Dans les bases de données, des curseurs, des itérateurs sont généralement implémentés, qui conservent leur état actuel et parcourent les données triées, permettant ainsi une complexité simple pour ces opérations.
Pourquoi n'utilisons-nous pas de curseurs pour l'intersection des données ?
Oui.
Nous avons dans LevelDB ou dans mergeset des lignes triĂ©es. Nous pouvons passer avec un curseur et trouver l'intersection. Mais pourquoi ne l'utilisons-nous pas ? Parce que c'est lent. Parce que les curseurs impliquent que pour chaque ligne, une fonction doit ĂȘtre appelĂ©e. L'appel de fonction prend 5 nanosecondes. Et si vous avez 100 000 000 de lignes, cela signifie que nous perdons une demi-seconde juste pour l'appel de fonction.
Ăa existe, oui. Et j'ai une derniĂšre question. Peut-ĂȘtre que cette question peut sembler un peu Ă©trange. Pourquoi, au moment de l'arrivĂ©e des donnĂ©es, ne peut-on pas calculer tous les agrĂ©gats nĂ©cessaires et les enregistrer dans le format requis ? Pourquoi stocker de gros volumes dans des systĂšmes comme VictoriaMetrics, ClickHouse, etc., pour ensuite passer beaucoup de temps Ă les traiter ?
Je vais donner un exemple pour que ce soit plus clair. Supposons, comment fonctionne un petit compteur de vitesse en jouet ? Il enregistre la distance que vous avez parcourue, en l'additionnant constamment Ă une valeur, et le temps Ă une autre. Puis il divise. Et obtient la vitesse moyenne. Vous pouvez faire Ă peu prĂšs la mĂȘme chose. Additionner Ă la volĂ©e tous les faits nĂ©cessaires.
Bien, j'ai compris la question. Votre exemple a du sens. Si vous savez quels agrégats vous avez besoin, c'est la meilleure façon de procéder. Mais le problÚme, c'est que les gens conservent ces métriques, certaines données dans ClickHouse, et ils ne savent pas encore comment ils vont les agréger, les filtrer à l'avenir, donc ils doivent garder toutes les données brutes. Mais si vous savez que vous devez calculer une moyenne, pourquoi ne pas le faire au lieu de conserver une multitude de valeurs brutes ? Mais cela n'est valable que si vous savez exactement ce dont vous avez besoin.
Au fait, les bases de donnĂ©es pour le stockage de sĂ©ries temporelles supportent le calcul d'agrĂ©gats. Par exemple, Prometheus supporte . C'est-Ă -dire que cela peut ĂȘtre fait si vous savez quels agrĂ©gats vous aurez besoin. Dans VictoriaMetrics, ce n'est pas encore le cas, mais Prometheus est gĂ©nĂ©ralement placĂ© devant elle, oĂč cela peut ĂȘtre rĂ©alisĂ© dans les rĂšgles d'enregistrement.
Par exemple, dans mon prĂ©cĂ©dent emploi, il fallait compter le nombre d'Ă©vĂ©nements dans une fenĂȘtre glissante sur la derniĂšre heure. Le problĂšme, c'est qu'il a fallu faire une implĂ©mentation personnalisĂ©e en Go, c'est-Ă -dire un service pour compter cela. Ce service s'est finalement rĂ©vĂ©lĂ© non trivial, car c'est difficile Ă compter. L'implĂ©mentation peut ĂȘtre simple si vous devez calculer certains agrĂ©gats Ă des intervalles de temps fixes. Mais si vous voulez compter des Ă©vĂ©nements dans une fenĂȘtre glissante, ce n'est pas aussi simple qu'il n'y paraĂźt. Je pense que cela n'est toujours pas implĂ©mentĂ© dans ClickHouse ou dans les bases de donnĂ©es de sĂ©ries temporelles, car c'est difficile Ă rĂ©aliser.
Et une autre question. Nous avons parlĂ© de moyennage, et je me souviens qu'il y avait autrefois quelque chose comme Graphite avec le backend Carbon. Et il savait rĂ©duire les anciennes donnĂ©es, c'est-Ă -dire laisser un point par minute, un point par heure, etc. En principe, c'est assez pratique si nous avons besoin de donnĂ©es brutes, disons, pour un mois, et tout le reste peut ĂȘtre rĂ©duit. Mais Prometheus et VictoriaMetrics ne prennent pas en charge cette fonctionnalitĂ©. Est-il prĂ©vu de le prendre en charge ? Si non, pourquoi ?
Merci pour votre question. Nos utilisateurs la posent rĂ©guliĂšrement. Ils demandent quand nous ajouterons la prise en charge de l'Ă©chantillonnage (downsampling). Il y a plusieurs problĂšmes. Tout d'abord, chaque utilisateur comprend sous downsampling quelque chose de diffĂ©rent : certains souhaitent obtenir n'importe quel point arbitraire sur un intervalle donnĂ©, d'autres dĂ©sirent des valeurs maximales, minimales ou moyennes. Si plusieurs systĂšmes Ă©crivent des donnĂ©es dans votre base, il est impossible de tout traiter de la mĂȘme maniĂšre. Il se peut que chaque systĂšme nĂ©cessite un Ă©chantillonnage diffĂ©rent. Et cela complique la mise en Ćuvre.
DeuxiĂšmement, VictoriaMetrics, tout comme ClickHouse, est optimisĂ© pour travailler avec de grands volumes de donnĂ©es brutes, donc il peut traiter un milliard de lignes en moins d'une seconde si vous disposez de nombreux cĆurs dans votre systĂšme. Le balayage des points de sĂ©ries temporelles dans VictoriaMetrics atteint 50 000 000 de points par seconde par cĆur. Et cette performance se scale sur les cĆurs disponibles. Par exemple, si vous avez 20 cĆurs, vous pourrez scanner un milliard de points par seconde. Cette caractĂ©ristique de VictoriaMetrics et ClickHouse rĂ©duit le besoin de downsampling.
Une autre caractéristique est que VictoriaMetrics compresse efficacement ces données. La compression est en moyenne de 0,4 à 0,8 octet par point en production. Chaque point consiste en un timestamp + une valeur, et elle est compressée à moins d'un octet en moyenne.
Serguei. J'ai une question. Quelle est la durée minimale d'un quantum d'enregistrement ?
Une milliseconde. Nous avons récemment eu une conversation avec d'autres développeurs de bases de données pour séries temporelles. Leur quantum minimum est d'une seconde. Dans Graphite, par exemple, c'est aussi une seconde. Dans OpenTSDB également, c'est une seconde. Dans InfluxDB, la précision est à la nanoseconde. Dans VictoriaMetrics, c'est une milliseconde, car dans Prometheus, c'est une milliseconde. VictoriaMetrics a initialement été développée comme un stockage à distance pour Prometheus. Mais aujourd'hui, il peut également stocker des données provenant d'autres systÚmes.
La personne Ă qui je parlais dit qu'ils ont une prĂ©cision d'une seconde â cela leur suffit, car cela dĂ©pend du type de donnĂ©es qui sont enregistrĂ©es dans la base de sĂ©ries temporelles. Si ce sont des donnĂ©es DevOps ou des donnĂ©es d'infrastructure que vous collectez Ă des intervalles de 30 secondes ou d'une minute, alors une prĂ©cision d'une seconde est suffisante, moins n'est pas nĂ©cessaire. En revanche, si vous collectez ces donnĂ©es Ă partir de systĂšmes de trading haute frĂ©quence, alors vous avez besoin d'une prĂ©cision Ă la nanoseconde.
La précision millisecondes de VictoriaMetrics convient à la fois pour les cas DevOps et peut convenir à la plupart des cas que j'ai mentionnés au début de ma présentation. La seule chose pour laquelle elle peut ne pas convenir est les systÚmes de trading haute fréquence.
Merci ! Et une autre question. Quelle est la compatibilité avec PromQL ?
Compatibilité complÚte. VictoriaMetrics prend entiÚrement en charge PromQL. De plus, elle ajoute une fonctionnalité supplémentaire avancée à PromQL, appelée . Concernant cette fonctionnalité étendue, il y a une présentation sur YouTube. J'ai parlé lors du Monitoring Meetup au printemps à Saint-Pétersbourg.
Canal Telegram .
Seuls les utilisateurs enregistrés peuvent participer au sondage. , s'il vous plaßt.
Qu'est-ce qui vous empĂȘche de passer Ă VictoriaMetrics comme stockage Ă long terme pour Prometheus ? (Ăcrivez dans les commentaires, je l'ajouterai au sondage))
71,4%Je n'utilise pas Prometheus5
28,6%Je ne savais pas pour VictoriaMetrics2
7 utilisateurs ont voté. 12 utilisateurs se sont abstenus.
Source : habr.com
