Migration.py mit echter Index-Migration bei Mapping-Änderungen

Das Migration.py Script konnte nur Indexe hinzufügen. Nun bauen wir eine echte Migration ein, die das Ändern von Index-Mappings erlaubt.

Einleitung

Wie schon im Blog-Artikel über Index-Aliase erwähnt, gibt es die Einschränkung in OpenSearch, dass man einen Index in seiner Mapping-Konfiguration kaum verändern kann. Tatsächlich kann man nur Mappings hinzufügen. Man ist also gezwungen, einen ganz neuen Index anzulegen und die Dokumente aus dem alten Index in den neuen Index zu kopieren. Der neue Index hat aber zwingend einen anderen Namen. Also benötigen wir immer einen Alias, damit nicht alle Clients ständig den Sourcecode ändern müssen, wenn wir nur was am Mapping ändern.

Es gibt also ein paar Schritte durchzuführen:

  • Neuen Index mit neuem Namen anlegen (da nehmen wir einfach die Version zu dem Namen)
  • Dokumente vom alten Index in den neuen Index kopieren
  • Den neuen Index mit einem eindeutigen Alias versehen, damit alle Clients immer den gleichen Namen verwenden können.

Beispiel:

  • toots_v1_001 anlegen
  • Dokumente vom alten toots Index nach toots_v1_001 kopieren
  • Alias toots für toots_v1_001 setzen und gleichzeitig vom alten Index löschen (wenn er dort war)

Sieht simpel aus. Aber man bekommt gleich Bauchschmerzen. Wir haben ja schon den Index toots der dann ja so heißt wie unser Alias.

Ok, damit haben wir den Sonderfall, wenn wir nicht-versionierte Indexe haben.

Dafür müssen wir wie folgt arbeiten:

  • Überprüfen, ob wir nur einen unversionierten toots Index ohne Alias haben. Wenn ja:
    • toots_v1_001 anlegen
    • Dokumente vom alten toots Index nach toots_v1_001 kopieren
    • Index toots löschen (ja, sorry - das ist hart)
    • Alias toots für toots_v1_001 setzen

Aber irgendjemand könnte auch von 0 anfangen. Es gibt keinen Index oder Alias. Dieser Fall ist am einfachsten:

  • Neuen Index toots_v1_001 erstellen
  • Alias toots auf toots_v1_001 anlegen

Das deckt unsere Fälle ab, die uns begegnen könnten.

Das Grundprinzip der Versionierung mit Alias-Namen habe ich mal visualisiert:

versioning

Migration.py anpassen

Wir müssen zwei neue Funktionen anlegen und _run_create_index anpassen.

Copy documents

OpenSearch bietet eine ReIndex API an, damit man Dokumente von einem Index in einen neuen Index kopieren und neu indizieren kann.

Die Python Methode sieht so aus:

def copy_documents(client: OpenSearch, index_from: str, index_to: str):
    reindex = {
        "source": {
            "index": index_from
        },
        "dest": {
            "index": index_to,
            "op_type": "create"
        }
    }
    response = client.reindex(body=reindex, requests_per_second=10_000, refresh=True)

    if response['failures']:
        raise RuntimeError(f"Failure on copying document from  {index_from} to {index_to}")
    return response

Da der Zielindex immer leer ist, reicht ein create als operation type. Die restlichen Parameter von reindex sind einmal ein Throttling (um nach 10.000 inserts etwas Pause zu haben) und der obligatorische Refresh.

Wenn Fehler auftreten, müssen wir abbrechen.

Switch Version

Die Index-Alias API bietet einen komfortablen Weg in einem Rutsch ein Alias von anderen Indexen wegzunehmen und ihn dann einem bestimmten Index zuzuweisen.

In Python sieht das so aus:

def switch_version(client: OpenSearch, index_alias: str, version):
    index_to = index_alias + "_v" + str(version).replace('.', '_')

    # Move or creates the alias. The remove works also in cases of non-existing alias.
    move_alias = {
        "actions": [
            {
                "remove": {
                    "index": "*",
                    "alias": index_alias
                }
            },
            {
                "add": {
                    "index": index_to,
                    "alias": index_alias
                }
            }
        ]
    }
    return client.indices.update_aliases(body=move_alias)

Egal wo der Index-Alias hinzugefügt wurde, wir löschen ihn überall. Zudem soll der Alias auf den neuen Index hinzugefügt werden.

_run_create_index anpassen

Leider haben wir nicht mehr eine Zeile, sondern nun etwas mehr. Wir haben nun drei Fälle zu prüfen:

  1. Wir haben echte Indexe mit Namen wie toots, following, usw. Alle ohne Version im Namen. Kein Alias vergeben
  2. Wir haben überhaupt keinen Index und fangen frisch von der grünen Wiese an
  3. Wir haben einen Migrationsschritt von Version a zu Version b. D.h. es gibt einen versionierten Index und einen Alias dazu.

So sieht das in python aus:

