Dec 22nd, 2020: [DE] Index-Patterns und ihre Tradeoffs für Logs, Metriken und Traces

Zeitserien-Daten, wie Logs, Metriken und Traces, sind einer der Hauptanwendungsfälle des Elastic Stack. Doch wie strukturiert man diese Daten am besten, um neben performantem Schreiben und Lesen noch andere Anforderungen abzudecken, wie beispielsweise Aufbewahrungszeitraum (Retention), Skalierung oder Updates?

Index-Pattern übernehmen diese Aufgabe in Elasticsearch und sie haben sich über die Jahre immer wieder weiterentwickelt. Dieser Artikel zeigt die verschieden Ansätze und stellt die letzten Neuerungen vor. Der Fokus liegt dabei auf Elasticsearch in Kombination mit Kibana und Beats mit Beispielen auf GitHub in xeraa/elastic-stack-index-patterns. Wobei Kibana Index-Pattern ein anderes Konzept sind und hier keine Beachtung finden.

Ein Index zum Start

Ein einzelner Index je Anwendungsfall oder Beat, beispielsweise filebeat oder metricbeat, klingt nach einer intuitiven Lösung, hat aber bei genauerer Betrachtung einige gravierende Nachteile:

  • Wachstum und Skalierung: Auf welche Größe legt man den Index aus? Das Skalierungskonzept in Elasticsearch hinter einem Index sind Shards — ein Index besteht aus einem oder mehreren dieser "Splitter", die auch die Verteilung über mehrere Elasticsearch-Nodes ermöglichen; wobei ein einzelner Shard nicht weiter aufgesplittet gespeichert werden kann. Angenommen man sammelt 10GB an Daten pro Tag ein, für welche Gesamtgröße legt man die Shard-Anzahl aus? Dabei möchte man die Shards zwar nicht zu klein halten, da sie einen Overhead mit sich bringen, aber auch nicht riesig werden lassen, da sie sonst den Cluster aus der Balance bringen. Um die 50GB ist dabei ein gängiger Zielwert, aber den wird man anfangs deutlich unterschreiten und später — speziell wenn das tägliche Datenvolumen wächst — deutlich überschreiten.
  • Aufbewahrungszeitraum: Aus Kostengründen oder teils aus auch aus rechtlichen Beschränkungen ist die Speicherdauer der Daten meist beschränkt. Elasticsearch besitzt zwar ein _delete_by_query mit dem man Daten, die älter als ein gewünschter Zeitpunkt sind, löschen kann, aber das ist eine ausgesprochen teure Operation. Die Unveränderbarkeit der Daten in Apache Lucene — der Library die Elasticsearch in Hintergrund zum Schreiben und Lesen verwendet — erfordert, dass Daten nur als gelöscht markiert werden und erst bei einem Merge entfernt werden, indem alle weiterhin aktiven Daten neu geschrieben werden. Klingt teuer und ist es auch. Deshalb ist von umfassenden und regelmäßigen _delete_by_query dringend abzuraten.
  • Update: Ebenfalls unveränderbar ist das Mapping eines Index — welche Felder existieren mit welchem Datentyp. Sollte man je den Datentyp von einem bestehenden Feld ändern wollen oder statt eines konkreten Wertes ein Unterdokument einfügen wollen, lässt sich das nicht ohne Umwege machen. Beispielsweise ist die Kombination aus { "log": "INFO: my message" } und { "log": { "level": "INFO", "message": "my message" } } in einem Index nicht möglich; zuerst hätte log einen String enthalten und danach ein Unterobjekt.

Durch die Kombination dieser drei Gründe ist ein einzelner Index für Zeitserien-Daten äußerst schlecht geeignet. Was sind mögliche Alternativen?

Tägliche Indizes als (Zwischen-) Lösung

Wenn man jeden Tag einen neuen Index anlegt, beispielsweise filebeat-2020.12.22, kann man einige der Probleme umgehen:

  • Wachstum und Skalierung: Angenommen das tägliche Datenvolumen sind wieder 10GB wie im vorigen Beispiel. Dann würde ein täglicher Index mit einem einzigen Shard relativ gut passen — konfigurierbar über ein Index Template. Das ist zwar eher am unteren Limit der gängigen Empfehlungen für Zeitserien, lässt aber noch etwas Platz für Wachstum. Sollte das tägliche Volumen 50GB dauerhaft überschreiten, kann man das Index Template auf zwei Shards anpassen und ab dem nächsten Tag würde diese Konfiguration verwendet werden. Seit Elasticsearch 7.0 verwendet jeder Index standardmäßig einen Shard, was für die meisten Projekte ein sinnvoller Startwert ist. Davor war der Standard fünf Shards je Index, was bei 10GB an Daten auf jeden Fall zu viel wäre.
  • Aufbewahrungszeitraum: Wenn die maximale Speicherdauer erreicht ist, kann man den ganzen Index löschen. Im Gegensatz zum Entfernen einzelner Dokumente ist das eine äußerst effiziente Operation.
  • Update: Änderungen am Mapping sind möglich. Während man das Mapping über ein Index Template leicht für den nächsten Tag vorbereiten kann, müssten aber alle schreibenden Beats genau zum Tageswechsel auf das neue Format wechseln; wie im vorigen Beispiel von log zu log.level und log.message. Wir kommen auf dieses Problem im nächsten Abschnitt zurück.

Die Beats verwenden standardmäßig diesen Ansatz und schreiben in den Index des aktuellen Tages. Bei der Suche kann man ein Wildcard-Pattern wie filebeat-* verwenden, um alle Daten zu durchsuchen ohne die zugrundeliegende Index-Strategie kennen zu müssen.

