22 décembre 2015 Cas Utilisateur

Elasticsearch comme base de données chronologique

Par Felix Barnsteiner

stagemonitor_logo.png

En tant que chef de projet de stagemonitor, un outil de monitoring des performances en open source, je recherche depuis quelque temps une base de données qui puisse remplacer la base de données chronologique de Graphite (TSDB), plutôt cool mais obsolète, comme backend. Les bases de données chronologiques sont des packs spécialisés permettant de stocker des données métriques (de performance), comme le délai de réponse de votre appli ou l'utilisation du processeur d'un serveur. En fin de compte, nous recherchions un datastore facile à installer, évolutif, capable de prendre en charge une multitude de fonctions et d'afficher les données de façon claire.

Nous avions déjà travaillé avec Elasticsearch et nous savions que ses produits étaient faciles à installer, évolutifs, offraient de nombreuses agrégations et disposaient d'un formidable outil de visualisation : Kibana. Mais nous ne savions pas si Elasticsearch était adapté aux données chronologiques. À ce propos, nous n'étions pas les seuls à nous poser la question. Le CERN (vous savez, les gars qui propulsent des protons) a même réalisé une comparaison des performances entre Elasticsearch, InfluxDB et OpenTSDB et Elasticsearch a été nommé grand vainqueur.

Le processus de décision

Elasticsearch est un outil formidable pour stocker, rechercher et analyser des données, structurées ou non, comme les textes libres, les logs système, les bases de données et plus encore. Avec de bons paramétrages, vous pouvez également bénéficier d'une plateforme pour stocker vos indicateurs chronologiques provenant d'outils comme collectd ou statsd.

Il peut également s'adapter facilement au fur et à mesure de l'ajout d'autres indicateurs. De plus, la redondance est intégrée grâce aux réplications de shards et les sauvegardes sont possibles avec Snapshot & Restore, ce qui facilite grandement la gestion de votre cluster et de vos données.

Elasticsearch est aussi largement basé sur les API, et grâce à des outils d'intégration comme Logstash, il est très facile de concevoir des pipelines de traitement de données capables de prendre en charge de larges volumes de données sans perdre en efficacité. Ajoutez Kibana à l'affaire et vous obtenez une plateforme permettant d'ingérer et d'analyser de multiples ensembles de données et d'effectuer des corrélations entre des indicateurs et d'autres données côte à côte.

Enfin, un autre avantage d'Elasticsearch, pas forcément visible au premier abord : au lieu de stocker des indicateurs transformés par calcul pour fournir une valeur finale qui s'affiche ensuite sur l'outil de visualisation, vous stockez les valeurs brutes puis vous exécutez l'ensemble d'agrégations puissantes intégrées à Elasticsearch sur ces valeurs d'origine. Cela signifie que si vous changez d'avis au bout de quelques mois de monitoring d'un indicateur et que vous souhaitez calculer ou afficher cet indicateur différemment, il vous suffit simplement de modifier l'agrégation sur l'ensemble de données, à la fois pour les données historiques et actuelles. En d'autres termes : vous pouvez à présent vous poser des questions auxquelles vous n'auriez jamais pensé quand les données ont été stockées, et obtenir les réponses !

En sachant tout cela, la question suivante nous trottait en tête : quelle était la meilleure méthode pour configurer Elasticsearch comme base de données chronologique ?

Premier point : les mappings

Il faut tout d'abord commencer par les mappings. En définissant les mappings à l'avance, vous garantissez le bon déroulement de l'analyse et du stockage des données dans Elasticsearch.

Voici un exemple de notre façon de procéder pour les mappings chez stagemonitor. Vous pouvez trouver l'original sur notre référentiel Github :

