Mesurer tout "objet" à grande échelle! Une introduction aux séries temporelles avec M3

Mesurer tout "objet" à grande échelle! Une introduction aux séries temporelles avec M3
The Author's Avatar

De nos jours, il est facile de dire que presque tout ce que nous faisons, tout ce que nous utilisons, et même tout ce qui nous entoure est capable de produire de la data. Mais ce qui est d’autant plus vrai, c’est que cette data est produite en temps réel pour décrire quelque chose qui se passe.

Il est donc logique de penser que les données doivent également être exploitées en temps réel pour pouvoir en tirer la plus grande valeur. De plus, et c’est peut-être le plus important, les données doivent être stockées et traitées dans un contexte temporel pour conserver toute leur signification. C’est, en effet, la condition nécessaire pour comprendre pleinement le contexte dans lequel une chose existe ou s’est produite.

Prenons quelques exemples concrets pour lesquels le contexte temporel (c.à.d: le temps) est un élément essentiel pour la signification de vos données :

  • L’enregistrement d’une performance sportive via une montre connectée (vitesse, position, fréquence cardiaque).
  • La mesure des conditions atmosphériques pour établir des prévisions météorologiques (vitesse du vent, température, pression atmosphérique, etc).
  • Le monitoring de l’utilisation des ressources système d’un serveur.
  • Le suivi de la consommation d’énergie d’un foyer.
  • La surveillance des cours des valeurs boursières, etc.

Tous ces exemples ont un point commun : il s’agit de données que nous voulons mesurer dans le temps pour en suivre l’évolution, pour détecter ou prédire des tendances (peut-être en corrélation avec d’autres événements), ou pour alerter lorsque des seuils sont atteints. Nous appelons plus communément ces données des séries temporelles.

L’explosion de l’IoT (Internet des objets), sur ces dernières années, a considérablement accéléré la nécessité de pouvoir stocker et analyser efficacement ces données temporelles, qui représentent le plus souvent des millions de nouvelles mesures produites chaque seconde.

Qu’est-ce qu’une série temporelle et qu’est-ce qu’une base de données temporelles (TSDB) ?

Les séries temporelles sont des séquences de points de données numériques qui sont générées (observées) dans un ordre successif. Chaque point de données représente une mesure (également appelée métrique). En plus de sa valeur, chaque mesure a : un nom, un timestamp et généralement un ou plusieurs labels qui décrivent l’objet réel mesuré.

Pour stocker ces données, nous pourrions parfaitement utiliser une base de données relationnelle (comme PostgreSQL) et créer une simple table SQL comme celle-ci :

CREATE TABLE timeseries (
 metric_name TEXT NOT NULL,
 metric_ts timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
 value double precision NOT NULL,
 labels json,
 PRIMARY KEY(metric_name, metric_ts)       
);

Puis, pour requêter et agréger, par exemple, tous les points stockés sur les dix dernières minutes, nous pourrions utiliser une requête SQL similaire à :

SELECT avg(value) FROM timeseries 
WHERE metric_name = 'heart_rate_bpm' 
AND metric_ts >= NOW() - INTERVAL '10 minutes';

Toutefois, cette solution peut ne pas être réellement efficace pour des applications dites data-intensive ou pour une utilisation à long terme. En effet, tôt ou tard, nous serions probablement limités par :

  • Les possibilités de scalabilité horizontale, que ce soit pour optimiser le stockage à long terme, garantir la résilience des données ou encore pour répondre à des besoins de déploiements multi-régions.

  • La capacité d’insérer massivement des millions de mesures par seconde: La plupart des bases de données relationnelles sont basées sur des structures d’index de type B-TREE.

  • La possibilité d’agréger automatiquement les données dans le temps. Par exemple, pour agréger toutes les mesures du mois précédent en points de 5 minutes.

