Datenstrukturen ändern, aber bitte ohne Ausfallzeiten!

PrintFriendly and PDF

Bei einem anstehenden Release mit Änderungen an den Datenstrukturen hatte man es früher einfach: Server herunterfahren, Migrationsskripte laufen lassen, Server wieder hochfahren, fertig. In Zeiten von Server-Clustern und 24/7-Verfügbarkeit sind Änderungen an der Datenstruktur heute nicht mehr ganz so einfach.

Strukturänderungen im laufenden Betrieb müssen gut geplant und schrittweise umgesetzt werden. Ansonsten läuft man schnell in Gefahr, inkonsistente Daten oder Fehler in der Anwendung zu erhalten.

Vorweg: Dieser Artikel steht ganz im Kontext von schemafreien Datenbanken, insbesondere Dokumentendatenbanken. Anders als bei relationalen Datenbanken lassen sich hier Datenstrukturen relativ einfach im laufenden Betrieb ändern, d.h. ohne Ausfallzeiten beispielsweise durch Tabellensperren.

Was soll erreicht werden?

Zunächst werfen wir einen Blick auf die Probleme, mit denen wir es zu tun haben: Die Anforderung lautet, bestehende Datenstrukturen ohne eine Ausfallzeit im Produktivbetrieb zu verändern.

Das bedeutet: Es gibt schon Daten in einer bisherigen Struktur und diese Daten werden von der Anwendung gelesen und geschrieben. Die neue Struktur ist inkompatibel mit der alten, es geht also nicht bloß um eine reine Erweiterung. Um ohne Ausfallzeiten ein neues Release einspielen zu können, ist ein Cluster notwendig, dessen Knoten nacheinander mit neuer Software bespielt werden. Es ist also nicht möglich, auf einmal einen neuen Softwarestand einzuspielen, der sofort überall in Betrieb geht. Das bedeutet auch, dass, während eine Datenmigration läuft, weiterhin alter Code auf die Datenbank zugreift.

Schließlich soll es möglich sein, jedes einzelne Release wieder zurückrollen zu können, falls erst in Produktion schwerwiegende Fehler auftauchen, die sich nicht durch schnelle Bugfixes beheben lassen.

Der Weg zum Ziel

Die wichtigste Erkenntnis ist, dass sich Datenstrukturänderungen nicht in einem Release durchführen lassen. Angenommen, man deployt ein Release, dass sowohl die Daten migriert als auch die Anwendung auf die neuen Strukturen umstellt. Wenn auf dem ersten Knoten im Cluster schon der neue Software-Stand läuft (und die Migration durchführt), greifen die übrigen Knoten noch mit dem alten Softwarestand auf die alten Datenstrukturen zu. Inkonsistenzen sind vorprogrammiert.

Daher ist es notwendig, eine Datenstrukturänderung in mehreren Schritten durchzuführen:

Schritt 1: Neue Strukturen einführen

Das erste Release führt die neuen Datenstrukturen ein, ohne die alten Strukturen zu verändern. Die Software wird so angepasst, dass alle Schreiboperationen sowohl die alten als auch die neuen Strukturen befüllen. Die Lesezugriffe gehen jedoch weiterhin ausschließlich auf die alten Strukturen.

Wenn dieses Release schief geht, lässt es sich problemlos zurückrollen. Eventuell müssen die neu angelegten Strukturen wieder gelöscht werden. Nach diesem Release ist sichergestellt, dass von nun an alle neu angelegten bzw. geänderten Datensätze die neuen Strukturen beinhalten.

Schritt 2: Daten in die neuen Strukturen kopieren

Das zweite Release migriert die vorhandenen Daten. Alle Datensätze, in denen die neuen Strukturen noch nicht (seit dem ersten Release) befüllt sind, werden migriert: Die alten Datenstrukturen bleiben weiterhin erhalten, die dort gespeicherten Informationen werden in die neuen Strukturen kopiert.

Bei einem Problem durch dieses Release lässt es sich ebenfalls problemlos zurückrollen. Gegebenenfalls müssen die kopierten Daten wieder gelöscht werden. Auch wenn die Durchführung der Migration lange dauert, ist dies kein Problem, denn jede Schreiboperation der Anwendung führt zum gleichen Ergebnis wie die Migration. Die Migration stellt also nur sicher, dass Datensätze, die nicht im Betrieb verändert werden, trotzdem die neue Struktur erhalten.

