04 November 2015 User Stories

Elasticsearch als Speicher für Time Series Data

Von Felix Barnsteiner

Als Projektmanager von stagemonitor, einem Open-Source Performance Monitoring Tool, habe ich kürzlich nach einer Datenbank gesucht, die unsere schöne, aber mit Altersschwächen behaftete Graphite Time Series DataBase (TSDB) im Backend ersetzen kann. TSDBs sind spezialisierte Pakete zum Speichern von (auf Performance bezogenen) Metrikdaten wie der Reaktionszeit Ihrer App oder der CPU-Auslastung eines Servers. Im Prinzip suchten wir nach einem skalierbaren Datenspeicher, der einfach zu installieren ist, zahlreiche Funktionen unterstützt und großartige Visualisierungsmöglichkeiten für die Metriken bietet.

Wir haben bereits mit Elasticsearch gearbeitet. Daher wissen wir, dass die Lösung einfach zu installieren und skalierbar ist, viele Aggregationen bietet und mit Kibana über ein großartiges Visualisierungs-Tool verfügt. Aber wir wussten nicht, ob Elasticsearch für Time Series Data geeignet war. Wir waren nicht die einzigen, die sich diese Frage stellten. Selbst CERN (Sie wissen schon, diese Leute, die Protonen im Kreis herumschießen) hat einen Performance-Vergleich zwischen Elasticsearch, InfluxDB und OpenTDSB durchgeführt und Elasticsearch zum Sieger erklärt.

Der Entscheidungsprozess

Elasticsearch ist ein fantastisches Tool zum Speichern, Suchen und Analysieren strukturierter und unstrukturierter Daten – auch Freitext, System-Logs, Datenbankeinträge und mehr. Mit den richtigen Einstellungen bekommt man eine tolle Plattform zum Speichern der Time Series-Metriken von Tools wie collectd oder statsd.

Außerdem lässt sich das System sehr einfach skalieren, wenn weitere Metriken hinzugefügt werden. Elasticsearch verfügt dank Shard-Replikas über integrierte Redundanz und ermöglicht einfache Backups mit Snapshot & Restore, sodass das Management Ihres Clusters und Ihrer Daten stark vereinfacht wird.

Elasticsearch setzt außerdem in hohem Maße auf APIs, sodass mit Integrations-Tools wie Logstash auf simple Weise Datenverarbeitungs-Pipelines entwickelt werden können, die sehr effizient große Datenmengen bewältigen. Wenn dann auch noch Kibana dazu kommt, haben Sie eine Plattform, mit der Sie mehrere Datensätze aufnehmen und analysieren und so Schlüsse aus Metriken und anderen Daten gleichzeitig ziehen können.

Ein weiterer Vorteil, der nicht sofort ins Auge springt: Anstatt Metriken zu speichern, die bereits zu darstellbaren Werten für ein Diagramm umgewandelt wurden, wird der Rohwert gespeichert – und im Anschluss daran werden die leistungsstarken, in Elasticsearch integrierten Aggregationen darauf aufbauend ausgeführt. Wenn Sie also nach einigen Monaten Überwachung einer Kennzahl Ihre Meinung ändern und die Kennzahl anders berechnen oder darstellen wollen, müssen Sie einfach nur die Aggregation des Datensatzes für die historischen und aktuellen Daten ändern. Anders gesagt: Sie können Fragen stellen und beantworten, über die Sie beim Speichern der Daten noch gar nicht nachgedacht haben!

Unter Berücksichtigung dieser Punkte stellten wir uns die logische Frage: Welche Methode eignet sich am besten zur Einrichtung von Elasticsearch als Time Series Database?

Erster Schritt: Mappings

Das Wichtigste am Anfang ist das Mapping. Wenn Sie Ihr Mapping im Voraus definieren, können Sie die Analyse und Speicherung der Daten in Elasticsearch so optimal wie möglich gestalten.

Hier ist ein Beispiel für unseren Mapping-Ansatz bei stagemonitor. Sie finden das Original in unserem Github-Repo:

{
  "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" }
      }
    }
  }
}

Sie können hier sehen, dass wir _source und _all deaktiviert haben, weil wir sowieso immer nur Aggregationen entwickeln werden und so Speicherplatz sparen können, weil das gespeicherte Dokument damit kleiner wird. Der Nachteil ist, dass wir die eigentlichen JSON-Dokumente nicht sehen und auch nicht auf neue Mappings oder Indexstrukturen reindexieren können (weitere Informationen zur Deaktivierung von _source finden Sie in der Dokumentation), aber für unseren Anwendungsfall mit den Metriken ist das kein Problem.

Noch einmal zur Erinnerung: Für die meisten Anwendungsfälle sollte _source nicht deaktiviert werden!

Außerdem analysieren wir die String-Werte nicht, da wir keine Volltextsuchen in den Metrik-Dokumenten durchführen. In diesem Fall filtern wir nur nach genauen Namen oder führen Begriffsaggregationen auf Feldern wie metricName, host oder application durch, damit wir unsere Metriken nach bestimmten Hosts filtern oder eine Liste aller Hosts aufrufen können. Außerdem ist es besser, doc_values so häufig wie möglich zu nutzen, um die Heap-Nutzung zu reduzieren.

