Upgrade-Strategien
Zwei Arten von Produktions-Upgrade:
| Art | Muster |
|---|---|
| Code-only-Upgrade | Neue Binary, gleiche Schemas. Rolling Deployment — alte + neue Versionen koexistieren kurz, außer über ein Release hinweg, das den Cluster-Wire ändert (siehe unten). |
| Schema-brechendes Upgrade | Neue Shapes für Events / State / Messages. Erst Migration, dann Rolling Deployment. |
Wähle die Art, folge dem Muster. Naiv zu mischen bricht Produktion — alte Nodes können neue Schemas nicht lesen oder umgekehrt.
Cluster-Wire-Kompatibilität pro Release
Abschnitt betitelt „Cluster-Wire-Kompatibilität pro Release“Ein Rolling Deployment setzt voraus, dass das Mixed-Version-Fenster überlebbar ist — für ein paar Minuten reden alte und neue Nodes miteinander. Diese Annahme ist eine Eigenschaft des Releases, nicht deines Codes, und zwei aufeinanderfolgende Releases brechen sie. Keiner der beiden Brüche ist eine Schema-Änderung in dem Sinn, in dem diese Seite das Wort benutzt — Events, State und Konfiguration bleiben unangetastet —, also greift auch keines der additiven Muster weiter unten.
| Upgrade | Mixed-Version-Fenster | Wie das Scheitern aussieht |
|---|---|---|
| v0.15.x → v0.16.0 | Nicht sicher (#112) | Jeder Gossip-Frame trägt eine sequence, und GossipMessage bekommt ein Pflichtfeld. Ein aktualisierter Peer weist die Frames eines alten Nodes zurück; ein alter Node ignoriert die eines aktualisierten. Nichts wirft einen Fehler — die Membership konvergiert still nie, solange beide Versionen laufen, der Cluster einigt sich also nie darauf, wer dazugehört. |
| v0.16.0 → v0.17.0 | Nicht sicher (#450) | Frames sind ein getaggter JSON-Baum statt blanker JSON.stringify-Ausgabe. Der meiste Alt-Verkehr dekodiert unverändert; ein Legacy-Body, der bereits die Gestalt eines reservierten Tags hatte, nicht. __map__, __set__, __regexp__, __bigint__, __url__, __number__ und __error__ werfen in beliebiger Tiefe, und ein Decoder-Throw kostet die ganze Verbindung mitsamt jedem Frame, der in denselben Chunk gebatcht wurde. Zwei scheitern stattdessen still: __bytes__ dekodiert zu einem Uint8Array, __date__ zu einer Invalid Date. Die Gegenrichtung ist schlicht verlustbehaftet — ein älterer Node liest den Tag-Wrapper als gewöhnliche Daten. |
Aktualisiere den Cluster in einem Schritt. Alle Nodes stoppen, dann alle Nodes auf der neuen Version starten. Keine Zwischenversion macht eine der beiden Etappen rollbar, ein Sprung von v0.15.x direkt auf v0.17.0 kreuzt also beide Brüche und braucht dieselbe Behandlung — einmal statt zweimal.
Wann das endet. Beide Brüche existieren aus demselben Grund: Das Cluster-Protokoll hat keine Versionsaushandlung, ein Node kann einen Peer also nicht fragen, was der spricht, und sich darauf einstellen. #823 fügt diesen Handshake hinzu. Bis dahin ist ein Mixed-Version-Fenster eine Gefahr und kein unterstützter Zustand, und diese Tabelle ist das, was vor einem Upgrade zu prüfen ist.
Code-only-Upgrades
Abschnitt betitelt „Code-only-Upgrades“Der häufige Fall. Bugfixes, Refactorings, Verhaltens-Tweaks ohne Änderung der persistierten Datenformen.
1. Die neue Binary bauen (Tag v1.2.3).2. Per Rolling Update deployen.3. K8s ersetzt Pods einen nach dem anderen.4. Jeder Pod: SIGTERM → Coordinated Shutdown → Drain → neuer Pod spawnt → Cluster-Rejoin.5. Fertig.Gossip + Sharding-Rebalance + Coordinated Shutdown des Clusters übernehmen die Choreographie. Gesamt-Downtime: null (bei richtiger Konfiguration; siehe Kubernetes-Deployment).
Voraussetzungen:
- Replicas ≥ 2. Ein-Replica-Cluster können nicht sauber drainen.
- Coordinated Shutdown mit vernünftigen Phasen-Timeouts konfiguriert.
- Health-Checks gaten Readiness korrekt.
Schema-brechende Upgrades
Abschnitt betitelt „Schema-brechende Upgrades“Immer wenn das Upgrade ändert:
- Event-Shapes in einem Journal.
- State-Shapes in einem Durable-State-Store.
- Message-Shapes, die Nodes einander während des Rolling-Fensters schicken könnten.
- Config-Keys, die sich zwischen Major-Versionen bewegen.
Das Muster: Mach die Änderung additiv, dann upgrade.
Muster — additive Event-Shapes
Abschnitt betitelt „Muster — additive Event-Shapes“Alter Code schrieb:
type DepositedV1 = { kind: 'deposited'; amount: number };Neuer Code will:
type DepositedV2 = { kind: 'deposited'; amount: number; currency: string };Schritt 1: Zwischen-Code deployen, der beide Shapes akzeptiert.
class Account extends PersistentActor<...> { override eventAdapter() { return defaultsAdapter<DepositedV2>({ manifest: 'Deposited', currentVersion: 2, defaults: { 1: { currency: 'USD' } }, }); }}Dieser Schritt:
- Schreibt V2-Events unter der neuen Form.
- Liest V1-Events mit
currencyauf USD defaulted. - Funktioniert in alten + neuen Clustern, weil alter Code seine eigene Form liest und Envelope-Wrapping ignoriert.
Roll das über Standard-Rolling-Deployment aus.
Schritt 2 (optional später) — den defaultsAdapter droppen,
sobald alle alten Events ausgealtert oder snapshottet sind. Meist
unbegrenzt zur Sicherheit behalten.
Siehe Migration Recipes für den Per-Muster-Walkthrough.
Muster — nicht-additive Schema-Änderungen
Abschnitt betitelt „Muster — nicht-additive Schema-Änderungen“Für Renames, Restructures, entfernte Felder brauchen die Mechaniken mehr Schritte:
1. Code deployen, der ALTES LIEST + NEUES SCHREIBT. (`migratingAdapter`)2. Vollständig ausrollen. Alle neuen Events sind jetzt in der neuen Form.3. Code deployen, der NUR NEUES LIEST (unterstützt Altes nicht mehr). Droppt den Migrating-Schritt.4. Optional: Bulk-Migration, um noch existierende alte Events in die neue Form umzuschreiben, wenn du Adapter-Komplexität droppen willst.Siehe migratingAdapter für die Implementierung.
Inter-Actor-Message-Änderungen
Abschnitt betitelt „Inter-Actor-Message-Änderungen“// v1-Nachricht: { kind: 'request' }// v2-Nachricht: { kind: 'request', traceId: string }Während eines Rolling Deployments könnten alte Nodes v1 an neue Nodes senden (oder umgekehrt). Der neue Code muss beide Versionen eingehender Nachrichten tolerieren.
Strategie:
- Das neue Feld als optional im Message-Typ ergänzen.
- Neuer Code kann Nachrichten ohne das Feld handhaben (es defaulten).
- Deployen. Alt → neu sendet ohne das Feld, funktioniert. Neu → alt sendet mit dem Feld, alt ignoriert es.
Sobald alles auf v2 ist, kann das Feld in einem späteren Deployment required werden.
Konfigurationsänderungen
Abschnitt betitelt „Konfigurationsänderungen“# v1 → v2: umbenannter Config-Keyactor-ts.cluster.gossip-interval = 1s # v1actor-ts.cluster.gossip-interval-ms = 1000 # v2 (umbenannt)Das Config-System des Frameworks migriert umbenannte Keys nicht automatisch. Zwei Strategien:
- Beide lesen im Code, der Config lädt; entweder den Namen ehren, bis du den neuen verlangen kannst.
- Migrations-Skripte laufen lassen, die
application.confzu den neuen Key-Namen umschreiben.
Einfacher: Config-Keys nicht umbenennen. Wenn doch nötig, alten Namen deprecaten + beim Start für ein Release warnen, bevor er entfernt wird.
Was, wenn du Downtime nicht vermeiden kannst?
Abschnitt betitelt „Was, wenn du Downtime nicht vermeiden kannst?“Manchmal ist der Schema-Break so schlimm, dass eine Online-Migration ehrlich gesagt unmöglich ist — anderes Storage-Backend, fundamentales Restructuring. Dann plane Downtime:
1. Wartungsfenster ankündigen.2. Coordinated Shutdown des gesamten Clusters.3. Offline-Migrations-Skripte laufen lassen (manchmal Stunden).4. Die neue Version hochfahren.5. Smoke-Test vor User-Traffic.Frequenz: idealerweise nie. Aber gelegentlich unvermeidlich.
Rollback-Strategie
Abschnitt betitelt „Rollback-Strategie“Hab immer einen Rollback-Plan:
- Code-only: Vorherige Binary funktioniert weiter gegen dieselben Schemas. Rollback per K8s-Rolling-Back.
- Schema-brechend: Der vorherige Code liest die neuen Shapes (weil Schritt 1 additiv war). Rollback ist sicher.
- Nicht-additive Änderung: schwieriger — der Rollback-Schritt muss auch die neue Form kennen. Vermeiden; wenn nötig, Feature-Flags nutzen, um den neuen Code-Pfad zu gaten, während der alte erreichbar bleibt.
Wohin als nächstes
Abschnitt betitelt „Wohin als nächstes“- Operations-Überblick — die Produktions-Checkliste.
- Rolling Migration — das praktische Schritt-für-Schritt-Rezept.
- Migration-Überblick — Schema-Evolution für Events + State.
- Migration Recipes — das Kochbuch.
- Coordinated Shutdown — die Graceful-Stop-Maschinerie, auf die Rolling Deploys angewiesen sind.
