Zum Inhalt springen

Blog ·

Die Migration, die nie lief

Ein Migrator, der Erfolg meldet und dabei eine Datei überspringt, ist schlimmer als einer, der abstürzt. Wie eine Sortierung still Migrationen verliert.

Von Provibr

Ein Schemamigrator, der abstürzt, ist ein guter Nachmittag. Sie bekommen einen Stacktrace, eine Zeilennummer und eine Datenbank, die immer noch in einem Zustand ist, den Sie verstehen.

Der schlechte Nachmittag ist der, an dem der Migrator ausgibt, das Schema sei auf dem neuesten Stand, sich mit null beendet und sich irrt. Wir hatten diesen Nachmittag mehr als einmal, aus mehr als einer Ursache, und der schlimmste davon endete damit, dass Produktionscode eine Spalte las, die es nicht gab.

Das haben wir gelernt, aufgeschrieben für alle, die Postgres-Migrationen aus einem Dateiverzeichnis fahren, gleich welches Werkzeug die Dateien erzeugt hat.

Die Entscheidung fällt an einer einzigen Zahl

Die meisten dateibasierten Migratoren führen eine Tabelle darüber, was angewandt wurde. Unserer auch: ein Schema, eine Tabelle, eine Zeile je Migration mit einem Hash und einem Erstellungszeitstempel.

Der Fehler ist die Annahme, diese Tabelle sei eine Menge, die der Migrator befragt. Das ist sie nicht. Lesen Sie den Code, und Sie finden eine Abfrage, die genau eine Zeile auswählt, die neueste nach Zeitstempel, und danach einen einzigen Vergleich je Migrationsdatei: ausführen, wenn der zuletzt angewandte Zeitstempel kleiner ist als der Zeitstempel dieser Datei.

Daraus folgen sofort drei Dinge, und keines davon steht in irgendjemandes Vorstellung.

Der Hash spielt bei der Entscheidung keine Rolle. Er wird geschrieben, wenn eine Migration läuft, und nie wieder gelesen, um irgendetwas zu entscheiden. Ein Hash in der Tabelle, der zu keiner Datei auf der Platte mehr passt, bedeutet meist, dass diese Datei nach ihrer Anwendung bearbeitet wurde, was unordentlich, aber harmlos ist. Es ist nicht so, dass das Werkzeug irgendetwas bemerken würde.

Nur die neueste Zeile zählt. Nicht die Menge der angewandten Dateien, nicht die Anzahl. Eine Zeile.

Sortiert wird nach Zeitstempel, nicht nach Dateiname. Die Zahlen vorne an Ihren Dateinamen sind für Menschen. Führt Ihr Werkzeug einen Sortierwert in einem Manifest, ist dieser Wert die Wahrheit und die Nummer Zierrat.

Zusammengenommen ergibt das die Eigenschaft, die Nachmittage kostet: Eine Migration, deren Zeitstempel nicht größer ist als der neueste angewandte Zeitstempel, wird auf dieser Datenbank nie laufen, niemals. Nicht bei dieser Auslieferung, nicht bei der nächsten, nicht nach zehn Versuchen. Es gibt keinen Fehler, denn aus Sicht des Werkzeugs ist nichts verkehrt.

Wie man wirklich dazu kommt

Es klingt nach etwas, das einem sorgfältigen Team nicht passieren könnte. So ist es uns passiert, zweimal, auf eine Weise, die mit Nachlässigkeit nichts zu tun hatte.

Zwei Zweige, dieselbe Nummer. Zwei Personen fügen parallel je eine Migration hinzu. Beide Werkzeuge vergeben die nächste freie Nummer, beide bekommen dieselbe, beide schreiben einen Zeitstempel. Der Zweig, der als zweiter zusammengeführt wird, bekommt die nächste Nummer, was die naheliegende Reparatur ist und nicht die ganze: Ist sein Zeitstempel weiterhin kleiner als der bereits angewandte, hat die Umbenennung den Dateinamen verändert und nicht die Entscheidung. Die Migration wird übersprungen. Das Symptom war in unserem Fall ein Aufzählungstyp, dem schlicht ein Wert fehlte, auf einer Datenbank, auf der jedes andere Objekt vorhanden und richtig war.

