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 nachtoots_v1_001
kopieren - Alias
toots
fürtoots_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 nachtoots_v1_001
kopieren - Index
toots
löschen (ja, sorry - das ist hart) - Alias
toots
fürtoots_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
auftoots_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:
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:
- Wir haben echte Indexe mit Namen wie
toots
,following
, usw. Alle ohne Version im Namen. Kein Alias vergeben - Wir haben überhaupt keinen Index und fangen frisch von der grünen Wiese an
- 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.
Comments
No comments yet. Be the first to react!