{
  "template": "stagemonitor-metrics-*",
  "settings": {
    "index": {
      "refresh_interval": "5s"
    }
  },
  "mappings": {
    "_default_": {
      "dynamic_templates": [
        {
          "strings": {
            "match": "*",
            "match_mapping_type": "string",
            "mapping":   { "type": "string",  "doc_values": true, "index": "not_analyzed" }
          }
        }
      ],
      "_all":            { "enabled": false },
      "_source":         { "enabled": false },
      "properties": {
        "@timestamp":    { "type": "date",    "doc_values": true },
        "count":         { "type": "integer", "doc_values": true, "index": "no" },
        "m1_rate":       { "type": "float",   "doc_values": true, "index": "no" },
        "m5_rate":       { "type": "float",   "doc_values": true, "index": "no" },
        "m15_rate":      { "type": "float",   "doc_values": true, "index": "no" },
        "max":           { "type": "integer", "doc_values": true, "index": "no" },
        "mean":          { "type": "integer", "doc_values": true, "index": "no" },
        "mean_rate":     { "type": "float",   "doc_values": true, "index": "no" },
        "median":        { "type": "float",   "doc_values": true, "index": "no" },
        "min":           { "type": "float",   "doc_values": true, "index": "no" },
        "p25":           { "type": "float",   "doc_values": true, "index": "no" },
        "p75":           { "type": "float",   "doc_values": true, "index": "no" },
        "p95":           { "type": "float",   "doc_values": true, "index": "no" },
        "p98":           { "type": "float",   "doc_values": true, "index": "no" },
        "p99":           { "type": "float",   "doc_values": true, "index": "no" },
        "p999":          { "type": "float",   "doc_values": true, "index": "no" },
        "std":           { "type": "float",   "doc_values": true, "index": "no" },
        "value":         { "type": "float",   "doc_values": true, "index": "no" },
        "value_boolean": { "type": "boolean", "doc_values": true, "index": "no" },
        "value_string":  { "type": "string",  "doc_values": true, "index": "no" }
      }
    }
  }
}


Vous voyez ici que nous avons désactivé _source et _all car nous n'allons faire que concevoir des agrégations : ainsi nous allons économiser de l'espace disque car le document stocké sera plus petit. L'inconvénient de cette méthode est que nous ne pourrons pas voir les documents JSON ni réindexer vers un nouveau mapping ou une nouvelle structure d'index (voir la documentation sur la désactivation de la source pour en savoir plus). Pour notre exemple, cela n'est pas un problème.

Répétons-le encore une fois : dans la majorité des cas d'utilisation, il ne faut pas désactiver la source !

Nous n'allons pas non plus analyser la valeur de chaque chaîne, car nous n'allons pas effectuer de recherche sur le texte complet dans les documents des indicateurs. Dans ce cas précis, nous allons seulement filtrer par nom exact ou effectuer des agrégations de termes sur les champs comme metricName, host ou application afin de pouvoir filtrer les indicateurs par certains hôtes ou obtenir la liste de tous les hôtes. Nous vous conseillons aussi d'utiliser doc_values autant que possible pour éviter l'utilisation de la mémoire heap.