def _run_create_index(client: OpenSearch, runner):
    index_alias = runner['index_name']
    version = runner['__self']['version']
    index_to = index_alias + "_v" + str(version).replace('.', '_')

    index_or_alias_exists: bool = client.indices.exists(index=index_alias, allow_no_indices=False)
    alias_exists: bool = client.indices.exists_alias(name=index_alias)

    if index_or_alias_exists and not alias_exists:
        # Case 1
        response = client.indices.create(index_to, body=runner['body'])
        # Now we cannot switch the version, because the old index with the name equal to alias exists
        # We must copy first the documents
        copy_documents(client, index_from=index_alias, index_to=index_to)
        # No failures, so we can remove the old index (yes, it's hard)
        client.indices.delete(index=index_alias)
        # Now we can switch the version (this means, we create an alias):
        switch_version(client, index_alias=index_alias, version=version)
    elif not index_or_alias_exists and not alias_exists:
        # Case 2
        response = client.indices.create(index_to, body=runner['body'])
        switch_version(client, index_alias=index_alias, version=version)
    elif index_or_alias_exists and alias_exists:
        # Case 3
        # We need the index behind the alias name to migrate the documents
        alias_response = client.indices.get_alias(name=index_alias)

        # We hope, we have not a spanning alias, this means only a single result:
        if len(alias_response) != 1:
            raise RuntimeError(f"The alias {index_alias} is a spanning alias over more than one index")

        index_from = next(iter(alias_response.keys()))

        if index_from != index_to:
            response = client.indices.create(index_to, body=runner['body'])
            copy_documents(client, index_from=index_from, index_to=index_to)
            switch_version(client, index_alias=index_alias, version=version)
        else:
            print(f"Warning: Existing index {index_from} is equal to the "
                  f"migration version. Maybe the history was deleted?")
    else:
        raise RuntimeError("No idea, what to do")

    return response

Am Anfang der Methode bereiten wir alle Parameter vor und suchen auf dem OpenSearch Cluster, ob der Index existiert und ob es einen Alias gibt. Die verschiedenen Kombinationen ermöglichen uns, die drei Fälle zu unterscheiden.

Ihr seht, Fall 2 ist der einfachste. Fall 3 ist das, was wir in Zukunft immer als Migrationen erwarten und Fall 1 müssen wir implementieren, weil unser altes Migrationsscript keine Versionierung beherrschte.

Das war es eigentlich schon.

Wenn das neue Migration.py Script ausgeführt wird (mit dem Aufruf von os-migration.py), werden (wenn ihr nicht schon das letzte Mal bei 0 angefangen habt), die Versionen 1.002 und 1.003 (da wo wir toots und accounts anlegen) plötzlich durchlaufen.

Wenn ihr dann die Indexe mal im Katalog anschaut, habt ihr ggf. ähnliche Ergebnisse:

health status index               uuid                   pri rep docs.count docs.deleted store.size pri.store.size
green  open   un-followers_v1_003 zcRQJo8RRfuC5d_O_uApkA   2   0          1            0     33.3kb         33.3kb
green  open   un-following_v1_003 RrgaScgCSIWxZH8wM5wjVg   2   0          0            0       416b           416b
green  open   following_v1_003    j_S1G_nvQ5-_tP1_stftzw   2   0        142            0    343.9kb        343.9kb
green  open   migration_history   E2Z421iORciEFDRL0RE7xA   2   0          4            0     42.2kb         42.2kb
green  open   toots_v1_004        5EDW1jbpTCmxQZ2s97QcYQ   2   0       6323            0      5.1mb          5.1mb
green  open   followers_v1_003    5aFps3PzRCaFUbMWtkFWpw   2   0        212            0    413.4kb        413.4kb
green  open   .kibana_1           oPhh4WkXTha54EXU4SdJ7A   1   0          1            0        5kb            5kb
green  open   toots_v1_002        o7OXHpaZQoebIqxhRdkz4g   2   0       6000            0      5.1mb          5.1mb

Die Version v1_004 für die Toots kommt mit dem neuen Migrations JSON aus dem git-Repo (ich brauchte ja was zum Testen).

Prolog

Es gibt immer noch Szenarien, die das Script nicht 100% abfangen kann. Aber mit etwas mehr als 400 Programmzeilen haben wir ein Migrationsscript, was den Aufbau einer Index-Collection erlaubt und sogar Veränderungen durchführt, ohne die Dokumente aus den alten Indexen zu verlieren. Kleinigkeiten fehlen noch: Die alten Indexe sollten unmittelbar geschlossen werden (damit niemand reinschreibt). Alte Indexe müssten auch mal irgendwann gelöscht werden. Und da wir aber in der Zwischenzeit die alten Indexe uns merken, wäre ein Undo einer Migration toll (ok, dann dürfen wir aber auch nicht hart einen Index löschen).

Das heben wir uns für einen anderen Artikel auf.

Wie immer würde es mich freuen, Feedback zu bekommen und vielleicht auch ein Boost des Artikels.