Wenn gleichzeitig viele Vorgänge ausgeführt werden, um das Datenbankschema zu ändern, kann der Dienst bei der Aufzeichnung nicht ordnungsgemäß funktionieren. Der Entwickler Vladimir Kolyasinsky erklärte, welche Vorgänge in PostgreSQL langfristige Sperren erfordern und wie das Yandex.Connect-Team während solcher Vorgänge fast 100% Schreibzugriff auf den Service bietet. Darüber hinaus lernen Sie die Bibliothek für Django kennen, mit der ein Teil der beschriebenen Prozesse automatisiert werden soll.
Wir haben schwere Lasten, Tausende von RPS und Ausfallzeiten in wenigen Minuten, ganz zu schweigen von mehr Zeit, sind inakzeptabel. Migrationen müssen nahtlos für den Benutzer erfolgen. Und mit solchen Lasten ist es nicht möglich, um vier Uhr morgens aufzustehen, etwas zu rollen, wenn keine Ladung vorhanden ist, und wieder ins Bett zu gehen - weil die Ladung rund um die Uhr geht.
- Guten Abend allerseits! Mein Name ist Vladimir, ich arbeite seit fünf Jahren bei Yandex. In den letzten zwei Jahren habe ich interne Dienste und Dienste für Organisationen entwickelt.
Ein wenig darüber, was diese Dienste für Organisationen sind. Wir nutzen seit langem eine große Anzahl interner Dienste: ein Wiki zum Speichern und Austauschen von Daten, einen Messenger für die schnelle Kommunikation mit Kollegen, einen Tracker für die Organisation des Arbeitsprozesses, Formulare für die Durchführung von Umfragen von innen und außen sowie viele andere Dienste.
Vor einiger Zeit haben wir entschieden, dass unsere Dienstleistungen cool sind und nicht nur innerhalb von Yandex, sondern auch außerhalb von Yandex nützlich sein können. Wir haben begonnen, sie auf eine einzige Yandex.Connect-Plattform zu bringen und dort vorhandene externe Dienste wie Mail für eine Domain hinzuzufügen.

Ich entwickle derzeit den Form Designer und das Wiki. Der verwendete Stapel besteht hauptsächlich aus Diensten, die in Python der zweiten und dritten Version geschrieben wurden. Django 1.9-1.11. Als Datenbank ist das meiste davon PostgreSQL. Es ist auch Sellerie mit MongoDB und SQS als Makler. All dies funktioniert in Docker.
Kommen wir zu dem Problem, mit dem wir konfrontiert sind. Dienste sind beliebt, sie werden täglich von Hunderttausenden von Menschen verwendet, Daten werden gesammelt, Tabellen werden immer häufiger und im Laufe der Zeit stören viele Vorgänge zum Ändern von Datenbankschemata, die gestern von Benutzern unbemerkt ausgeführt wurden, den normalen Betrieb von Diensten.
Heute werden wir darüber sprechen, wie wir mit solchen Situationen umgehen und wie wir eine hohe Verfügbarkeit von Lese- und Schreibdiensten erreichen.
Betrachten wir zunächst, für welche Operationen mit PostgreSQL lange Sperren für die Tabelle erforderlich sind. Mit Sperren meine ich jede Art von Sperre, die den normalen Betrieb der Tabelle stört - sei es der exklusive Zugriff, der das Schreiben und Lesen stört, oder schwächere Sperrstufen, die nur das Schreiben verhindern.
Als nächstes werden wir sehen, wie Sperren während solcher Operationen vermieden werden. Dann werden wir darüber sprechen, welche Operationen mit PostgreSQL anfangs schnell sind und keine langen Sperren erfordern. Lassen Sie uns am Ende über unsere Bibliothek zero_downtime_migrations sprechen, mit der wir einige der zuvor beschriebenen Techniken automatisieren, um lange Sperren zu vermeiden.
Vorgänge, die eine lange Sperre erfordern:

