Zero Downtime Deployment und Datenbanken

Zero Downtime Deployment und Datenbanken

In diesem Artikel wird ausführlich erklärt, wie man Probleme mit der Datenbankkompatibilität beim Deployment behebt. Wir werden erläutern, was mit Ihren Anwendungen in Produktion passieren kann, wenn Sie versuchen, ein Deployment ohne Vorbereitung durchzuführen. Anschließend gehen wir die Phasen des Anwendungslebenszyklus durch, die erforderlich sind, um eine Nullausfallzeit zu erreichen (Anm. d. Übers.: im Folgenden — zero downtime). Das Ergebnis unserer Vorgänge wird die Anwendung einer abwärtskompatiblen Änderung der Datenbank auf eine abwärtskompatible Weise sein.

Wenn Sie die Codebeispiele aus dem Artikel verstehen möchten, finden Sie sie auf GitHub.

Einführung

Zero downtime deployment

Was ist das mysteriöse zero downtime deployment? Можно сказать, это когда ваше приложение развернуто так, что вы можете успешно вводить новую версию приложения на продакшн, в то время как пользователь не замечает его недоступности. С точки зрения пользователя и компании, это наилучший из возможных сценариев деплоя, поскольку таким образом можно вводить новые функции и устранять ошибки без перебоев в работе.

Wie erreicht man das? Es gibt mehrere Wege, einer davon ist:

  • Wenden Sie Version Nr. 1 Ihres Dienstes an.
  • Führen Sie die Datenbankmigration durch.
  • Veröffentlichen Sie Version Nr. 2 Ihres Dienstes parallel zur Version Nr. 1.
  • Sobald Sie sehen, dass Version Nr. 2 funktioniert, entfernen Sie Version Nr. 1.
  • Fertig!

Einfach, oder? Leider ist es nicht so einfach und wir werden das später ausführlich betrachten. Lassen Sie uns jetzt einen weiteren ziemlich verbreiteten Deployment-Prozess überprüfen — das Blue-Green-Deployment.

Haben Sie schon einmal von blue green deployment? С Cloud Foundry это чрезвычайно легко сделать. Просто гляньте на diesen Artikel, bei dem wir dies ausführlicher beschreiben. Kurz zusammengefasst, erinnern wir daran, wie man das Blue-Green-Deployment durchführt:

  • Stellen Sie sicher, dass zwei Kopien Ihres Produktionscodes („blue“ und „green“) betrieben werden;
  • Leiten Sie den gesamten Verkehr in die Blue-Umgebung, d.h. die Produktions-URLs sollten dorthin zeigen;
  • Veröffentlichen und testen Sie alle Änderungen der Anwendung in der Green-Umgebung;
  • Schalten Sie die URLs von der Blue- auf die Green-Umgebung um.

Das Blue-Green-Deployment ist ein Ansatz, der es Ihnen ermöglicht, neue Funktionen einfach einzuführen, ohne sich Sorgen machen zu müssen, dass die Produktion ausfällt. Das liegt daran, dass selbst wenn etwas schiefgeht, Sie schnell auf die vorherige Umgebung zurückkehren können, indem Sie einfach einen 'Schalter umlegen'.

Nachdem Sie all das oben Geschriebene gelesen haben, könnten Sie sich fragen: Was hat das Zero Downtime mit dem Blue-Green-Deployment zu tun?

Nun, sie haben einiges gemeinsam, da die Unterstützung von zwei Kopien derselben Umgebung doppelte Anstrengungen erfordert. Deshalb halten einige Teams, wie Martin Fowler, an einer Variante dieses Ansatzes fest:

Eine andere Möglichkeit besteht darin, dieselbe Datenbank zu verwenden, indem blau-grüne Schalter für die Web- und Domain-Schichten erstellt werden. Bei diesem Ansatz können Datenbanken oft ein Problem darstellen, insbesondere wenn Sie deren Schema ändern müssen, um eine neue Version der Software zu unterstützen.

Und hier kommen wir zum Hauptproblem dieses Artikels. Datenbank. Lassen Sie uns diesen Satz noch einmal betrachten.

Migrieren Sie die Datenbank.

Jetzt sollten Sie sich die Frage stellen – was ist, wenn die Änderung der Datenbank nicht abwärtskompatibel ist? Wird meine erste Version der Anwendung nicht kaputtgehen? Tatsächlich wird genau das geschehen…

