Migrieren eines Datenbankschemas ohne Ausfallzeit für postgresql am Beispiel von django

Einführung


Hallo Habr!


Ich möchte die Erfahrung des Schreibens von Migrationen für Postgres und Django teilen. Hier geht es hauptsächlich um Postgres. Django ist hier eine gute Ergänzung, da das Datenschema für Modelländerungen sofort automatisch migriert wird, dh eine ziemlich vollständige Liste der Arbeitsvorgänge zum Ändern des Schemas. Django kann durch jedes bevorzugte Framework / jede bevorzugte Bibliothek ersetzt werden - die Ansätze werden höchstwahrscheinlich ähnlich sein.


Ich werde nicht beschreiben, wie ich dazu gekommen bin, aber jetzt, wo ich die Dokumentation lese, komme ich auf die Idee, dass es notwendig war, dies früher mit größerer Sorgfalt und Aufmerksamkeit zu tun, daher empfehle ich es sehr.


Bevor ich weiter gehe, möchte ich die folgenden Annahmen treffen.


Sie können die Logik der Arbeit mit der Datenbank der meisten Anwendungen in drei Teile unterteilen:


  1. Migrationen - Ändern des Datenbankschemas (Tabellen). Nehmen wir an, wir führen sie immer in einem Thread aus.
  2. Geschäftslogik - direkte Arbeit mit Daten (in Benutzertabellen), arbeitet ständig und wettbewerbsfähig mit denselben Daten.
  3. Datenmigrationen - Ändern Sie keine Datenschemata, sie funktionieren im Wesentlichen wie Geschäftslogik. Wenn wir über Geschäftslogik sprechen, meinen wir standardmäßig auch Datenmigrationen.

Ausfallzeit ist ein Zustand, in dem ein Teil unserer Geschäftslogik für eine für den Benutzer spürbare Zeit nicht verfügbar ist / fällt / geladen wird. Nehmen wir an, dies sind einige Sekunden.


Das Fehlen von Ausfallzeiten kann eine kritische Bedingung für ein Unternehmen sein, an die sich alle Anstrengungen halten müssen.


Rollout-Prozess


Die Hauptanforderungen beim Rollout:


  1. Wir haben eine Arbeitsbasis.
  2. Wir haben mehrere Maschinen, auf denen sich die Geschäftslogik dreht.
  3. Autos mit Geschäftslogik sind hinter dem Balancer versteckt.
  4. Unsere Anwendung funktioniert gut vor, während und nach der fortlaufenden Migration (der alte Code funktioniert korrekt mit dem alten und dem neuen Datenbankschema).
  5. Unsere Anwendung funktioniert gut vor, während und nach der Aktualisierung des Codes auf den Autos (der alte und der neue Code funktionieren korrekt mit dem aktuellen Datenbankschema).

Wenn es eine große Anzahl von Änderungen gibt und der Rollout diese Bedingungen nicht mehr erfüllt, wird er in die erforderliche Anzahl kleinerer Rollouts unterteilt, die diese Bedingungen erfüllen. Andernfalls treten Ausfallzeiten auf.


Direkte Einführung:


  1. überflutete die Migration;
  2. entfernte einen Computer vom Balancer, aktualisierte den Computer und startete neu, gab den Computer an den Balancer zurück;
  3. wiederholte den vorherigen Schritt, um alle Autos zu aktualisieren.

Die umgekehrte Rollout-Reihenfolge ist für das Löschen von Tabellen und Spalten in einer Tabelle relevant, wenn wir automatisch Migrationen gemäß dem geänderten Schema erstellen und das Vorhandensein aller Migrationen zu CI überprüfen:


  1. entfernte einen Computer vom Balancer, aktualisierte den Computer und startete neu, gab den Computer an den Balancer zurück;
  2. wiederholte den vorherigen Schritt, um alle Autos zu aktualisieren;
  3. überflutete die Migration.

Theorie


Postgres ist eine ausgezeichnete Datenbank. Wir können eine Anwendung schreiben, die dieselben Daten in Hunderten und Tausenden von Streams schreibt und liest, und mit hoher Wahrscheinlichkeit können wir sicher sein, dass unsere Daten gültig bleiben und im Allgemeinen nicht vollständig ACID beschädigt werden. Postgres implementiert verschiedene Mechanismen, um dies zu erreichen. Einer davon ist das Blockieren.


