Routing-Strategien
Eine Routing-Strategie ist eine Funktion: gegeben die Routee-Liste und etwas Zustand, gib zurück, welche(r) Routee(s) die nächste Nachricht empfangen sollen.
type RoutingStrategy = ( routees: ReadonlyArray<ActorRef>, state: { readonly messageIndex: number },) => Iterable<ActorRef>;Gib eine Ref für Single-Target-Routing zurück, mehrere für Fan-Out
oder nichts zum Verwerfen. Der lokale Router liefert drei
Implementierungen aus, plus einen custom-Slot; der Cluster-Router
fügt Consistent-Hashing hinzu.
Round-Robin
Abschnitt betitelt „Round-Robin“Router.roundRobin(4, routeeProps);Zykelt durch die Routee-Liste — Nachricht 1 → Routee 1, Nachricht 2 → Routee 2, …, Nachricht 5 → Routee 1 wieder. Implementierung:
function roundRobinStrategy(): RoutingStrategy { return (routees, state) => { if (routees.length === 0) return []; return [routees[state.messageIndex % routees.length]]; };}Schafft:
- Gleichmäßige Verteilung nach Nachrichten-Count (nicht nach Nachrichten-Kosten).
- Deterministisch und inspizierbar — ein Debugger sieht genau, welcher Routee jede Nachricht bekommen hat.
- Re-Routing bei Resize: wenn ein Routee verschwindet oder ein
neuer auftaucht, landet derselbe
messageIndexauf einem anderen Routee.
Schafft nicht:
- Load-Balancing nach Arbeitskosten. Nachricht 100 könnte ein schwerer Job sein; der Router weiß es nicht. Wenn ein Routee per Zufall alle teuren Jobs bekommt, fällt er zurück.
- Eine “Bleibe beim selben Routee für verwandte Nachrichten”-Garantie liefern. Verwende dafür Consistent-Hashing.
Richtiger Default für homogene Workloads — die Nachrichten-Verarbeitungszeiten sind grob gleich über Nachrichten.
Router.random(4, routeeProps);function randomStrategy(): RoutingStrategy { return (routees) => { if (routees.length === 0) return []; return [routees[Math.floor(Math.random() * routees.length)]]; };}Wählt einen Routee gleichverteilt zufällig.
Schafft:
- Gleiche statistische Verteilung wie Round-Robin auf lange Sicht, aber kein geteilter Zustand — nützlich in zustandslosen / rein funktionalen Setups.
- Resilienter gegen “synchronisierte” Sender. Wenn zwei Aufrufer mit ihren eigenen Indizes kooperieren, kann Round-Robin dieselben Routees hämmern; Random nicht.
Schafft nicht:
- Dir deterministisches Verhalten in Tests geben. Injiziere einen seedbaren RNG und schreibe eine eigene Strategie, wenn Reproduzierbarkeit wichtig ist.
Richtige Wahl, wenn Zustandslosigkeit wichtiger als Vorhersagbarkeit ist.
Broadcast
Abschnitt betitelt „Broadcast“Router.broadcast(4, routeeProps);function broadcastStrategy(): RoutingStrategy { return (routees) => routees; // jeder Routee}Sendet jede Nachricht an jeden Routee. Der Pool läuft im Gleichschritt — nützlich für Fan-Out-Formen:
- Cache-Invalidierung: jeder Routee hält einen Cache; wenn sich ein Key ändert, sagt der Broadcast es allen.
- Periodischer Refresh: jeder Routee liest die Config neu, wenn
eine
Refresh-Nachricht ankommt. - Heartbeat: jeder Routee checkt bei einem Tick ein.
Schafft:
- N-Wege-Fan-Out mit N-Wege-Arbeit. Jede Nachricht wird N-mal verarbeitet. Gesamtdurchsatz ist N × Per-Routee-Durchsatz, aber jeder Routee sieht die volle Nachrichten-Last.
Schafft nicht:
- Arbeit parallelisieren — jeder Routee tut dieselbe Arbeit. Das ist Fan-Out, nicht Load-Balancing.
- Sinn für Request/Response — jeder Routee antwortet, der Aufrufer sieht N Antworten.
Wenn du Broadcast für manche Nachrichten willst, aber Routing für
andere, halte den Router non-Broadcast und wickele gelegentliche
Nachrichten in Broadcast<T> — siehe
Router.
Consistent-Hashing (nur Cluster)
Abschnitt betitelt „Consistent-Hashing (nur Cluster)“import { ClusterRouter } from 'actor-ts';
ClusterRouter.props({ cluster, routerType: 'consistent-hashing', routeePath: '/user/worker', extractKey: (msg) => msg.userId,});Berechnet einen Hash von extractKey(msg) und wählt den Routee,
dessen eigener Hash am nächsten ist (Rendezvous-Hashing). Gleicher
Key → gleicher Routee, deterministisch, über den Cluster hinweg.
Schafft:
- Stickiness. Ein langer Stream von Nachrichten getagged mit
userId=42landet immer beim selben Routee. Der Routee kann Per-Key-Zustand halten (Cache, in-Progress-Session) ohne Koordinator. - Topologie-stabil. Einen Routee hinzuzufügen oder zu
entfernen, mischt nur die Keys neu, deren nächster Hash sich
geändert hat — ein Anteil proportional zu
1/N, nicht alle.
Schafft nicht:
- Perfekt unter schiefen Key-Workloads balancieren. Wenn 80 % des
Traffics
userId=42ist, trägt dieser eine Routee 80 % der Last. Schiefe Keys brauchen einen anderen Ansatz — siehe Sharding für das schwerere Per-Key-Actor-Pattern. - An einen festen Routee pinnen. Topologie-Änderungen mischen manche Keys; für harte Garantien verwende einen Singleton oder eine sharded Entity.
Richtige Wahl für session-affines Routing in Cluster-Setups, bei denen der Key-Space einigermaßen uniform ist.
Eigene Strategien
Abschnitt betitelt „Eigene Strategien“import { Router, type RoutingStrategy } from 'actor-ts';
// Route immer für die ersten 100 Nachrichten an den ersten Routee// (einen Cache aufwärmen, bevor die Last verteilt wird), dann Round-Robin.const warmupStrategy: RoutingStrategy = (routees, state) => { if (routees.length === 0) return []; if (state.messageIndex < 100) return [routees[0]]; return [routees[state.messageIndex % routees.length]];};
system.spawnAnonymous(Router.custom(4, workerProps, warmupStrategy));Alles, was RoutingStrategy erfüllt, funktioniert. Der
Zustands-Slot bekommt den monotonen Message-Index — das ist der
einzige Zustand, den der lokale Router hält. Für Strategien, die
mehr Zustand brauchen (einen Hash-Ring, einen Per-Routee-Load-Gauge),
closeover eigenen Zustand in der Funktion:
function smallestMailboxStrategy(): RoutingStrategy { return (routees) => { // Stub — das Framework exponiert die Mailbox-Größe pro Routee in v1 nicht. // Eine echte Implementierung bräuchte einen Side-Channel, um jeden Routee zu fragen. return [routees[0]]; };}Das Framework exponiert die Mailbox-Größe nicht von außen, eine echte “Smallest-Mailbox”-Strategie braucht also Side-Channel-Mechaniken (jeden Routee nach seiner aktuellen Tiefe fragen). Das ist ein Roadmap-Item; bis es geshippt wird, sind eigene Strategien auf Informationen begrenzt, die im Router-Actor selbst verfügbar sind.
Was nicht implementiert ist
Abschnitt betitelt „Was nicht implementiert ist“Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Router — die Factories, die jede Strategie in spawn-fertige Props wickeln.
- Pool vs Group — wie sich diese Strategien verhalten, wenn sie auf einen festen Pool vs eine dynamische Gruppe von Routees angewendet werden.
- Cluster-Router — wo Consistent-Hashing lebt.
- Sharding — die schwerere Alternative, wenn Keys echte Per-Key-Actors brauchen.