So neigen Unternehmen trotz der enormen Vorteile von Zero Downtime / Blue Green Deployment dazu, dem folgenden, sichereren Prozess zur Bereitstellung ihrer Anwendungen zu folgen:

  • Bereiten Sie ein Paket mit der neuen Version der Anwendung vor
  • Schalten Sie die laufende Anwendung aus
  • Führen Sie die Skripte zur Migration der Datenbank aus
  • Setzen Sie die neue Version der Anwendung in Betrieb und starten Sie sie

In diesem Artikel werden wir detailliert beschreiben, wie Sie mit Datenbanken und Code arbeiten können, um die Vorteile des Zero Downtime Deployments zu nutzen.

Probleme mit Datenbanken

Wenn Sie eine zustandslose Anwendung haben, die keine Daten in der Datenbank speichert, können Sie sofort Zero Downtime Deployment erreichen. Leider muss der Großteil der Software irgendwo Daten speichern. Deshalb sollten Sie Ihre Änderungen am Schema gut überdenken. Bevor wir in die Einzelheiten eintauchen, wie man das Schema so ändert, dass ein Deployment ohne Ausfallzeiten möglich wird, konzentrieren wir uns zunächst auf das Versionskontrollschema.

Versionskontrollschema

In diesem Artikel werden wir Flyway als Werkzeug zur Versionskontrolle verwenden (Anm. der Redaktion: Es geht um Datenbankmigrationen). Natürlich werden wir auch eine Spring Boot-Anwendung schreiben, die integrierte Unterstützung für Flyway hat und das Schema während der Einrichtung des Anwendungs-Kontextes migriert. Bei der Verwendung von Flyway können Sie Migrationsskripte im Ordner Ihrer Projekte speichern (standardmäßig in classpath:db/migration). Hier sehen Sie ein Beispiel für solche Migrationsdateien

└── db
 └── migration
     ├── V1__init.sql
     ├── V2__Add_surname.sql
     ├── V3__Final_migration.sql
     └── V4__Remove_lastname.sql

In diesem Beispiel sehen wir 4 Migrationsszenarien, die, falls sie zuvor nicht ausgeführt wurden, nacheinander beim Start der Anwendung ausgeführt werden. Lassen Sie uns eine der Dateien betrachten (V1__init.sql) als Beispiel.