Index erstellen. Standardmäßig werden Lesevorgänge in der Tabelle nicht blockiert, aber alle Schreibvorgänge werden für die gesamte Zeit, in der der Index erstellt wird, blockiert. Dementsprechend ist der Dienst schreibgeschützt.
Zu diesen Vorgängen gehört auch das Hinzufügen einer neuen Spalte mit einem Standardwert, da PostgreSQL unter der Haube die gesamte Tabelle überschreibt und für diese Zeit sowohl zum Lesen als auch zum Schreiben blockiert wird. Außerdem werden alle Indizes überschrieben.
Über das Ändern des Spaltentyps - Ähnliches passiert, die Platte wird auch wieder überschrieben. Es ist zu beachten, dass dies nicht nur bei großen Tabellen lange dauert, sondern auch für kurze Zeit bis zu das Doppelte des von der Tabelle belegten freien Speichers erfordert.
Außerdem erfordert die VACUUM FULL-Operation dieselbe Sperrstufe wie die vorherigen Operationen - dies ist exklusiver Zugriff. VACUUM FULL blockiert auch alle Lese- und Schreibvorgänge in die Tabelle.
Die letzten beiden Vorgänge fügen der Spalte eindeutige Eigenschaften hinzu und fügen im Allgemeinen CONSTRAINT hinzu. Sie müssen auch für die Dauer der Datenüberprüfung gesperrt werden, obwohl sie viel weniger Zeit in Anspruch nehmen als die zuvor betrachteten, da sie keine Tabellen unter der Haube überschreiben.


Index erstellen. Hier ist es ganz einfach, es kann mit dem Schlüsselwort CONCURRENTLY erstellt werden. Was ist der Unterschied? Dieser Vorgang dauert länger, da nicht nur ein, sondern mehrere Durchgänge durch die Tabelle ausgeführt werden. Außerdem wird auf den Abschluss aller aktuellen Vorgänge gewartet, die möglicherweise den Index ändern können. Und es kann auch fehlschlagen - zum Beispiel, wenn beim Erstellen eines eindeutigen Index ein eindeutiger Index verletzt wird. Dann wird der Index als ungültig markiert und muss gelöscht und neu erstellt werden. Der Befehl REINDEX wird nicht empfohlen, da er genauso funktioniert wie der reguläre CREATE INDEX, dh die Tabelle wird zum Schreiben gesperrt.
In Bezug auf das Löschen des Index - ab Version 9.3 können Sie den Index auch KONKURRENT löschen, um ein Blockieren während seines Löschens zu vermeiden, obwohl dies im Allgemeinen ein so schneller Vorgang ist.

Schauen wir uns an, wie Sie eine neue Spalte mit einem Standardwert hinzufügen. Hier ist eine Standardoperation, die ausgeführt wird, wenn wir einen solchen Befehl ausführen möchten, einschließlich Django, der eine solche Operation ausführt.
Wie kann ich es umschreiben, um ein Überschreiben der Tabelle zu vermeiden? Fügen Sie in einer Transaktion zunächst eine neue Spalte ohne Standardwert hinzu und fügen Sie in einer separaten Anforderung einen Standardwert hinzu. Was ist der Unterschied hier? Wenn wir einer vorhandenen Spalte einen Standardwert hinzufügen, werden die vorhandenen Daten in der Tabelle nicht geändert. Nur Metadatenänderungen. Das heißt, für alle neuen Zeilen ist dieser Standardwert bereits garantiert. Es bleibt uns überlassen, alle vorhandenen Zeilen zu aktualisieren, die sich zum Zeitpunkt der Ausführung dieses Befehls in der Tabelle befanden. Was wir in Stapeln von mehreren tausend Kopien tun werden, um eine große Datenmenge nicht für lange Zeit zu blockieren.
Nachdem wir alle Daten aktualisiert haben, muss SET NOT NULL nur ausgeführt werden, wenn eine NOT NULL-Spalte erstellt wird. Wenn wir nicht erstellen, dann nicht. Auf diese Weise können Sie vermeiden, dass die Tabelle überschrieben wird, wenn Sie diese Art von Änderung vornehmen.
Eine solche Folge von Befehlen dauert länger als die Ausführung eines regulären Befehls, da sie von der Größe der Tabelle und der Anzahl der darin enthaltenen Indizes abhängt. Der übliche Befehl blockiert einfach alle Operationen und überschreibt die Tabelle unabhängig von der Last, da derzeit keine Last vorhanden ist. Dies ist jedoch nicht so wichtig, da die Tabelle während des Vorgangs zum Lesen und Schreiben zur Verfügung steht. Es dauert lange, Sie müssen nur folgen und das war's.