De plus, il est fort à parier que des hotspots peuvent se créer lors d’insertions massives de certaines mesures. Cela pourrait alors entraîner de mauvaises performances, selon le type d’index utilisé par la base de données, en raison des accès concurrents.

Pour toutes ces raisons, il est généralement préférable d’utiliser des solutions spécialement conçues pour un stockage et un traitement efficace des données temporelles; c’est à dire, une base de données temporelles (Time Series Database).

Vous trouverez ci-dessous quelques-unes des TSDB les plus connues :

Enfin, il existe également d’autres solutions très populaires telles que Prometheus et Graphite qui sont parfois assimilées (peut-être à tort) à des TSBD en raison de leur capacité à stocker des séries temporelles. Mais ce sont en réalité des systèmes de monitoring qui utilisent des caractéristiques proches de celles des TSDB pour le stockage des métriques.

Dans cet article, nous allons introduire une solution plus récente : M3, une plateforme distribuée pour les series temporelles.

M3, plus qu’une base de données, une plateforme distribuée pour les series temporelles.

M3DB, A distributed timeseries database

M3 est une plateforme distribuée pour le traitement de séries temporelles qui a été développée par Uber pour répondre à ses besoins croissants de stockage et d’accès aux billions de mesures que la plateforme génère chaque jour dans le monde entier.

La plateforme M3 est disponible en open-source sous la licence Apache v2.0 depuis 2018 sur GitHub. Elle est entièrement développée en Go et a été conçue pour pouvoir scaler horizontalement afin de prendre en charge à la fois des écritures à haut débit et des requêtes à faible latence.

La plateforme M3 offre des caractéristiques clés qui en font une solution complète et robuste pour le stockage et le traitement de données temporelles :

  • Gestion du clustering : La plateforme M3 s’appuie sur etcd pour gérer l’ensemble des nœuds qui composent un cluster.

  • Réplication : Les séries temporelles sont répliquées sur les nœuds avec une configuration ajustable pour atteindre l’équilibre souhaité entre performance, disponibilité, durabilité et cohérence.

  • Compression: M3 fournit un algorithme de compression efficace inspiré de Gorilla TSZ.

  • Consistance des données configurable : M3 supporte différents niveaux de cohérence pour les requêtes en écriture et en lecture (c.à.d: One, Quorum, All).

  • Gestion des écritures en retard : M3 peut gérer de manière transparente les écritures considérées en retards pendant une période de temps configurable.

  • Intégration avec Prometheus : M3 a un support intégré pour PromQL et peut être utilisé comme un stockage à long terme pour Prometheus.

Toutes ces fonctionnalités sont offertes par les différents composants qui composent la plate-forme M3 : M3 DB, M3 Coordinator, M3 Queries et M3 Aggregator.

Examinons maintenant de plus près ces quatre composantes.

Aperçu des composantes M3

M3 DB

M3DB est la base de données distribuée offrant un stockage durable et évolutif pour les séries temporelles et des indexes inversés.

M3 Coordinator

M3 Coordinator est le service dédié à la coordination des lectures et des écritures dans M3DB depuis des systèmes externes. Par exemple, il peut agir comme une passerelle entre Prometheus (ou d’autres systèmes comme Graphite) et M3DB. Enfin, M3 Coordinator est également utilisé pour configurer les autres composants de la plate-forme.

Utilisation de M3DB comme stockage à long terme de Prometheus

Prometheus est un système de monitoring très populaire qui est rapidement devenu la solution de facto à utiliser pour le monitoring d’infrastructures et d’applications CloudNative (en particulier celles qui fonctionnent dans Kubernetes). Un des principaux avantages de Prometheus est sa facilité d’utilisation et d’opérabilité en production. Cela s’explique, entre autres, par le fait que chaque instance de Prometheus fonctionne indépendamment des autres et ne repose que sur son stockage local pour garantir la durabilité des données.

