Engineering

Versionsverwaltung in Elasticsearch

Eines der Grundprinzipien von Elasticsearch besteht darin, dass Sie Ihre Daten optimal nutzen können. Suchtechnologie war früher auf reine Lesevorgänge beschränkt, bei denen eine Suchmaschine mit Daten aus einer einzigen Quelle gefüttert wurde. Mit zunehmender Nutzung und immer engerer Integration von Elasticsearch in Ihre Anwendung müssen die Daten manchmal von mehreren Komponenten aktualisiert werden. Dies führt zu Parallelität, und Parallelität führt zu Konflikten. Mit der Versionsverwaltung von Elasticsearch können Sie diese Konflikte bewältigen.

Beispiel für die Notwendigkeit einer Versionsverwaltung

Nehmen wir zur Veranschaulichung an, dass wir eine Website haben, deren Nutzer T-Shirt-Designs bewerten. Die Website ist einfach aufgebaut. Sie listet sämtliche Designs auf und die Nutzer können für jedes Design einen Daumen nach oben oder nach unten vergeben. Anschließend zählt die Website die positiven und negativen Bewertungen für jedes T-Shirt.

Ein Eintrag für die Suchmaschine sieht ungefähr so aus:

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 999
}'

Wie Sie sehen, werden pro T-Shirt-Design ein Name und ein Bewertungszähler gespeichert, um den aktuellen Wert zu verwalten.

Der Einfachheit und Skalierbarkeit halber ist die Website komplett zustandslos. Wenn jemand eine Seite öffnet und auf eine Schaltfläche klickt, wird eine AJAX-Anfrage an den Server gesendet, um Elasticsearch anzuweisen, den Zähler zu erhöhen. Eine naive Implementierung könnte beispielsweise die aktuelle Bewertungsanzahl um eins erhöhen und den Wert an Elasticsearch senden:

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 1000
}'

Diese Herangehensweise hat einen gravierenden Nachteil: es könnten Bewertungen verloren gehen. Angenommen, Adam und Eva sehen sich gleichzeitig dieselbe Seite an. In diesem Moment werden auf der Seite 999 Bewertungen angezeigt. Beiden gefällt das Design und sie klicken auf den Daumen nach oben. Daraufhin erhält Elasticsearch zwei identische Kopien der oben gezeigten Aktualisierungsanfrage für das Dokument, die jeweils anstandslos ausgeführt werden. Anstelle der tatsächlichen Bewertungsanzahl von 1.001 werden jetzt also 1.000 Bewertungen angezeigt. Ups.

Natürlich können Sie mit der update-API schlauer vorgehen und angeben, dass die Bewertungsanzahl erhöht werden soll, anstatt einen absoluten Wert anzugeben:

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update' -d'
{
"script" : "ctx._source.votes += 1"
}'

Auf diese Weise ruft Elasticsearch das Dokument zunächst intern ab, führt die Aktualisierung aus und indexiert es dann erneut. Damit ist die Erfolgsquote zwar wesentlich höher, aber das Risiko besteht weiterhin. Während des kurzen Zeitfensters zwischen dem Abrufen und dem erneuten Indexieren des Dokuments kann etwas schiefgehen.

Um dieses und andere, kompliziertere Szenarien zu bewältigen, wird Elasticsearch mit einer integrierten Versionsverwaltung ausgeliefert.

Die Elasticsearch-Versionsverwaltung

Jedes Dokument, das Sie in Elasticsearch speichern, hat eine zugehörige Versionsnummer. Diese Versionsnummer ist eine positive Ganzzahl zwischen 1 und 2 63-1 (inklusive). Wenn Sie ein Dokument zum ersten Mal indexieren, wird ihm die Versionsnummer 1 zugeteilt und in der Antwort von Elasticsearch zurückgegeben. Hier sehen Sie beispielsweise das Ergebnis des ersten curl-Befehls in diesem Blogeintrag:

{
"ok": true,
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 1
}

Für jeden Schreibvorgang an diesem Dokument, egal ob es sich um einen index-, update- oder einen delete-Befehl handelt, erhöht Elasticsearch die Versionsnummer um 1. Diese Erhöhung ist atomisch und findet garantiert statt, wenn die Operation erfolgreich zurückgegeben wurde.

Außerdem gibt Elasticsearch die aktuelle Version von Dokumenten in der Antwort von GET-Operationen (in Echtzeit) zurück und kann angewiesen werden, sie mit jedem Suchergebnis zurückzugeben.

Optimistische Sperren

In unserem einfachen Beispiel brauchen wir eine Lösung für ein Szenario, in dem es vorkommen kann, dass zwei Nutzer dasselbe Dokument gleichzeitig aktualisieren. Früher wurden dafür Sperren verwendet: Vor der Aktualisierung wird eine Sperre erworben, dann wird die Aktualisierung durchgeführt und zuletzt die Sperre freigegeben. Wenn Sie eine Sperre für ein Dokument haben, können Sie sich darauf verlassen, dass niemand das Dokument ändern kann.

