Engineering

Duplikate ereignisbasierter Daten in Elasticsearch effizient verhindern

Der Elastic Stack wird für viele unterschiedliche Anwendungsfälle genutzt. Mit am häufigsten ist die Speicherung und Analyse unterschiedlicher Ereignistypen oder Daten auf Basis von Zeitreihen, wie Sicherheitsereignissen, Logs und Metriken. Solche Ereignisse bestehen oft aus Daten, die mit einem bestimmten Zeitstempel verknüpft sind. Dieser gibt an, wann das Ereignis aufgetreten ist oder erfasst wurde, wobei es häufig keinen natürlichen Schlüssel gibt, um das Ereignis eindeutig zu identifizieren.

In einigen Anwendungsfällen und vielleicht auch bei einigen Datentypen innerhalb eines Anwendungsfalls kann es wichtig sein, dass die Daten in Elasticsearch nicht dupliziert werden. Doppelte Dokumente können zu falschen Analysen und Suchfehlern führen. Im Blogpost Little Logstash Lessons: Handling Duplicates begannen wir dies bereits im letzten Jahr anzuschauen. Heute gehen wir ein bisschen mehr ins Detail und beantworten oft gestellte Fragen.

In Elasticsearch indexieren

Wenn Sie Daten in Elasticsearch indexieren, sollten Sie auf die Antwort warten, um sicher zu sein, dass die Daten erfolgreich indexiert wurden. Falls ein Fehler auftrat, z. B. ein Verbindungsfehler oder ein Knoten-Absturz, können Sie nicht sicher sein, ob tatsächlich Daten indexiert wurden. In einem solchen Fall wird in der Regel einfach ein erneuter Versuch gestartet. Dadurch könnte dasselbe Dokument jedoch mehrmals indexiert werden.

Im Blogpost über die Behandlung von Duplikaten hatten wir beschrieben, dass dies vermieden werden kann, indem für jedes Dokument im Client eine eindeutige ID definiert wird, anstatt Elasticsearch die ID automatisch während des Indexierens zuweisen zu lassen. Sobald ein doppeltes Dokument demselben Index hinzugefügt wird, führt dies dann zu einer Aktualisierung. Das Dokument wird also nicht ein zweites Mal geschrieben, wodurch Duplikate vermieden werden.

UUIDs im Vergleich mit hashbasierten IDs

Wenn Sie sich überlegen, welche Kennungsart verwendet werden sollte, stehen Ihnen zwei Hauptarten zur Verfügung.

Universally Unique Identifiers (UUIDs) sind Kennungen auf Basis von 128-Bit Zahlen, die über verteilte Systeme hinweg erstellt werden können und aus praktischer Sicht trotzdem eindeutig sind. Diese Kennungsart hängt im Allgemeinen nicht von den Inhalten des Ereignisses ab, mit dem sie verknüpft ist.

Um UUIDs zur Vermeidung von Duplikaten zu verwenden, ist es wichtig, dass die UUID erstellt und mit dem Ereignis verknüpft wird, bevor das Ereignis eine Grenze passiert, die sicherstellt, dass das Ereignis genau einmal zugestellt wird. In der Praxis bedeutet dies meist, dass die UUID gleich zu Beginn mit dem Ereignis verknüpft werden muss. Falls das System, aus dem das Ereignis stammt, keine UUID erstellen kann, muss eine unterschiedliche Kennungsart verwendet werden.

Die anderen wichtigsten Kennungsarten verwenden eine Hashfunktion, um einen numerischen Hashwert auf Basis der Inhalte des Ereignisses zu erstellen. Die Hashfunktion wird für einen bestimmten Inhalt stets denselben Wert liefern. Dieser erstellte Wert ist jedoch nicht unbedingt eindeutig. Eine Hashkollision tritt auf, wenn zwei unterschiedliche Ereignisse zum selben Hashwert führen. Die Wahrscheinlichkeit einer Hashkollision hängt von der Anzahl der Ereignisse im Index und auch von der Art der verwendeten Hashfunktion sowie der Länge des von ihr erstellten Werts ab. Ein Hash mit einer Mindestlänge von 128 Bit, also z. B. MD5 oder SHA1, ist in vielen Fällen ein guter Kompromiss zwischen Länge und niedriger Kollisionswahrscheinlichkeit. Wenn die Eindeutigkeit noch stärker gewährleistet werden soll, kann ein noch längerer Hash wie SHA256 verwendet werden.