CREATE TABLE PERSON (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
first_name varchar(255) not null,
last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

Alles spricht für sich: Sie können SQL verwenden, um festzulegen, wie Ihre Datenbank verändert werden soll. Für weitere Informationen über Spring Boot und Flyway lesen Sie Spring Boot Docs.

Mit dem Versionsverwaltungstool von Spring Boot profitieren Sie von 2 großen Vorteilen:

  • Sie trennen Änderungen an der Datenbank von Änderungen am Code
  • Die Datenbankmigration erfolgt zusammen mit der Bereitstellung Ihrer Anwendung, d.h. Ihr Bereitstellungsprozess wird vereinfacht

Probleme mit der Datenbank lösen

Im nächsten Abschnitt des Artikels konzentrieren wir uns auf die Prüfung zweier Ansätze für Datenbankänderungen.

  • Rückwärtsinkompatibilität
  • Rückwärtskompatibilität

Der erste Ansatz wird als Warnung betrachtet, dass man keine Zero Downtime-Dbereitstellungen ohne vorherige Vorbereitung durchführen sollte… Der zweite bietet eine Lösung, wie man Deployments ohne Ausfallzeiten durchführen und gleichzeitig Rückwärtskompatibilität gewährleisten kann.

Das Projekt, an dem wir arbeiten werden, wird eine einfache Spring Boot Flyway-Anwendung sein, die sollte man keine Aufmerksamkeit schenken, sie gehören zum Domain und werden im Rahmen des Tests nicht betrachtet. Insgesamt würde eine Anfrage zur Suche von Ketten in einer solchen graphischen Struktur wie folgt aussehen: c first_name und last_name in der Datenbank hat (Anm. d. Ü.: sollte man keine Aufmerksamkeit schenken, sie gehören zum Domain und werden im Rahmen des Tests nicht betrachtet. Insgesamt würde eine Anfrage zur Suche von Ketten in einer solchen graphischen Struktur wie folgt aussehen: ist eine Tabelle, und first_name und last_name ist ein Feld darin). Wir möchten last_name in surname.

Annahmen

Bevor wir in die Details eintauchen, müssen wir ein paar Annahmen über unsere Anwendungen klären. Das Hauptziel, das wir erreichen möchten, wird ein ziemlich einfacher Prozess sein.

Hinweis. Business PRO-TIPP. Prozesse zu vereinfachen kann Ihnen viel Geld bei der Wartung sparen (je mehr Personen in Ihrem Unternehmen arbeiten, desto mehr Geld können Sie einsparen)!

Keine Datenbankrückrollungen durchführen

Das vereinfacht den Bereitstellungsprozess (einige Datenbankrückrollungen sind praktisch unmöglich, wie z.B. eine Rückrollung nach einer Löschung). Wir ziehen es vor, nur Anwendungen zurückzurollen. So wird, selbst wenn Sie unterschiedliche Datenbanken haben (z.B. SQL und NoSQL), Ihre Bereitstellungspipeline gleich aussehen.

Es sollte IMMER die Möglichkeit bestehen, die Anwendung um eine Version zurückzusetzen (nicht mehr)

Rollbacks sollten nur bei Bedarf durchgeführt werden. Wenn in der aktuellen Version ein Fehler vorliegt, der schwer zu beheben ist, müssen wir die Möglichkeit haben, die letzte funktionierende Version zurückzugeben. Wir gehen davon aus, dass diese letzte funktionierende Version die vorherige ist. Die Unterstützung der Kompatibilität von Code und Datenbank für mehr als einen Rollout wäre äußerst schwierig und kostspielig.

Notiz. Um die Lesbarkeit zu verbessern, werden wir im Rahmen dieses Artikels die Hauptversion der Anwendung ändern.

Schritt 1: Ausgangszustand

Anwendungsversion: 1.0.0
DB-Version: v1

Kommentar

Dies wird der Ausgangszustand der Anwendung sein.

Änderungen der DB

Die DB enthält last_name.

CREATE TABLE PERSON (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY,
    first_name varchar(255) not null,
    last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

Änderungen des Codes

Die Anwendung speichert Daten der Person in last_name:

/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sample.flyway;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public void setLastName(String lastname) {
        this.lastName = lastname;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + this.firstName + ", lastName=" + this.lastName
                + "]";
    }
}

Rückwärtsinkompatible Umbenennung einer Spalte

Betrachten wir ein Beispiel, wie man den Spaltennamen ändert:

Achtung. Das folgende Beispiel wird absichtlich zu einem Fehler führen. Wir zeigen dies, um das Problem der Datenbankkompatibilität zu demonstrieren.

Anwendungsversion: 2.0.0.BAD

DB-Version: v2bad

Kommentar

Die aktuellen Änderungen erlauben es uns NICHT, zwei Instanzen (alte und neue) gleichzeitig auszuführen. Daher wird ein Zero-Downtime-Deployment schwer zu erreichen sein (unter Berücksichtigung der Annahmen ist es tatsächlich unmöglich).

A/B-Testing

Der aktuelle Stand ist, dass wir Anwendungsversion 1.0.0, im Produktionsumfeld und die DB v1. Wir müssen eine zweite Instanz der Anwendung in Version 2.0.0.BAD, bereitstellen und die Datenbank auf v2bad.

Schritte:

  1. wurde eine neue Instanz der Anwendung Version 2.0.0.BAD, die die Datenbank auf v2bad
  2. in der Datenbank v2bad Spalte last_name existiert nicht mehr - sie wurde in surname
  3. Das Update der Datenbank und der Anwendung war erfolgreich, und einige Instanzen arbeiten in 1.0.0, andere in 2.0.0.BAD. Alle sind mit der DB verbunden. v2bad
  4. Alle Instanzen der Version 1.0.0 werden Fehler ausgeben, da sie versuchen werden, Daten in die Spalte last_name, die nicht mehr existiert, einzufügen.
  5. Alle Instanzen der Version 2.0.0.BAD werden problemlos funktionieren.

Wie Sie sehen, ist ein A/B-Testing nicht möglich, wenn wir rückwärtsinkompatible Änderungen an der DB und der Anwendung vornehmen.

Rollback der Anwendung

Nehmen wir an, dass wir nach dem Versuch, ein A/B-Deployment zu durchführen (Anm. d. Übers.: Wahrscheinlich meinte der Autor hier A/B-Testing) entschieden haben, dass wir die Anwendung auf die Version 1.0.0. zurücksetzen müssen. Angenommen, wir möchten kein Rollback der Datenbank durchführen.

Schritte:

  1. Wir stoppen die Anwendungsinstanz der Version 2.0.0.BAD
  2. Die Datenbank ist immer noch v2bad
  3. da die Version 1.0.0 nicht versteht, was surnamewir sehen Fehler
  4. die Hölle ist entfesselt, wir können nicht zurückkehren

Wie Sie sehen, wenn wir nicht rückwärtskompatible Änderungen an der DB und der Anwendung vornehmen, können wir nicht zu einer vorherigen Version zurückkehren.

Protokolle der Skriptausführung

Nicht rückwärtskompatibles Szenario:

01) Führen Sie 1.0.0 aus
02) Warten Sie, bis die App (1.0.0) gestartet ist
03) Erzeugen Sie eine Person, indem Sie POST localhost:9991/person an Version 1.0.0 aufrufen
04) Führen Sie 2.0.0.BAD aus
05) Warten Sie, bis die App (2.0.0.BAD) gestartet ist
06) Erzeugen Sie eine Person, indem Sie POST localhost:9991/person an Version 1.0.0 aufrufen <-- das sollte fehlschlagen
07) Erzeugen Sie eine Person, indem Sie POST localhost:9992/person an Version 2.0.0.BAD aufrufen <-- das sollte erfolgreich sein