Postgres hat verschiedene Arten von Sperren. Weitere Details finden Sie hier . Als Teil des Themas werde ich nur auf Sperren auf Tabellen- und Aufzeichnungsebene eingehen.


Sperren auf Tabellenebene


Auf Tabellenebene verfügt Postgres über verschiedene Arten von Sperren . Das Hauptmerkmal besteht darin, dass Konflikte auftreten, dh, zwei Vorgänge mit widersprüchlichen Sperren können nicht gleichzeitig ausgeführt werden:


ACCESS SHAREROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE
ACCESS SHAREX.
ROW SHAREX.X.
ROW EXCLUSIVEX.X.X.X.
SHARE UPDATE EXCLUSIVEX.X.X.X.X.
SHAREX.X.X.X.X.
SHARE ROW EXCLUSIVEX.X.X.X.X.X.
EXCLUSIVEX.X.X.X.X.X.X.
ACCESS EXCLUSIVEX.X.X.X.X.X.X.X.

Beispielsweise müssen der ALTER TABLE tablename ADD COLUMN newcolumn integer und der ALTER TABLE tablename ADD COLUMN newcolumn integer SELECT COUNT(*) FROM tablename einzeln ausgeführt werden, da wir sonst nicht herausfinden können, welche Spalten an COUNT(*) .


Bei Django-Migrationen (vollständige Liste unten) gibt es die folgenden Vorgänge und die entsprechenden Sperren:


BlockierenOperationen
ACCESS EXCLUSIVECREATE SEQUENCE , DROP SEQUENCE , CREATE TABLE , DROP TABLE , ALTER TABLE , DROP INDEX
SHARECREATE INDEX
SHARE UPDATE EXCLUSIVECREATE INDEX CONCURRENTLY , DROP INDEX CONCURRENTLY CREATE INDEX CONCURRENTLY , DROP INDEX CONCURRENTLY ALTER TABLE VALIDATE CONSTRAINT

Von den Kommentaren haben nicht alle ALTER TABLE ACCESS EXCLUSIVE Locking, auch Django-Migrationen haben nicht CREATE INDEX CONCURRENTLY und ALTER TABLE VALIDATE CONSTRAINT , aber sie werden etwas später für eine sicherere Alternative zu Standardoperationen benötigt.


Wenn Migrationen nacheinander in einem Thread ausgeführt werden, sieht alles gut aus, da die Migration nicht mit einer anderen Migration in Konflikt steht, unsere Geschäftslogik jedoch nur während der Migration und des Konflikts funktioniert.


BlockierenOperationenKonflikte mit SperrenKonflikte mit Operationen
ACCESS SHARESELECTACCESS EXCLUSIVEALTER TABLE , DROP INDEX
ROW SHARESELECT FOR UPDATEACCESS EXCLUSIVE , EXCLUSIVEALTER TABLE , DROP INDEX
ROW EXCLUSIVEINSERT , UPDATE , DELETEACCESS EXCLUSIVE , EXCLUSIVE , SHARE ROW EXCLUSIVE , SHAREALTER TABLE , DROP INDEX , CREATE INDEX

Zwei Punkte können hier zusammengefasst werden:


  1. Wenn es eine Alternative mit einfacherem Sperren gibt, können Sie sie als CREATE INDEX und CREATE INDEX CONCURRENTLY .
  2. Die meisten Migrationen zur Änderung des Datenschemas stehen in Konflikt mit der Geschäftslogik. Darüber hinaus stehen sie im Widerspruch zu ACCESS EXCLUSIVE . Das heißt, wir können SELECT nicht einmal SELECT während diese Sperre gedrückt gehalten wird, und erwarten hier möglicherweise eine Ausfallzeit, außer in dem Fall, dass dieser Vorgang nicht sofort funktioniert und unsere Ausfallzeit auftritt ein paar Sekunden.

