Engineering

Mapping-Änderungen ohne Ausfallzeiten

Update November 2, 2015: Make sure to check out the updates with Elasticsearch mappings introduced in the 2.0 release.

Ein befreundeter Entwickler schickte mir folgenden Tweet:

Mein größtes Problem mit Elasticsearch als Modell ist, dass ich bei jeder Schemaänderung neu indexieren muss. Bei der Größe der Datensätze dauert das verdammt lange und so habe ich eine Menge Ausfallzeit. Zu viel für die meisten Anwendungsfälle.

Es gibt recht viele Möglichkeiten, Schema-/Mapping-Änderungen ohne Ausfallzeiten zu realisieren, aber es gibt zu viele Optionen, um sie in einem Tweet zu erklären. Daher dieser Blogeintrag.

Das Problem – warum man Mappings nicht ändern kann

Sie können nur das finden, was in Ihrem Index gespeichert ist. Um Ihre Daten durchsuchbar zu machen, muss Ihre Datenbank wissen, welche Art von Daten das jeweilige Feld enthält und wie es indexiert werden soll. Wenn Sie einen Feldtyp z. B. von String in Datum ändern, werden alle Daten für dieses Feld, die Sie bereits indexiert haben, nutzlos. So oder so müssen Sie dieses Feld neu indexieren.

Das betrifft nicht nur Elasticsearch, sondern alle Datenbanken, die Indizes für die Suche einsetzen. Und wenn sie das nicht tun, opfern sie Geschwindigkeit für Flexibilität.

Elasticsearch (und Lucene) speichert seine Indizes in unveränderlichen Segmenten – jedes Segment ist ein invertierter Index in „Miniformat“. Diese Segmente werden niemals an Ort und Stelle aktualisiert. Durch die Aktualisierung eines Dokuments wird ein neues Dokument erzeugt und das alte als gelöscht markiert. Wenn Sie mehr Dokumente hinzufügen (oder bestehende Dokumente aktualisieren), werden neue Segmente erstellt. Im Hintergrund läuft ein Zusammenführungsprozess, der mehrere kleine Segmente zu einem neuen großen Segment vereint. Danach werden die alten Segmente vollständig entfernt.

Üblicherweise enthält ein Index in Elasticsearch Dokumente verschiedener Typen. Jeder _type hat sein eigenes Schema oder Mapping. Ein einzelnes Segment kann Dokumente jeglichen Typs enthalten. Wenn Sie also die Felddefinition für ein einziges Feld in einem einzigen Typ ändern wollen, bleibt Ihnen nichts anderes übrig, als alle Dokumente in Ihrem Index erneut zu indexieren.

Das Hinzufügen von Feldern hat keine Nachteile

Ein Segment enthält nur Indizes für Felder, die wirklich in den Dokumenten für dieses Segment existieren. Das bedeutet, dass Sie jederzeit neue Felder mit der put_mapping API hinzufügen können, ohne dass eine erneute Indexierung erforderlich ist.

Erneute Indexierung Ihrer Daten

Der Prozess zur erneuten Indexierung Ihrer Daten ist recht einfach. Zuerst erstellen Sie einen neuen Index mit dem neuen Mapping und neuen Einstellungen:

curl -XPUT localhost:9200/new_index -d '
{
    "mappings": {
        "my_type": { ... new mapping definition ...}
    }
}
'

Dann ziehen Sie die Dokument aus Ihrem alten Index mit einer Scrolled Search und indexieren sie in den neuen Index mit der Bulk-API. Viele Client-APIs bieten eine reindex()-Methode, die das für Sie übernimmt. Sobald Sie fertig sind, können Sie den alten Index löschen.

Hinweis: Achten Sie darauf, bei Ihrer Suchanfrage search_type=scan anzugeben. Dadurch wird die Sortierung deaktiviert und „Deep Paging“ effizient.

Das Problem bei diesem Ansatz ist, dass sich der Indexname ändert, sodass Sie in Ihrer Anwendung den neuen Indexnamen einrichten müssen.

Index-Aliasse geben uns die Flexibilität zur erneuten Indexierung von Daten im Hintergrund, sodass die Änderung vollständig transparent für unsere Anwendung erfolgt. Ein Alias ist wie ein symbolischer Link, der auf einen unserer realen Indizes verweisen kann.