Informationen zum Ändern des Spaltentyps. Der Ansatz ähnelt dem Hinzufügen einer Spalte mit einem Standardwert. Wir fügen zuerst eine separate Spalte des Typs hinzu, den wir benötigen, und fügen dann Trigger hinzu, um Daten in der ursprünglichen Spalte so zu ändern, dass sie gleichzeitig in beide Spalten in eine neue Spalte mit dem von uns benötigten Datentyp geschrieben werden. Alle neuen Einträge werden sofort in diese beiden Spalten verschoben. Wir müssen alle vorhandenen aktualisieren. Was wir in Teilen tun, wie auf der vorherigen Folie, war ähnlich.
Danach bleibt es in einer Transaktion, den Trigger zu löschen, die alte Spalte zu löschen und die alte Spalte in eine neue umzubenennen. So haben wir das gleiche Ergebnis erzielt: Wir haben den Typ der Spalte geändert, während das Sperren der Tabelle nicht lange dauerte.

Informationen zum Hinzufügen einer eindeutigen Spalte. Zum Zeitpunkt der Erstellung wird eine Sperre vorgenommen. Es kann vermieden werden, wenn Sie wissen, dass die Eindeutigkeit in PostgreSQL durch die Erstellung eines eindeutigen Index garantiert wird. Wir selbst können den erforderlichen eindeutigen Index mit CONCURRENTLY erstellen. Erstellen Sie nach dem Erstellen dieses Index CONSTRAINT mit diesem Index. Danach verschwindet die Definition des Anfangsindex aus der Tabelle, und das Ergebnis, das die Definition der Tabelle anzeigt, unterscheidet sich nach Ausführung dieser beiden Operationen nicht mehr.

Und im Allgemeinen beim Hinzufügen von CONSTRAINT. Mit dieser Technik können Sie das Blockieren beim Überprüfen von Daten vermeiden. Wir fügen zuerst CONSTRAINT mit dem Schlüsselwort NOT VALID hinzu. Dies bedeutet, dass diese CONSTRAINT nicht für alle Zeilen in der Tabelle ausgeführt werden kann. Gleichzeitig wird diese CONSTRAINT für alle neuen Zeilen bereits angewendet, und entsprechende Ausnahmen werden ausgelöst, wenn sie nicht ausgeführt werden.
Wir können nur alle vorhandenen Werte validieren, was mit einem separaten Befehl VALIDATE CONSTRAINT durchgeführt werden kann, und gleichzeitig stört dieser Befehl weder das Lesen noch das Schreiben in die Tabelle. Eine Tabelle für diese Zeit wird verfügbar sein.
Vorgänge, die in PostgreSQL zunächst schnell funktionieren und keine langen Sperren erfordern:

Eine dieser Operationen besteht darin, eine Spalte ohne Standardwerte und ohne Einschränkungen hinzuzufügen. Da keine Änderungen an der Tabelle selbst vorgenommen werden, ändern sich nur die Metadaten. Und alle NULL-Werte, die wir als Ergebnis von SELECT sehen, werden einfach in der Ausgabe gemischt.
Das Hinzufügen von Standardwerten zu einer vorhandenen Beschriftung ist ein schneller Vorgang, da sich nur Metadaten ändern. Die Tabelle und die Sperre werden buchstäblich für die wenigen Millisekunden verwendet, die zur Eingabe dieser Informationen erforderlich sind.
Auch das schnelle Einstellen von SET NOT NULL dauert hier etwas länger als zuvor beschrieben, etwa einige Sekunden pro Tabelle mit 30 Millionen Datensätzen. Diese Zeit kann auch vermieden werden, wenn es darauf ankommt.
Das Umbenennen einer Spalte und das Ändern der Länge einer Spalte führt auch nicht zum Überschreiben einer Spalte. Das Löschen einer Spalte und im Allgemeinen vieler Entitäten in PostgreSQL ist ebenfalls eine schnelle Operation.

In Bezug auf das Hinzufügen einer NOT NULL-Spalte. Um ein Blockieren während der Validierung zu vermeiden, können Sie die zuvor erwähnte Methode ausführen - fügen Sie CONSTRAINT hinzu, das CHECK entspricht (Spalte IST NICHT NULL), NICHT GÜLTIG, und validieren Sie es mit einem separaten Befehl.
Der Unterschied im Allgemeinen besteht darin, dass diese Einschränkung auf Tabellenebene und nicht auf Spaltenebene in der Tabellendefinition besteht. Ein weiterer Unterschied besteht darin, dass die Leistung um etwa ein Prozent beeinträchtigt werden kann. In diesem Fall erfolgt keine Blockierung, wenn der Dienst stark ausgelastet ist. Selbst einige Sekunden Blockierung können dazu führen, dass sich eine große Anzahl von Transaktionen ansammelt und ein Problem mit dem Dienst auftritt.

