8 min read

Rückwärtskompatibilität der Daten: Entwickeln Sie Ihre Datenbank ohne Ausfallzeiten

Published on
February 8, 2021
Author
Gábor Farkas
Gábor Farkas
Cloud Architect
Subscribe to our newsletter
Subscribe
Rückwärtskompatibilität der Daten: Entwickeln Sie Ihre Datenbank ohne Ausfallzeiten

Die Entwicklung von Datenstrukturen in einer kontinuierlich betriebenen Anwendung mit einer NoSQL-Datenbank kann eine Herausforderung sein. Dies sind einige der Erfahrungen, die wir bisher bei der Entwicklung und dem Betrieb von AODocs gemacht haben, einem cloud-nativen, serverlosen Dokumentenmanagementsystem, das von Millionen von Nutzern auf der ganzen Welt verwendet wird.

Keine Ausfallzeiten

Ich will nicht sagen, dass ich die Entwicklung klassischer Java EE-Anwendungen mit relationalen Datenbanken vermisse, aber bestimmte Dinge waren zweifellos einfacher.

  • Die Arbeit mit Wartungsunterbrechungen zwischen den Releases war ein natürlicher Bestandteil des Release-Prozesses. In diesen Zeitfenstern konnten wir Aktualisierungsskripte auf der Datenbank ausführen und die Datenkonsistenz überprüfen, bevor wir der Anwendung erlaubten, sich erneut mit der Datenbank zu verbinden.
  • Es gab bewährte Tools wie Liquibase, um Datenbankänderungen zu verwalten.

Bei vielen Cloud-nativen Anwendungen mit NoSQL-Datenbanken funktioniert dieser Ansatz nicht wirklich.

  • In unserem Beispiel können wir uns keine Ausfallzeiten bei der Wartung leisten; die Anwendung muss jederzeit und überall auf der Welt voll verfügbar sein.
  • Wir können keine Schema-Update-Anweisungen ausführen, da es kein Schema gibt, und es gibt keine Daten-Update-Anweisungen, die wir auf Tabellen ausführen können. Streng genommen gibt es keine Tabellen, sondern nur Entitätsarten. Das bedeutet, dass wir Datenaktualisierungen zwingend durchführen müssen, indem wir die Entitäten eine nach der anderen verarbeiten.

Mehrere Anwendungsversionen greifen auf dieselbe Datenbank zu

Da es bei uns keine Wartungsstillstände gibt, wird es Zeiträume geben, in denen mehrere Anwendungsversionen auf dieselbe Datenbank zugreifen, auch wenn diese sehr kurz sind. Wir führen auch schrittweise Rollouts von Hauptversionen durch, bei denen zwei Anwendungsversionen wochenlang gleichzeitig in Betrieb sind. Aber selbst wenn wir nur eine neue Hotfix-Version einspielen, wird es einige Minuten geben, in denen Anfragen der alten Version noch ausgeführt werden und die neue Version ebenfalls ihren Dienst aufgenommen hat. Dies ist unvermeidlich, ohne dass es zu einer tatsächlichen Ausfallzeit kommt.

Die beiden Anwendungsversionen müssen die Daten in kompatibler Weise verwalten, sowohl rückwärts als auch vorwärts. Wenn eine Anfrage der neuen Version die Daten auf die neue Art und Weise aktualisiert, muss die alte Version weiterhin in der Lage sein, sie zu lesen. Wenn die alte Version dann die Daten mit dem alten Schema überschreibt, muss die neue Version sie immer noch lesen können und normal arbeiten.

Die Regel der 3 Versionen

Jede Änderung an der Datenstruktur muss sehr sorgfältig geplant und koordiniert werden. Selbst eine kleine Änderung wird in der Regel unter Berücksichtigung von 3 Anwendungsversionen durchgeführt. In der Version N wird die neue Funktion den Kunden zum ersten Mal zur Verfügung gestellt. Um die Kompatibilität zu gewährleisten, müssen wir in der Regel mit den Vorarbeiten für die vorherige Version beginnen.

  1. vN-1 beginnt mit der Unterstützung der neuen Daten in dem Sinne, dass sie normal funktionieren muss, wenn sie eine Entität liest, die die neue Datenstruktur hat. Normalerweise schreibt es die Daten immer noch auf die alte Art und Weise, oder zumindest auf eine Art und Weise, die mit vN-2 zum Zeitpunkt der Veröffentlichung kompatibel ist.
  2. vN kann immer noch die alte Darstellung lesen, aber es schreibt die Daten auf die neue Art.
  3. Wenn die Einführung abgeschlossen ist, kann es damit beginnen, die neuen Funktionen zu unterstützen, die mit den neuen Daten verbunden sind, falls vorhanden.
  4. Während der Lebensdauer von vN führen wir einen Daten-Upgrade-Prozess durch, um sicherzustellen, dass alle Entitäten auf die neue Darstellung umgestellt werden.
  5. vN+1 bereinigt den Datenmigrationscode und entfernt die Leseunterstützung für den alten Mechanismus. Der tatsächliche Zeitplan für diese Freigaben kann variieren.