App in Version 1.0.0 starten
Erzeugen Sie eine Person in Version 1.0.0
POST an 127.0.0.1:9991/person senden. Das ist die Antwort:

{"firstName":"b73f639f-e176-4463-bf26-1135aace2f57","lastName":"b73f639f-e176-4463-bf26-1135aace2f57"}

App in Version 2.0.0.BAD starten
Erzeugen Sie eine Person in Version 1.0.0
POST an 127.0.0.1:9991/person senden. Das ist die Antwort:

curl: (22) Die angeforderte URL hat den Fehler zurückgegeben: 500 Internal Server Error

Erzeugen Sie eine Person in Version 2.0.0.BAD
POST an 127.0.0.1:9995/person senden. Das ist die Antwort:

{"firstName":"e156be2e-06b6-4730-9c43-6e14cfcda125","surname":"e156be2e-06b6-4730-9c43-6e14cfcda125"}

Änderungen der DB

Migrationsskript, das umbenennt last_name in surname

Ursprüngliches Flyway-Skript:

CREATE TABLE PERSON (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY,
    first_name varchar(255) not null,
    last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

Skript, das umbenennt last_name.

-- Diese Änderung ist nicht rückwärtskompatibel - A/B-Tests sind nicht möglich
ALTER TABLE PERSON CHANGE last_name surname VARCHAR;

Änderungen des Codes

Wir haben den Feldnamen geändert lastName auf surname.

Umbenennen der Spalte auf rückwärtskompatible Weise

Dies ist die häufigste Situation, mit der wir konfrontiert werden können. Wir müssen nicht rückwärtskompatible Änderungen vornehmen. Wir haben bereits bewiesen, dass wir für einen Ausfallfreien Rollout die Migration der Datenbank nicht einfach anwenden sollten, ohne zusätzliche Maßnahmen zu ergreifen. In diesem Abschnitt des Artikels werden wir 3 Deployments der Anwendung zusammen mit Datenbankmigrationen durchführen, um das gewünschte Ergebnis zu erzielen und gleichzeitig die Rückwärtskompatibilität zu wahren.

Hinweis. Wir erinnern uns, dass wir eine DB der Version v1haben. Sie enthält die Spalten first_name und last_name. Wir müssen ändern last_name auf surname. Wir haben auch eine Anwendung in Version 1.0.0, die derzeit nicht verwendet surname.

Schritt 2: Fügen Sie surname hinzu

Anwendungsversion: 2.0.0
DB-Version: v2

Kommentar

Durch das Hinzufügen einer neuen Spalte und das Kopieren ihres Inhalts schaffen wir rückwärtskompatible Änderungen der DB. Gleichzeitig wird, wenn wir das JAR zurücksetzen oder wenn wir ein funktionierendes altes JAR haben, keine Störung während der Ausführung auftreten.

Neue Version bereitstellen

Schritte:

  1. führen Sie die Datenbankmigration durch, um die neue Spalte zu erstellen surname. Jetzt hat Ihre DB der Version v2
  2. kopieren Sie die Daten von last_name in surname. Bitte beachten Sie, was bedeutet, dass Sie, wenn Sie viele dieser Daten haben, eine Batchmigration in Betracht ziehen sollten!
  3. Schreiben Sie den Code, wo sie verwendet werden BEIDE und neu, und alt Spalte. Jetzt ist Ihre Anwendung Version 2.0.0
  4. lesen Sie den Wert aus der Spalte surname, wenn er nicht null, oder aus last_name, wenn surname nicht gesetzt. Sie können getLastName() aus dem Code entfernen, da es null bei einer Rücksetzung Ihrer Anwendung 3.0.0 bis 2.0.0.

Wenn Sie Spring Boot Flyway verwenden, werden diese beiden Schritte beim Start der Version 2.0.0 der Anwendung ausgeführt. Wenn Sie das Datenbankversionierungstool manuell ausführen, müssen Sie dafür zwei unterschiedliche Aktionen ausführen (zuerst die Datenbankversion manuell aktualisieren und dann die neue Anwendung bereitstellen).

Wichtig. Denken Sie daran, dass die neu erstellte Spalte NICHT DÜRFE zu sein NOT NULL. Wenn Sie eine Rücksetzung durchführen, weiß die alte Anwendung nicht von der neuen Spalte und wird sie während der Einfügung nicht setzen. Wenn Sie jedoch dieses Limit hinzufügen und Ihre DB v2, benötigt dies, dass ein Wert für die neue Spalte gesetzt wird. Dies führt zu Verstoß gegen die Einschränkungen.

Wichtig. Sie sollten die Methode getLastName()entfernen, da in der Version 3.0.0 im Code das Konzept der Spalte fehlt. last_nameDas bedeutet, dass dort null gesetzt wird. Sie können die Methode beibehalten und Überprüfungen hinzufügen an null, aber eine viel bessere Lösung wäre sicherzustellen, dass in der Logik getSurname() Sie den richtigen nicht-null Wert gewählt haben.

A/B-Testing

Der aktuelle Stand ist, dass wir Anwendungsversion 1.0.0, bereitgestellt in der Produktion, und die DB in v1. Wir müssen eine zweite Instanz der Version der Anwendung bereitstellen 2.0.0, die die Datenbank auf v2.

Schritte:

  1. wurde eine neue Instanz der Anwendung Version 2.0.0, die die Datenbank auf v2
  2. aktualisiert, während einige Anfragen von den Instanzen der Version verarbeitet wurden. 1.0.0
  3. Das Update war erfolgreich, und Sie haben mehrere funktionierende Instanzen der Version der Anwendung 1.0.0 und andere Versionen. 2.0.0. Alle kommunizieren mit der DB in v2
  4. Version 1.0.0 verwendet in der DB die Spalte Nachname, während die Version 2.0.0 verwendet. Sie stören sich nicht gegenseitig, und es sollten keine Fehler auftreten.
  5. Version 2.0.0 speichert Daten sowohl in der alten als auch in der neuen Spalte, was Abwärtskompatibilität gewährleistet.

Wichtig. Wenn Sie Anfragen haben, die Elemente basierend auf Werten aus der alten/neuen Spalte zählen, sollten Sie daran denken, dass Sie jetzt Duplikate von Werten haben (wahrscheinlich sind sie in der Migration). Zum Beispiel, wenn Sie die Anzahl der Benutzer zählen möchten, deren Nachname (wie auch immer die Spalte genannt wird) mit dem Buchstaben Abeginnt, könnten Sie vor Abschluss der Datenmigration (altneu Spalte) inkonsistente Daten haben, wenn Sie eine Anfrage an die neue Spalte stellen.

Rollback der Anwendung

Jetzt haben wir die Version der Anwendung 2.0.0 und die Datenbank in v2.

Schritte:

  1. setzen Sie Ihre Anwendung auf die Version zurück 1.0.0.
  2. Version 1.0.0 verwendet in der DB die Spalte surname, deshalb muss das Rollback erfolgreich sein

DB-Änderungen

Die Datenbank enthält eine Spalte mit dem Namen last_name.

Ursprüngliches Flyway-Skript:

CREATE TABLE PERSON (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY,
    first_name varchar(255) not null,
    last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

Hinzufügungsskript surname.

Achtung. Denken Sie daran, dass es NICHT ERLAUBT ist, irgendwelche NOT NULL-Beschränkungen für die hinzugefügte Spalte hinzuzufügen. Wenn Sie das JAR zurücksetzen, hat die alte Version keine Kenntnis von der hinzugefügten Spalte und wird ihr automatisch den Wert NULL zuweisen. Bei einer solchen Einschränkung wird die alte Anwendung einfach fehlschlagen.

-- HINWEIS: Dieses Feld kann keine NOT NULL-Beschränkung haben, da die alte Version bei einem Rollback nichts von diesem Feld weiß
-- und es immer auf NULL setzen wird
ALTER TABLE PERSON ADD surname varchar(255);

-- WIR GEHEN DARAUF AUS, DASS ES EIN SCHNELLER MIGRATIONSPROZESS IST - ANDERNFALLS MÜSSTEN WIR IN BATCHES MIGRIEREN
UPDATE PERSON SET PERSON.surname = PERSON.last_name

Änderungen des Codes

Wir speichern die Daten in last_name, als auch in surname. Dabei lesen wir aus last_name, da diese Spalte am relevantesten ist. Während des Deployments konnten einige Abfragen von einer Instanz der Anwendung bearbeitet werden, die noch nicht aktualisiert war.

/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sample.flyway;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;
    private String surname;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    /**
     * Reading from the new column if it's set. If not the from the old one.
     *
     * When migrating from version 1.0.0 -> 2.0.0 this can lead to a possibility that some data in
     * the surname column is not up to date (during the migration process lastName could have been updated).
     * In this case one can run yet another migration script after all applications have been deployed in the
     * new version to ensure that the surname field is updated.
     *
     * However it makes sense since when looking at the migration from 2.0.0 -> 3.0.0. In 3.0.0 we no longer
     * have a notion of lastName at all - so we don't update that column. If we rollback from 3.0.0 -> 2.0.0 if we
     * would be reading from lastName, then we would have very old data (since not a single datum was inserted
     * to lastName in version 3.0.0).
     */
    public String getSurname() {
        return this.surname != null ? this.surname : this.lastName;
    }

    /**
     * Storing both FIRST_NAME and SURNAME entries
     */
    public void setSurname(String surname) {
        this.lastName = surname;
        this.surname = surname;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + this.firstName + ", lastName=" + this.lastName + ", surname=" + this.surname
                + "]";
    }
}

Schritt 3: Entfernen von last_name aus dem Code

Anwendungsversion: 3.0.0

DB-Version:v3

Kommentar

Hinweis des Übersetzers: Offensichtlich hat der Autor im ursprünglichen Artikel diesen Blocktext fälschlicherweise aus Schritt 2 kopiert. In diesem Schritt sollten Änderungen am Anwendungscode vorgenommen werden, um die Funktionalität, die die Spalte verwendet, zu entfernen. last_name.

Durch das Hinzufügen einer neuen Spalte und das Kopieren ihres Inhalts haben wir eine rückwärtskompatible DB-Änderung geschaffen. Wenn wir das JAR zurücksetzen oder ein funktionierendes altes JAR haben, wird es während der Ausführung nicht fehlschlagen.

Rollback der Anwendung

Derzeit haben wir eine Anwendung der Version 3.0.0 und eine Datenbank v3. Die Version 3.0.0 speichert keine Daten in last_name. Das bedeutet, dass in surname die aktuellsten Informationen gespeichert sind.

Schritte:

  1. setzen Sie Ihre Anwendung auf die Version zurück 2.0.0.
  2. Version 2.0.0 verwendet und last_name und surname.
  3. Version 2.0.0 übernimmt surname, wenn es nicht null ist, andernfalls —last_name

Änderungen der DB

In der DB gibt es keine strukturellen Änderungen. Es wird das folgende Skript ausgeführt, das die endgültige Migration der alten Daten durchführt:

-- WIR GEHEN DARAUF AUS, DASS ES EIN SCHNELLER MIGRATIONSPROZESS IST - ANDERNFALLS MÜSSTEN WIR IN BATCHES MIGRIEREN
-- AUCH ÜBERPRÜFEN WIR NICHT, OB WIR EXISTIERENDE EINTRÄGE ÜBERSCHREIBEN. WIR MÜSSTEN DIE
-- EINTRAGSVERSIONEN VERGLEICHEN, UM SICHERZUSTELLEN, DASS WIR WENN BEREITS EIN EINTRAG MIT EINER HÖHEREN VERSION EXISTIERT,
-- DIESEN NICHT ÜBERSCHREIBEN.
UPDATE PERSON SET PERSON.surname = PERSON.last_name;

-- FALLEN DER NOT NULL-BESCHRENGUNGEN; ANDERNFALLS WERDEN SIE VERSUCHEN, DEN NULL-WERT DES LAST_NAME
-- MIT EINER NOT_NULL-BESCHRÄNKUNG EINZUFÜGEN.
ALTER TABLE PERSON MODIFY COLUMN last_name varchar(255) NULL DEFAULT NULL;

Änderungen des Codes

Hinweis des Übersetzers: Die Beschreibung dieses Blocks wurde ebenfalls fälschlicherweise vom Autor aus Schritt 2 kopiert. Gemäß der Logik des Artikels sollten die Änderungen im Code in diesem Schritt darauf abzielen, Elemente zu entfernen, die mit der Spalte last_name.

wir speichern die Daten in last_name, als auch in surname. Darüber hinaus lesen wir aus der Spalte last_name, da sie am relevantesten ist. Während des Rollouts können einige Anfragen von einer Instanz bearbeitet werden, die noch nicht aktualisiert wurde.

/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sample.flyway;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String surname;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getSurname() {
        return this.surname;
    }

    public void setSurname(String lastname) {
        this.surname = lastname;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + this.firstName + ", surname=" + this.surname
                + "]";
    }
}