Oft bedeutet dies jedoch auch, dass während der Aktualisierung niemand das Dokument lesen kann, bis die Aktualisierung abgeschlossen wurde. Diese Art von Sperre funktioniert, hat jedoch ihren Preis. In Systemen mit hohem Durchsatz hat sie zwei wichtige Nachteile:

  • Oft ist sie einfach nicht nötig. Mit einer guten Implementierung sind Konflikte sehr selten. Natürlich kommen sie vor, aber nur bei einem Bruchteil der im System ausgeführten Operationen.
  • Die Sperre geht davon aus, das Sie sich dafür interessieren. Wenn Sie nur eine Webseite anzeigen wollen, sind leicht veraltete aber einheitliche Werte vermutlich in Ordnung, auch wenn das System weiß, dass sich der Wert gleich ändern wird. Lesevorgänge müssen nicht immer auf den Abschluss laufender Schreibvorgänge warten.

Mit der Elasticsearch-Versionsverwaltung können Sie mühelos ein anderes Schema verwenden, die sogenannten optimistischen Sperren. Anstatt jedes Mal eine Sperre zu erwerben, teilen Sie Elasticsearch mit, welche Version des Dokuments Sie erwarten. Wenn sich das Dokument in der Zwischenzeit nicht geändert hat, wird Ihre Operation ohne jegliche Sperre erfolgreich ausgeführt. Wenn das Dokument in der Zwischenzeit geändert wurde und eine neuere Version existiert, werden Sie von Elasticsearch darauf hingewiesen und können entsprechend reagieren.

In unserem Beispiel mit der Bewertungsseite funktioniert diese Schema wie folgt. Wenn wir eine Seite mit einem T-Shirt-Design anzeigen, notieren wir uns die aktuelle Version des Dokuments. Dieser Wert wird in der Antwort auf die get-Anforderung für die Seite zurückgegeben:

curl -XGET 'http://localhost:9200/designs/shirt/1'
{
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 4,
"exists": true,
"_source": {
"name": "elasticsearch",
"votes": 1002
}
}

Wenn ein Nutzer eine Bewertung abgibt, weisen wir Elasticsearch an, nur den neuen Wert (1.003) zu indexieren, wenn sich in der Zwischenzeit nichts geändert hat: (beachten Sie den zusätzlichen version-Parameter in der Abfragezeichenfolge)

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=4' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

Intern muss Elasticsearch dazu nur die beiden Versionsnummern vergleichen. Dies ist viel ressourcenschonender als eine Sperre abzurufen und freizugeben. Wenn das Dokument nicht geändert wurde, wird die Operation mit dem Statuscode 200 OK erfolgreich ausgeführt. Falls das Dokument jedoch geändert wurde (und sich die interne Versionsnummer dadurch erhöht hat), schlägt die Operation mit dem Statuscode 409 Conflict fehl. Unsere Website kann jetzt entsprechend reagieren. Sie ruft das neue Dokument ab, erhöht die Bewertungsanzahl und wiederholt die Anfrage mit der neuen Versionsnummer. Die Erfolgschance ist dabei sehr hoch. Wenn es nicht klappt, wiederholen wir den Vorgang einfach.

Dieses Muster ist so gängig, dass der update-Endpoint von Elasticsearch es Ihnen abnehmen kann. Mit dem retry_on_conflict-Parameter können Sie angeben, dass die Operation bei einem Versionskonflikt wiederholt werden soll. Dies ist besonders praktisch in Kombination mit skriptgesteuerten Aktualisierungen. Dieser curl-Befehl weist Elasticsearch beispielsweise an, fünf Aktualisierungsversuche zu unternehmen, bevor ein Fehler ausgegeben wird:

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update?retry_on_conflict=5' -d'
{
"script" : "ctx._source.votes += 1"
}'

Beachten Sie, dass die Versionsüberprüfung komplett optional ist. Sie können die Überprüfung beim Aktualisieren bestimmter Felder vornehmen (wie etwa votes) und bei anderen Feldern ignorieren (typischerweise Textfelder, wie etwa name). Entscheidend sind dabei die Anforderungen Ihrer Anwendung und Ihre Kompromissbereitschaft.

Verwenden Sie bereits eine Versionsverwaltung?

Neben dem internen System unterstützt Elasticsearch auch Dokumentversionen aus anderen Systemen. Angenommen, Sie haben Ihr Daten in einer anderen Datenbank gespeichert, die eine eigene Versionsverwaltung verwendet, oder Sie haben eine bestimmte Logik, die das Verhalten Ihrer Versionsverwaltung vorgibt. In diesen Fällen können Sie weiterhin die Elasticsearch-Versionsverwaltung verwenden, indem Sie den external-Versionstyp angeben. Elasticsearch unterstützt beliebige numerische Versionsverwaltungssysteme (im Bereich von 1 bis 263-1), sofern garantiert wird, dass die Nummer mit jeder Änderung am Dokument erhöht wird.