Weil eine hashbasierte Kennung von dem Inhalt des Ereignisses abhängt, ist es möglich, sie in einer späteren Verarbeitungsphase zuzuweisen, denn es wird stets derselbe Wert berechnet. Dadurch können Sie diese ID-Arten zu einem beliebigen Zeitpunkt vor dem Indexieren der Daten in Elasticsearch zuweisen, was einen flexiblen Entwurf einer Ingest-Pipeline erlaubt.

Logstash unterstützt die Berechnung von UUIDs sowie eine Reihe beliebter und häufig verwendeter Hashfunktionen über das Plugin Fingerprint Filter.

Effiziente Dokument-ID wählen

Falls Elasticsearch die Dokumentkennung während des Indexierens zuweisen darf, kann es Optimierungen durchführen, weil es weiß, dass die erstellte Kennung im Index noch nicht existieren kann. Dadurch verbessert sich die Performance des Indexierens. Bei Kennungen, die extern erstellt wurden und zusammen mit dem Dokument übergeben werden, muss Elasticsearch diesen Vorgang wie eine potenzielle Aktualisierung behandeln. Es überprüft also, ob die Dokumentkennung in bestehenden Indexsegmenten bereits existiert, was zusätzliche Arbeit erfordert und daher langsamer ist.

Nicht alle externen Dokumentkennungen sind gleichwertig. Kennungen, die im Laufe der Zeit auf Basis der Sortierreihenfolge allmählich steigen, führen zu einer besseren Indexierungsperformance als rein zufällige Kennungen. Der Grund dafür liegt daran, dass Elasticsearch in diesem Fall schnell feststellen kann, ob eine Kennung in älteren Indexsegmenten existiert, indem es einfach die kleinste und größte Kennung in diesem Segment betrachtet, statt nach der genauen Kennung suchen zu müssen. In diesem Blogpost wird das beschrieben. Der Artikel ist schon etwas älter, aber noch immer relevant.

Hashbasierte Kennungen und viele Arten von UUIDs sind im Allgemeinen zufällig. Bei einer Abfolge von Ereignissen, die jeweils einen definierten Zeitstempel aufweisen, können wir diesen Zeitstempel als Präfix für die Kennung verwenden. Dadurch ist es möglich, die Kennungen zu sortieren, und die Indexierungsperformance verbessert sich.

Durch das Erstellen einer Kennung mit einem Zeitstempel als Präfix wird auch die Wahrscheinlichkeit einer Hashkollision verringert, weil der Hashwert dann nur pro Zeitstempel eindeutig sein muss. Selbst beim Erfassen eines sehr hohen Volumens können auf diese Weise kürzere Hashwerte verwendet werden.

Diese Kennungsarten können wir in Logstash mit dem Plugin Fingerprint Filter erstellen. Dabei generieren wir mit dem Filter eine UUID bzw. einen Hashwert und einen Ruby-Filter, um den Zeitstempel als Zeichenfolge in Hexadezimalcodierung zu erstellen. Wenn wir davon ausgehen, dass uns ein Feld namens Nachricht vorliegt, für das wir einen Hashwert berechnen können, und der Zeitstempel des Ereignisses bereits im Feld @timestamp geparst wurde, können wir die Bestandteile der Kennung wie folgt erstellen und in den Metadaten speichern:

fingerprint {
  source => "message"
  target => "[@metadata][fingerprint]"
  method => "MD5"
  key => "test"
}
ruby {
  code => "event.set('@metadata[tsprefix]', event.get('@timestamp').to_i.to_s(16))"
}

Diese beiden Felder können dann verwendet werden, um eine Dokumentkennung im Output-Plugin von Elasticsearch zu generieren:

elasticsearch {
  document_id => "%{[@metadata][tsprefix]}%{[@metadata][fingerprint]}"
}

Hieraus ergibt sich eine Dokumentkennung in Hexadezimalcodierung mit einer Länge von 40 Zeichen, z. B. 4dad050215ca59aa1e3a26a222a9bbcaced23039. Ein vollständiges Konfigurationsbeispiel finden Sie in dieser Zusammenfassung.

Performanceauswirkungen beim Indexieren