Schritt 4: Entfernen von last_name aus der DB

Anwendungsversion: 4.0.0

DB-Version: v4

Kommentar

Da der Versionscode 3.0.0 die Spalte nicht verwendet hat last_name, wird während der Ausführung nichts Schlimmes passieren, wenn wir auf 3.0.0 nach der Entfernung der Spalte aus der Datenbank zurückkehren.

Protokolle der Skriptausführung

Wir werden es folgendermaßen tun:

01) Version 1.0.0 starten
02) Warten, bis die App (1.0.0) hochgefahren ist
03) Eine Person generieren durch Aufruf von POST localhost:9991/person zur Version 1.0.0
04) Version 2.0.0 starten
05) Warten, bis die App (2.0.0) hochgefahren ist
06) Eine Person generieren durch Aufruf von POST localhost:9991/person zur Version 1.0.0
07) Eine Person generieren durch Aufruf von POST localhost:9992/person zur Version 2.0.0
08) App (1.0.0) beenden
09) Version 3.0.0 starten
10) Warten, bis die App (3.0.0) hochgefahren ist
11) Eine Person generieren durch Aufruf von POST localhost:9992/person zur Version 2.0.0
12) Eine Person generieren durch Aufruf von POST localhost:9993/person zur Version 3.0.0
13) App (3.0.0) beenden
14) Version 4.0.0 starten
15) Warten, bis die App (4.0.0) hochgefahren ist
16) Eine Person generieren durch Aufruf von POST localhost:9993/person zur Version 3.0.0
17) Eine Person generieren durch Aufruf von POST localhost:9994/person zur Version 4.0.0

