Zum Inhalt springen
Deutsch

Vector Clocks

Eine Vector Clock ist ein logischer Zeitstempel, der über Replicas verteilt ist. Jede Replica unterhält einen Per-Replica-Counter; jedes persistierte Event enthält die aktuelle Vector Clock zum Schreibzeitpunkt.

Gegeben zwei Vector Clocks a und b:

RelationWann
a < b (“a happens-before b”)Jede Komponente a[i] ≤ b[i] und mindestens eine strikt kleiner.
a == bJede Komponente gleich.
a nebenläufig zu bWeder < noch >. Einige Komponenten haben a > b, andere a < b.

Der dritte Fall — nebenläufig — ist das, was die Replicated-ES-Maschinerie verwendet, um zu erkennen “zwei Replicas haben unabhängig geschrieben, und keine hat den Write der anderen gesehen.”

type VectorClockData = Readonly<Record<ReplicaId, number>>;

Ein plain Object. Replicas, die nicht vorhanden sind, werden als 0 behandelt — nützlich für Kompaktheit auf dem Wire (Komponenten überspringen, die null sind).

Die VectorClock-Klasse des Frameworks wickelt das ein:

import { VectorClock } from 'actor-ts';
let vc = VectorClock.empty();
vc = vc.tick('replica-a'); // → { 'replica-a': 1 }
vc = vc.tick('replica-a'); // → { 'replica-a': 2 }
vc = vc.merge(VectorClock.fromData({ 'replica-b': 3 }));
// → { 'replica-a': 2, 'replica-b': 3 }
MethodeZweck
empty()Null-Clock.
fromData(record)Aus einem plain Object bauen.
get(replicaId)Eine Komponente lesen.
tick(replicaId)Die Komponente einer Replica erhöhen — beim Persistieren aufgerufen.
merge(other)Per-Replica-Maximum.
compareTo(other)Gibt 'before' / 'after' / 'equal' / 'concurrent' zurück.
toJSON()Plain-Object-Form für die Serialisierung.
Replica A schreibt event_A1 bei vc { A: 1 } ← A's Counter erhöht sich
Replica B (ohne Vorwissen über A's Event):
schreibt event_B1 bei vc { B: 1 } ← B's Counter erhöht sich
Beide Events landen im geteilten Journal.
Später liest Replica A das Event von B:
vc { B: 1 } verglichen mit A's aktuellem { A: 1 }:
→ nebenläufig (A > B für A's Komponente, B > A für B's Komponente)
→ Conflict Resolver läuft, um State aus beiden Branches zu mergen

Das Framework:

  1. Inkrementiert bei jedem Persist die Komponente dieser Replica.
  2. Bettet die volle Vector Clock in das persistierte Event ein.
  3. Beim Lesen des Events einer anderen Replica vergleicht die eingebettete Vector Clock mit der Vector Clock des lokalen States.
  4. Wenn nebenläufig → den Conflict Resolver aufrufen.
  5. Wenn < (kausal vorher) → bereits angewendet oder strikt älter; ignorieren (oder normal in den State falten).
  6. Wenn > (kausal später) → auf den State anwenden.
t1: Replica A persistiert event_A1 bei vc { A: 1 }
state_A = applyEvent(initial, event_A1)
state_A's vc = { A: 1 }
t2: Replica B persistiert event_B1 bei vc { B: 1 } ← nebenläufig
state_B = applyEvent(initial, event_B1)
state_B's vc = { B: 1 }
t3: Replica A liest event_B1.
Vergleich von event_B1's vc { B: 1 } mit state_A's vc { A: 1 }:
→ nebenläufig
Resolver aufrufen:
merged_state = resolver.resolve(state_A, [event_A1, event_B1])
state_A = merged_state
state_A's vc = { A: 1, B: 1 } (der Merge)
t4: Replica A persistiert event_A2 bei vc { A: 2, B: 1 }
state_A = applyEvent(merged_state, event_A2)
t5: Replica B sieht schließlich die gesamte Kette — die eigene,
A's Pre-Merge-Events, plus A's Post-Merge-Events.
Löst alle nebenläufigen Branches auf dieselbe Weise auf.

Schließlich konvergieren beide Replicas — bei ausreichender Cross-Replica-Sichtbarkeit und einem deterministischen Resolver.

Vector Clocks geben keine Totalordnung. Zwei Events bei { A: 5 } und { B: 5 } sind nebenläufig — keines ist “zuerst passiert.” Die Reihenfolge, in der sie im Journal erscheinen, ist zufällig.

Das ist der ganze Punkt — Replicas schreiben unabhängig; Clocks verfolgen Kausalität, nicht Wall-Clock-Zeit.

Für eine Totalordnung (selten) paare es mit einem Single-Writer- Lease (siehe Single-Writer-Lease).

Vector Clocks serialisieren als plain JSON-Objekte:

{
"_vc": { "A": 5, "B": 3, "C": 1 }
}

Null-Wert-Komponenten werden typischerweise für Kompaktheit weggelassen; das Framework behandelt fehlende Keys als 0.

Über die Zeit hinterlassen pensionierte Replicas (dekommissionierte Regionen, entfernte Nodes) Einträge in Vector Clocks:

vc { A: 100, B: 50, C: 25, RETIRED-D: 7 }

Die Komponente der pensionierten Replica ist irrelevant, nimmt aber weiterhin Platz ein. Vector-Clock-Garbage-Collection ist für v1 außerhalb des Scopes — das Framework entfernt diese Einträge nie, sodass eine Clock mit jeder jemals gesehenen Replica wächst.

Das ist für ein stabiles Cluster in Ordnung. Ein Deployment mit viel Node-Churn wird irgendwann Kompaktierung wollen, doch das ist künftige Arbeit und heute kein unterstütztes Feature.

Tust du normalerweise nicht. Das Framework verwaltet sie intern; dein Conflict Resolver erhält Events mit ihren Vector Clocks bereits verglichen — du entscheidest nur, wie der State angesichts der konfligierenden Events gemergt wird.

Für Diagnostik gilt: Die aktuelle Clock des Actors wird privat gehalten und ist nicht über einen öffentlichen Getter zugänglich. Sie wird mit jedem persistierten Event und Snapshot mitgeführt, inspiziere sie also im Journal- / Snapshot-JSON — dem _vc-Feld, das oben unter Wire-Format gezeigt wird — statt sie vom Actor abzulesen.