Das Löschen von Daten in PostgreSQL ist im Allgemeinen ein schneller Vorgang, da die Daten nicht sofort gelöscht werden, sondern nur die Spalte in den Attributen der Tabelle als veraltet markiert wird und die Daten tatsächlich erst nach dem Start des nächsten Vakuums gelöscht werden.

Reden wir über die
Bibliothek . Ich spreche von Django, Migration. Im Allgemeinen ist Django eine Bibliothek für Python, ein Webframework. Ursprünglich wurde es erstellt, um schnell Websites wie Nachrichten zu erstellen. Seitdem wurde es erheblich aktualisiert. Es gibt ein ORM-System, mit dem Sie mit Datensätzen in der Datenbank und mit Tabellen kommunizieren können, als wären sie Python-Objekte oder -Klassen. Das heißt, jede Tabelle hat in Python eine eigene Klasse. Wenn wir Änderungen an unserem Python-Code vornehmen, dh neue Attribute wie Spalten zur Tabelle hinzufügen, bemerkt Django beim Erstellen der Migration diese Änderungen und erstellt die Migrationsdateien, um Spiegeländerungen an der Datenbank selbst vorzunehmen, damit sie nicht voneinander abweichen.
Die Bibliothek wurde geschrieben, um einige der zuvor diskutierten Techniken zur Vermeidung langer Sperren auf dem Tisch während solcher Migrationen zu automatisieren. Es funktioniert mit Django seit Version 1.8 bis 2.1 einschließlich und Python von 2.7 bis 3.7 einschließlich.
In Bezug auf die aktuellen Funktionen der Bibliothek wird eine Spalte mit einem Standardwert ohne Sperren hinzugefügt, ob nullbar oder nicht. Dies erstellt einen CONCURRENTLY-Index sowie die Möglichkeit, bei einem Absturz neu zu starten. Wenn wir in der Standard-Django-Implementierung eine Spalte mit einem Standardwert hinzufügen, ist die Tabelle gesperrt, und wenn sie groß ist, kann es meiner Erfahrung nach 40 Minuten dauern, bis sie gesperrt ist. Die Tabelle ist gesperrt, und das war's. Warten Sie, bis die Änderungen kopiert und vorgenommen wurden. 30 Minuten sind vergangen - sie haben den Verbindungsfehler zur Datenbank abgefangen, die Migration fällt ab, die Änderungen werden nicht festgeschrieben, und Sie müssen erneut starten, erneut 40 Minuten warten und die Tabelle für diese Zeit erneut blockieren.

In der Bibliothek können Sie die Migration von dem Ort fortsetzen, an dem sie unterbrochen wurde. Wenn Sie abstürzen und neu starten, wird ein Dialogfeld angezeigt, in dem verschiedene Aktionsoptionen verfügbar sind. Sie können also sagen, dass Sie die Daten weiterhin aktualisieren möchten. Dies ist normalerweise eine Datenaktualisierung, da dies der längste Prozess ist. Die Migration wird einfach dort fortgesetzt, wo sie aufgehört hat. Eine solche Operation dauert auch länger als eine Standardoperation mit Tabellensperre, aber gleichzeitig bleibt der Dienst zu diesem Zeitpunkt betriebsbereit.

Über die Verbindung als Ganzes. Es gibt Dokumentation; Kurz gesagt, Sie müssen die Engine in den Django-Datenbankeinstellungen durch die Engine aus der Bibliothek ersetzen. Es gibt auch verschiedene Mixins, wenn Sie Ihre Motoren zum Verbinden verwenden.