Ein sehr einfaches Beispiel

Wir arbeiten derzeit mit Java 8 in App Engine, mit Cloud Datastore. In diesem Setup wird normalerweise Objectify verwendet, die De-facto-Standardbibliothek für ORM-Mapping mit Java.

Nehmen wir an, wir haben eine einfache Entität:

Das boolesche Feld, das Sie oben sehen, speichert, ob der angegebene Benutzer Benachrichtigungen von unserer Anwendung erhalten möchte. (Die @Data-Annotation stammt aus Lombok.)

Nehmen wir an, wir möchten unsere Erinnerungsfunktion so weiterentwickeln, dass die Benutzer die Erinnerungsfrequenz auf täglich einstellen können. Eine Möglichkeit, dies darzustellen, besteht darin, das boolesche Feld in ein Enum zu ändern, das drei Werte unterstützt: KEINE, EINMAL, TÄGLICH.

Hinweis: Dies ist kein tatsächliches Beispiel. Diese Darstellung soll nicht korrekt sein, sondern ist absichtlich falsch. Dieses spezielle Beispiel kann auch auf andere Weise modelliert werden, die keine Schemaänderung, sondern nur eine Schemaerweiterung erfordert. Aber nicht alle Modelländerungen lassen sich ohne Schemaänderung umsetzen, und oft ist es besser, das Schema zu aktualisieren, als an einem schlechteren Datenmodell festzuhalten.

Wir werden also ein Schema-Update-Mapping haben, das wie folgt aussieht:

Schema-Update-Zuordnung auf AODocs

In Version vN-1 müssen wir in der Lage sein, Entitäten zu lesen, die nach der neuen Struktur geschrieben wurden. Eine Möglichkeit, dies zu tun, ist in diesem Beispiel die Verwendung von @AlsoLoad, das von Objectify bereitgestellt wird:

Wenn dieser Code in Version vN-1 auf eine Entität der neuen Struktur trifft, wendet er ein Standard-Rückwärts-Mapping an. Beachten Sie, dass Objectify Felder aus der zugrundeliegenden Entität löscht, wenn keine entsprechenden Java-Felder deklariert sind; wenn also diese Version die Entität speichert, wird das Feld reminderMode wieder null sein.

In der Version vN können wir uns dann in erster Linie auf das neue Feld verlassen, aber wir müssen immer noch in der Lage sein, Entitäten aus dem alten Schema zu lesen.

Aber was passiert mit dem DAILY-Wert?

Sie haben wahrscheinlich bemerkt, dass unsere Rückwärtsabbildung der Aufzählung auf boolesche Werte nicht vollständig ist. Wenn ein Kunde diesen Wert in der neuen Version auf DAILY setzt, geht dieser Wert bei einer Aktualisierung der Entität in einer alten Version einfach verloren, und der Benutzer wird wieder zu ONCE-Erinnerungen zurückkehren.

Dies lässt sich nicht immer vollständig vermeiden; die Lösung hängt vom konkreten Fall ab.

  1. In diesem speziellen Fall können wir dies zuverlässig lösen, indem wir den ReminderMode in vN-1 speichern und ihn nur ändern, wenn der Benutzer die Einstellung auf vN-1 aktualisiert. Dies kann der Fall sein, wenn wir eine Version über einen längeren Zeitraum zurücksetzen müssen.
  2. In anderen Fällen ist es nicht einfach zu entscheiden, ob es sinnvoll ist, die neue Einstellung in der alten Version beizubehalten, auch wenn sie dort nicht wirksam ist. Beide Verhaltensweisen können in Grenzfällen zu unerwartetem Verhalten für den Endbenutzer führen.
  3. In unserem Beispiel könnte ein Benutzer, der die neue Einstellung TÄGLICH bereits angewendet hat und für einen längeren Zeitraum zurückgesetzt werden muss, feststellen, dass die Erinnerungseinstellung wieder auf EINMAL steht. Dann könnte er beschließen, eine andere Einstellung zu aktualisieren, die ebenfalls eine geeignete Lösung für seinen aktuellen Geschäftsfall ist, und er könnte verwirrt sein, wenn die täglichen Erinnerungen erst ein paar Tage später wieder auftauchen, wenn wir mit dem Rollout der Version fortfahren.

Wenn das Risiko oder der Schweregrad eines unerwünschten Verhaltens während dieser Rollout-Phase, in der zwei Anwendungsversionen gleichzeitig in Betrieb sind, hoch ist, können wir Funktionskennzeichen verwenden, damit die Kunden die neue Funktion erst nutzen können, wenn der Versions-Rollout abgeschlossen ist.

