Zum Inhalt springen
Deutsch

Shard-Introspektion

Eine Region zu starten gibt dir genau eine Ref — die der Region — und das war bisher die ganze Geschichte. Du konntest Nachrichten durch das Sharding schicken, aber es nichts fragen: nicht “welche Shards gibt es”, nicht “wo liegt Shard 7”, nicht “gib mir einen Handle auf Entity user-42”.

Vier APIs auf ClusterSharding schließen diese Lücke, dazu ein Cluster-Event für die Push-Seite.

shards(typeName) antwortet clusterweit, von jedem Node aus:

const shards = await cluster.sharding.shards<CounterCommand>('counter');
for (const shard of shards) {
console.log(
`shard ${shard.shardId} on ${shard.node}`,
`— ${shard.entityCount} entities`,
shard.local ? '(here)' : '',
);
}

Jeder Eintrag ist ein ShardInfo:

type ShardInfo<TMessage = unknown> = {
readonly shardId: number;
readonly node: NodeAddress; // node currently hosting the shard
readonly regionPath: string; // region on that node
readonly entityCount: number; // live entities when its region answered
readonly resident: boolean; // shard actor materialised at that moment
readonly local: boolean; // hosted by the node that asked
readonly ref: ActorRef<ShardMessage<TMessage>>;
};

resident unterscheidet einen Shard, der läuft und gerade leer ist, von einem, der leer passiviert wurde — entityCount: 0 meldet beides. Auf die Erreichbarkeit hat es keinen Einfluss: ref funktioniert so oder so. Lies es, wenn du shardPassivationIdleMs einstellst oder zählen willst, wie viele Actors ein Node tatsächlich hält.

Der Eintrag trägt eine lebende Ref, nicht nur Platzierungsdaten — eine Abfrage beantwortet also sowohl “was gibt es da draußen” als auch “wie rede ich damit”. Der Preis: ShardInfo ist nicht JSON-serialisierbar. Wenn du eine serialisierbare Sicht brauchst, nimm shardMap(typeName) oder den Management-Endpunkt GET /cluster/shards, der genau diese serialisiert.

Shards ohne Zuhause fehlen in der Liste. Niemand hat nach ihnen gefragt, also hatte der Koordinator keinen Anlass, sie zu platzieren: ein Typ mit 64 Shards, der drei Entity-IDs gesehen hat, meldet ein bis drei Shards, nicht 64.

Der Koordinator besitzt die Shard-Karte, aber nicht die Entity-Zahlen — die kennt nur die Region, die den Shard hostet. Der Aufruf fächert deshalb an alle registrierten Regions aus und joint die Antworten gegen die Allokationskarte. Also ein zusätzlicher Round-Trip.

Die Fan-out-Frist ist eine Teilantwort-Frist: eine Region, die zu langsam ist oder zwischen Karten-Schnappschuss und Fan-out gegangen ist, steuert entityCount: 0 bei, statt den ganzen Aufruf scheitern zu lassen. Du bekommst eine vollständige Shard-Liste mit einer konservativen Zahl, keine Exception.

const shard = await cluster.sharding.shardRefFor<CounterCommand>('counter', 7);

Hat Shard 7 noch kein Zuhause, platziert dieser Aufruf ihn — genau das, was eine erste Nachricht an ihn auch getan hätte — und antwortet, sobald der Koordinator entschieden hat.

Ein Shard, der leer passiviert wurde, braucht keine Sonderbehandlung: die Ref bleibt über die Lücke hinweg gültig, und der Shard wird neu erstellt, sobald etwas für ihn eintrifft. Lokal passiert das schon beim Fragen, denn ein Handle auf einen nicht laufenden Actor wäre ein Handle auf nichts; bei einem Shard auf einem anderen Node stellt die Ref über dessen Region zu, die den Shard vor dem Weiterleiten materialisiert. shardRefFor() weckt einen lokalen Shard also sofort, einen entfernten das erste tell.

Die Ref ist das echte Ding. Ein Shard ist ein Actor (/system/cluster/sharding/region-counter/shard-7), und die Ref trägt diesen Pfad als Identität, egal von welchem Node du gefragt hast — was sich unterscheidet, ist der Weg: direkt zum Actor, wenn dieser Node ihn hostet, über die besitzende Region, wenn nicht. tell funktioniert damit von überall:

// Bring an entity up without sending it a message.
shard.tell({ kind: 'sharding.StartEntity', entityId: 'user-42' });
// Address one entity through the shard.
shard.tell({
kind: 'sharding.EntityEnvelope',
entityId: 'user-42',
message: { id: 'user-42', kind: 'increment' },
});