Wie sich unterschiedliche Kennungsarten auswirken, hängt stark von Ihren Daten, der Hardware und den Anwendungsfällen ab. Wir können zwar einige allgemeine Hinweise geben, Sie sollten jedoch Benchmarks ausführen, um die genaue Auswirkung auf Ihren Anwendungsfall zu ermitteln.

Für einen optimalen Durchsatz beim Indexieren ist es stets am effizientesten, die von Elasticsearch automatisch generierten Kennungen zu verwenden. Weil dann nicht auf Aktualisierungen überprüft werden muss, ändert sich die Indexierungsperformance kaum, wenn die Größe der Indizes und Shards zunimmt. Es wird daher empfohlen, diese Option wenn immer möglich zu verwenden.

Die Aktualisierungsüberprüfungen aufgrund der Verwendung externer IDs benötigen zusätzliche Plattenzugriffe. Deren Auswirkung hängt davon ab, wie effizient die benötigten Daten durch das Betriebssystem zwischengespeichert werden können, wie schnell der Speicher ist und wie gut er mit zufälligen Leseoperationen umgehen kann. Das Indexieren wird auch oft langsamer, wenn Indizes und Shards größer werden und immer mehr Segmente überprüft werden müssen.

Rollover API verwenden

Bei traditionellen zeitbasierten Indizes deckt jeder Index einen bestimmten vordefinierten Zeitraum ab. Index- und Shard-Größen können sich daher sehr unterscheiden, wenn das Datenvolumen im Zeitablauf schwankt. Uneinheitliche Shard-Größen sind nicht wünschenswert und können Performanceprobleme verursachen.

Die Rollover Index API wurde erstellt, um zeitbasierte Indizes flexibel mit mehreren Kriterien (nicht nur der Zeit) zu verwalten. Sie ermöglicht den Übergang auf einen neuen Index, sobald der bestehende Index eine bestimmte Größe, Dokumentanzahl und/oder ein gewisses Alter erreicht hat. Dies führt zu wesentlich besser berechenbaren Shard- und Indexgrößen.

Dadurch wird jedoch die Verbindung zwischen dem Zeitstempel des Ereignisses und dem zugehörigen Index unterbrochen. Solange Indizes lediglich auf Zeit basieren, landet ein Ereignis stets im selben Index, unabhängig davon, wie spät es sich innerhalb des Zeitfensters ereignete. Durch dieses Prinzip ist es möglich, Duplikate mit externen Kennungen zu verhindern. Wenn die Rollover API genutzt wird, ist eine vollständige Verhinderung von Duplikaten nicht mehr möglich, wenn auch ihre Wahrscheinlichkeit verringert wird. Es ist dann möglich, dass zwei doppelte Ereignisse auf beiden Seiten eines Übergangs eintreten. Sie landen in unterschiedlichen Indizes, obwohl sie denselben Zeitstempel aufweisen, was in diesem Fall zu keiner Aktualisierung führt.

Falls die Verhinderung von Duplikaten eine strikte Voraussetzung ist, wird die Verwendung der Rollover API daher nicht empfohlen.

Auf unvorhersehbaren Traffic anpassen

Auch wenn die Rollover API nicht genutzt werden kann, gibt es trotzdem Möglichkeiten, die Shard-Größe anzupassen, falls der Traffic schwankt und zu große bzw. zu kleine zeitbasierte Indizes verursacht.

Wenn die Shards zu groß wurden, beispielsweise durch einen Traffic-Zuwachs, können Sie die Split Index API verwenden, um den Index in eine größere Anzahl von Shards aufzuteilen. Bei dieser API muss bei der Indexerstellung eine Einstellung vorgenommen werden, sie muss also über eine Indexvorlage hinzugefügt werden.

Falls der Traffic andererseits zu niedrig war und die Shards ungewöhnlich klein sind, können Sie die Shrink Index API nutzen, um die Anzahl der Shards im Index zu verringern.

Fazit

In diesem Blogpost wurde erläutert, dass Sie Duplikate in Elasticsearch verhindern können, indem Sie eine externe Dokumentkennung vor dem Indexieren von Daten in Elasticsearch festlegen. Die Art und die Struktur der Kennung kann sich beträchtlich auf die Indexierungsperformance auswirken. In jedem Anwendungsfall ist die Auswirkung jedoch unterschiedlich. Deshalb wird empfohlen, Benchmarks durchzuführen. So finden Sie heraus, was in Ihrem Fall und Ihrem bestimmten Szenario optimal ist.