Ein Beispiel für die Arbeit ist das Hinzufügen einer Spalte mit einem Standardwert. Hier fügen wir Spalten mit einem booleschen Wert hinzu, standardmäßig True. Welche Operationen werden vom Standard-SchemaEditor ausgeführt? Die Vorgänge, die Sie sehen können, wenn Sie SQL ausführen, werden migriert. Dies ist sehr nützlich, da aufgrund der Art der Migration nicht immer klar ist, was Django dort tatsächlich ändern kann. Und es ist nützlich zu beginnen und zu sehen, ob die von uns erwarteten Operationen abgeschlossen sind und ob etwas Überflüssiges und Unnötiges da ist.
Welche Befehle führt SchemaEditor aus? Zunächst wird einer Transaktion eine neue Spalte hinzugefügt, der Standardwert wird hinzugefügt. Bis ein solches Update zurückgibt, dass es Null aktualisiert hat, werden die Daten aktualisiert.
Dann wird SET NOT NULL in der Spalte festgelegt und der Standardwert wird gelöscht, wodurch das Verhalten von Django wiederholt wird, das den Standardwert nicht in der Datenbank, sondern auf der logischen Ebene im Code speichert.
Hier gibt es im Allgemeinen auch Raum zum Wachsen. Sie können beispielsweise einen Hilfsindex erstellen, um solche Zeilen mit einem NULL-Wert schnell zu finden, wenn Sie sich der Aktualisierung der gesamten Tabelle nähern.

Sie können auch die maximale ID für die Aktualisierungszeit festlegen, zu der wir die Migration gestartet haben, sodass Sie anhand der ID schnell Werte finden können, die wir noch nicht aktualisiert haben.
Im Allgemeinen entwickelt sich die Bibliothek, wir akzeptieren Pool-Anfragen. Wen kümmert es - mach mit.
Es ist zu beachten, dass Migrationen mit dem Wachstum der DBs eine unvermeidliche Eigenschaft haben, sich zu verlangsamen. Sie müssen verfolgen, welche Sperren die Tabelle nimmt, und SQL-Migrationen ausführen, um zu sehen, welche Vorgänge angewendet werden. In Yandex.Connect verwenden wir diese Bibliothek, sofern die Funktionen dies zulassen. Und wo sie es nicht zulassen, führen wir selbst selbst gefälschte Django-Migrationen durch und führen unsere SQL-Abfragen aus.
So erreichen wir eine hohe Verfügbarkeit von Lese- und Schreibdiensten. Wir haben schwere Lasten, Tausende von RPS und Ausfallzeiten in wenigen Minuten, ganz zu schweigen von mehr Zeit, sind inakzeptabel. Migrationen müssen vom Benutzer unbemerkt erfolgen. Und mit solchen Lasten ist es nicht möglich, um vier Uhr morgens aufzustehen, etwas zu rollen, wenn keine Ladung vorhanden ist, und wieder ins Bett zu gehen - weil die Ladung rund um die Uhr geht.
Es ist erwähnenswert, dass selbst schnelle Operationen in PostgreSQL aufgrund der Funktionsweise der Sperrwarteschlange in PostgreSQL immer noch zu einer Verlangsamung des Dienstes und zu Fehlern führen können.
Stellen Sie sich vor, ein Vorgang wird gestartet, der selbst für einige Millisekunden exklusiven Zugriff erfordert. Ein Beispiel für eine solche Operation ist das Hinzufügen einer Spalte ohne Standardwert. Stellen Sie sich vor, dass es zum Zeitpunkt des Starts in einer anderen Transaktion eine andere lange Operation gibt - beispielsweise SELECT mit Aggregation. In diesem Fall wird unsere Operation für sie anstehen. Dies geschieht, weil der Zugriff auf exklusive Konflikte mit allen anderen Arten von Sperren erfolgt.
Während unser Vorgang des Hinzufügens einer Spalte auf eine Sperre wartet, stehen alle anderen in der Warteschlange und werden erst ausgeführt, wenn sie abgeschlossen ist. Gleichzeitig kann die ausgeführte Operation - SELECT mit Aggregation - nicht mit den anderen in Konflikt stehen, und wenn wir die Spalte nicht erstellt hätten, wären sie nicht in der Warteschlange gestanden, sondern parallel ausgeführt worden.
Diese Situation kann zu großen Problemen beim Dienst führen. Bevor Sie ALTER TABLE oder einen anderen Vorgang starten, für den eine exklusive Zugriffssperre erforderlich ist, müssen Sie daher sicherstellen, dass im Moment keine langen Abfragen in die Datenbank gelangen. Oder Sie können einfach ein sehr kleines Protokoll-Timeout einfügen. Wenn es dann nicht möglich wäre, das Schloss schnell zu öffnen, würde die Operation fallen. Wir könnten es einfach neu starten und die Tabelle für eine lange Zeit nicht sperren, während die Operation auf die Gewährung einer Bewilligung für Sperren wartet. Das ist alles, danke.