Routing — Überblick
Ein Router ist ein Actor, dessen einzige Aufgabe es ist, jede
eingehende Nachricht an einen (oder mehrere) seiner Routees
weiterzuleiten. Du spawnst den Router einmal; hinter ihm sitzen N
Worker-Actors; Sender tellen dem Router und wissen nie, wie viele
Routees existieren.
Zwei Formen liefert das Framework aus:
| Form | Wo es lebt | Routees sind… |
|---|---|---|
Router (lokal) | Ein Node, ein Actor-System. | Kinder des Routers, zur Spawn-Zeit erstellt aus demselben Actor. |
ClusterRouter | Über Cluster-Nodes hinweg. | Up-Members des Clusters, aus Mitgliedschaft abgeleitet + ein Routee-Pfad. |
Beide exponieren dieselbe externe Schnittstelle: eine einzelne
ActorRef<TMessage>, die Aufrufer tellen. Der Unterschied ist was
auf der anderen Seite ist.
Wann zu einem Router greifen
Abschnitt betitelt „Wann zu einem Router greifen“Drei Patterns:
- CPU-lastige Arbeit parallelisieren. Ein einzelner Actor ist durch seine Eine-nach-der-anderen-Garantie gebottlenecked; ein 4-Routee-Round-Robin-Router gibt dir 4-fache Parallelität, ohne das Message-Ordering innerhalb eines Routees zu brechen.
- Last über Nodes verteilen. Ein Cluster-Router mit
role-gefilterten Routees gibt dir Fan-Out über jeden Node, der die
'compute'-Rolle trägt. Füge einen Node hinzu, der Router nimmt ihn auf; entferne einen Node, der Router stoppt das Senden an ihn. - Arbeit deterministisch an einen Routee pinnen. Consistent-Hashing (nur Cluster) legt jede Nachricht mit demselben Key auf denselben Routee — nützlich, wenn jeder Routee Per-Key-Zustand hält (einen Cache, eine Session, einen Zähler).
- N Routees um dieselbe Antwort rennen lassen. Scatter/Gather stellt jedem Routee dieselbe Frage und liefert die Antwort dessen, der zuerst antwortet — das N-Fache an Arbeit gegen das Minimum aus N Latenzen. Das ist ein anderes Ziel als die ersten drei: Tail- Latenz, nicht Durchsatz. Siehe Scatter/Gather.
Ein minimales Beispiel
Abschnitt betitelt „Ein minimales Beispiel“import { ActorSystem, Router, Actor } from 'actor-ts';
class Worker extends Actor<{ payload: string }> { override onReceive(message: { payload: string }): void { this.log.info(`worked on ${message.payload}`); }}
const system = ActorSystem.create('demo');
const pool = system.spawn( Router.roundRobin(4, Worker), 'workers',);
pool.tell({ payload: 'a' }); // → worker-1pool.tell({ payload: 'b' }); // → worker-2pool.tell({ payload: 'c' }); // → worker-3pool.tell({ payload: 'd' }); // → worker-4pool.tell({ payload: 'e' }); // → worker-1 (Round-Robin wraps)Die pool-Ref sieht für Aufrufer wie ein einzelner Actor aus; unter
der Haube zykelt der Routing-Actor durch vier Worker-Kinder.
Die fünf Strategien
Abschnitt betitelt „Die fünf Strategien“| Strategie | Was sie tut | Am besten für |
|---|---|---|
round-robin | Ein Routee pro Nachricht, zyklisch. Gleichmäßige Verteilung nach Nachrichten-Count. | Homogene Workloads. |
random | Ein Routee pro Nachricht, gleichverteilt zufällig. | Wie Round-Robin, aber zustandslos. |
broadcast | Jeder Routee bekommt jede Nachricht. | Notifications, Cache-Invalidierungen. |
smallest-mailbox | Ein Routee pro Nachricht, der mit der kürzesten Queue. | Ungleiche Kosten pro Nachricht. |
consistent-hashing (nur Cluster) | Ein Routee pro Nachricht, Key-gepinnt. | Per-Key-Zustand (Sharding-Light). |
smallest-mailbox ist die Strategie, die nach Rückstau statt nach
Nachrichten-Count balanciert. Round-Robin vergibt Züge blind — ein
Routee, der eine teure Nachricht gezogen hat, bekommt seinen nächsten
1-aus-N trotzdem pünktlich und fällt zurück; Smallest-Mailbox liest
die Queue-Tiefe jedes Routees und wählt den flachsten, ein
beschäftigter Routee wird also so lange nicht mehr gewählt, bis er
aufgeholt hat. Bezahlt wird das mit einer Tiefenabfrage pro Routee
pro Nachricht — deshalb bleibt Round-Robin der Default.
Beide Router haben sie, aber sie erfahren die Tiefe unterschiedlich. Der lokale liest die Mailboxen seiner Routees direkt — sie sind seine eigenen Kinder, im selben Prozess. Der Cluster-Router kann das nicht: eine entfernte Mailbox liegt auf einem anderen Node, und pro Nachricht zu fragen würde den Router hinter einen Netzwerk-Roundtrip parken. Er routet stattdessen auf einer gecachten Tiefe, aufgefrischt auf einem Hintergrund-Tick — etwas Aktualität gegen eine Routing-Entscheidung eingetauscht, die synchron bleibt. Siehe Cluster-Router dazu, was dieser Nachlauf kosten kann und was nicht.
Siehe Strategien für den Deep Dive plus
den Broadcast-Nachrichten-Wrapper, der die Strategie pro Nachricht
überschreibt.
Alle fünf sind One-Shot: Der Router wählt Routees, leitet weiter,
fertig — die Antwort, falls es eine gibt, ist Sache des Routees und des
ursprünglichen Senders. Router.scatterGatherFirstCompleted ist der
eine Router, der nicht in diese Form passt: Er fragt jeden Routee und
antwortet dem Aufrufer selbst mit der ersten eintreffenden Antwort. Er
ist deshalb keine Strategie, die du Router.custom reichen kannst,
sondern eine eigene Factory — siehe
Scatter/Gather.
Pool vs Group
Abschnitt betitelt „Pool vs Group“Der lokale Router ist immer ein Pool — er erstellt seine
Routees selbst. Der Cluster-ClusterRouter ist eher eine Group
— die Routees existieren bereits (ein Actor pro Up-Member an einem
bekannten Pfad), und der Router findet sie nur.
Für die meisten Fälle ist Pool das, was du willst. Das Group-Modell taucht auf, wenn du willst, dass bestehende Actors (z.B. Shard-Regionen, Fixed-Name-Workers, die beim Startup gespawnt wurden) gerouteten Traffic empfangen.
Siehe Pool vs Group, wann jede Form die richtige Passung ist.
Router und Supervision
Abschnitt betitelt „Router und Supervision“Der Router ist ein regulärer Actor — er hat seine eigene
Supervisor-Strategie und watcht jeden Routee. Wenn ein Routee
crashed:
- Ohne Eingriff wendet der Parent des Routees (der Router) seine
Supervisor-Strategie an. Standardmäßig ist das die
defaultStrategydes Frameworks — Restart bis zu 10/Minute. - Der neu gestartete Routee tritt dem Pool an demselben Pfad wieder bei. Der Router muss nichts Besonderes tun.
- Alles, was während des kurzen Restart-Fensters an den Routee geroutet wurde, bleibt in seiner Mailbox — die Mailbox wird für den Restart suspendiert und danach wieder aufgenommen (dieselbe Instanz, nicht gedrained), sodass diese Nachrichten verarbeitet werden, sobald der Routee wieder läuft.
Für Per-Routee-Supervisor-Strategien konfiguriere sie auf den Routee-Options:
import { ActorOptions } from 'actor-ts';
const routeeOptions = ActorOptions.create().withSupervisorStrategy(stoppingStrategy);
system.spawn(Router.roundRobin(4, Worker, routeeOptions), 'workers');Jetzt wird jeder Worker, der throwt, gestoppt statt neu gestartet — und der Router beobachtet ihn sterben, aber seine Ref bleibt im Pool. Round-Robin gibt diesem Slot weiterhin etwa jede N-te Nachricht, die dann bis zum Neustart des ganzen Routers in Dead Letters landet. Das würdest du mit einem höher-Level-Supervisor kombinieren, der entscheidet, wann der ganze Pool neu gespawnt wird.
Router sind nicht (der einzige) Weg zur Parallelisierung
Abschnitt betitelt „Router sind nicht (der einzige) Weg zur Parallelisierung“Ein Router gibt dir N unabhängige Actors, die jeweils eine
Nachricht nach der anderen verarbeiten. Zwei Alternativen, die
es zu kennen lohnt:
- Sharding ist das richtige Werkzeug, wenn jede Arbeitseinheit einen Key hat und du genau einen lebenden Actor pro Key brauchst (mit Failover, Rebalancing, etc.). Siehe Sharding.
- DistributedPubSub ist das richtige Werkzeug für Fan-Out, bei dem die Subscriber-Menge dynamisch ist — Actors kommen und gehen, und jeder von ihnen kann publishierte Events empfangen. Siehe DistributedPubSub.
Routing ist ein Pool mit fester Größe und deterministischer Strategie. Wenn das ist, was du brauchst, ist es das einfachste Werkzeug; wenn nicht, greife zu etwas anderem.
Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Router — die
Router.roundRobin(...),.random(...),.broadcast(...),.smallestMailbox(...),.custom(...)-Factories. - Strategien — was jede Strategie tut, plus eigene schreiben.
- Scatter/Gather — jeden Routee gleichzeitig fragen und mit der ersten Antwort antworten.
- Pool vs Group — wann einen Router verwenden, der seine Routees spawnt, vs einen, der sie findet.
- Cluster-Router — das mitgliedschafts-getriebene Cluster-Äquivalent.