Es muss eine Auswahl geben, oder wir vermeiden immer ACCESS EXCLUSIVE , ACCESS EXCLUSIVE wir erstellen neue Platten und kopieren die Daten dort - zuverlässig, aber lange Zeit für eine große Datenmenge, oder wir machen ACCESS EXCLUSIVE so schnell wie möglich und machen zusätzliche Warnungen vor Ausfallzeiten - es ist potenziell gefährlich, aber schnell.


Sperren aufzeichnen


Auf der Aufzeichnungsebene gibt es auch Sperren https://www.postgresql.org/docs/current/static/explicit-locking.html#LOCKING-ROWS . Sie stehen ebenfalls in Konflikt, wirken sich jedoch nur auf unsere Geschäftslogik aus:


FOR KEY SHAREFOR SHAREFOR NO KEY UPDATEFOR UPDATE
FOR KEY SHAREX.
FOR SHAREX.X.
FOR NO KEY UPDATEX.X.X.
FOR UPDATEX.X.X.X.

Dies ist der Hauptpunkt bei Datenmigrationen. Wenn wir also eine UPDATE -Datenmigration auf der gesamten Platte durchführen, wartet der Rest der Geschäftslogik, die die Daten aktualisiert, auf die Freigabe der Sperre und überschreitet möglicherweise unseren Ausfallzeitschwellenwert. Daher ist es besser, Aktualisierungen in Teilen für Datenmigrationen durchzuführen. Es ist auch erwähnenswert, dass bei Verwendung komplexerer SQL-Abfragen für Datenmigrationen die Aufteilung in Teile schneller funktionieren kann, da ein optimalerer Plan und optimale Indizes verwendet werden können.


Die Reihenfolge der Operationen


Ein weiteres wichtiges Wissen ist, wie Operationen ausgeführt werden, wann und wie sie Sperren aufheben und aufheben:


Bild


Hier können Sie folgende Elemente hervorheben:


  1. Ausführungszeit des Vorgangs - für die Migration ist es die Zeit, in der die Sperre gehalten wird. Wenn die schwere Sperre längere Zeit gehalten wird, tritt eine Ausfallzeit auf. ALTER TABLE ADD COLUMN SET DEFAULT kann beispielsweise mit CREATE INDEX oder ALTER TABLE ADD COLUMN SET DEFAULT (in Postgres 11 ist dies besser).
  2. Die Wartezeit für widersprüchliche Sperren - das heißt, die Migration wartet, bis alle widersprüchlichen Anforderungen erfüllt sind. Zu diesem Zeitpunkt warten neue Anforderungen auf unsere Migration. Langsame Anforderungen können hier sehr gefährlich sein, entweder einfach nicht optimal oder analytisch. Daher sollten währenddessen keine langsamen Anforderungen auftreten Migration.
  3. Die Anzahl der Anfragen pro Sekunde - Wenn viele Anfragen über einen längeren Zeitraum bearbeitet werden, können freie Verbindungen schnell beendet werden und anstelle eines problematischen Ortes kann die gesamte Datenbank in Ausfallzeiten geraten (es gibt nur ein Verbindungslimit für den Superuser). Hier müssen Sie langsame Anfragen vermeiden und die Anzahl der Anfragen reduzieren Starten Sie beispielsweise Migrationen während der minimalen Auslastung und trennen Sie kritische Komponenten mit ihren eigenen Datenbanken in verschiedene Dienste.
  4. Es gibt viele Migrationsvorgänge in einer Transaktion. Je mehr Vorgänge in einer Transaktion ausgeführt werden, desto länger wird die schwere Sperre gehalten. Daher ist es besser, schwere Operationen zu trennen, keine ALTER TABLE VALIDATE CONSTRAINT oder Datenmigrationen in einer Transaktion mit einer schweren Sperre.

Zeitüberschreitungen


lock_timeout verfügt über Einstellungen wie lock_timeout und statement_timeout , die den Beginn von Migrationen sowohl vor schlecht geschriebener Migration als auch vor schlechten Bedingungen schützen können, unter denen eine Migration ausgelöst werden kann. Sie können sowohl global als auch für die aktuelle Verbindung installiert werden.


