Joining und Seeds
Ein Node tritt einem Cluster bei, indem er einen Seed-Node
kontaktiert. Der Seed sendet per Gossip seine aktuelle
Mitgliedschaftssicht zurück; der Joiner wird als joining
hinzugefügt, propagiert per Gossip, und sobald der Leader ihn sieht
(plus Konvergenz), wechselt er zu up.
Diese Seite behandelt die Mechanik dieses Handshakes plus die Seed-Discovery-Ebene darüber.
Der einfachste Fall — explizite Seeds
Abschnitt betitelt „Der einfachste Fall — explizite Seeds“import { ActorSystem } from 'actor-ts';import { Cluster, ClusterOptions } from 'actor-ts/cluster';
const system = ActorSystem.create('my-app');
const clusterOptions = ClusterOptions.create() .withHost('10.0.0.5') .withPort(2552) .withSeeds(['10.0.0.5:2552', '10.0.0.6:2552', '10.0.0.7:2552']);const cluster = await Cluster.join( system, clusterOptions,);Drei Seeds. Der Joiner kontaktiert sie der Reihe nach, bis einer
antwortet. Sobald irgendein Seed akzeptiert, propagiert der Gossip
des Clusters das neue Mitglied; Konvergenz zu up geschieht
innerhalb weniger Sekunden in einem gesunden Netzwerk.
Die Seed-Liste ist nur ein Bootstrap-Hinweis — sobald der Node beigetreten ist, lernt er alle anderen Peers per Gossip kennen. Seeds müssen nach dem Join nicht mehr besonders sein.
Konfiguration
Abschnitt betitelt „Konfiguration“type ClusterOptionsType = { host: string; // Interface zum Binden (Wildcard ok) advertisedHost?: string; // Adresse, die Peers wählen (nie Wildcard) port: number; // TCP-Port dieses Nodes seeds?: string[]; // Peer-Adressen fürs Bootstrap roles?: string[]; // Rollen-Tags failureDetector?: Partial<...>; transport?: Transport; gossipIntervalMs?: number; seedRetryIntervalMs?: number; // Retry-Intervall, falls kein Seed antwortet // ...};Die seed-bezogenen Knöpfe:
| Einstellung | Standard | Was |
|---|---|---|
seeds | [] | Liste von "host:port"-Strings. Leer = “ich bin der erste”. |
seedRetryIntervalMs | 3000 | Falls kein Seed antwortet, wiederhole die Liste so oft, bis einer antwortet. |
selfElection | 'immediate' | Wann dieser Node allein einen Cluster bilden darf: 'immediate' (nur bei leerer Seed-Liste), 'never', oder eine Frist in Millisekunden. Wird vom Cluster-Bootstrap gesetzt. |
Der erste Node
Abschnitt betitelt „Der erste Node“const clusterOptions = ClusterOptions.create() .withHost('0.0.0.0') .withPort(2552) .withSeeds([]);const cluster = await Cluster.join( system, clusterOptions,);0.0.0.0 ist dort die Bind-Adresse. Was dieser Node per Gossip
verteilt, wird getrennt aufgelöst und ist nie eine Wildcard — ohne
weitere Konfiguration verteilt er 127.0.0.1, was für einen
Single-Node-Entwicklungslauf genau richtig ist und der Grund, warum
das Paar keine Beachtung braucht, solange es keine zweite Maschine
gibt. Siehe Cluster — Überblick.
Eine leere seeds-Liste (oder eine, die komplett unerreichbar ist)
bedeutet, dass dieser Node den Cluster selbst bootstrapt. Er
befördert sich automatisch zum Leader; künftige Joiner kontaktieren
ihn.
Das macht die Single-Node-Entwicklung trivial — keine zu pflegende Seed-Liste. Füge später einen zweiten Node hinzu, indem du ihm die Adresse des ersten als Seed gibst.
Für Produktion bestimme einen Node mit leerer Seed-Liste und gib den übrigen dessen Adresse — oder, besser, nutze den Cluster-Bootstrap, der einen festgelegten ersten Node überflüssig macht.
Die symmetrische Seed-Liste startet nicht kalt
Abschnitt betitelt „Die symmetrische Seed-Liste startet nicht kalt“Es liegt nahe, jedem Node dieselbe Liste mit allen Nodes zu geben. Diese Konfiguration bildet nie einen Cluster:
Cluster entfernt die eigene Adresse aus der Seed-Liste, also bleibt
bei einer symmetrischen Liste keinem Node die leere Liste, die
selfElection: 'immediate' verlangt. Jeder Node bleibt für immer
joining; es gibt keinen Leader, und der Leader ist es, der
joining → up befördert.
Zwei Auswege:
- Ein festgelegter erster Node mit
seeds: [], der Rest zeigt auf ihn. Einfach, aber dieser Node ist beim Start etwas Besonderes — was Container und Autoscaler unhandlich machen. - Cluster-Bootstrap — jeder Node bekommt dieselbe Konfiguration, und das Framework wählt genau einen aus, der den Gleichstand bricht. Das ist die Wahl, sobald Nodes gleichzeitig starten.
Die Retry-Schleife über seedRetryIntervalMs bleibt wichtig, löst aber
ein anderes Problem: einen Seed, der irgendwann erreichbar ist. Ein
erstes Mitglied kann sie nicht herbeizaubern.
Sie trägt aber die Diagnose. Ein Node, der nach ein paar Kontaktrunden
immer noch joining ist, kein up-Mitglied kennt und auf keine
Selbstwahl mehr wartet, loggt eine WARN-Zeile, die benennt, was er sieht
und was zu ändern ist — die unbeantworteten Seed-Adressen, wenn niemand
geantwortet hat, oder die Kombination aus Seed-Liste und selfElection
von oben, wenn alle Peers da sind und alle warten. Sie erscheint einmal,
nicht pro Runde.
Diese Zeile ist deshalb wichtig, weil das Symptom weit von der Ursache
entfernt auftritt: einem so blockierten Cluster fehlt jedes up-Mitglied,
ein Cluster-Singleton wählt seinen Host
aus den up-Mitgliedern, und das Erste, was die meisten Anwendungen
bemerken, ist deshalb ein AskTimeoutError von einem Singleton-Proxy.
Seed-Discovery — jenseits einer statischen Liste
Abschnitt betitelt „Seed-Discovery — jenseits einer statischen Liste“Eine hartcodierte Seed-Liste reicht für Tests und kleine Cluster. Für Produktion, in der Nodes dynamische IPs haben (Container, K8s Pods), nutze einen Seed-Provider:
| Provider | Wann |
|---|---|
| Config | Statische Liste (der Fall oben). |
| DNS | Löst _actor-ts._tcp.example.com SRV-Records auf. |
| Kubernetes API | Listet Pods, die einem Label-Selector entsprechen. |
| Aggregate | Fällt durch mehrere Provider durch (z. B. K8s, dann DNS). |
import { KubernetesApiSeedProvider, KubernetesApiSeedProviderOptions } from 'actor-ts/discovery';
const kubernetesApiSeedProviderOptions = KubernetesApiSeedProviderOptions.create() .withNamespace('default') .withServiceName('actor-ts') .withPort(2552);const seedProvider = new KubernetesApiSeedProvider( kubernetesApiSeedProviderOptions,);
const seeds = await seedProvider.discover();
const clusterOptions = ClusterOptions.create() .withHost(process.env.POD_IP!) .withPort(2552) .withSeeds(seeds);const cluster = await Cluster.join( system, clusterOptions,);Der Provider liefert eine Momentaufnahme von Seed-Adressen; das Framework nutzt sie, um den Join zu bootstrappen. Siehe Discovery-Überblick für das Seed-Provider-Modell.
Den Join-Fortschritt beobachten
Abschnitt betitelt „Den Join-Fortschritt beobachten“Zuerst das Ein-Aufruf-Gate — awaitReady löst auf, sobald dieser Node
up ist und, mit minimumMembers, sobald das Cluster seine erwartete
Größe erreicht hat; isReady ist die synchrone Probe:
await cluster.awaitReady({ minimumMembers: 3, timeoutMs: 30_000 });cluster.isReady(); // probe the same predicate without waitingcluster.selfMember(); // this node's own record — status includedMit gesetztem timeoutMs lehnt es mit ClusterReadyTimeoutError ab,
statt raten zu lassen; ohne wartet es unbegrenzt. Siehe
Cluster-Bootstrap → Auf Readiness warten
dafür, wie Cluster.bootstrap dasselbe Warten standardmäßig anwendet.
Für Logik, die auf einzelne Übergänge reagiert, die Events abonnieren:
import { SelfUp, MemberUp } from 'actor-ts/cluster';
cluster.subscribe((evt) => { if (evt instanceof SelfUp) { console.log(`dieser Node ist jetzt Up`); } else if (evt instanceof MemberUp) { console.log(`Peer ${evt.member.address} hat Up erreicht`); }});Zwei zentrale Events:
SelfUpfeuert einmal, wenn dieser Node aufupübergeht.MemberUpfeuert jedes Mal, wenn irgendein Mitglieduperreicht.
MemberUps von Hand zu zählen, um “sind mindestens 3 Nodes up?” zu
beantworten, ist genau das, was awaitReady({ minimumMembers: 3 })
ersetzt — die Subscription ist zum Reagieren auf Änderungen da, nicht
als Startup-Gate.
Was schiefgehen kann
Abschnitt betitelt „Was schiefgehen kann“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Cluster-Überblick — das größere Bild.
- Cluster-Bootstrap — Stable Observation und Initial-Seed-Wahl für gleichzeitige Starts.
- Weakly-up — Gradual-Join-Semantik für langsame Konvergenz.
- Failure Detector — wie Heartbeats die Mitgliedschaftssicht nach dem Join frisch halten.
- Discovery-Überblick — Seed-Provider für dynamische Umgebungen.
- Downing-Strategien — Split-Brain-Auflösung nach der Cluster-Bildung.