Mais cette simplicité est aussi la source de ses limites : Prometheus n’a pas été conçu pour être un stockage de données durable à long terme, permettant d’effectuer des requêtes d’analyse sur des données historiques. De plus, il peut-être complexe de faire scaler Prometheus sans l’utilisation d’une solution tierce (exemples: Thanos, Cortex).

Ainsi, M3DB peut être utilisée comme un stockage de données distant, multi-tenant et scalable pour Prometheus.

M3 Queries

M3 Queries est le service M3 dédié à l’exposition des métriques et des métadonnées des séries temporelles stockées dans M3DB. M3 Queries permet l’exécution de requêtes distribuées sur un cluster M3. Enfin, il permet de récupérer aussi bien des métriques en temps réel qu’historiques à des fins d’analyse. Pour ce faire, M3 Queries offre deux moteurs de queries : Prometheus/PromQL (par défaut) et M3 Engine.

Le fait que M3 Queries supporte PromQL par défaut est un énorme avantage. En effet, l’API HTTP est compatible avec le plugin Prometheus de Grafana. De cette façon, il est possible de passer facilement d’un monitoring Prometheus/Grafana à M3, sans avoir à refaire les requêtes de ses dashboards.

M3 Aggregator

Enfin, M3 Aggregator a pour rôle d’agréger les métriques, avant qu’elles ne soient stockées dans M3DB, en suivant les règles stockées dans etcd, à des fins d’échantillonnage.

Le schéma ci-dessous illustre comment M3 peut être utilisé pour fédérer plusieurs instances de Prométhée :

Plate-forme M3 : comme stockage à long terme de Prometheus

Aperçu de l’architecture de M3DB

Le cluster M3DB est composé de deux types de nœuds : StorageNode et SeedNode.

  • Un StorageNode exécute le processus m3dbnode qui stocke les séries temporelles et sert à la fois les requêtes en écriture et en lecture.

  • Un SeedNode est similaire au StorageNode mais exécute également un serveur etcd embarqué pour gérer la coordination du cluster.

Habituellement, pour des déploiements importants, il est préférable d’utiliser un cluster etcd dédié. Dans ce cas, seuls les StorageNode M3DB sont déployés.

Enfin, en plus de ces deux types de nœuds, nous aurons également plusieurs nœuds dédiés aux services: M3 Coordinator et M3 Queries.

Le schéma suivant illustre les différents types de nœuds.

M3 DB Cluster Deployment Overview

Maintenant, voyons de plus près comment fonctionnent un StorageNode et le moteur de stockage de M3 DB.

L’architecture interne d’un nœud est composée de deux parties distinctes : un modèle en mémoire et un stockage persistant.

Modèle mémoire

Premièrement, la mémoire est conçue selon un modèle d’objets hiérarchiques dans lequel: chaque nœud contient une seule Database qui possède un ou plusieurs Namespace. Ensuite, localement à chaque nœud, un Namespace possède de multiples Shards qui à leurs tours possèdent de multiples Series. Enfin, chaque Series possède un Buffer et plusieurs Cached blocks.

Database > Namespaces > Shards > Series > (Buffer, Cached Blockeds)

Stockage Persistant

Deuxièmement, pour mettre en œuvre le stockage persistant, l’instance M3DB utilise d’une part un Commit-Log pour assurer la cohérence des données en cas de défaillance du nœud, et d’autre part, plusieurs fichiers FileSet pour stocker efficacement les séries temporelles, les indexes inversés et les métadonnées.

Le schéma suivant tente d’illustrer ces différents concepts sous une forme concise :

M3DB Architecture Overview: Memory Model & Persistent Storage

Maintenant, décrivons le rôle de chacun de ces éléments.

Namespace

Un namespace a un nom unique et un ensemble distinct d’options de configuration (c.à.d: retention, block-size, etc.).

Shard

Les shards permettent de répartir les séries temporelles de manière homogène sur tous les nœuds. Par défaut, 4096 shards virtuels sont configurés. Les shards sont répliqués et l’affectation d’un shard à un nœud est stockée dans etcd.