App in Version 1.0.0 starten
Eine Person in Version 1.0.0 generieren
POST an 127.0.0.1:9991/person senden. Dies ist die Antwort:

{"firstName":"52b6e125-4a5c-429b-a47a-ef18bbc639d2","lastName":"52b6e125-4a5c-429b-a47a-ef18bbc639d2"}

App in Version 2.0.0 starten

Eine Person in Version 1.0.0 generieren
POST an 127.0.0.1:9991/person senden. Dies ist die Antwort:

{"firstName":"e41ee756-4fa7-4737-b832-e28827a00deb","lastName":"e41ee756-4fa7-4737-b832-e28827a00deb"}

Eine Person in Version 2.0.0 generieren
POST an 127.0.0.1:9992/person senden. Dies ist die Antwort:

{"firstName":"0c1240f5-649a-4bc5-8aa9-cff855f3927f","lastName":"0c1240f5-649a-4bc5-8aa9-cff855f3927f","surname":"0c1240f5-649a-4bc5-8aa9-cff855f3927f"}

App 1.0.0 beenden

App in Version 3.0.0 starten

Eine Person in Version 2.0.0 generieren
POST an 127.0.0.1:9992/person senden. Dies ist die Antwort:
{"firstName":"74d84a9e-5f44-43b8-907c-148c6d26a71b","lastName":"74d84a9e-5f44-43b8-907c-148c6d26a71b","surname":"74d84a9e-5f44-43b8-907c-148c6d26a71b"}

