Zum Inhalt springen
Deutsch

Upgrade-Strategien

Zwei Arten von Produktions-Upgrade:

ArtMuster
Code-only-UpgradeNeue 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 UpgradeNeue 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.

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.

UpgradeMixed-Version-FensterWie das Scheitern aussieht
v0.15.x → v0.16.0Nicht 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.0Nicht 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.

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.

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.

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 currency auf 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.

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.

// 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:

  1. Das neue Feld als optional im Message-Typ ergänzen.
  2. Neuer Code kann Nachrichten ohne das Feld handhaben (es defaulten).
  3. 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.

# v1 → v2: umbenannter Config-Key
actor-ts.cluster.gossip-interval = 1s # v1
actor-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.conf zu 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.

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.

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.