Ein anderer Ansatz

Das Beispiel mit den Erinnerungen könnte eigentlich in zwei Versionen migriert werden. Wir könnten es vermeiden, die Migration in vN-1 vorzubereiten, wenn der Code in vN sowohl die booleschen als auch die enum-Felder deklariert. In diesem Fall:

  • Die Abwärtskompatibilität wird erreicht, indem das boolesche Feld beim Speichern der Entität auf der Grundlage der Aufzählung ausgefüllt wird.
  • Die Datenmigration erfolgt durch das Auffüllen des Enum-Feldes aus dem Boolean-Feld, wenn das Enum-Feld null ist. Dies ist auch ausreichend, um die Vorwärtskompatibilität zu gewährleisten. Wenn die Entität mit der vorherigen Version gespeichert wird, ist der Enum-Wert Null. Das heißt, wenn die Benutzer den Wert DAILY bereits gesetzt haben, wird er bei einem Versions-Rollback auf ONCE zurückgesetzt. Ob dies ein Problem ist oder nicht, hängt vom konkreten Fall ab, der im vorherigen Abschnitt besprochen wurde.

Dieser Ansatz wird häufiger angewandt, wenn es sich um eine rein technische (nicht benutzerorientierte) oder eine funktional äquivalente Datenstrukturänderung handelt.

Das Codebeispiel für unseren Fall:

In diesem speziellen Fall ist keine Änderung des Codes für vN-1 erforderlich. Dies lässt sich nicht immer erreichen. Die allgemeine Regel ist, dass wir sicherstellen müssen, dass vN-1 richtig funktioniert, wenn es eine von vN geschriebene Entität liest. Wenn wir z. B. einen neuen möglichen Enum-Wert zu einem enum-typisierten Feld hinzufügen, müssen wir diesen Enum-Wert irgendwie in der vorherigen Version hinzufügen und behandeln, sonst bekommen wir eine Ausnahme beim Lesen der Entität.

Eine neue Unicity-Beschränkung

Nehmen wir an, wir haben eine Entität, die von Kunden bearbeitet werden kann. Irgendwann stellen wir fest, dass der Anzeigename dieser Entität eindeutig sein sollte, so dass die Benutzer nicht zwei Entitäten mit demselben Namen haben können. Die Regel der 3 Versionen gilt auch hier.

In vN-1 gibt es nichts zu tun, um Kompatibilität zu gewährleisten. (Dies kann davon abhängen, wie wir die Regeln interpretieren, aber fangen wir hier an.)

vN beginnt mit der Anwendung der Einheitsbedingung. Das Problem dabei ist, dass vN-1 immer noch doppelte Namen erstellen kann und dass wir die Daten noch nicht migriert haben, was bedeutet, dass jedes zuvor erstellte Duplikat noch vorhanden ist. Je nachdem, wo genau wir die Einzigartigkeitsprüfung anwenden, kann dies problematisch sein, wenn die Einzigartigkeitsprüfung auch Systemaktionen für diese Entität verhindert.

  • Eine Lösung besteht darin, alle Aktualisierungen an dieser Entität zuzulassen, aber wenn der Name geändert wird, darf er nur in etwas Eindeutiges geändert werden

Ein anderer Ansatz besteht darin, die Einzigartigkeitsprüfung nur dann zuzulassen, wenn es einen Benutzer gibt, der den Wert aktualisiert. Dieser ist nicht immer leicht zu identifizieren. Im Falle eines Frontends über einen regulären REST-API-Aufruf können wir beispielsweise nicht sicher sein, ob es am anderen Ende einen tatsächlichen Benutzer gibt, der in der Lage ist, unsere Fehlermeldung sinnvoll zu bearbeiten, oder ob wir mit unserem Fehler einen Geschäftsintegrationsprozess blockieren würden.

Wenn vN vollständig ausgerollt ist, können wir einen Migrationsprozess durchführen, der problematischen Entitäten automatisch einen eindeutigen Namen zuweist.

In vN+1 können wir die "erleichterten" Regeln bereinigen und die Einzigartigkeitsregel vollständig anwenden.

Kein Patentrezept

Das klingt alles sehr mühsam. Und das ist es auch. Und oft ist es nicht nur umständlich, sondern auch komplex. Obwohl das allgemeine Muster gilt, ist jeder Fall ein wenig anders und erfordert eine sehr sorgfältige Überlegung, Gestaltung und Ausführung.

Wir haben mehrere Kontrollpunkte in verschiedenen Teilen unseres Entwicklungsprozesses eingebaut, um sicherzustellen, dass alle Änderungen, die sich auf die Kompatibilität auswirken könnten, bemerkt und ordnungsgemäß verwaltet werden.

Author
Gábor Farkas
Cloud Architect
Subscribe to our newsletter
Subscribe