Zum Inhalt springen
Deutsch

Allokationsstrategie

Der Sharding-Koordinator wählt für jeden Shard einen Node:

  • Beim ersten Kontakt — wenn eine Shard-ID erstmals auftaucht, führt der Koordinator allocate(shardId, candidates, currentShards) aus, um ihren Besitzer zu wählen.
  • Beim Rebalance — alle paar Sekunden führt der Koordinator rebalance(currentShards, candidates, inProgress) aus, um Shards zu finden, die wandern sollten.

Die zwei eingebauten Strategien tauschen Einfachheit gegen Balance:

StrategieAllokationRebalance
HashAllocationStrategy (Default)shardId mod sorted-candidatesNur wenn sich die Kandidatenmenge ändert.
LeastShardAllocationStrategyKandidat mit den wenigsten Shards.Drainiert den geschäftigsten Node in die übrigen.
import { HashAllocationStrategy } from 'actor-ts';
sharding.start({
typeName: 'cart',
entityProps: Props.create(() => new CartActor()),
extractEntityId: (msg) => msg.entityId,
allocationStrategy: new HashAllocationStrategy(), // Default
});

Die Formel:

const sorted = [...candidates].sort();
return sorted[shardId % sorted.length];

Macht gut:

  • Deterministisch — bei derselben Kandidatenmenge landen dieselben Shards immer auf denselben Nodes.
  • Minimales Rebalance — nur Shards, deren Hash-Ziel sich ändert, müssen wandern (wenn Nodes joinen oder gehen).
  • Kein State — die Strategie verfolgt nichts; sie ist eine reine Funktion.

Macht nicht:

  • Nach Last balancieren — ungleiche Entity-Aktivität bedeutet, dass manche Nodes beschäftigter werden als andere, selbst bei gleicher Shard-Anzahl.
  • Langsame Nodes kompensieren — ein kämpfendes Mitglied bekommt trotzdem seinen fairen Anteil an Shards.

Richtig für gleichmäßige Workloads, in denen Shards ungefähr gleich teuer sind, oder als sinnvoller Default, während du misst, ob du etwas Schlaueres brauchst.

import { LeastShardAllocationStrategy } from 'actor-ts';
sharding.start({
// ...
allocationStrategy: new LeastShardAllocationStrategy(
/* rebalanceThreshold */ 1,
/* maxSimultaneousRebalance */ 3,
),
});

Die Formel:

  • Allokation: wähle den Kandidaten mit den wenigsten aktuell gehosteten Shards (Gleichstand wird per Adress-Ordnung gebrochen).
  • Rebalance: wenn max(shardCount) - min(shardCount) >= rebalanceThreshold, bewege Shards weg vom geschäftigsten Node zum am wenigsten beschäftigten. Höchstens maxSimultaneousRebalance Shards pro Durchgang.

Macht gut:

  • Konvergiert zur Balance, selbst wenn Nodes ungleichmäßig beigetreten/gegangen sind.
  • Gedrosseltes Rebalance — begrenzte Per-Pass-Bewegungen vermeiden Thrashing.
  • Address-Tiebreak — deterministische Ordnung ohne Überraschungen.

Macht nicht:

  • Per-Shard-Last kennen — sie zählt Shards, nicht Arbeit. Zwei Shards mit sehr unterschiedlicher Arbeitsmenge sehen gleich aus.
  • Sofort reagierenmaxSimultaneousRebalance kappt die Bewegung pro Durchgang; in einem 100-Shard-Cluster, der einen neuen Node gewinnt, dauert volles Rebalance ~33 Durchgänge (× rebalanceInterval — typisch eine Minute oder so).
new LeastShardAllocationStrategy(
/* rebalanceThreshold */ 2, // erst auslösen, wenn Ungleichgewicht ≥ 2
/* maxSimultaneousRebalance */ 5, // bis zu 5 Shards pro Durchgang bewegen
);
  • rebalanceThreshold — größerer Wert = weniger Churn, mehr Toleranz für Ungleichgewicht. 1 lässt jedes Ungleichgewicht ein Rebalance auslösen; 5 wartet auf echten Unterschied.
  • maxSimultaneousRebalance — größerer Wert = schnellere Konvergenz, mehr Handoff-Verkehr. 3 ist eine vernünftige Mitte.

Für 100-Shard-Cluster mit häufigem Mitgliedschafts-Churn beide erhöhen (Threshold ~3, Max ~10). Für 16-Shard-Cluster, bei denen jeder Shard-Move teuer ist (großer State), beide niedrig halten.

Implementiere die Schnittstelle:

interface AllocationStrategy {
allocate(
shardId: number,
candidates: ReadonlyArray<NodeAddress>,
currentShards: ReadonlyMap<string, ReadonlySet<number>>,
): NodeAddress;
rebalance(
currentShards: ReadonlyMap<string, ReadonlySet<number>>,
candidates: ReadonlyArray<NodeAddress>,
rebalanceInProgress: ReadonlySet<number>,
): Set<number>;
}

Nützlich für app-spezifische Regeln:

  • Bestimmte Shards an bestimmte Nodes pinnen — z. B. Shards mit IDs < 10 gehen immer auf coordinator-Rollen-Nodes.
  • Affinität / Anti-Affinität — bestimmte Shards zusammenhalten oder immer trennen.
  • Kapazitätsbewusst — jede Node-Metrik abfragen, nach verfügbarem Speicher gewichten.
class RoleAffinityAllocationStrategy implements AllocationStrategy {
constructor(private readonly coordinatorAddrs: Set<string>) {}
allocate(shardId, candidates, currentShards) {
if (shardId < 10) {
// Pinne Shards mit niedrigen IDs an Koordinator-Nodes
const coord = candidates.find(c => this.coordinatorAddrs.has(c.toString()));
if (coord) return coord;
}
return new HashAllocationStrategy().allocate(shardId, candidates, currentShards);
}
rebalance() { return new Set(); } // kein Rebalance für das Beispiel
}

Die Strategie läuft im Koordinator, der ein Singleton auf dem Cluster-Leader ist. Sie muss deterministisch genug sein, dass zwei Koordinatoren (während einer Leader-Wechsel-Übergabe) zur selben Entscheidung konvergieren würden — oder sich zumindest nicht gegenseitig die Arbeit zunichtemachen.

allocate läuft einmal pro neuer Shard-ID — begrenzt durch numShards (Default 100). Selbst eine teure Strategie ist hier in Ordnung.

rebalance läuft alle rebalanceIntervalMs (Default 2 s). Es sieht den vollen aktuellen Zustand. Halte es unter ~100 ms, um die übrige Arbeit des Koordinators nicht zu blockieren.

Für die meisten Cluster:

  1. Default (HashAllocationStrategy) — fang hier an. Miss.
  2. LeastShardAllocationStrategy — wechsele, wenn der Default sichtbare Hotspots produziert (ein Node CPU-saturiert, andere idle).
  3. Eigene — nur wenn keine der eingebauten passt und du konkrete Hinweise hast, dass die Anpassung hilft.

Greife nicht präventiv zu einer eigenen Strategie. Die Eingebauten decken ~95 % der Fälle ab.