Eine Person in Version 3.0.0 generieren
POST an 127.0.0.1:9993/person senden. Dies ist die Antwort:
{"firstName":"c6564dbe-9ab5-40ae-9077-8ae6668d5862","surname":"c6564dbe-9ab5-40ae-9077-8ae6668d5862"}

App 2.0.0 beenden

App in Version 4.0.0 starten

Eine Person in Version 3.0.0 generieren
POST an 127.0.0.1:9993/person senden. Dies ist die Antwort:

{"firstName":"cbe942fc-832e-45e9-a838-0fae25c10a51","surname":"cbe942fc-832e-45e9-a838-0fae25c10a51"}

Eine Person in Version 4.0.0 generieren
POST an 127.0.0.1:9994/person senden. Dies ist die Antwort:

{"firstName":"ff6857ce-9c41-413a-863e-358e2719bf88","surname":"ff6857ce-9c41-413a-863e-358e2719bf88"}

Änderungen der DB

Bezüglich v3 löschen wir einfach die Spalte last_name und fügen fehlende Einschränkungen hinzu.

-- DIE SPALTE ENTFERNEN
ALTER TABLE PERSON DROP last_name;

-- EINSCHRÄNKUNGEN HINZUFÜGEN
UPDATE PERSON SET surname='' WHERE surname IS NULL;
ALTER TABLE PERSON ALTER COLUMN surname VARCHAR NOT NULL;