Es gibt noch zwei weitere, ziemlich aggressive Optimierungen, die möglicherweise nicht für alle Anwendungsfälle geeignet sind. Die erste ist die Nutzung von "index": "no" für alle Metriken. Dadurch wird die Indexgröße reduziert, gleichzeitig können wir aber auch die Werte nicht mehr durchsuchen. Das ist ok so, wenn wir in einem Diagramm alle Werte darstellen wollen und nicht nur eine Teilmenge wie die Werte zwischen 2,7182 und 3,1415. Durch die Verwendung des kleinsten Zahlentyps (für uns war es float) können wir den Index weiter optimieren. Wenn Ihr Anwendungsfall Werte außerhalb des Bereichs eines float erfordert, können Sie doubles nutzen.

Weiter geht's: Optimierung für langfristiges Speichern

Der nächste wichtige Schritt für die Optimierung dieser Daten für das langfristige Speichern ist Force Merge (zuvor optimize genannt) der Indizes nach der Indexierung der gesamten Daten. Das umfasst die Zusammenführung der bestehenden Shards zu einigen wenigen und die Entfernung jeglicher gelöschter Dokumente im gleichen Schritt. Die alte Bezeichnung „optimize“ ist ein wenig irreführend als Begriff in Elasticsearch: Der Prozess verbessert zwar die Ressourcennutzung, kann aber einen Großteil der CPU- und Festplattenressourcen in Anspruch nehmen, weil das System alle gelöschten Dokumente bereinigt und dann alle zugrundeliegenden Lucene-Segmente zusammenführt. Deshalb empfehlen wir Force Merge außerhalb von Stoßzeiten oder auf Nodes mit mehr CPU- und Festplattenressourcen durchzuführen.

Der Merge-Prozess erfolgt automatisch im Hintergrund, aber nur während Daten in den Index geschrieben werden. Wir möchten ihn explizit aufrufen, sobald wir sicher sind, dass alle Ereignisse an Elasticsearch gesendet wurden und der Index nicht mehr durch Hinzufügungen, Aktualisierungen oder Löschungen bearbeitet wird.

Ein optimize-Prozess sollte am besten erst 24 bis 48 Stunden nach Erstellung des neuesten Index durchgeführt werden (egal ob stündlich, täglich, wöchentlich etc.), damit jegliche verspätete Events Elasticsearch erreichen können. Danach können wir ganz einfach Curator zur Handhabung des optimize-Aufrufs verwenden:

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

Ein weiterer großer Vorteil bei der Ausführung dieses optimize-Prozesses nach dem Schreiben sämtlicher Daten ist, dass wir automatisch synced flush anwenden, was uns bei der Wiederherstellungsgeschwindigkeit von Clustern und Neustarts von Nodes hilft.

Wenn Sie stagemonitor benutzen, wird der optimize-Prozess automatisch jede Nacht ausgelöst, sodass Sie in diesem Fall nicht auf Curator angewiesen sind.

Das Ergebnis

Für einen Test dieses Verfahrens haben wir einen randomisierten Satz von knapp 23 Millionen Datenpunkte von unserer Plattform an Elasticsearch gesendet. Das entspricht etwa dem Aufkommen einer Woche. Unten sehen Sie Proben der Daten:

{
    "@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
}

Nach einigen Indexierungs- und Optimierungszyklen hatten wir folgende Zahlen:

  Ursprungsgröße Nach der Optimierung
Probe 1 2.2G 508.6M
Probe 2 514.1M
Probe 3 510.9M
Probe 4 510.9M
Probe 5 510.9M

Sie sehen, wie wichtig der optimize-Prozess war. Obwohl Elasticsearch diese Aufgabe im Hintergrund ausführt, lohnt sich auch eine eigene Ausführung für langfristige Speicher allein.

Welche Schlüsse können wir nun aus all diesen Daten in Elasticsearch ziehen? Hier sind einige Beispiele unserer Errungenschaften mit dem System:

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

Wenn Sie die von uns hier durchgeführten Tests bei sich wiederholen möchten, finden Sie den Code im stagemonitor Github-Repo.

Die Zukunft

Mit Elasticsearch 2.0 kommen viele Funktionen, die das System noch flexibler und besser geeignet für Nutzer von Time Series Data machen.

Pipeline-Aggregationen oeröffnen eine ganz neue Welt der Analyse und Umwandlung für Datenpunkte. Es ist zum Beispiel möglich, Diagramme mit gleitenden Mittelwertenzu glätten, mit einer Holt-Winters-Vorhersage zu prüfen, ob die Daten mit historischen Mustern übereinstimmen, oder sogar Ableitungen zu berechnen.

Wie Sie sich vielleicht erinnern, mussten wir im oben beschriebenen Mapping doc_values zur Heap-Effizienzverbesserung manuell aktivieren. In 2.0 sind doc_values standardmäßig für jedes not_analyzed-Feld aktiviert, was weniger Arbeit für Sie bedeutet!

Über den Autor – Felix Barnsteiner

felix-barsteiner.jpegFelix Barnsteiner ist der Entwickler des Open-Source Performance Monitoring Projekts stagemonitor. Tagsüber arbeitet er bei der iSYS Software GmbH in München an eCommerce-Lösungen.