Tücken der Versionierung

Um das Problem beim Update zu umgehen, haben die Beats noch eine weitere Variable im Indexnamen, nämlich die Version. Das tatsächliche Pattern in Beats 6.x ist beispielsweise filebeat-%{[beat.version]}-%{+yyyy.MM.dd}. Der konkrete Index wäre dann filebeat-6.7.0-2020.12.22 und wieder über filebeat-* erreichbar. Bei einem Update würde ein neuer Index filebeat-6.8.0-2020.12.22 (mit potentiellen Mapping-Änderungen) erstellt werden und ebenfalls Teil des filebeat-* Wildcard-Patterns werden. Dabei ist die beat.version die Version des Beat. Dass für diesen Tag zwei Indizes erstellt werden, ist dabei ein wohl vertretbarer Tradeoff.

Im Beispielsrepository lässt sich das Standardverhalten ausprobieren: Im 6.8/ Verzeichnis mit docker-compose up starten und nach einigen Augenblicken ist Kibana auf http://localhost:5601 verfügbar. In den Dev Tools sieht man die verfügbaren Indizes mit GET _cat/indices. GET filebeat-*/_search durchsucht sie und mit GET filebeat-6.8.13-2020.12.22/_settings sieht man die Einstellungen, wie beispielsweise fünf Shards.

Gleichmäßige Größenverteilung

So weit so gut, aber was passiert, wenn das tägliche Datenvolumen stark schwankt? Beispielsweise weil an den Wochentagen mehr Benutzer die Infrastruktur verwenden und mehr Logs erzeugen als am Wochenende. Oder weil jemand das Log-Level einer Applikation hochgesetzt hat, um ein Problem zu debuggen, dadurch aber ein Vielfaches der sonst üblichen Logs erzeugt hat. Vielleicht hat auch eine Werbeaktion zu einem Besucheransturm geführt. In all diesen Fällen funktionieren unsere täglichen Index-Pattern nicht wie geplant und erzeugen sehr unterschiedliche Shard-Größen, was wir vermeiden wollen. Und wer weniger als 10GB an Daten pro Tag erzeugt hat, war mit dem täglichen Pattern ebenfalls nie gut aufgestellt.

Um dieses Problem zu lösen, gibt es ein weiteres Konzept in Elasticsearch: Rollover. Bei einem Rollover kann man zu einem neuen Index "hinüberrollen", wenn eine von drei möglichen Bedingungen erfüllt ist:

  • Index-Größe, beispielsweise 50GB.
  • Anzal der Dokumente in einem Index wie 1.000.000.
  • Index-Alter, beispielsweise 4h oder 7d.

Rollover gab es schon länger, aber die Handhabung war relativ komplex. Erst in Kombination mit Index Lifecycle Management (ILM) wurde es deutlich einfacher und seit Elastic Stack 7.0 ist die Verwendung von ILM der neue Standard der Default Beats; die OSS Version verwendet weiterhin filebeat-%{[beat.version]}-%{+yyyy.MM.dd}.

Die ILM-Policy der Beats konfiguriert das Rollover standardmäßig auf 50GB und 30 Tage pro Index mit einem Shard. Auszuprobieren wiederum im 7.10/ Verzeichnis mit docker-compose up. Die ILM-Konfiguration lässt sich entweder mit GET _ilm/policy/filebeat oder im zugehörigen Kibana UI nachvollziehen:

Dabei ist das konfigurierbare Index-Pattern {now/d}-000001, was konkret filebeat-7.10.1-2020.12.22-000001 bedeutet. Erreicht man noch am selben Tag die Rollover-Bedingung, wird ein Index filebeat-7.10.1-2020.12.22-000002 erstellt. Unabhängig vom täglichen Datenvolumen werden gleichmäßige Indizes erstellt, wobei spätestens nach 30 Tagen ein neuer Index erzeugt wird.

Die ILM-Policy setzt dabei auch auf Aliases und insbesondere auf is_write_index. Das Ergebnis von GET _alias/filebeat* ist:

{
  "filebeat-7.10.1-2020.12.22-000001" : {
    "aliases" : {
      "filebeat-7.10.1" : {
        "is_write_index" : true
      }
    }
  }
}

Filebeat in Version 7.10.1 schreibt gegen den filebeat-7.10.1 Alias und ILM "biegt" das durch is_write_index auf filebeat-7.10.1-2020.12.22-000001 um. Das heißt, der Beat muss sich um den konkreten Ziel-Index nicht mehr kümmern, da sich (das von Elasticsearch verwaltete) ILM dessen annimmt.

Außerdem kann ILM Data-Tiers verwalten — bei Elasticsearch in Hot, Warm und Cold unterteilt. Die Annahme ist, dass im Lebenszyklus von Zeitserien unterschiedliche Hardware-Profile zum Einsatz kommen können. Aktiv geschriebene und häufig gelesene Daten auf Hot, ältere und nur mehr unregelmäßig gelesene Daten auf Warm und selten genutzte Daten auf Cold. Abschließend lässt sich mit ILM auch ein Schritt zum Löschen einrichten, wobei die Standard-Policies nur Rollover konfigurieren und die weiteren Schritte für diesen Artikel zu weit gehen würden.

Zusammenfassung

Index-Patterns sind meist im Hintergrund verborgen. Sie haben im Lauf der Zeit aber einen deutlichen Wandel durchlaufen, um optimal zu Zeitserien-Daten zu passen. Ich hoffe, dass der Artikel geholfen hat die Motive und Zusammenhänge besser zu verstehen beziehungsweise als Motivation auf eine aktuelle Version des Elastic Stack dienen kann.

1 Like

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.