Apps für die Cloud: Database Schema Migration

In einem früheren Blogpost haben wir aufgezeigt, wie Datenbank Schemamigrationen funktionieren und wie diese als Teil der Applikation umgesetzt werden. In diesem Teil zeigen wir, auf was man im Cloud-Umfeld achten muss und wie Migrationen umgesetzt werden müssen.

Weitere Blogposts zum Thema „Apps für die Cloud“ gibt es hier: Cloud Native AppsDevOps, Continuous Delivery Pipelines auf APPUiO

Migration im Cloud-Umfeld

Step by Step

Es gibt verschiedene Gründe, warum eine Applikation zur gleichen Zeit in verschiedenen Versionen deployed sein kann. Durch ein Blue-green Deployment beispielsweise, können wir die Downtime einer Applikation bei einem Update verhindern. Dazu wird Version grün einer Applikation deployed, wobei Version blau der selben Version stets aktiv ist. Ein vorgeschalteter Router leitet dabei allen Traffic an Version blau weiter, wobei Version grün noch inaktiv ist. Der Router wechselt danach auf Version grün und leitet allen Traffic an diese Version weiter. Nun ist Version blau inaktiv und kann als Rollback-Version verwendet werden oder mit der nächst höheren Version überschrieben werden.

Der Router kann natürlich auch zuerst nur ein kleiner Teil des Traffic an Version grün weiterleiten, um die neue Version vorerst nur einem kleinen Teil der User zur Verfügung zu stellen und entsprechend ein neues Feature zu testen. Dadurch reduziert sich das Risiko, eine fehlerhafte Version auf der Produktivumgebung zu haben, die allen Benutzern verfügbar ist. Hierbei sprechen wir von einem Canary Release.

 

Mit dem Updateverfahren Rolling Update auf der Container Platform APPUiO, werden während dem Deployment einer neuen Applikationsversion Requests kontinuierlich auf die neue Version geroutet. Dies so lange bis die entsprechend gewünschte Anzahl Applikations Container der neuen Version verfügbar sind. Ist dies der Fall, wird die alte Version entfernt.

Für uns bedeutet dies nun, dass während einem Update zwischenzeitlich mehrere unterschiedliche Versionen der Applikation gleichzeitig aktiv sind. Diese müssen natürlich beide mit der aktuellen Version der Datenbank kompatibel sein. Dazu müssen Datenbank Migrationen oft in mehreren Schritten durchgeführt werden.

Will man nun beispielsweise eine Spalte auf der Datenbank umbenennen, bedeutet dies konkret, dass zwischenzeitlich zwei Spalten existieren. Zuerst muss man die neue Spalte hinzufügen und erst wenn alle Applikationsinstanzen aktualisiert wurden und die neue Spalte verwenden, kann die alte Spalte gelöscht werden.

Damit die Daten in den Spalten synchron bleiben, kann man entweder die Applikation so implementieren, dass sie für eine gewisse Zeit in zwei Spalten schreibt oder die Datenbank sorgt dafür, dass diese beiden Spalten die gleichen Daten enthalten. Das Umsetzen auf Datenbankebene empfiehlt sich insbesondere wenn mehrere unabhängige Applikationen die gleiche Datenbank verwenden. Typischerweise wird dies in der Datenbank mit Triggern implementiert.

Die nachfolgende Grafik zeigt nun die Schritte, die für ein Umbenennen einer Spalte notwendig sind, wenn die Applikation für die Konsistenz der Daten verantwortlich ist:

  1. Die Applikation in der Version V1 benutzt die alte Spalte.
  2.  In Version V2 der Applikation darf zuerst nur eine neue Spalte mit dem neuen Namen hinzugefügt werden. Die Applikation liest dabei noch von der alten Spalte, muss jedoch sowohl in die alte, wie in die neue Spalte schreiben.
  3. In Version V3 darf davon ausgegangen werden, dass keine V1 der Applikation mehr existiert. Da also alle Applikationen in die neue Spalte schreiben, darf neu von dieser Spalte gelesen werden. Es muss jedoch immer noch in beide Spalten geschrieben werden, da V2 nach wie vor die alte Spalte liest.
  4. Danach kann in Version V4 nur noch in die neue Spalte geschrieben werden, da V3 bereits von der neuen Spalte liest.
  5. Die alte Spalte darf schlussendlich erst in Version V5 gelöscht werden.

Möchte man den Datentyp einer Spalte ändern, muss man die selbe Prozedur wie für das Umbenennen einer Spalte anwenden.

Grundsätzlich stellen wir fest, dass die DB Changes im Rahmen des Deployments der Applikation erfolgen muss. Auf APPUiO und generell OpenShift eignen sich dafür sogenannte Lifecycle Hooks, konkret ein Pre Deployment Hook.

Datenmigration

Auf die Datenmigration wurde im vorherigen Abschnitt absichtlich nicht eingegangen. Das erste Problem ist, wann die Datenmigration durchgeführt werden soll. Die Migration muss vor Version V3 erfolgen, da ab dieser Version bereits die neue Spalte gelesen wird. Sie muss jedoch nach Version V2 erfolgen, da erst ab dieser Version beide Spalten geschrieben werden und nur so garantiert ist, dass wir kein Update der Applikation verlieren.

Fügen wir also zwischen Version V2 und V3 die Version V2.1 ein, die folgende Datenmigration durchführt:

