Quorum Reads und Writes
Die Default-Operationen update / get von DistributedData sind
nur lokal — sie werden sofort auf die lokale Replika
angewendet; die Propagation passiert via Gossip. Für Workloads,
die stärkere Garantien brauchen, warten die Varianten
updateAsync und getAsync auf explizite
Replika-Bestätigungen.
Local — der Default
Abschnitt betitelt „Local — der Default“dd.update<GCounter>( 'hits', GCounter.empty, (c) => c.increment(dd.selfReplicaId(), 1),);
const counter = dd.get<GCounter>('hits');Diese Operationen:
update— wende lokal an, fire-and-forget. Kehrt sofort zurück.get— lese die Sicht der lokalen Replika.
Unter der Haube propagiert Gossip Updates über die nächsten Runden an andere Replikas.
Das ist der richtige Default — 99 % der DD-Operationen nutzen diese.
Wann Local nicht reicht
Abschnitt betitelt „Wann Local nicht reicht“Drei häufige Fälle:
- Read-after-Write über Nodes hinweg. Node A schreibt, Node B liest sofort — der Write ist vielleicht noch nicht gegossipt, also sieht Node B veraltete Daten.
- Bestätigter Write. Die App will wissen “mindestens N andere Replikas haben mein Update, bevor ich weitermache” — z. B. bevor dem Client geantwortet wird.
- Strongest-known Read. Wenn Veraltung für diesen Read inakzeptabel ist (Saldo-Check vor einer Zahlung), zwinge den Read, die Mehrheit der Replikas zu konsultieren.
updateAsync — auf Write-Bestätigungen warten
Abschnitt betitelt „updateAsync — auf Write-Bestätigungen warten“await dd.updateAsync<GCounter>( 'hits', GCounter.empty, (c) => c.increment(dd.selfReplicaId(), 1), { consistency: 'majority', timeoutMs: 2_000 },);Wendet lokal an + schickt an die Gossip-Schicht und wartet dann auf Bestätigungen der konfigurierten Anzahl Replikas.
consistency | Worauf gewartet wird |
|---|---|
'local' (Default) | Nur auf sich selbst — das lokale Anwenden ist das ACK. |
'majority' | ⌊N/2⌋ + 1 Replikas haben geACKt. |
'all' | Jede Replika der Up-Member hat geACKt. |
{ from: 3 } | from Replikas haben geACKt (begrenzt auf [1, N]). |
Das Promise:
- Resolved, wenn genug ACKs innerhalb von
timeoutMseintreffen. - Rejected mit einem Timeout-Fehler, wenn nicht.
Ein Timeout macht den lokalen Write nicht rückgängig — der Wert ist bereits lokal angewendet und gossipt normal weiter. Der Reject signalisiert nur “ich bin mir nicht sicher, ob genug Replikas ihn innerhalb der Deadline gesehen haben”.
getAsync — Lesen mit Konsistenz
Abschnitt betitelt „getAsync — Lesen mit Konsistenz“const counter = await dd.getAsync<GCounter>('hits', { consistency: 'majority' });Dieselben consistency-Optionen. Der Replicator fragt andere
Replikas nach ihrer Sicht, merged die Antworten und gibt den
gemergten Wert zurück.
Das heißt, ein Majority Read sieht mindestens jeden Write, der Majority-bestätigt wurde — den letzten “confirmed”-Wert.
Konsistenz-Level wählen
Abschnitt betitelt „Konsistenz-Level wählen“'majority' ist der Sweet Spot für “wichtige” Reads/Writes —
deckt die meisten Failure-Szenarien zu moderater Latenz ab.
'all' garantiert Konsistenz, scheitert aber, wenn auch nur
eine Replika down (oder langsam) ist — das macht es spröde.
Pragmatische Muster
Abschnitt betitelt „Pragmatische Muster“Majority schreiben + lokal lesen
Abschnitt betitelt „Majority schreiben + lokal lesen“// Mit Majority schreiben — garantiert, dass andere Replikas Bescheid wissenawait dd.updateAsync('counter', ..., { consistency: 'majority' });
// Lokal lesen — schnell, und der Wert spiegelt (eventually) wider,// was andere Writer mit Majority geschrieben habenconst value = dd.get('counter');Das ist das häufige Produktionsmuster. Writes zahlen die Majority-Kosten; Reads sind billig. Der Cluster konvergiert irgendwann.
Majority vor einer kritischen Entscheidung lesen
Abschnitt betitelt „Majority vor einer kritischen Entscheidung lesen“// Majority lesen — den letzten bekannten Wert im Cluster sehenconst balance = await dd.getAsync<PNCounter>('balance', { consistency: 'majority' });
if (balance.value() < 0) { // ... Transaktion ablehnen}Für einmalige Reads, bei denen Veraltung zählt, zahl die Kosten für einen starken Read. Mach nicht jeden Read zu einem Majority Read — das hebelt das Local-First-Design aus.
All-Write für unwiderrufliche Änderungen
Abschnitt betitelt „All-Write für unwiderrufliche Änderungen“await dd.updateAsync('config-locked', ..., { consistency: 'all' });Selten — wenn du brauchst, dass jede Replika die Änderung gesehen hat, bevor es weitergeht. Spröde (jede ausgefallene Replika lässt die Operation scheitern), also wirklich nur für echt-unwiderrufliche Zustandsübergänge.
Timeouts
Abschnitt betitelt „Timeouts“await dd.updateAsync('x', ..., { consistency: 'majority', timeoutMs: 5_000,});Default-Timeout ist feste 5 Sekunden (DEFAULT_ASK_TIMEOUT_MS),
unabhängig vom Gossip-Intervall. Pro Call überschreiben, wenn:
- Strengeres Latenz-Budget — niedriger setzen; früh ablehnen, wenn Konsistenz nicht erreicht werden kann.
- Lockerer — bei cross-region Clustern mit hoher Gossip-RTT das Timeout erhöhen.
Bei Timeout wirft das Promise einen Fehler, den du fangen kannst:
try { await dd.updateAsync('x', ..., { consistency: 'majority', timeoutMs: 1_000 });} catch (e) { // Der Write hat lokal funktioniert, aber das Quorum kam nicht rechtzeitig. // Entscheide: erneut versuchen, Fehler nach oben geben, Eventual-Consistency akzeptieren.}Die Pending-Menge begrenzen
Abschnitt betitelt „Die Pending-Menge begrenzen“Jeder Quorum-Call, der sich nicht schon mit dem Self-ACK auflösen kann,
parkt einen Eintrag — ein Promise, einen Timer, die Menge der Peers, auf
die er wartet — bis genug Antworten da sind oder die Deadline abläuft.
Zwei Settings begrenzen diese Menge, im Code oder über
actor-ts.distributed-data:
import { DistributedDataId, DistributedDataOptions } from 'actor-ts';
const distributedDataOptions = DistributedDataOptions.create() .withMaxPendingQuorumRequests(1_000) .withMaxQuorumTimeout(30_000);
const dd = system.extension(DistributedDataId).start(cluster, distributedDataOptions);| Option | Default | Wirkung |
|---|---|---|
maxPendingQuorumRequests | 1000 | Maximal offene Quorum-Requests gleichzeitig. updateAsync und getAsync teilen sich das Budget. 0 deaktiviert es. |
maxQuorumTimeout | 30000 | Obergrenze in ms für das timeoutMs eines Calls; ein größerer Wert wird heruntergeschnitten. 0 deaktiviert sie. |
Über dem Cap lehnt der Call sofort mit einem Fehler ab, der das Setting beim Namen nennt — er wird nicht eingereiht und wartet nicht:
try { await dd.updateAsync('x', ..., { consistency: 'majority' });} catch (e) { // "DistributedData refused a quorum write on "x": 1000 quorum requests // are already pending (max-pending-quorum-requests = 1000)"}Wie beim Timeout macht eine Ablehnung den lokalen Write nicht rückgängig — der Wert ist vor dem Quorum angewandt und gossipt normal weiter.
In Produktion beobachten
Abschnitt betitelt „In Produktion beobachten“Mit aktivierten Metriken veröffentlicht der Replikator vier Serien — die Label-Mengen sind fest, sie kosten also eine Handvoll Serien und nicht mehr:
| Metrik | Art | Labels |
|---|---|---|
distributed_data_quorum_pending | Gauge | — |
distributed_data_quorum_timeouts_total | Counter | operation = write | read |
distributed_data_quorum_rejected_total | Counter | operation = write | read |
distributed_data_dropped_values_total | Counter | — |
Alarmierungswürdig ist vor allem das letzte Paar. Ein steigendes
..._rejected_total heißt, dass Aufrufer dem Cap davonlaufen — entweder
ein durchgegangener Producer oder ein für die Last zu niedriges Cap. Ein
steigendes ..._dropped_values_total heißt, dass ein Peer CRDT-Payloads
schickt, die diese Replika nicht dekodieren kann — ein kaputter oder
feindlicher Peer, kein Tuning-Problem.
Was Quorum nicht garantiert
Abschnitt betitelt „Was Quorum nicht garantiert“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Distributed Data im Überblick — das Gesamtbild.
- Replikation — wie lokale Writes an Peers propagieren.
- Durable Storage — für State, der Full-Cluster-Restarts überlebt.
- Daten gestalten — wann DDs Eventual Consistency die falsche Wahl ist.
