Zum Inhalt springen
Deutsch

Cluster-Router

Der lokale Router erstellt seine Routees als eigene Kinder — ein fester Pool auf einem Node. Der ClusterRouter ist anders: seine Routees sind Actors anderer Nodes an einem bekannten Pfad, und die Menge der Routees ändert sich mit der Cluster-Mitgliedschaft.

ClusterRouter auf node-A

/user/worker

auf node-A

/user/worker

auf node-B

/user/worker

auf node-C

eines pro Up-Mitglied mit role=compute

Jedes Up-Mitglied mit der Rolle compute (konfigurierbar) hat einen Worker an /user/worker; der Router routet eingehende Nachrichten gemäß einer Strategie über sie. Füge einen Node hinzu, der Router nimmt ihn auf; entferne einen Node, der Router hört auf, ihm zu senden — ohne Neustart.

Siehe Pool vs. Group für die Unterscheidung zwischen lokalen Pools und Cluster-Groups.

import { ActorSystem, Cluster, ClusterRouter, Props, Actor } from 'actor-ts';
class Worker extends Actor<{ payload: string }> {
override onReceive(msg: { payload: string }): void {
this.log.info(`gearbeitet an ${msg.payload}`);
}
}
const system = ActorSystem.create('my-app');
const cluster = await Cluster.join(system, { host, port, seeds, roles: ['compute'] });
// 1. Jeder Node spawnt seinen eigenen Worker an /user/worker
system.spawn(Props.create(() => new Worker()), 'worker');
// 2. Jeder Node kann einen Cluster-Router bauen, der diese Worker anspricht
const router = system.spawn(
ClusterRouter.props({
cluster,
routerType: 'round-robin',
routeePath: '/user/worker',
role: 'compute',
}),
'compute-router',
);
// 3. Sage dem Router etwas — die Nachricht wird an den Worker eines Nodes geroutet
router.tell({ payload: 'work-1' });

Das Muster: jeder Node deployed die Routee-Actors lokal; einer (oder mehrere) Nodes spawnen einen ClusterRouter, der auf sie zielt. Die Strategie des Routers entscheidet, welcher Worker die einzelne Nachricht bekommt.

interface ClusterRouterOptions<TMsg> {
cluster: Cluster;
routerType: 'round-robin' | 'random' | 'consistent-hashing' | 'broadcast';
routeePath: string;
role?: string;
extractKey?: (msg: TMsg) => string;
}
FeldErforderlichWas
clusterJaDer Cluster — für Mitgliedschaftsverfolgung + den Wire-Transport.
routerTypeJaEine der vier Strategien.
routeePathJaDer Pfad, an dem der Routee-Actor auf jedem Node lebt (typisch /user/<actorName>).
roleNeinWenn gesetzt, sind nur Mitglieder mit dieser Rolle Routees.
extractKeyWenn routerType: 'consistent-hashing'Extrahiert den Routing-Key aus einer Nachricht.
StrategieWas sie tut
'round-robin'Ein Routee pro Nachricht, zyklisch.
'random'Ein Routee pro Nachricht, gleichverteilt zufällig.
'consistent-hashing'Pinnt gleiche extractKey an gleichen Routee via Rendezvous-Hashing.
'broadcast'Schickt an jeden Routee.

Die ersten drei sind 1-von-N-Routing; Broadcast ist Fan-out. Siehe Strategien für die Auswahlhilfe — gleiche Trade-offs gelten, nur eben über Cluster-Nodes statt über Pool-Mitglieder verteilt.

ClusterRouter.props({
cluster,
routerType: 'consistent-hashing',
routeePath: '/user/cache',
extractKey: (msg) => msg.userId,
});

Erforderlich für 'consistent-hashing'. Die Funktion zieht einen String-Key aus jeder Nachricht; der Router pinnt Nachrichten mit demselben Key per Rendezvous-Hashing an denselben Node.

Nützlich, wenn jeder Routee per-Key-Zustand hält: einen Cache für die Daten dieses Users, eine Session, einen laufenden Workflow. Topologieänderungen verschieben einen Bruchteil der Keys (proportional zur Hinzufügung/Entfernung), nicht alle.

Wenn extractKey immer denselben Wert zurückgibt, geht jede Nachricht an denselben Routee (de facto ein Singleton). Stelle sicher, dass er über deine tatsächliche Workload variiert.

Bei jeder Gossip-Runde leitet der Router seine Routee-Menge aus den Up-Mitgliedern des Clusters neu ab. Auslöser eines Neuaufbaus:

  • MemberUp — ein neues Up-Mitglied mit passender Rolle. Hinzufügen.
  • MemberRemoved — ein entferntes Mitglied. Rauswerfen.

Die Menge ist deterministisch geordnet (per Adresse), sodass Round-Robin-Counter über Neuaufbauten hinweg vernünftig bleiben.

router.tell({ payload: 'a' });
// → wenn kein Up-Mitglied `role` matched, wird die Nachricht mit einem Warn-Log fallengelassen

Wichtig: eine leere Routee-Menge bedeutet, dass Nachrichten in Dead Letters fallen. Das Framework puffert nicht, während es auf Routees wartet — das würde stillschweigend unbegrenzt wachsen.

Für “fang erst an zu bedienen, wenn der Pool mindestens N Routees hat”, abonniere MemberUp und gate die Anfragebehandlung an einem Zähler.

ClusterRouter.props({
cluster,
role: 'compute',
// ...
});

Nur Up-Mitglieder mit der Rolle compute sind Kandidaten. Nützlich für asymmetrische Cluster:

  • Nodes mit compute-Rolle erledigen schwere Arbeit.
  • Nodes mit gateway-Rolle behandeln HTTP-Traffic.
  • Nodes mit coordinator-Rolle hosten Singletons.

Die Rolle wird zur Cluster.join-Zeit pro Node deklariert. Das role-Feld des Routers filtert; ohne es ist jedes Up-Mitglied ein Kandidat.

Wenn der lokale Node ein Kandidat ist (die Rolle passt), kann der Router auf einen Worker auf demselben Node routen. Der Transport behandelt Loopback gleich wie jede Cross-Node-Zustellung — über den Local-Loopback-Pfad des Transports.

Das bedeutet, dass die Lastverteilung des Routers symmetrisch ist — keine Bevorzugung lokaler Routees, kein Penalty. Round-Robin nimmt dich wie jeden anderen Node in den Zyklus auf.

router.stop();
// oder: router.tell(PoisonPill.instance);

Stoppt den Router-Actor. Routees bleiben unberührt — sie sind auf anderen Nodes; sie laufen weiter. Das ist das Group-Router-Modell: der Router besitzt das Routing, nicht die Routees selbst.

Zum Vergleich: ein lokaler Pool-Router stoppt seine Routees kaskadierend beim Stoppen. Siehe Pool vs. Group.

Die ClusterRouter API-Referenz deckt die vollständigen Optionen ab.