Änderungen des Codes

Es gibt keine Änderungen im Code.

Ausgabe

Wir haben die inkompatible Änderung des Spaltennamens erfolgreich rückgängig gemacht, indem wir mehrere abwärtskompatible Deployments durchgeführt haben. Nachfolgend eine Zusammenfassung der durchgeführten Maßnahmen:

  1. Deployment der Anwendung Version 1.0.0 c v1 Datenbankschema (Spaltenname = last_name)
  2. Deployment der Anwendung Version 2.0.0, das die Daten speichert in last_name und surname. Die Anwendung liest aus last_name. Die DB befindet sich in der Version v2, die Spalten wie enthält last_name, als auch surname. surname ist eine Kopie von last_name. (HINWEIS: Diese Spalte sollte kein not null-Einschränkung haben)
  3. Deployment der Anwendung Version 3.0.0, die die Daten nur in surname und aus surname liest. Was die DB betrifft, findet die letzte Migration statt last_name in surname. Auch die Einschränkung NOT NULL wird von last_name. Die DB ist jetzt in der Version v3
  4. Deployment der Anwendung Version 4.0.0 — im Code werden keine Änderungen vorgenommen. Das Deployment der Datenbank v4, die löscht last_name. Hier können Sie alle fehlenden Einschränkungen in der DB hinzufügen.

Mit diesem Ansatz können Sie immer auf eine Version zurückrollen, ohne die Kompatibilität von DB/Anwendung zu brechen.

Code

Der gesamte Code, der in diesem Artikel verwendet wird, ist verfügbar unter Github. Weiter unten eine zusätzliche Beschreibung.

Projekte

Nach dem Klonen des Repositories sehen Sie die folgende Ordnerstruktur.

├── boot-flyway-v1              - 1.0.0 Version der App mit v1 des Schemas
├── boot-flyway-v2              - 2.0.0 Version der App mit v2 des Schemas (rückwärtskompatibel - App kann zurückgerollt werden)
├── boot-flyway-v2-bad          - 2.0.0.BAD Version der App mit v2bad des Schemas (rückwärtsinkompatibel - App kann nicht zurückgerollt werden)
├── boot-flyway-v3              - 3.0.0 Version der App mit v3 des Schemas (App kann zurückgerollt werden)
└── boot-flyway-v4              - 4.0.0 Version der App mit v4 des Schemas (App kann zurückgerollt werden)

Skripte

Sie können die in den folgenden Skripten beschriebenen Szenarien ausführen, die rückwärtskompatible und inkompatible Änderungen an der DB demonstrieren.

Um zu sehen den Fall mit rückwärtskompatiblen Änderungen, führen Sie aus:

.\/scripts\/scenario_backward_compatible.sh

Und um zu sehen den Fall mit rückwärtsinkompatiblen Änderungen, führen Sie aus:

.\/scripts\/scenario_backward_incompatible.sh

Spring Boot Sample Flyway

Alle Beispiele stammen von Spring Boot Sample Flyway.

Sie können einen Blick darauf werfen http://localhost:8080/flyway, dort ist eine Liste der Skripte.

Dieses Beispiel enthält auch die H2-Konsole (unter http://localhost:8080/h2-console), damit Sie den Zustand der Datenbank anzeigen können (der Standard-Jdbc-URL ist - jdbc:h2:mem:testdb).

Zusätzlich

Lesen Sie auch andere Artikel in unserem Blog:

Quelle: habr.com

60GB SSD 8Gb DDR4