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”.

Drei 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 wurdeentityCount: 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, liefert der Management-Endpunkt GET /cluster/shards stattdessen eine reine Datenform.

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.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”.

  • 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-EndpunkteGET /cluster/shards, die serialisierbare Sicht auf dieselbe Karte.