SET lock_timeout TO '2s' , vermeiden Sie Ausfallzeiten, wenn Sie vor der Migration auf langsame Anforderungen / Transaktionen warten: https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-LOCK-TIMEOUT .


SET statement_timeout TO '2s' vermeidet Ausfallzeiten beim Starten einer starken Migration mit einer starken Sperre: https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-STATEMENT-TIMEOUT .


Deadlocks


Bei Deadlocks bei Migrationen geht es nicht um Ausfallzeiten, aber es ist nicht angenehm, wenn die Migration geschrieben wird. In einer Testumgebung funktioniert es einwandfrei, aber beim Rolling auf dem Produkt werden Deadlocks abgefangen. Die Hauptprobleme können eine große Anzahl von Vorgängen in einer Transaktion und einem Fremdschlüssel sein, da in beiden Tabellen Sperren erstellt werden. Daher ist es besser, die Migrationsvorgänge zu trennen. Je atomarer, desto besser.


Datensatzspeicher


Postgres speichert Werte verschiedener Typen auf unterschiedliche Weise : Wenn Typen auf unterschiedliche Weise gespeichert werden, erfordert die Konvertierung zwischen ihnen eine vollständige Neufassung aller Werte. Glücklicherweise werden einige Typen auf dieselbe Weise gespeichert und müssen beim Ändern nicht neu geschrieben werden. Beispielsweise werden Zeilen unabhängig von ihrer Größe gleich gespeichert, und das Verringern / Erhöhen der Abmessung einer Zeile erfordert kein Umschreiben. Beim Verringern muss jedoch überprüft werden, ob alle Zeilen eine kleinere Größe nicht überschreiten. Andere Typen können ebenfalls auf ähnliche Weise gespeichert werden und haben ähnliche Eigenschaften.


Multiversion Concurrency Control (MVCC)


Gemäß der Dokumentation basiert die Postgres-Konsistenz auf Datenmultiversion, dh, jede Transaktion und Operation sieht ihre eigene Version der Daten. Diese Funktion kommt mit dem Konkurrenzzugriff perfekt zurecht und bietet auch einen interessanten Effekt, wenn ein Schema wie das Hinzufügen und Entfernen von Spalten nur dann geändert wird, wenn keine zusätzlichen Operationen zum Ändern von Daten, Indizes oder Konstanten vorhanden sind. Danach werden die Einfüge- und Aktualisierungsoperationen auf niedriger Ebene neu erstellt Datensätze mit allen erforderlichen Werten. Durch Löschen wird der entsprechende Datensatz als gelöscht markiert. VACUUM oder AUTO VACUUM ist für die Reinigung der verbleibenden Rückstände verantwortlich.


Django Beispiel