Von Hand gesetzte Zeitstempel, die nicht in der Reihenfolge lagen. Unsere sind von Hand gesetzte runde Zahlen statt einer echten Uhr, was sie lesbar macht und leicht falsch zu bekommen. Eine Migration bekam einen Stempel, der früher lag als zwei bereits angewandte. Der Migrator meldete Erfolg. In der Produktion wäre das ein Typfehler auf jeder einzelnen Integrationszeile gewesen, und so etwas möchte man nicht von einem Kunden erfahren.

Beide Fälle teilen eine Form, die einen Namen verdient. Die Buchführung hängt an einem Sortierwert, nicht an einer Identität. Ein Buch, das an der Identität hängt („habe ich diese Migration angewandt?“), kann nichts überspringen, in welcher Reihenfolge die Dinge auch eintreffen. Ein Buch, das an der Sortierung hängt („wie weit bin ich gekommen?“), kann es, und es ist ein Entwurf, den die meisten Migratoren teilen, weil er den häufigen Weg zu einem einzigen Zeilenlesevorgang macht.

Der Zähler ist nicht die Prüfung

Nach dem ersten Vorfall haben wir die naheliegende Absicherung eingebaut: nach dem Migrieren die Anzahl der Zeilen in der Migrationstabelle ansehen und bestätigen, dass sie gestiegen ist.

Diese Absicherung ist nötig und sie genügt nicht, was wir herausfanden, indem wir zusahen, wie sie bestand, während das, was wir brauchten, fehlte. Ein Migrationslauf brachte die Zeilenzahl von achtundneunzig auf einhundertvier. Sechs Migrationen gingen hinein. Die, auf die es uns ankam, war nicht darunter. Die Zahl stieg, alles meldete Erfolg, und jede eingeloggte Seite im Produkt antwortete mit einer 500 auf eine Spalte, die es nicht gab.

Eine Anzahl sagt nichts darüber, welche Migrationen liefen. Wollen Sie wissen, ob ein Objekt existiert, ist die einzige ehrliche Prüfung, die Datenbank zu fragen, ob dieses Objekt existiert: die Tabellen auflisten, die Tabelle beschreiben, die Aufzählungswerte auswählen. Wir vergleichen die Tabellenliste der gerade migrierten Umgebung mit der, der wir vertrauen.

Es gibt eine Spielart dieses Fehlschlags, die mit Zeitstempeln nichts zu tun hat und dieselbe grüne Ausgabe erzeugt. Unser Migrator reist in einem unserer Dienst-Abbilder mit. Jemand baute nur den anderen Dienst neu, ließ die Migration aus dem veralteten Abbild laufen und bekam ein fröhliches „das Schema ist auf dem neuesten Stand“ von einem Container, der von den beiden fraglichen Migrationen nie gehört hatte. Das Werkzeug log nicht. Es beantwortete eine Frage über die Dateien, die es hatte, und es hatte alte Dateien.

Alles oder nichts, in einer Transaktion

Ein Detail in der Umsetzung unseres Migrators ist es wert, bekannt zu sein, weil es verändert, wie Sie sich erholen: Jede ausstehende Migration läuft in einer einzigen Transaktion, mit dem Buchungseintrag für jede einzelne darin.

Das ist eine gute Nachricht und eine Einschränkung. Die gute Nachricht ist, dass ein Fehlschlag mitten in einer Reihe von sieben Ihnen keine der sieben angewandt hinterlässt statt dreieinhalb. Es gibt keinen halb migrierten Zustand, über den nachzudenken wäre, und genau das wollen Sie in dem Moment, in dem Sie am wenigsten nachdenken können.