Schritt 3: Anwendungslogik auf die neuen Strukturen umstellen

Das dritte Release ist das Entscheidende: Jetzt soll die Anwendungslogik die Daten nicht mehr aus der alten Struktur lesen, sondern aus der neuen. Beide Strukturen bleiben aber erhalten, ebenso wird weiterhin parallel in die alte und die neue Struktur geschrieben.

Bei dieser Umstellung können die meisten Fehler entstehen. Daher ist es hier besonders wichtig, zurückrollen zu können. Da aber durch die ersten beiden Releases sichergestellt ist, dass die alten und die neuen Strukturen über alle Datensätze hinweg konsistent sind, macht es für die Funktionalität der Anwendung keinen Unterschied, aus welche Strukturen die Daten gelesen werden.

Falls die Datenstrukturänderungen einhergehen mit funktionalen Änderungen bzw. Änderungen an der Bedienoberfläche, sollte man sich überlegen, ob man beides bündelt. Es ist möglich – jedoch wäre ein Zurückrollen für die Anwender sichtbar.

Schritt 4: Schreiboperationen in die alten Strukturen abstellen

Mit diesem Release beginnt das Aufräumen. Jetzt kann die Anwendungslogik so aufgeräumt werden, dass auch die Schreiboperationen die alten Strukturen ignorieren und nur noch in die neuen Strukturen schreiben.

Falls dieses Release zurückgerollt werden müsste, ist nicht mehr sichergestellt, dass alte und neue Strukturen konsistent sind. Da seit dem vorigen Release die alten Strukturen aber schon nicht mehr gelesen werden, bedeutet dies nur ein geringes Risiko.

Schritt 5: Alte Datenstrukturen löschen

Im letzten Release werden die alten Strukturen gelöscht. Die Trennung vom vorigen Schritt ist notwendig, damit erst alle Knoten im Cluster ihre Schreiboperationen auf die alten Strukturen einstellen, bevor die alten Strukturen entfernt werden.

Die Gefahr in diesem Release steckt darin, die falschen Daten zu löschen. Ein Zurückrollen wäre nur möglich, indem man ein Backup der Datenbank einspielt.

Fazit

Ein schrittweises Vorgehen zieht die Umstellung von Datenstrukturen zwar in die Länge. Es stellt jedoch sicher, dass jedes einzelne Release wieder zurückgerollt werden kann und dass während jeder Produktivsetzung Knoten mit dem alten und dem neuen Stand parallel laufen können. Ausfallzeiten können dadurch vollständig vermieden werden.

Gibt es Alternativen?

Die genannten Anforderungen enthalten nicht explizit den Wunsch, veraltete Datenstrukturen vollständig zu entfernen. Schemafreie Datenbanken erlauben es, Datensätze in unterschiedlichen Strukturen zu speichern. Eine Alternative ist daher, die Daten nicht zu migrieren, sondern zu versionieren. Im Rahmen eines neues Releases wird eine neue Version einer Datenstruktur eingeführt. Der neue Softwarestand schreibt die Daten nur in der neuen Struktur, kann jedoch noch Daten aus der alten Struktur lesen.

Eine solche Versionierung vermeidet es, Daten(strukturen) migrieren zu müssen. Bei mehrfachen Strukturänderungen führt eine Versionierung jedoch in der Regel dazu, dass zu jeder Strukturversion Datensätze übrig bleiben. Leseoperationen müssen mit allen alten Strukturen klar kommen. Schon nach wenigen Versionssprüngen wird die Migrationslogik beim Lesen der Daten unübersichtlich. Ebenso unangenehm ist es, dass auch die Suchoperationen in der Datenbank alle alten Strukturen berücksichtigen müssen.

Schließlich erhöht sich die mentale Last für die Entwickler, wenn sie nicht nur die aktuellen, sondern auch alle ehemaligen Strukturen kennen müssen. Insbesondere neue Entwickler stehen dann vor dem Problem, sich auch in die alten Strukturen einarbeiten zu müssen.

Gibt es weitere Alternativen, die genannten Anforderungen zu erfüllen? Wenn ja, dann freue ich mich über einen entsprechenden Kommentar.