Series

Une series est une séquence de points de données. Chaque series est associée à un ID qui est haché à l’aide de l’algorithme murmur3 afin de déterminer le shard cible qui possède la série.

Buffer

Un buffer contient tous les points de données qui n’ont pas encore été écrits sur le disque (c.à.d: les nouvelles écritures) ainsi que certaines données chargées lors du bootstraping du nœud. Un buffer crée également un block pour les nouvelles écritures qui est ensuite flushé sur le disque en fonction de la taille du block configuré.

Block

Un block contient des données temporelles compressées. Un block est mis en cache après une demande de lecture. Enfin, un block a une taille fixe qui est configurée lors de la création du namespace. Par exemple, la taille d’un block peut être exprimée en heures ou en jours (2d).

Commit Log

Un commit-log est une structure dite en append-only (équivalente à un write-ahead-log ou un binary-log d’autres bases de données) dans laquelle chaque point de données est écrit de manière séquentielle et non compressée. Le nœud MD3B exécute périodiquement un processus de snapshotting qui compresse ce fichier. Enfin, le commit-log est principalement utilisé pour garantir la cohérence des données lors d’une reprise après sinistre. Pour garantir la non perte des données, il peut être configuré pour fsync à chaque écriture.

FileSet Files

Les fichiers FileSet sont la principale unité de stockage à long terme de M3DB. Un FileSet est utilisé pour stocker les valeurs des séries temporelles pour un shard/block spécifique de manière compressée.

Un FileSet comprend tous les fichiers suivants :

  • Info file: Stocke l’ensemble des métadonnées concernant le FileSet, telles que le début de la fenêtre temporelle du block et sa taille.

  • Summaries file: Stocke un sous-ensemble du fichier d’index (Index file) afin que le contenu soit gérable en mémoire et pour rechercher, de manière linéaire, le fichier d’index pour une série spécifique.

  • Index file: Stocke les métadonnées de la série afin de retrouver efficacement l’emplacement d’une serie dans un fichier de données.

  • Data file : Stocke les valeurs des séries temporelles de manière compressée.

  • Bloom filter file : Le filtre de Bloom est utilisé dans le read-path de M3DB pour déterminer si une série existe ou non sur le disque.

  • Digests file: Stocke un digest de tous les fichiers présents dans le FileSet pour la vérification de l’intégrité.

  • Checkpoint file: Stocke un digest du Digests file pour permettre de vérifier rapidement la complétude d’un FileSet.

Pour aller plus loin

Pour commencer avec M3, le mieux est de suivre la page “How-to” de la documentation officielle : https://docs.m3db.io/how_to/single_node/

La documentation fait également référence à de multiples vidéos qui présentent M3 et les motivations qui ont conduit à son développement chez Uber : https://docs.m3db.io/overview/media/

Conclusion

M3 est une solution relativement nouvelle qui offre une architecture simple et efficace avec une conception similaire à Apache Cassandra. M3 s’intègre de manière transparente aux solutions de monitoring existantes telles que Prometheus et permet de fournir un stockage de données scalable et durable. Enfin, M3 peut être utilisé aussi bien pour effectuer des requêtes à faible latence sur des données chaudes que pour des requêtes analytiques sur des données historiques.

Cet article est également disponible en anglais sur Medium.

The Author's Avatar
écrit par

Florian est Co-fondateur et CEO de StreamThoughts. Passionné par les systèmes distribués, il se spécialise depuis les technologies d’event-streaming comme Apache Kafka. Aujourd’hui, il accompagne les entreprises dans leur transition vers les architectures orientées streaming d’événements. Florian a été nommé Confluent Community Catalyst pour les années 2019/2021. Il est contributeur sur le projet Apache Kafka Streams et fait partie des organisateurs du Paris Apache Kafka Meetup.