Und um zu fragen, was ein Shard hält:

const stats = await shard.ask<ShardStats>({ kind: 'sharding.GetShardStats' });
// { shardId: 7, entityCount: 2, entityIds: ['user-42', 'user-99'] }

entityRefFor ist das Gegenstück zur Region-Ref — ein Handle auf eine einzelne Entity, wo immer sie gerade liegt:

const entity = cluster.sharding.entityRefFor<CounterCommand>('counter', 'user-42');
entity.tell({ kind: 'increment', by: 1 });
const value = await entity.ask<number>({ kind: 'get' });

Zwei Dinge sind anders als beim Reden mit der Region:

  • Die Nachricht trägt ihren Routing-Key nicht mehr selbst. An eine Region zu senden heißt, dass extractEntityId die ID wieder aus deiner Nachricht herausgraben muss — jeder Nachrichtentyp braucht also eine. Der Handle benennt seine Entity direkt, extractEntityId wird nie befragt.
  • Er ist synchron zu bekommen. Der Shard ist hash(entityId) % numShards, es muss also nichts nachgeschlagen werden. Eine Nachricht für einen Shard, dessen Zuhause noch unbekannt ist, puffert die Region — genau wie bisher.

Location-transparent ist er, weil er über die lokale Region routet, und die weiß bereits, wie sie jeden Node erreicht. Eine Proxy-Region (startProxy) reicht aus, um einen auszugeben — der Node muss selbst nichts hosten.

ask funktioniert auf einer Entity-Ref unabhängig davon, wo die Entity liegt: die Region reicht deinen Sender an die Entity durch, die Antwort kommt also direkt zu dir zurück.

Für die Push-Seite abonnierst du ShardMapChanged:

import { match, P } from 'ts-pattern';
import { ShardMapChanged } from 'actor-ts/cluster';
cluster.subscribe((event) =>
match(event)
.with(P.instanceOf(ShardMapChanged), (e) => onShardMapChanged(e))
.otherwise(() => onOtherEvent()));
function onShardMapChanged(event: ShardMapChanged): void {
// event.type — the sharded type name
// event.shards — ReadonlyMap<shardId, regionKey>
// event.regions — the region table, with per-region shard counts
// event.version — increments once per broadcast
}

Das Event feuert auf jedem Node, nicht nur auf dem Leader: der Koordinator broadcastet an jede Region, und die Region publiziert lokal. Genau das macht es für einen Anwendungs-Listener oder ein Per-Node-Dashboard überhaupt brauchbar.

Broadcasts werden zusammengefasst. Allokationsänderungen kommen einzeln pro Shard, und ein frischer Cluster platziert alle Shards auf einmal — version zählt deshalb Broadcasts, nicht einzelne Zuweisungen. Lies sie nicht als “wie viele Shards sind umgezogen”.

Weil das Event jeden Node erreicht, kann ClusterSharding sich einfach das letzte merken — genau das gibt shardMap(typeName) als reines JSON zurück:

const map = cluster.sharding.shardMap('cart');
if (map !== null) {
// map.regions — one entry per region, with its shard ids
// map.shardHome — [{ shard, regionKey }], one per placed shard
// map.version — the coordinator's broadcast counter
// map.leader, map.takenAt — who published it, and when it arrived here
}

Synchron und kostenlos: kein Round-Trip, kein DistributedData, kein coordinatorStateStore. Es ist das, was der Endpunkt GET /cluster/shards serialisiert.

Der Rückgabewert ist null, bis der Koordinator einmal für den Typ publiziert hat — das schließt jeden Node ein, der weder Region noch Proxy dafür gestartet hat, denn der Koordinator broadcastet nur an registrierte Regionen. Und shardHome bleibt leer, bis wirklich ein Shard platziert ist, während sich regions schon bei der Registrierung füllt: lies regions für “wer nimmt teil”, shardHome für “was ist platziert”.

Greif stattdessen zu shards(), wenn du Entity-Zahlen oder lebende Refs brauchst, und zu ShardMapChanged, wenn du die Änderungen statt des letzten Stands willst.

  • Sharding — Überblick — die Region-, Shard- und Entity-Actors, auf die diese APIs dir Refs geben.
  • Rebalance — warum Platzierung sich bewegt und was dabei mit einer Shard-Ref passiert.
  • HTTP-Management-Endpunkte — GET /cluster/shards, die serialisierbare Sicht auf dieselbe Karte.