- Knock hat ein Verfahren entwickelt, um Postgres, den zentralen Datenspeicher seiner Notification-Workflow-Engine, von AWS RDS Aurora 11.9 auf 15.3 anzuheben und ohne Auswirkungen auf Kunden umzuschalten
- Ohne Maßnahmen vor dem Ausmusterungsdatum von Postgres 11.9 bei Amazon RDS am 29. Februar 2024 hätte Knock ein erzwungenes Upgrade samt Downtime in Kauf nehmen müssen
- In-Place-Upgrades und
pg_dump/pg_restorewurden ausgeschlossen, weil sie lange Unterbrechungen erfordern; stattdessen fiel die Wahl auf logische Replikation mitPUBLICATION/SUBSCRIPTIONauf einer neuen DB - Die Replikationsstrategie wurde je nach Tabellengröße und Schreibmuster aufgeteilt: kleine Tabellen wurden direkt repliziert, große Append-only-Tabellen mit
copy_data = falseund Snapshot-Backfill kombiniert - Der finale Wechsel hielt Verbindungen zu beiden DBs offen, änderte ein Flag, gab laufenden Queries 500 ms Zeit und pausierte anschließend neue Anfragen an die neue DB für 1 Sekunde, um das Risiko von Stale Reads zu reduzieren; so war der Cutover in wenigen Sekunden erledigt
Upgrade-Ziele und Einschränkungen
- Knock ist für seine Notification-Workflow-Engine auf Postgres angewiesen und nutzt Postgres für Workflow-Konfigurationen, Nachrichtenvorlagen, das Sammeln von Millionen Logs sowie das Queuing von Background-Jobs
- Als relationale Datenbank benötigt Postgres bei Upgrades mindestens einen Neustart; Major-Version-Upgrades können wegen Änderungen an der Speicherung von Daten und Indizes auf der Festplatte eine vollständige Abschaltung von mehreren Minuten oder mehr erfordern
- Postgres 11.9, das seit Gründung des Unternehmens im Einsatz war, sollte bei Amazon RDS ausgemustert werden; ohne gesonderte Maßnahmen drohten ein erzwungenes Upgrade und mögliche erzwungene Downtime
- Die Upgrade-Bedingungen waren darauf ausgerichtet, das Betriebsrisiko zu senken
- Sprung auf die möglichst aktuelle Version, Postgres 15.3 für Aurora
- Keine Downtime über 60 Sekunden zulassen, idealerweise 0 System-Downtime
- Abschluss vor Amazons Frist im Februar 2024
- Minimale Kundenauswirkungen, etwa 0 API-Fehlerantworten
- Das Verfahren als Runbook dokumentieren, damit es für das nächste Upgrade wiederverwendbar ist
- Der Sprung von 11.9 auf 15.3 entspricht einem Upgrade über 4 Major-Versionen; viermalige In-Place-Upgrades wurden daher als Option ausgeschlossen
Vorbereitung: Risiken senken und Observability
- Das Postgres-Upgrade wurde angegangen, indem zuerst eine Risikoliste erstellt und dann Risiken mit hoher Auswirkung, die sich im Voraus leicht beseitigen ließen, reduziert wurden
- Lange Downtime
- Datenverlust
- Änderungen der DB-Performance für Application-Workloads
- Änderungen an Häufigkeit oder Verhalten von
VACUUM - Notwendigkeit, Replikations-Slots zu migrieren
- Über die Postgres-Release-Notes wurden Änderungen zwischen Versionen geprüft und Risiken wie geändertes
VACUUM-Verhalten oder die Notwendigkeit von Reindexing bei bestimmten Upgrades identifiziert - Während des Upgrades müssen System- und Datenbankmetriken kontinuierlich geprüft werden
- Max TXN ID zur Vermeidung von Transaction-Wraparound
- DB-CPU-Auslastung
- Wartende Sessions auf der Writer-Instanz
- Query-Latenz
- Latenz der API-Antworten der Anwendung
- Knock überwachte zusätzlich anwendungsspezifische Metriken, etwa die Zeit, bis aus einer API-Anfrage eine Benachrichtigung wird
- Ohne rechtzeitig verfügbare Metriken befindet man sich während des Upgrade-Prozesses praktisch im Blindflug
Ausgeschlossene Ansätze: In-Place-Upgrade und Dump/Restore
- Das In-Place-Upgrade von AWS RDS wird über die AWS-Konsole ausgeführt: AWS stoppt die DB, führt Upgrade-Skripte aus und bringt sie anschließend wieder online
- Je nach Datenmenge und Umfang der Änderungen zwischen Versionen kann dieser Vorgang Minuten bis mehrere Stunden oder länger dauern
- Auch nachdem die DB wieder online ist, können Wartungsarbeiten wie
VACUUModerREINDEXnötig sein, sodass sie nicht sofort vollständig nutzbar ist - Bei
pg_dumpundpg_restoremüssen alle Anwendungen von der bestehenden DB getrennt werden, um ein verlässliches Backup zu erhalten; bei großen DBs dauern Dump und Restore selbst lange - Beide Ansätze wurden ausgeschlossen, weil sie die Downtime-Grenze von Knock voraussichtlich deutlich überschritten hätten
Gewählter Ansatz: Upgrade per logischer Replikation
- Die endgültige Wahl fiel auf logische Replikation mit Postgres-
PUBLICATIONundSUBSCRIPTION - Der grundlegende Ablauf war wie folgt
- Eine neue DB mit der Zielversion von Postgres starten
- Konfiguration, Extensions, Tabellenstruktur, Benutzer usw. übertragen
- Auf der bestehenden DB eine Publication erstellen und auf der neuen DB eine Subscription konfigurieren
- Tabellen zur Publication hinzufügen
- Nach Abschluss der Replikation Tests durchführen, um verbleibende Risiken zu prüfen
- Sobald die neue DB-Konfiguration ausreichend verifiziert ist, die Anwendung auf die neue DB umschalten
- Die bestehende DB entfernen
- Statt ein großes Upgrade auf einmal auszuführen, konnte Knock in schrittweisen Etappen vorgehen und die neue DB mit echten Daten und realem Workload testen
- Sobald die neue DB bereit war, dauerte die Umschaltung selbst nur wenige Sekunden, wodurch Zeitpunkt und Art des Cutovers besser kontrollierbar waren
Kernpunkte der Replikationskonfiguration
- Die logische Replikation von Postgres nutzt Parameter, die für Replikations-Slots erforderlich sind; bei einfachen Anwendungen kann die wichtigste Änderung sein,
wal_levelauflogicalzu setzen - Wer bereits Replikations-Slots für Read Replicas, DB-Failover, Data-Warehouse-Synchronisierung usw. verwendet, muss relevante Parameter wie
max_replication_slotsgemäß Dokumentation anpassen - Die Tabellenstruktur der neuen DB muss mit der bestehenden DB identisch, aber leer sein
- Ein Schema-Snapshot kann mit
pg_dumpallund den Optionen--schema-only,--no-role-passwordserzeugt und mit dem SQL für die neue DB verglichen werden, um Unterschiede zu korrigieren - Beim Erstellen der Publication auf der bestehenden DB und der Subscription auf der neuen DB wurden zentrale Optionen gesetzt
enabled = false: damit die Synchronisierung nicht sofort startetcreate_slot = true: damit Postgres den Replikations-Slot verwaltetcopy_data = true: kopiert standardmäßig Tabelleninhaltedisable_on_error = true: stoppt die Subscription bei unerwarteten Fehlern, damit das Problem behoben und anschließend fortgesetzt werden kann
- Wenn man mit
FOR ALL TABLESalle Tabellen auf einmal in eine Publication aufnimmt, kann das bei großen DBs Performance-Probleme verursachen; Knock fügte Tabellen daher mitALTER PUBLICATION ... ADD TABLEeinzeln hinzu
Tabellenklassifizierung und Replikationsstrategie
- Knock teilte Tabellen nach Plattengröße und Tuple-Anzahl ein
- Kleine Tabellen, die in wenigen Minuten synchronisiert werden können
- Große Tabellen, die annähernd append-only sind
- Große Tabellen, bei denen die meisten Rows häufig aktualisiert werden
- Als „klein“ galten bei Knock Tabellen mit unter 50 GB und unter 10 Millionen Tuples
- In Postgres ist ein Tuple die Speichereinheit für Inserts oder Updates; selbst bei wenigen Rows kann die Replikation länger dauern, wenn viele nicht bereinigte Tuples vorhanden sind
- Ein
VACUUMvor der Replikation kann helfen, die Anzahl der Tuples zu reduzieren, die die Source-DB in die Ziel-DB kopieren muss - Die Dauer der Tabellensynchronisierung hängt direkt mit Plattengröße und Tuple-Anzahl zusammen; lang laufende Synchronisierungen können
VACUUMauf der Primary-DB behindern und zu Performance-Abfall sowie dem Risiko eines Transaction-Wraparound führen
Replikation kleiner Tabellen
- Kleine Tabellen wurden verarbeitet, indem die Tabelle auf der bestehenden DB zur Publication hinzugefügt und die Subscription auf der neuen DB refreshed wurde
- Postgres übernahm das Kopieren der Tabelle, die Synchronisierung und das Anwenden späterer Änderungen
- Sehr kleine Tabellen können in weniger als einer Sekunde synchronisiert werden
Replikation großer Append-only-Tabellen
- Für große Tabellen ohne Updates oder mit Updates nur auf neueren Rows kann mit
copy_data = falseeine separate Publication/Subscription erstellt werden - Knock verwendete das Suffix
_nocopyim Namen, um sie von der normalen Replikation zu unterscheiden - Zunächst werden nur neue Änderungen repliziert; historische Daten werden separat aus einem Backup oder Snapshot backfilled
- Das in AWS RDS Aurora verwendete Verfahren war wie folgt
- Produktions-DB-Snapshot erstellen
- Snapshot in eine neue DB-Instanz wiederherstellen
- Den Namen der zu replizierenden Snapshot-DB-Tabelle um ein Suffix wie
_snapshotergänzen - In der Ziel-DB ebenfalls eine Snapshot-Tabelle mit gleichem Schema erstellen
- Publication/Subscription von der Snapshot-DB zur Ziel-DB konfigurieren
- Replikationsfortschritt überwachen
- Sobald die Replikation aufgeholt hat, mit
INSERT ... ON CONFLICT DO NOTHINGin die eigentliche Zieltabelle zusammenführen
- Bei sehr großen Tabellen kann dieser Prozess mehrere Tage dauern; da er im Hintergrund läuft, sollte er die Produktionsumgebung jedoch nicht beeinträchtigen
- Nach dem Merge wurde die Konsistenz durch Vergleich der Row-Anzahl geprüft; anschließend wurden die Snapshot-Tabelle in der Ziel-DB, die Snapshot-Subscription und die Snapshot-DB-Instanz entfernt
Große, häufig aktualisierte Tabellen
- Große Tabellen, bei denen die meisten Rows häufig aktualisiert werden, sind am schwierigsten; eine lange laufende Replikation kann die Ausführung von
AUTOVACUUMbehindern - Mögliche Maßnahmen sind
- Prüfen, ob die Tabellengröße durch Housekeeping reduziert werden kann
- Prüfen, ob kürzlich
VACUUMausgeführt wurde - Prüfen, ob sich die Tabelle in kleinere Teile partitionieren lässt
- Prüfen, ob Row-Updates nach einer bestimmten Zeit aufhören, sodass die Tabelle wie append-only behandelt werden kann
- Ist die Source-DB älter als PG 15, sind die Optionen begrenzt; dann muss nach dem Vorgehen für kleine Tabellen repliziert und per Monitoring geprüft werden, ob es zu Serviceverschlechterungen kommt
- Falls nötig, lässt sich ein Rollback durchführen, indem die Tabelle aus der Publication entfernt und die Subscription refreshed wird
- Bei sehr großen Tabellen kann die Replikation zu Zeiten mit wenig Traffic gestartet werden, um Last und Auswirkungen auf Schreibaktivität zu reduzieren
Aufgeteilte Replikation großer Tabellen ab PG 15
- Wenn die Source-DB PG 15 oder neuer ist, lässt sich die Replikation großer Tabellen über mehrere Publications in kleinere Stücke aufteilen
- Dieser Ansatz verhält sich ähnlich wie Partitionierung oder Sharding, kostet aber den Einsatz zusätzlicher Replikations-Slots
- Knock wechselte von 11.9 auf 15.3, konnte diesen Ansatz daher nicht nutzen und hat ihn nicht selbst getestet
- Ein Beispiel ist, Rows mithilfe eines Primary-Key-Hashs und einer
WHERE-Klausel auf mehrere Publications zu verteilen - Als handhabbare Stückgröße betrachtete Knock etwa 100 GB an Daten, ohne Indizes
Replikationsstatus prüfen und abbrechen
- Wird einer Subscription eine Tabelle hinzugefügt, kann der Status in der Ziel-DB über
pg_subscription_rel.srsubstategeprüft werdeni: Initialisierungd: Tabelleninhalte kopierenf: Kopieren abgeschlossen, wartet auf finale Synchronisierungs: Abschluss der initialen Synchronisierungr: normale Replikation läuft
- Die Phase
dmuss alte Postgres-Transaction-IDs halten und kann dadurchVACUUMeffektiv blockieren, was zu Performance-Problemen oder Transaction-ID-Wraparound führen kann - Wenn man sich dem Wraparound nähert, ist es besser, die Migration abzubrechen und in kleinere Stücke aufzuteilen
- Um die Replikation einer bestimmten Tabelle zu stoppen, entfernt man die Tabelle aus der Publication der bestehenden DB und refreshed die Subscription der neuen DB
- Wenn man lediglich die Subscription deaktiviert, hält die Source-DB möglicherweise weiterhin alte Transaction-IDs fest, sodass das Performance-Problem nicht behoben wird
- Im Notfall können die gesamte Publication und Subscription gelöscht und von vorne begonnen werden; Postgres räumt die zugehörigen Replikations-Slots auf
Einschränkungen bei der Migration von Replikations-Slots
- Postgres-Replikations-Slots speichern das Aktivitätslog der DB, das andere DBs oder Anwendungen konsumieren können
- Der Fortschritt eines Slots wird über die Log Sequence Number, kurz LSN, verfolgt; die LSN ist für die Primary-Postgres-DB eindeutig
- Die LSN eines Replikations-Slots aus der bestehenden DB kann nicht unverändert in die neue DB kopiert werden
- Anwendungen, die Replikations-Slots konsumieren, etwa Data-Warehouse-Tools, müssen eine Migrationsstrategie gemäß der Dokumentation des jeweiligen Tools festlegen
- Wenn eigene Anwendungen Replikations-Slots nutzen, hilft ein Idempotenzmechanismus, doppelte Transaktionen zwischen bestehender und neuer DB zu entfernen
Finale Validierung
- Nachdem alle Tabellen zur Publication hinzugefügt wurden und die Subscription aufgeholt hat, muss validiert werden, ob die Tabellen zueinander passen
- Wegen der Verzögerung logischer Replikation ist es schwierig, dass bestehende und neue DB im exakt gleichen Moment vollkommen identisch sind; ein Vergleich der Row-Anzahlen kann jedoch bestätigen, dass sie hinreichend nah beieinander liegen
- Knock schrieb ein Skript, das für jede Tabelle die Row-Anzahl in bestehender und neuer DB zählt
- Bei Tabellen mit einer
inserted_at-Spalte wurden nur Rows verglichen, die älter als 10 Sekunden waren, unter der Annahme, dass die letzten 10 Sekunden bald repliziert werden - Für einige Tabellen wurden zusätzlich zufällige Row-Samples verglichen, um zu prüfen, ob die Tabelleninhalte übereinstimmen
Umschaltmethode für die Anwendung
- Für den finalen Cutover kann die Anwendung so geändert werden, dass sie Verbindungen zu beiden DBs hält
- DBs mit wenig Traffic wurden auf einfache Weise migriert, indem die Konfiguration auf die neue DB geändert und die Anwendung neu gestartet wurde
- Bei Anwendungen mit hoher paralleler Aktivität mussten konkurrierende Schreibvorgänge zwischen bestehender und neuer DB vermieden werden
- Knocks Cutover-Skript arbeitete in folgender Reihenfolge
- Alle Anwendungsinstanzen anweisen, neue Queries an die neue DB zu senden
- Laufenden DB-Queries 500 ms Zeit zum Abschließen geben und sie danach zwangsweise abbrechen
- In der ersten Sekunde nach dem Flag-Wechsel Anfragen an die neue DB künstlich pausieren, damit Pending Transactions Zeit haben, in die neue DB repliziert zu werden
- Danach DB-Aktivität normalisieren, aber auf die neue DB zeigen lassen
- Einige spezielle DB-Workloads stoppen und neu starten, damit sie sich erneut mit der neuen DB verbinden
- Knock bestätigte, dass 500 ms deutlich länger als die meisten DB-Queries waren und es keine Fehler durch erzwungenes Trennen von Verbindungen gab
Umgang mit Sequences
- Die logische Replikation von Postgres synchronisiert Sequences nicht
- Selbst wenn Sequence-Werte in der bestehenden DB verwendet werden, steigen die Sequence-Werte in der neuen DB nicht an
- Knock führte direkt vor dem Feature-Flag-Wechsel ein Skript aus, das sich mit beiden DBs verbindet
- Für jede Sequence der bestehenden DB mit
SELECT nextval('sequence_name')den nächsten Wert holen - In der neuen DB mit
SELECT setval('sequence_name', value::int4 + 100000)die Sequence vorziehen
- Für jede Sequence der bestehenden DB mit
- Dieser Ansatz erzeugt Lücken in den Sequences, aber da Knocks Sequences bigint sind, war das Überspringen von 100.000 Werten praktisch 0 % des nutzbaren Sequence-Raums
- Die Größe der Lücke sollte an die Menge der Sequence-Werte angepasst werden, die während des tatsächlichen Cutovers verwendet werden
Vor dem Cutover zu prüfen
- Die Checkliste vor der finalen Umschaltung deckt die operative Bereitschaft breit ab
- Ob die Row-Anzahlen aller Tabellen wie erwartet übereinstimmen
- Ob alle Subscriptions enabled sind und fehlerfrei laufen
- Ob das Schema übereinstimmt und Migration-Releases eingefroren werden können
- Ob die neue DB passend zum Workload dimensioniert ist
- Ob Read Replicas benötigt werden, um die Cluster-Topologie von bestehender und neuer DB anzugleichen
- Ob
REINDEXund grundlegendeVACUUM-Wartung auf der neuen DB durchgeführt wurden - Ob die Postgres-Release-Notes erneut auf mögliche Application-Regressions geprüft wurden
- Ob automatisierte und manuelle Tests auf einer Staging-DB mit der neuen Version durchgeführt wurden
- Ob die belastendsten Queries mit
pg_benchlastgetestet wurden - Ob noch reduzierbare Risiken verbleiben
- Ob das Cutover-Verfahren in Staging- oder Testumgebungen mehrfach geübt wurde
- Ob unmittelbar vor dem Cutover ein DB-Backup erstellt wurde
Tatsächliches Ergebnis der Umschaltung
- Knock replizierte über mehrere Wochen hinweg Tabelle für Tabelle, vor allem nach Geschäftsschluss und zu Zeiten mit dem niedrigsten Traffic
- In der Staging-Umgebung wurde der Cutover mehrfach geübt, um das Verfahren so zu verfeinern, dass es mit wenig Operator-Eingriff funktioniert
- Nachdem die PG-15-Replica und der Code für die Anwendungsumschaltung bereit waren, führte Knock die finalen Checks durch und schaltete das Flag um
- Der tatsächliche Cutover war innerhalb weniger Sekunden abgeschlossen; abgesehen von einem bewusst kurzen Latency-Blip zum Warten auf Replikation lief die Anwendung weiter
- Anschließend wurden die temporären Application-Änderungen zurückgerollt, alle Verbindungen dauerhaft auf die neue DB umgestellt und die Subscription der neuen DB sowie die bestehende DB entfernt
- Knock schloss die Migration von Postgres 11.9 auf 15.3 ohne Ausfallzeit ab
Fazit
- Vier Postgres-Major-Versionen auf einmal zu überspringen ist mühsam, aber möglich
- Der Ansatz mit logischer Replikation kann sicherer sein als geplante Downtime, weil vor dem eigentlichen Cutover mehrfach geübt, getestet und nachgearbeitet werden kann
- Wenn während des Prozesses Probleme auftreten, lässt sich die Publication der bestehenden DB löschen und neu beginnen, sodass das Verfahren ohne Serviceverschlechterung zurückgedreht werden kann
- Perfekte 100%ige Verfügbarkeit ist technisch nicht möglich, aber eine Zero-Downtime-Migration hilft, Systeme ohne größere Serviceunterbrechungen weiterzubetreiben
1 Kommentare
Meinungen auf Hacker News
Der Ansatz, Tabelleninhalte vollständig einzeln zu kopieren, erzeugt eine viel zu hohe I/O-Last und funktioniert bei sehr großen Tabellen nicht.
Eine bessere Methode ist, einen Replikations-Slot anzulegen, einen Snapshot zu erstellen, ihn auf einer neuen Instanz wiederherzustellen, den LSN vorwärts zu bewegen und ab dort zu replizieren. So erhält man ein logisches Replikat mit allen Daten, und dieses Replikat kann man dann upgraden.
Im Instacart-Artikel wird die Methode beschrieben: https://archive.ph/K5ZuJ
Wenn ich mich richtig erinnere, enthielt der Artikel ein paar kleine Fehler, aber der grundlegende Ablauf funktionierte, und ich habe damit mehrfach Instanzen im TB-Bereich upgegradet.
Wenn man zuerst die logische Replikation startet und dann pg_upgrade ausführt, besteht ein Korruptionsrisiko. Die entsprechende Diskussion findet sich auf pgsql-hackers: https://www.postgresql.org/message-id/flat/20230217075433.u5...
Zur Lösung sollte man zuerst den logischen Slot anlegen, den neuen Cluster bis zur LSN-Position des Slots vorwärts bewegen, aber die logische Replikation noch nicht starten, dann pg_upgrade ausführen und erst nachdem der Cluster mit der neuen PostgreSQL-Version hochgefahren ist, die logische Replikation starten.
Postgres.ai hat kürzlich genau diesen Ansatz verwendet, um mehrere Multi-TiB-Cluster von GitLab unter hoher Last ohne Downtime upzugraden, zusammen mit PAUSE/RESUME in PgBouncer. Für später diese Woche ist ein Vortrag von Alexander Sosna geplant: https://www.postgresql.eu/events/pgconfeu2023/schedule/sessi...
Das tabellenweise Vorgehen war deutlich umständlicher, wirkte aber verlässlicher.
Mit dieser Methode wurden viele sehr große und aktive Datenbanken erfolgreich upgegradet.
https://www.instacart.com/company/how-its-made/zero-downtime...
Der Ansatz ist interessant und gut dokumentiert, aber der Satz „moderne Kunden erwarten 100 % Verfügbarkeit“ stört mich.
Das entspricht weder meiner Präferenz als Kunde noch meiner Erfahrung als Anbieter. Bei vielen Workloads ist Konsistenz viel wichtiger als Verfügbarkeit.
Wenn ein Anbieter ein Downtime-Fenster ankündigt, wirkt das oft eher wie ein Zeichen, dass er sorgfältig mit meinen Daten umgeht, und beruhigt mich.
Ich wollte Vertrauen sowohl in die Zuverlässigkeit des Produkts als auch in die Konsistenz des Workloads schaffen. Natürlich ist es deutlich besser, Kundenerwartungen zu managen und bewusst Downtime für langfristig bessere Uptime einzuplanen, als Instabilität hinter vorgetäuschter Konsistenz zu verstecken.
Wenn man regelmäßige Wartungsfenster im Voraus erwartbar macht, kann das insgesamt sogar zu einer robusteren Architektur führen. Wenn Kunden Sicherheitsvorkehrungen bauen, um Downtime zu verkraften, erhöht das die Resilienz; und wenn das Team den Kunden in dieser Hinsicht vertrauen kann, gewinnt es Zeit, in ein besseres Produkt zu investieren.
Nach dem nächsten Major-Version-Upgrade schreibe ich vielleicht einen Artikel mit dem Titel „Erwartungen an Downtime zu setzen ist der Weg zu sehr hoher Uptime“.
Als AWS-Kunde erwarte ich 100 % Verfügbarkeit, weil meine Kunden weltweit verteilt sind und es kein Zeitfenster gibt, in dem ich Downtime einplanen könnte.
AWS unterstützt inzwischen Blue/Green Deployments: https://aws.amazon.com/about-aws/whats-new/2023/10/amazon-rd...
Nach einigem Hin und Her mit AWS blieb das Experiment mehrere Stunden hängen, und erst später räumte die AWS-UI ein, dass der Wechsel nicht angewendet worden war. Zum Glück ist es sicher fehlgeschlagen, aber ich habe kein Vertrauen, dass man bei Datensätzen größer als ein paar GB den tatsächlichen Umschaltzeitpunkt zuverlässig treffen kann.
Vielleicht ist es beim nächsten Mal möglich.
Das ist hervorragend.
Ich habe ein Tool gebaut, das den Großteil dessen automatisiert, was ihr durchgemacht habt; wenn es nützlich ist oder ihr es mit Feedback/Ideen erweitern wollt, seid ihr jederzeit willkommen: https://github.com/shayonj/pg_easy_replicate
Die Erkenntnisse aus großen Tabellen könnten für so ein Tool interessant sein. Wenn es einfacher wird, pro Tabelle die passende Strategie anzuwenden, könnte es für Teams, die solche Migrationen künftig durchführen, zu einem unverzichtbaren Tool werden.
Dass „für einen Dienst wie Knock keinerlei Downtime erlaubt ist, unabhängig davon, ob sie geplant ist oder nicht“, halte ich für fragwürdig.
Bei komplexen Systemen gibt es Störungen und Downtime. Eine vorab angekündigte 15-minütige Downtime ist für fast jedes SaaS-Business in Ordnung. Es ist kein Krankenhaus und kein Kraftwerk.
Wenn man glaubt, der eigene Dienst sei wichtiger, als er tatsächlich ist, entsteht viel Schein-Arbeit. Hätte man die dafür aufgewendete Engineering-Zeit in Verbesserungen am Produkt oder an der Produktivität des Entwicklungsteams gesteckt, wären die Nutzer wahrscheinlich glücklicher gewesen. Besonders dann, wenn Benachrichtigungen in eine Queue gestellt und nach der Downtime nachgeholt werden können.
Wenn es ein Enterprise-SLA mit Entschädigungsbedingungen für 15 Minuten Downtime gibt, ließe sich das rechtfertigen, aber meistens ist das nicht der Fall. In der Praxis gab es wahrscheinlich schon ein paar ähnlich lange oder längere Ausfälle.
Bei Datenbankmigrationen ist der Arbeitsaufwand zwischen „kurzer Downtime“ und „ohne Downtime“ in der Regel erheblich unterschiedlich, daher ist das umso wichtiger. Wenn es wie hier um eine einmalige Sache geht und die aktuelle PostgreSQL-Version von RDS standardmäßig unterstützt wird, halte ich das für besonders schwer zu rechtfertigen.
Wir haben auch über ein Wartungsfenster gesprochen, aber die Frage, die uns weiter beschäftigt hat, war, wie wir das Upgrade mit Produktionsdaten proben können. Eine mit den Produktionsdaten synchronisierte PG-15-Replik war extrem wichtig, um zu verifizieren, dass sich die Workloads wie erwartet verhalten.
Mit einer Echtzeit-Replik kann man Probeläufe mit minimalen Auswirkungen auf die Produktionsumgebung durchführen.
Eine große Erkenntnis aus dieser Migration war, wie nützlich es ist, bei solchen Projekten jedes denkbare Risiko zu verfolgen und abzumildern. Am Ende erschien uns das Risiko eines In-Place-Upgrades größer als das Risiko des gewählten Wegs; das war eine Entscheidung unabhängig davon, ob es ein Wartungsfenster gibt oder nicht.
Als Bonus wird dieser Blogbeitrag künftig, falls wir diesen Ansatz noch einmal brauchen, als Ausgangspunkt dienen und uns Wochen sparen. Ich hoffe, er hilft auch anderen Teams in ähnlichen Situationen.
Auch Epic, einer der größten Anbieter elektronischer Patientenakten in den USA, hat für Upgrades mindestens einmal im Monat geplante Downtime, jeweils etwa 30 bis 60 Minuten.
Man kann den Zeitpunkt des Neustarts nicht steuern. Wenn man den Prozess startet, kann der Umschwenk nach einer Stunde, zwei Stunden oder drei Stunden beginnen, und man weiß weder, wann der Neustart erfolgt, noch kann man ihn kontrollieren.
Wenn es Replikas gibt, werden sie parallel aktualisiert und zu beliebigen Zeitpunkten neu gestartet, was es noch unangenehmer macht.
Wenn man also, abhängig von der Datenbankgröße, keine beliebige Nichtverfügbarkeit in einem Zeitfenster von bis zu mehreren Stunden verkraften kann, ist logische Replikation für RDS-Upgrades faktisch der einzige Weg.
Je größer die Instanz ist, desto schwieriger wird das Problem.
Wenn Jira am Tag 15 Minuten ausfällt, hat das normalerweise keine großen Auswirkungen. In der Arbeits-Queue gibt es andere Dinge, und selbst im schlimmsten Fall, wenn mehrere Störungen zusammenkommen, gibt es Dokumentationsarbeit, die man jemandem versprochen hat.
Wenn aber die gesamte Atlassian-Produktsuite gleichzeitig ausfällt, wird es deutlich schwieriger, Pufferarbeit vorzuhalten, mit der man weitermachen kann. Wenn man alle Apps eines Unternehmens auf dasselbe Storage-Array setzt, kann der Produktivitätsverlust von 5 % auf 95 % springen.
So ein Wettbewerber stellt meine Anforderungen über seine eigene Bequemlichkeit.
Dein Ausfall ist auch mein Ausfall.
Wir machen diesen Prozess gerade bei hava.io durch.
Wir aktualisieren von AWS RDS PostgreSQL 11.13 auf 15.5.
Am Ende haben wir uns für einen relativ einfachen Ansatz entschieden: unidirektionale Replikation mit pglogical. Da wir mit derselben Methode schon einmal eine Migration ohne Downtime von Google Cloud SQL zu AWS RDS durchgeführt hatten, waren wir zuversichtlich, dass es ohne für Kunden sichtbare Auswirkungen funktionieren würde.
pglogical macht solche Migrationen ziemlich einfach. Es ist nicht immer schnell, aber wenn man ein paar Tage warten kann, bis die gesamte Datenbank schrittweise auf die neue Instanz repliziert ist, ist es in Ordnung.
Dieser Ansatz gab uns außerdem mehr Freiheit, Storage-Typ und -Größe zu ändern. Wir hatten den Storage überdimensioniert, um IOPS zu bekommen, und wollten daher den Storage-Typ wechseln und auch die Größe reduzieren. Mit einer einfachen Snapshot-Wiederherstellung ging das also nicht.
Ich frage mich, ob damit die Funktion gemeint ist, die AWS in der Phase „Sales Engineering“ versprochen hatte.
Als wir tatsächlich zu einem Major-Version-Upgrade gezwungen waren, konnten sie sie nicht bereitstellen.
Dass man ein Replikat nicht aus einem Backup initialisieren kann, überrascht mich.
Wenn das möglich gewesen wäre, hätte man sich viel Mühe beim Streamen der stabilen Inhalte der bestehenden Datenbank auf den neuen Server sparen können.
Und das ist nicht „ohne Downtime“, sondern es gibt ein paar Sekunden Ausfallzeit beim Umschalten des Dienstes auf den neuen Server.
Der Artikel lässt aus, wie die Konsistenz gewahrt wurde. Man kann die Anwendung zum Beispiel nicht einfach für eine gewisse Zeit an beide Server hängen. Reads könnte man vielleicht von beiden bedienen lassen, aber auch das ist nicht perfekt, und Writes müssen zwingend nur auf einen Server gehen.
Schließlich gibt es auch keine Rollback-Option. Bei solchen Verschiebungen großer Datenmengen auf einmal geht spätabends manchmal etwas schief. Deshalb braucht man immer einen Plan, mit dem man auf den vorherigen Schritt zurückgehen und mit der Gewissheit schlafen kann, dass der Dienst am Morgen noch läuft.
Besonders schwierig wird es, wenn man bereits Write-Transaktionen an den neuen Server geschickt hat und aus irgendeinem Grund zum alten Server zurück muss; die Daten sind dann bereits inkonsistent.
Ohne irgendeine Replikationsmethode oder ohne dies in die Anwendungsschicht zu verlagern, fehlen dem wiederhergestellten System Writes.
Man könnte die App zum Beispiel so ändern, dass sie Dual Writes macht. Soweit ich weiß, haben Teams, die ihre gesamte Anwendung von einem RDBMS auf eine völlig andere Datenbank wie Apache Cassandra replattformiert haben, das auch so gemacht.
In unserer Situation wirkte Dual Writing riskanter, als Streaming-Replikation mit den Bordmitteln von PostgreSQL einzurichten. Für manche Teams kann es aber die bessere Wahl sein.
Zu „nicht ohne Downtime“ und „Details zur Wahrung der Konsistenz fehlen“: Der Artikel hat ausführlich behandelt, wie wir die Konsistenz gewahrt und API-Downtime vermieden haben. Der Kernpunkt ist, dass die App mit beiden Datenbanken verbunden war, die neue Datenbank aber noch nicht als primäre verwendete.
Dann haben wir über LaunchDarkly ein Umschaltsignal an alle App-Instanzen gesendet; LaunchDarkly hält zu allen Instanzen Verbindungen mit niedriger Latenz.
In der ersten Sekunde nach dem Signal stellten die Server Datenbank-Requests in eine Queue, damit die Replikation aufholen konnte. Dadurch gab es einen kurzen Latenz-Spike, der aber bewusst einkalkuliert und innerhalb des akzeptablen Rahmens war. Nach dieser kurzen Pause flossen die Requests wie gewohnt weiter, richteten sich aber an die neue Datenbank, und die Umstellung war abgeschlossen.
Für verbleibenden Traffic zur alten Datenbank haben wir außerdem ein erzwungenes Trennen der Verbindungen mit einem Timeout von 500 ms eingebaut. Dieser Wert lag deutlich über der p99-Query-Zeit, sodass laufende Queries nicht zwangsweise beendet wurden. Dadurch stoppte der Traffic zur alten Datenbank, und die Replikation hatte genug Zeit aufzuholen.
Die Rollback-Option fehlte im Blogpost, aber wir hatten auch geprüft, eine alternative Datenbank mit PG 11.9 aufzusetzen und die 15.3-Datenbank in diese dritte Datenbank zu replizieren. Wenn wir hätten abbrechen müssen, hätten wir auf diese Datenbank derselben Version vorwärts rollen können.
Nachdem wir den Upgrade-Ablauf in Staging mehrfach geübt und die Erfolgschancen bestätigt hatten, entschieden wir uns, diesen Weg nicht zu nutzen. Durch die mehrfachen Probeläufe waren wir bei der tatsächlichen Umstellung zuversichtlich. Auch in Produktion haben wir mit einem Canary-Deployment einige Read-only-Workloads gegen die 15.3-Instanz validiert und sie wie ein Read Replica behandelt.
Um Probleme spätabends zu vermeiden, haben wir die Umstellung absichtlich am frühen Abend eines Wochenendes durchgeführt. Sie war sorgfältig geskriptet und geprobt, um das Risiko menschlicher Fehler zu reduzieren.
Bei einem katastrophalen Fehler war das System auch darauf vorbereitet, auf die alte Datenbank zurückzugehen. In diesem Fall wären einige Daten verloren gegangen, die in die neue Datenbank geschrieben worden waren, und für die zentralen Teile hatten wir eine Abstimmung vorbereitet. Um das Risiko von Datenverlust zu reduzieren, haben wir während der Umstellung einige Background-Jobs vorübergehend angehalten und so die Zahl der Writes verringert.
Diese Details haben wir nicht in den Blog aufgenommen, weil wir uns eher auf PostgreSQL-bezogene Details als auf Knock-spezifische Überlegungen konzentrieren wollten. Teams, die dieses Playbook anwenden wollen, sollten in ihrem eigenen Kontext immer eine Risikoliste erstellen und Gegenmaßnahmen definieren.
Der Teil zu Sequences ist definitiv interessant.
Ich verwende seit einiger Zeit kaum noch Sequences, sondern hauptsächlich sequenzielle UUIDs beziehungsweise UUID v7 oder Ansätze wie HiLo.
https://en.wikipedia.org/wiki/Hi/Lo_algorithm
Sie erstellt auf Basis des IETF-Entwurfs eine 12-Bit-Sequence und setzt die UUID aus den aktuellen Millisekunden der UNIX-Epoch und 62 Bit Zufallsdaten zusammen.
Der Kern ist,
uuidv7_seqanzulegen und die Funktiongenerate_uuidv7()mitclock_timestamp(),NEXTVALundRANDOM()einen Wert im UUID-v7-Format zurückgeben zu lassen.An mehreren Stellen verwenden wir KSUID und UUID v4. Diese „Falle“ gilt für alle Sequences, daher ist sie als allgemeiner Hinweis bei solchen Migrationen erwähnenswert.
[1]: https://segment.com/blog/a-brief-history-of-the-uuid/
Ich will die enorme Arbeit, das erfolgreich durchgezogen zu haben, nicht kleinreden, aber ich frage mich, warum nicht bei jeder neuen Version in kleinen Schritten aktualisiert wurde.
Als Lesestoff ist es großartig, aber es fühlt sich an wie eine Geschichte von Seeleuten, die nicht um einen großen Sturm herumfahren, sondern sich entscheiden, mitten hindurchzufahren, obwohl sie wissen, dass es tragisch enden kann.
Waren kleine Upgrades in diesem Fall keine Option? Ich frage mich, ob es nach dem Motto war: „Schon ein kleines Upgrade kostet genauso viel Downtime wie ein großes, also haben wir es so lange wie möglich hinausgezögert.“ In der Einleitung scheint es Hinweise darauf zu geben, aber vielleicht lese ich da zu viel hinein.
Es war weniger ein „wir haben es aufgeschoben, bis wir in die Ecke gedrängt waren“, sondern eher ein „wenn es nicht kaputt ist, repariere es nicht“, obwohl wir wussten, dass wir irgendwann springen müssen.
Selbst wenn die tatsächliche Antwort unter 60 Sekunden liegt, hätte man diese Downtime auf dem Weg bis 15 mehrfach erlebt.