Diese Migration kann nun aber je nach Grösse der Tabelle sehr viel Zeit in Anspruch nehmen. Da die Datenbank die Atomarität des Statements sicherstellen muss, werden alle Rows der Tabelle gelockt. (Dies hängt natürlich vom Isolationslevel sowie vom Locking-Mechanismus der Datenbank ab. Hier wird jedoch nicht weiter darauf eingegangen.) Diese Locks führen dazu dass unsere Applikation in dieser Zeit blockiert werden kann. Wir sind also gezwungen unsere Migration in einzelnen Schritten durchzuführen.

V2.1:

V2.2:

Dasselbe Problem ergibt sich natürlich auch wenn wir eine NOT NULL Spalte hinzufügen wollen, die dadurch mit Standartwerten gefüllt werden muss. Wir fügen also eine NULLABLE Spalte hinzu, füllen die Spalte in kleinen Schritten mit Default-Werten und ändern die Spalte schlussendlich zu NOT NULL.

Im Buch Refactoring Databases – Evolutionary Database Design erklären Scott W. Ambler and Pramod J. Sadalage zahlreiche weitere Migrationsszenarien. Die mit Triggern umgesetzten Datenmigrationen können wie im oben erwähnten Beispiel auch durch schrittweise Migration von Datenbank und Applikation kombiniert mit DML Migrationen durchgeführt werden.

Rollback

Eine immer wiederkehrende Frage ist, wie man mit Fehlern bei der Datenbankmigration umgeht. Die Migrationen an sich sollten natürlich transaktional ablaufen. Bei der Verwendung einer Datenbank die transaktionales DDL unterstützt (beispielsweise PostgreSQL oder SQL Server [1]) benötigt dies keinen Zusatzaufwand. Schlägt ein Statement in einer Migration fehl, führt dies zu einem Rollback der Transaktion und somit zum Abbruch des Deployments. Wenn eine Datenbank eingesetzt wird, die ein implizites COMMIT nach jedem DDL Statement durchführt (beispielsweise MySQL oder Oracle [2]) gibt es zwei Möglichkeiten. Entweder man definiert pro Migration nur ein DDL Statement, welches durch seine Atomarität bereits transaktional ist oder man definiert im Datenbankmigrations-Tool ein explizites Rollback Szenario. Dies wurde im Blogpost Tech up! zu Database Schema Migration bereits erwähnt.

Wie wir bereits gesehen haben benötigt ein einfaches Refactoring bereits mehrere Schritte (in unserem Fall vier), die in einzelnen Migrationen durchgeführt werden. Bedingt durch die Tatsache, dass Datenbankrefactorings in mehrere Schritte und Deployments aufgeteilt werden, können ganze Refactorings nicht komplett in einem Schritt durch ein Rollback rückgängig gemacht werden. Hier bleibt nur noch die Vorwärtsstrategie indem man die bereits durchgeführten Migrationen manuell, also als neue Datenbankmigrationen, rückgängig macht.

Grundsätzlich lassen sich folgende Faktoren zusammenfassen, die das Risiko für fehlschlagende Migrationen verringern:

  • Aufteilen von Refactorings in mehrere Schritte
  • Datenbankchanges werde im Rahmen des CI/CD Prozesses permanent getestet.
  • Integrationstest auf einer möglichst produktionsnahen Umgebung in die Build Pipeline integrieren
  • Datenbankchanges abwärtskompatibel implementieren.
  • Separation of Concern, eine Applikation pro Datenbank.

NoSQL Migrationen

Warum brauchen wir Schema Migrationen in NoSQL Datenbanken, wenn diese ja schemalos sind? Auch wenn kein explizites Schema definiert ist, gibt es meist ein implizites Schema bedingt durch den Zugriffspfad der Applikation. Ein implizites Schema bedeutet, dass ein Dokument gespeichert werden kann, ohne dass dafür ein Schema definiert werden muss. Um die Daten in einem Dokument zu lesen benötigen wir jedoch einen definierten Zugriffspfad. Dieser Zugriffspfad ist unser implizites Schema. Für die Migration dieses Schemas werden die selben Datenmigrationstechniken wie für relationale Datenbanken verwendet.

Aktuell unterstützen weder Flyway noch Liquibase nativ NoSQL Datenbanken. Für die meisten NoSQL Datenbanken gibt es jedoch diverse andere Tools für die Datenbankmigration. Eine Möglichkeit wäre auch ein hybrider Einsatz von Flyway mit einer relationalen und einer NoSQL Datenbank. Flyway verwendet die relationale Datenbank dabei lediglich für das Speichern der Metadaten und migriert die NoSQL Datenbank mit Java Migrationen.

Fazit

Da bei einem Update mindestens zwei Versionen der selben Applikation online sein können, müssen beide mit der aktuellen Version der Datenbank kompatibel sein. Ausserdem müssen die Changes so geschrieben werden, dass ein Rollback auf die tiefere Version möglich ist. Erfahrung und Literatur helfen uns dabei, die Datenbankmigrationen im Sinne von CI/CD als Sourcecode zu implementieren und mit der Applikation zu deployen und zu testen. Es zeigt sich, dass beim Implementieren von Datenbankmigration die selben Konzepte wie bei der Implementation des Features im Sinne von Continuous Integration gelten, also kleine Schritte, mehrmals täglich einchecken und builden, Changes aufteilen, Testen.

 

[1] Um ein automatisches Rollback zu erzwingen muss bei SQL Server SET XACT_ABORT ON gesetzt werden.
[2] Oracle unterstützt ab Version 11g R2 das Feature Edition-Based Redefinition welches möglicherweise als Alternative für transaktionales DDL verwendet werden kann.

Kommentare sind geschlossen.