Il existe également deux optimisations plus agressives, qui ne conviendront pas à tous les cas d'utilisation. La première est d'utiliser "index": "no" pour toutes les valeurs d'indicateurs. Cela réduit la taille de l'index mais signifie également que nous ne pouvons effectuer de recherche au sein des valeurs. Cela ne pose pas de problème si vous souhaitez uniquement afficher toutes les valeurs sur un graphique et non un sous-ensemble, comme les valeurs situées entre 2,7182 et 3,1415. En utilisant le type de nombre le plus petit (dans notre cas, il s'agissait de Float), nous pouvons optimiser l'index davantage. Si les valeurs de votre cas sont situées en dehors de la fourchette, vous pouvez utiliser le double.

Ensuite : optimisation pour le stockage à long terme

L'étape suivante la plus importante pour optimiser les données pour un stockage à long terme est de forcer l'opération de "merge" (auparavant appelée "optimize") des index après que toutes les données aient été indexées. Cela passe simultanément par la fusion de tous les shards existants en quelques-uns seulement et la suppression de tous les documents supprimés. Ce terme d'« optimisation » peut porter à confusion dans Elasticsearch. Ce processus améliore bien l'utilisation des ressources mais peut mobiliser beaucoup de ressources du processeur et du disque car le système purge tous les documents supprimés puis fusionne les segments Lucene sous-jacents. C'est pourquoi nous conseillons de forcer la fusion pendant les périodes calmes et de l'exécuter sur des nœuds qui possèdent davantage de ressources de processeur et de disque.

Le processus de fusion est exécuté automatiquement en arrière-plan, uniquement lorsque les données sont écrites dans l'index. Il doit être lancé une fois que vous êtes sûr que tous les événements ont été envoyés vers Elasticsearch et que l'index n'est plus en cours de modification par des ajouts, des mises à jour ou des suppressions.

En général, il faut laisser 24 à 48 h après la création du dernier index (qu'elle soit horaire, journalière, hebdomadaire etc.) avant de lancer l'optimisation, pour que les derniers événements aient le temps d'atteindre Elasticsearch. Une fois que cette période est écoulée, nous pouvons facilement utiliser Curator pour s'occuper de l'appel à l'API "optimize" :

$ curator optimize --delay 2 --max_num_segments 1 indices --older-than 1 --time-unit days --timestring %Y.%m.%d --prefix stagemonitor-metrics-


L'exécution de l'optimisation une fois que toutes les données ont été écrites présente un autre avantage : nous pouvons appliquer automatiquement le "flush" synchronisé ce qui accélère le redémarrage des noeuds et donc des clusters.

Si vous utilisez stagemonitor, le processus d'optimisation est déclenché automatiquement toutes les nuits, et vous n'avez pas besoin d'utiliser Curator dans ce cas.

Résultat

Pour tester ce processus, nous avons envoyé un ensemble aléatoire d'un peu plus de 23 millions de points de données depuis notre plateforme vers Elasticsearch, ce qui équivaut environ à une semaine de données. Voici un exemple des données :

{
    "@timestamp": 1442165810,
"name": "timer_1",
    "application": "Metrics Store Benchmark",
    "host": "my_hostname",
    "instance": "Local",
    "count": 21,
    "mean": 714.86,
    "min": 248.00,
    "max": 979.00,
    "stddev": 216.63,
    "p50": 741.00,
    "p75": 925.00,
    "p95": 977.00,
    "p98": 979.00,
    "p99": 979.00,
    "p999": 979.00,
    "mean_rate": 2.03,
    "m1_rate": 2.18,
    "m5_rate": 2.20,
    "m15_rate": 2.20
}


Après quelques cycles d'indexation et d'optimisation, nous avons constaté les chiffres suivants :

  Taille d'Origine Après Optimisation
Echantillon 1 2.2G 508.6M
Echantillon 2 514.1M
Echantillon 3 510.9M
Echantillon 4 510.9M
Echantillon 5 510.9M


Voyez à quel point le processus d'optimisation a été efficace. Même si Elasticsearch fait le travail en arrière-plan, cela vaut le coup de lancer le processus rien que pour le stockage à long terme.

Maintenant que ces données sont dans Elasticsearch, qu'allons-nous découvrir ? Voici quelques exemples de ce que nous avons pu établir avec ce système :

stagemonitor-applications-metrics.png
stagemonitor-hosts-metrics.png
stagemonitor-instances-metrics.png
stagemonitor-response-times.png
stagemonitor-throughputstatuscode-line.png
stagemonitor-toprequests-metric.png
stagemonitor-pageloadtime-line.png
stagemonitor-slowestrequestsmedian-line.png
stagemonitor-highestthroughput-line.png
stagemonitor-slowestrequests-line.png
stagemonitor-mosterrors-line.png

Si vous souhaitez refaire le test que nous avons expliqué ici, vous trouverez le code dans le référentiel de stagemonitor sur Github.

L'avenir

Elasticsearch 2.0 contient de nombreuses fonctionnalités flexibles qui conviendront parfaitement aux utilisateurs de données chronologiques.

Les agrégations de pipelines ouvrent un nouveau champ des possibles pour l'analyse et la transformation de points de données. Par exemple, il est possible de lisser des graphiques à l'aide de moyennes glissantes, d'utiliser une prévision Holt Winters pour vérifier si les données correspondent aux tendances historiques ou même de calculer des dérivées.

Enfin, dans le mapping décrit plus haut, nous avons activé manuellement doc_values pour améliorer l'efficacité de la mémoire heap. Dans Elasticsearch 2.0, les doc_values sont activées par défaut pour tous les champs not_analyzed, ce qui vous simplifie la vie !


felix-barsteiner.jpegFelix Barnsteiner est le développeur du projet en open source de monitoring des performances stagemonitor. Le jour, il travaille sur les solutions e-commerce chez iSYS Software GmbH à Munich en Allemagne.