Um Elasticsearch mitzuteilen, dass Sie eine externe Versionsverwaltung verwenden möchten, geben Sie den version_type-Parameter zusammen mit dem version-Parameter in jeder Anforderung an, die etwas an den Daten verändert. Zum Beispiel:

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=526&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

Mit einer externen Versionsverwaltung erfährt Elasticsearch nicht zwangsläufig von allen Änderungen. Dies hat subtile Auswirkungen auf die Implementierung der Versionsverwaltung.

Sehen Sie sich den oben gezeigten Indexierungsbefehl an. Mit der internal-Versionsverwaltung bedeutet er so viel wie: „Indexiere diese Dokumentaktualisierung nur, wenn die aktuelle Version gleich 526 ist.“ Wenn die Version übereinstimmt, erhöht Elasticsearch den Wert um 1 und speichert das Dokument. Mit einer externen Versionsverwaltung können wir diese Anforderung jedoch nicht umsetzen. Möglicherweise erhöht die externe Versionsverwaltung den Wert nicht immer um 1. Möglicherweise wird die Versionsnummer um beliebige andere Werte erhöht (etwa eine zeitbasierte Versionsverwaltung). Vielleicht ist es auch unpraktisch, jede einzelne Versionsänderung an Elasticsearch zu übertragen. Aus all diesen Gründen verhält sich die external-Versionsverwaltung etwas anders.

Wenn Sie version_type auf external festlegen, speichert Elasticsearch die angegebene Versionsnummer, ohne sie zu erhöhen. Anstatt nach einer exakten Übereinstimmung zu suchen, gibt Elasticsearch außerdem nur einen Versionskonfliktfehler zurück, wenn die aktuell gespeicherte Version höher als die Version im Indexierungsbefehl ist. Das bedeutet in der Praxis: „Speichere diese Information nur, wenn in der Zwischenzeit niemand dieselbe oder eine aktuellere Version gespeichert hat.“ Die oben gezeigte Anforderung wird also erfolgreich ausgeführt, wenn die gespeicherte Versionsnummer niedriger als 526 ist. Für 526 und höher schlägt die Anforderung fehl.

Achtung: Achten Sie beim Verwenden der external-Versionsverwaltung immer darauf, die aktuelle version (plus version_type) in allen index-, update- oder delete-Aufrufen anzugeben. Falls Sie den Wert nicht angeben, verarbeitet Elasticsearch die Anforderung mit dem internen System und erhöht die Versionsnummer fälschlicherweise.

Einige abschließende Worte zu Löschvorgängen.

Löschvorgänge sind problematisch für jede Versionsverwaltung. Wenn die Daten nicht mehr existieren, kann das System nicht mehr genau wissen, ob neue Anforderungen veraltet sind oder tatsächlich neue Informationen enthalten. Angenommen, wir löschen einen Datensatz wie folgt:

curl -XDELETE 'http://localhost:9200/designs/shirt/1?version=1000&version_type=external'

Der Löschvorgang bezog sich auf Version 1.000 des Dokuments. Wenn wir einfach alles löschen, was wir darüber wissen, wird der folgende Befehl falsch ausgeführt, wenn er außer der Reihe ankommt:

curl -XPOST 'http://localhost:9200/designs/shirt/1/?version=999&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 3001
}'

Wenn wir vergessen haben, dass das Dokument jemals existiert hat, nehmen wir diesen Aufruf an und erstellen ein neues Dokument. Die Version der Operation (999) verrät uns jedoch, dass es sich um einen alten Aufruf handelt und dass das Dokument weiterhin gelöscht bleiben soll.

Ganz einfach, denken Sie sich vielleicht. Wir löschen einfach nichts, sondern merken uns die Löschvorgänge zusammen mit den entsprechenden Dokumenten-IDs und deren Versionen. Das löst zwar unser Problem, hat aber einen Preis. Wenn die Nutzer immer wieder Dokumente indexieren und anschließend löschen, gehen uns bald die Ressourcen aus.

Elasticsearch verwendet daher einen Mittelweg zwischen den beiden Lösungen. Die Löschvorgänge werden eine Minute lang aufbewahrt und dann vergessen. Diesen Vorgang nennt man auch „Garbage Collection“. In den meisten Fällen reichen 60 Sekunden aus, um das System zu aktualisieren und verzögerte Anfragen zu empfangen. Wenn dies bei Ihnen nicht der Fall ist, können Sie index.gc_deletes in Ihrem Index auf einen anderen Zeitraum festlegen.