Der typische Workflow sieht wie folgt aus. Zuerst erstellen Sie einen Index und hängen eine Version oder einen Zeitstempel an den Namen an:

curl -XPUT localhost:9200/my_index_v1 -d '
{ ... mappings ... }
'

Create an alias which points to the index:

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "add": {
            "alias": "my_index",
            "index": "my_index_v1"
        }}
    ]
}
'

Jetzt kann Ihre Anwendung mit my_index kommunizieren, als wäre es ein realer Index.

Wenn Sie Ihre Daten erneut indexieren müssen, können Sie einen neuen Index erstellen und eine neue Versionsnummer anhängen:

curl -XPUT localhost:9200/my_index_v2 -d '
{ ... mappings ... }
'

Indexieren Sie Ihre Daten von my_index_v1 in den neuen my_index_v2. Ändern Sie dann den my_index Alias, damit er auf den neuen Index verweist. Dies kann atomar in einem Schritt durchgeführt werden:

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "remove": {
            "alias": "my_index",
            "index": "my_index_v1"
        }},
        { "add": {
            "alias": "my_index",
            "index": "my_index_v2"
        }}
    ]
}
'

Und schließlich den alten Index löschen:

curl -XDELETE localhost:9200/my_index_v1

Sie haben erfolgreich all Ihre Daten im Hintergrund erneut indexiert – ganz ohne Ausfallzeiten. Ihre Anwendung weiß nicht einmal, dass der Index geändert wurde.

Das ist zwar die Standard-Vorgehensweise beim Management von Schema-Änderungen, aber Ihnen stehen weitere Optionen zur Verfügung, die ich im Folgenden besprechen werde.

Alte Daten interessieren mich nicht

Was, wenn Sie den Datentyp für ein einziges Feld ändern wollen und es Ihnen nicht wichtig ist, dass die alten Daten nicht durchsuchbar sind? In diesem Fall haben Sie mehrere Möglichkeiten:

Das Mapping löschen

Update November 2, 2015: Please note that delete mappings are not supported in Elasticsearch 2.0+.

Wenn Sie das Mapping für einen bestimmten Typ löschen, können Sie die put_mapping API nutzen, um ein neues Mapping für diesen Typ im bestehenden Index zu erstellen.

Hinweis: Wenn Sie ein Mapping für einen Typ löschen, löschen Sie damit auch alle Dokument dieses Typs im Index.

Das ist besonders nützlich, wenn Sie das Mapping für einen Typ ändern wollen, der nur wenige Dokumente enthält.

Das Feld umbennen

Sie können jederzeit neue Felder hinzufügen, also könnten Sie einfach ein neues Feld mit einem anderen Namen und einer anderen Definition für alle zukünftigen Dokumente hinzufügen. Natürlich würde das die Änderung des Feldnamens in Ihrer Anwendung erfordern.

Upgrade auf ein Multi-Field

Multi-Fields ermöglichen die Verwendung eines einzelnen Feldes für mehrere Zwecke. Ein typischer Anwendungsfall ist die Indexierung, z. B. die eines Titelfeldes, mit zwei Methoden: als ein analyzed String zur Abfrage und als not_analyzed String zur Sortierung.

Alle Skalarfelder (d. h. alle Felder außer Felder des Typs object oder nested) können mit der put_mapping API ohne erneute Indexierung in ein Multi-Field umgewandelt werden. Zum Beispiel haben wir ein Feld namens created, das momentan als String zugeordnet ist:

{
    "created": { "type": "string"}
}

Wir können es in ein Multi-Field umwandeln und ein Unterfeld mit Datum hinzufügen:

curl -XPUT localhost:9200/my_index/my_type/_mapping -d '
{
    "my_type": {
        "properties": {
            "created": {
                "type":   "multi_field",
                "fields": {
                    "created": { "type": "string" },
                    "date":    { "type": "date"   }
                }
            }
        }
    }
}
'

Das ursprüngliche created Feld existiert immer noch als „Haupt“-Unterfeld und kann als created oder created.created abgefragt werden. Die neue Datumsvariante kann als created.date abgefragt werden und wird nur für neue Dokumente befüllt.