Die Einschränkung ist, dass eine Anweisung, die Postgres nicht in einem Transaktionsblock ausführen kann, die ganze Reihe mitreißt, und dass eine sehr lange Migration Sperren für die ganze Reihe hält statt nur für ihre eigene Dauer. Beides hat uns noch nicht getroffen. Beides ist die Art von Sache, die man gelesen haben möchte, bevor es so weit ist.

Eine reparieren, ohne die nächste zu verschlimmern

Nehmen wir an, Sie finden sie: eine Migration in Ihrem Verzeichnis, die diese Datenbank nie ausgeführt hat. Der Reflex ist, das Manifest zu reparieren, den Zeitstempel zu senken oder anzuheben, bis das Werkzeug sie aufgreift, und erneut zu laufen.

Senken Sie ihn nicht, und heben Sie ihn vorsichtig an.

Der Grund ist dieselbe Regel, die das Problem verursacht hat. Senken Sie den Zeitstempel einer Migration, die anderswo bereits angewandt wurde, haben Sie nichts zum Laufen gebracht; Sie haben eine Datei erzeugt, deren Stempel nun unter der Hochwassermarke der Datenbanken liegt, auf denen sie lief, und die nächste Migration, die Sie mit einem Stempel dazwischen schreiben, wird dort übersprungen. Sie haben eine Umgebung repariert und den nächsten Fehlschlag in einer anderen scharf gemacht.

Was funktioniert, ist, das SQL selbst anzuwenden, in einer einzigen Transaktion, und danach dafür zu sorgen, dass die Buchführung die Wirklichkeit abbildet. Der Migrator kann es nicht für Sie erledigen, denn von seiner Seite aus gibt es nichts zu tun.

Was wir jetzt tun

Fünf Regeln, alle billig, alle aufgeschrieben, weil wir sie je einmal nicht befolgt haben.

  1. Ausliefern und Migrieren sind ein Vorgang, nicht zwei. Die Lücke dazwischen ist ein Fenster, in dem neuer Code ein altes Schema liest. Unsere kostete uns eine Verwaltungsseite für jeden Administrator und einen Abrechnungsauftrag, der einen ganzen Nachmittag lang still an Spalten scheiterte, die es noch nicht gab.
  2. Bauen Sie alles neu, bevor Sie migrieren, nicht nur den Dienst, den Sie geändert haben. Der Migrator reist in einem Abbild, und ein Abbild, das Sie nicht neu gebaut haben, enthält die Migrationen, die Sie nicht hinzugefügt haben.
  3. Prüfen Sie die Sortierwerte, nicht die Dateinamen, immer wenn zwei Personen parallel gearbeitet haben. Eine Datei umzubenennen ändert nicht, was das Werkzeug vergleicht.
  4. Überprüfen Sie das Objekt, nicht den Zähler. Fragen Sie nach einer Migration, die eine Spalte hinzufügt, nach dieser Spalte. Das kostet einen Befehl und ist die einzige Prüfung, die nicht ins Leere bestehen kann.
  5. Senken Sie nie einen Sortierwert, der irgendwo bereits angewandt wurde. Es sieht aus wie eine örtliche Reparatur und ist eine Landmine in der Ferne.

Der unbehagliche Faden durch all das ist, dass sich das Werkzeug jedes Mal korrekt verhalten hat. Es ist nie abgestürzt, hat nie etwas beschädigt, hat nie über sein Tun gelogen. Es hat eine engere Frage beantwortet als die, die wir zu stellen glaubten, und einen Satz ausgegeben, der über diese engere Frage wahr war.

Das ist die Klasse von Fehlschlag, gegen die sich Gewohnheiten aufzubauen lohnt, bei Migrationen und überall sonst. Ein Werkzeug, das laut scheitert, lehrt Sie, was es tut. Ein Werkzeug, das eng gelingt, lehrt Sie nichts, bis zu dem Tag, an dem es darauf ankommt.

Fangen Sie mit einem Host und einem Agenten an

Konto erstellen, den Agenten mit einem einzigen Befehl installieren und Ihren ersten Proxmox-Host verbinden. Die Testphase läuft 14 Tage und fragt nicht nach Zahlungsdaten.