Wir haben jetzt eine Vorstellung davon, wovon Ausfallzeiten abhängen können und wie sie vermieden werden können. Bevor Sie jedoch Wissen anwenden, können Sie sich ansehen, was Django sofort bietet ( https://github.com/django/django/blob/2.1.2/django) /db/backends/base/schema.py und https://github.com/django/django/blob/2.1.2/django/db/backends/postgresql/schema.py ):


Betrieb
1CREATE SEQUENCE
2DROP SEQUENCE
3CREATE TABLE
4DROP TABLE
5ALTER TABLE RENAME TO
6ALTER TABLE SET TABLESPACE
7ALTER TABLE ADD COLUMN [SET DEFAULT] [SET NOT NULL] [PRIMARY KEY] [UNIQUE]
8ALTER TABLE ALTER COLUMN [TYPE] [SET NOT NULL|DROP NOT NULL] [SET DEFAULT|DROP DEFAULT]
9ALTER TABLE DROP COLUMN
10ALTER TABLE RENAME COLUMN
11ALTER TABLE ADD CONSTRAINT CHECK
12ALTER TABLE DROP CONSTRAINT CHECK
13ALTER TABLE ADD CONSTRAINT FOREIGN KEY
14ALTER TABLE DROP CONSTRAINT FOREIGN KEY
15ALTER TABLE ADD CONSTRAINT PRIMARY KEY
16ALTER TABLE DROP CONSTRAINT PRIMARY KEY
17ALTER TABLE ADD CONSTRAINT UNIQUE
18ALTER TABLE DROP CONSTRAINT UNIQUE
19CREATE INDEX
20DROP INDEX

Django deckt meine Migrationsbedürfnisse sehr gut ab. Jetzt können wir mit unserem Wissen sichere und gefährliche Vorgänge für Migrationen ohne Ausfallzeiten besprechen.


Wir werden sicherere Migrationen mit SHARE UPDATE EXCLUSIVE Locking oder ACCESS EXCLUSIVE , was sofort funktioniert.
Wir werden gefährliche Migrationen mit SHARE und ACCESS EXCLUSIVE Sperren aufrufen, die viel Zeit in ACCESS EXCLUSIVE nehmen.


Ich werde im Voraus einen nützlichen Link zur Dokumentation mit großartigen Beispielen hinterlassen.


Erstellen und löschen Sie eine Tabelle


CREATE SEQUENCE , DROP SEQUENCE , CREATE TABLE , DROP TABLE können als sicher bezeichnet werden, da die Geschäftslogik entweder nicht mehr mit der migrierten Tabelle funktioniert und das Löschen einer Tabelle mit FOREIGN KEY etwas später erfolgt.


Stark unterstützte Arbeitsblattvorgänge


ALTER TABLE RENAME TO - Ich kann es nicht als sicher bezeichnen, da es schwierig ist, eine Logik zu schreiben, die mit einer solchen Tabelle vor und nach der Migration funktioniert.


ALTER TABLE SET TABLESPACE - unsicher, da es die Platte physisch bewegt, und dies kann bei einem großen Volumen lange dauern.


Andererseits sind diese Vorgänge äußerst selten. Alternativ können Sie die Erstellung einer neuen Tabelle und das Kopieren von Daten in diese anbieten.


Spalten erstellen und löschen


ALTER TABLE ADD COLUMN , ALTER TABLE DROP COLUMN - kann als sicher bezeichnet werden (Erstellung ohne DEFAULT / NOT NULL / PRIMARY KEY / UNIQUE), da die Geschäftslogik entweder nicht mit einer migrierten Spalte funktioniert, das Verhalten beim Löschen einer Spalte mit FOREIGN KEY, andere Konstanten und Indizes werden später kommen.


ALTER TABLE ADD COLUMN SET DEFAULT , ALTER TABLE ADD COLUMN SET NOT NULL , ALTER TABLE ADD COLUMN PRIMARY KEY nullbare Spalten und weitere Änderungen.


Es ist erwähnenswert, dass das schnellere SET DEFAULT in Postgres 11 als sicher angesehen werden kann, aber es wird in Django nicht sehr nützlich, da Django SET DEFAULT nur zum Füllen der Spalte verwendet und dann DROP DEFAULT und im Intervall zwischen Migration und Aktualisierung von Maschinen mit erstellt In der Geschäftslogik können Datensätze erstellt werden, in denen keine Standardeinstellungen vorhanden sind, dh die Datenmigration.


Stark unterstützte Vorgänge in einem Arbeitsblatt


ALTER TABLE RENAME COLUMN - Ich kann es auch nicht als sicher bezeichnen, da es schwierig ist, eine Logik zu schreiben, die mit einer solchen Spalte vor und nach der Migration funktioniert. Vielmehr wird dieser Vorgang auch nicht häufig sein, da alternativ vorgeschlagen werden kann, eine neue Spalte zu erstellen und Daten in diese zu kopieren.


Spaltenwechsel


ALTER TABLE ALTER COLUMN TYPE - Der Vorgang kann sowohl gefährlich als auch sicher sein. Sicher, wenn postgres nur das Schema ändert und die Daten bereits im erforderlichen Format gespeichert sind und keine zusätzlichen Typprüfungen erforderlich sind, zum Beispiel:


  • varchar(LESS) von varchar(LESS) zu varchar(MORE) ;
  • varchar(ANY) von varchar(ANY) zu text ;
  • numeric(LESS, SAME) von numeric(LESS, SAME) zu numeric(MORE, SAME) .

ALTER TABLE ALTER COLUMN SET NOT NULL ist gefährlich, da es die darin enthaltenen Daten durchläuft und nach NULL ALTER TABLE ALTER COLUMN SET NOT NULL Glücklicherweise kann dieses Konstrukt durch ein anderes ersetzt werden. CHECK IS NOT NULL . Es ist erwähnenswert, dass dieser Ersatz zu einem anderen Schema führt, jedoch mit identischen Eigenschaften.


ALTER TABLE ALTER COLUMN DROP NOT NULL , ALTER TABLE ALTER COLUMN SET DEFAULT , ALTER TABLE ALTER COLUMN DROP DEFAULT - sichere Operationen.


Erstellen und Löschen von Indizes und Konstanten


ALTER TABLE ADD CONSTRAINT CHECK und ALTER TABLE ADD CONSTRAINT FOREIGN KEY sind unsichere Vorgänge, können jedoch als NOT VALID und dann als ALTER TABLE VALIDATE CONSTRAINT .


ALTER TABLE ADD CONSTRAINT PRIMARY KEY und ALTER TABLE ADD CONSTRAINT UNIQUE unsicher, da sie einen eindeutigen Index im Inneren erstellen. Sie können jedoch einen eindeutigen Index als CONCURRENTLY erstellen und dann die entsprechende Konstante mithilfe eines vorgefertigten Index über USING INDEX .


CREATE INDEX ist eine unsichere Operation, aber ein Index kann als CONCURRENTLY .


ALTER TABLE DROP CONSTRAINT CHECK , ALTER TABLE DROP CONSTRAINT FOREIGN KEY , ALTER TABLE DROP CONSTRAINT PRIMARY KEY , ALTER TABLE DROP CONSTRAINT UNIQUE , DROP INDEX - sichere Operationen.


Es ist erwähnenswert, dass ALTER TABLE ADD CONSTRAINT FOREIGN KEY und ALTER TABLE DROP CONSTRAINT FOREIGN KEY zwei Tabellen gleichzeitig sperren.


Wissen in Django anwenden


Django hat eine Operation in Migrationen, um SQL auszuführen: https://docs.djangoproject.com/de/2.1/ref/migration-operations/#django.db.migrations.operations.RunSQL . Über diese state_operations können Sie die erforderlichen state_operations und alternative Vorgänge für Migrationen anwenden, wobei Sie state_operations - die Migration, die wir ersetzen.


Dies funktioniert gut für den Code, obwohl zusätzliches Schreiben erforderlich ist. Sie können die Dirty Work jedoch im DB-Backend belassen, z. B. https://github.com/tbicr/django-pg-zero-downtime-migrations/blob/master/django_zero_downtime_migrations_postgres_backend/schema .py sammelt die beschriebenen Vorgehensweisen und ersetzt unsichere Vorgänge durch sichere Gegenstücke. Dies funktioniert für Bibliotheken von Drittanbietern.


Am Ende


Diese Praktiken ermöglichten es mir, ein identisches Schema, das von django erstellt wurde, sofort zu erhalten, mit Ausnahme des Ersetzens des Konstrukts CHECK IS NOT NULL anstelle von NOT NULL und einiger Konstruktnamen (z. B. für ALTER TABLE ADD COLUMN UNIQUE und eine Alternative). Ein weiterer Kompromiss kann die mangelnde Transaktionsfähigkeit für alternative Migrationsvorgänge sein, insbesondere wenn CREATE INDEX CONCURRENTLY und ALTER TABLE VALIDATE CONSTRAINT .


Wenn Sie nicht über postgres hinausgehen, gibt es viele Möglichkeiten, das Datenschema zu ändern, und sie können unter bestimmten Bedingungen in Kombination variiert werden:


  • mit jsonb als schamlose lösung
  • die Möglichkeit, Ausfallzeiten zu vermeiden
  • Anforderung, Migrationen ohne Ausfallzeiten durchzuführen

Auf jeden Fall hoffe ich, dass sich das Material als nützlich erwiesen hat, um entweder die Betriebszeit zu erhöhen oder das Bewusstsein zu erweitern.

Source: https://habr.com/ru/post/de425063/


All Articles