Zum Inhalt springen
Deutsch

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.

ClusterSeed-Nodesjoining-NodeClusterSeed-Nodesjoining-Nodejoining → weakly-up? → upüber ein paar Gossip-RundenJoin-AnkündigungGossip JoinGossip — aktuelle Sicht

Diese Seite behandelt die Mechanik dieses Handshakes plus die Seed-Discovery-Ebene darüber.

import { ActorSystem, Cluster, ClusterOptions } from 'actor-ts';
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.

type ClusterOptionsType = {
host: string; // Adresse dieses Nodes
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:

EinstellungStandardWas
seeds[]Liste von "host:port"-Strings. Leer = “ich bin der erste”.
seedRetryIntervalMs3000Falls 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.
const clusterOptions = ClusterOptions.create()
.withHost('0.0.0.0')
.withPort(2552)
.withSeeds([]);
const cluster = await Cluster.join(
system,
clusterOptions,
);

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.

Es liegt nahe, jedem Node dieselbe Liste mit allen Nodes zu geben. Diese Konfiguration bildet nie einen Cluster:

n3n2n1n3n2n1All three fresh, all given seed list [n1, n2, n3]filters itself out → seeds = [n2, n3] → not emptysame on both — everyone waits to be let inno member is `up`, so there is no leader,so nobody is ever promotedjoin announcementjoin announcement

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.

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:

ProviderWann
ConfigStatische Liste (der Fall oben).
DNSLöst _actor-ts._tcp.example.com SRV-Records auf.
Kubernetes APIListet Pods, die einem Label-Selector entsprechen.
AggregateFä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.

import { SelfUp, MemberUp } from 'actor-ts';
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:

  • SelfUp feuert einmal, wenn dieser Node auf up übergeht. Nützliches Gate, um Arbeit zu starten, die Cluster-Mitgliedschaft erfordert.
  • MemberUp feuert jedes Mal, wenn irgendein Mitglied up erreicht.

Für Startup-Logik, die andere Mitglieder braucht (“warte, bis mindestens 3 Nodes up sind, bevor Traffic bedient wird”), zähle MemberUps nach SelfUp.