Aliasse für mehr Flexibilität benutzen

Manchmal sind die oben genannten Ansätze nicht genug. Vielleicht umfasst Ihre Anwendung 100.000 Benutzerdokumente und 10.000.000 Blog-Dokumente. Sie möchten das Mapping für die Benutzerdokumente ändern, ohne alle Blogs erneut indexieren zu müssen.

Es spricht nichts dagegen, verschiedene Typen in verschiedenen Indizes zu speichern. Elasticsearch kann über mehrere Indizes hinweg genau so einfach wie in einem einzigen Index suchen. So müssen Sie nur den Index erneut indexieren, der den Typ enthält, den Sie ändern möchten. Und mit vernünftigem Einsatz von Aliassen kann der erneute Indexierungsprozess immer noch vollständig transparent für Ihre Anwendung erfolgen.

Mit diesem Ansatz sollte Ihre Anwendung einen separaten Alias pro Typ nutzen. Anstatt zum Beispiel in my_index zu indexieren, würden Sie die Benutzerdokumente in my_index_user und Blog-Dokumente in my_index_blog indexieren:

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "add": {
            "alias": "my_index_user",
            "index": "my_index_v2"
        }},
        { "add": {
            "alias": "my_index_blog",
            "index": "my_index_v2"
        }}
    ]
}
'

Zum Suchen in Benutzer- und Blog-Dokumenten geben Sie einfach beide Aliasse an:

curl localhost:9200/my_index_blog,my_index_user/_search

Wenn Sie das Benutzer-Mapping ändern wollen, erstellen Sie zuerst einen neuen Index nur für Benutzer und wählen die richtige Anzahl von Primary Shards nur für die Benutzerdokumente:

curl -XPUT localhost:9200/my_index_users_v1 -d '
{
    "settings": {
        "index": {
            "number_of_shards": 1
        }
    },
    "mappings": {
        "user": { ... new user mapping ... }
    }
}
'

Sie müssen nur die Benutzerdokumente vom alten in den neuen Index indexieren:

curl 'localhost:9200/my_index_user/user?scroll=1m&search_type=scan' -d '
{
    "size": 1000
}
'

Und den Alias ändern:

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "remove": {
            "alias": "my_index_user",
            "index": "my_index_v2"
        }},
        { "add": {
            "alias": "my_index_user",
            "index": "my_index_user_v1"
        }}
    ]
}
'

Sie können eine Anfrage delete-by-query nutzen, um die Benutzerdokumente aus dem alten Index zu löschen:

curl -XDELETE localhost:9200/my_index_v1/user

Ab jetzt können Sie jederzeit den oben beschriebenen Standard-Neuindexierungsansatz nutzen, wenn Sie das Mapping für Benutzerdokumente ändern wollen.

Aliasse ohne erneute Indexierung nutzen

Wenn Sie Ihre Änderungen nur auf neue Dokumente anwenden wollen, können Sie immer noch die Methode mit Aliassen nutzen, ohne neu indexieren zu müssen. Sie müssten immer noch einen neuen Index namens my_index_user_v1 erstellen, aber jetzt würden Sie zwei Aliasse erstellen: my_index_user zur Indexierung und my_index_users (Plural) zur Suche:

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "add": {
            "alias": "my_index_user",
            "index": "my_index_user_v1"
        }},
        { "add": {
            "alias": "my_index_users",
            "index": "my_index_user_v1"
        }},
        { "add": {
            "alias": "my_index_users",
            "index": "my_index_v1"
        }},
    ]
}
'

Das Alias my_index_user verweist nur auf den neuen Index und alle neuen Benutzerdokumente würden mit diesem Alias indexiert werden. Das Alias my_index_users verweist sowohl auf den neuen als AUCH den alten Index. So können Sie gleichzeitig in beiden Indizes suchen. Der alte Index nutzt das alte Mapping, der neue Index nutzt das neue.

Wie Sie sehen bietet Elasticsearch eine Fülle von Möglichkeiten zur Verwaltung Ihrer Indizes und Änderungen können mit ein bisschen Voraussicht ohne Ausfallzeiten vorgenommen werden.