Hier ist ein Fehler, den ich zweimal beobachtet habe, in zwei verschiedenen Engines. Ein Export läuft um 10:00 und merkt sich den höchsten Last-Modified-Zeitstempel, den er gesehen hat. Eine Transaktion, die um 09:58 begann, ist noch offen, committet um 10:03 und trägt einen Zeitstempel aus der Zeit ihres Beginns. Der nächste Export fragt nur nach Zeilen, die neuer als 10:00 sind, und sieht sie nie. Die Übertragungsgröße war hervorragend, und die Daten waren falsch.

Verfolgen Sie den Export, der eine Zeile verlor

Verfolgen Sie die Reihenfolge der Ereignisse statt das SQL. Die Anwendung schreibt die Änderungszeit der Zeile beim Beginn der Transaktion, nicht beim Commit. Der Exporter liest ein Wanduhr-Wasserzeichen, während die Transaktion noch offen ist. Dann committet die Transaktion mit einer Änderungszeit, die älter als das gespeicherte Wasserzeichen ist. Der nächste Batch des Consumers beginnt nach dem Wasserzeichen, also liegt die Zeile hinter dem Cursor und wird nie wieder ausgewählt.

Nichts protokolliert einen Fehler, weil nichts fehlgeschlagen ist. Das macht diese Fehlerklasse teuer: Der Delta-Feed ist schnell, die Zeilenzahlen wirken plausibel, und die fehlende Zeile taucht Wochen später als Abstimmungsticket auf, das niemand reproduzieren kann.

Sequenznummern haben dieselbe Fehlerform aus einem anderen Grund. Eine monotone ID sagt Ihnen die Reihenfolge, in der Werte vergeben wurden, nicht die Reihenfolge, in der Transaktionen committet haben. Eine Transaktion, die eine niedrige Nummer gezogen hat, kann nach einer mit hoher Nummer committen, also kann auch ein Cursor, der auf der Vergabereihenfolge basiert, ein Loch hinterlassen.

Fünf Zusagen, die der Feed geben muss

Bevor Sie einen Transport wählen, verlangen Sie vom Quellsystem einen Vertrag mit fünf Klauseln. Fehlt eine Klausel, haben Sie eine Heuristik und keinen Change Feed.

  • Abdeckung: Inserts, Updates und Deletes, jeweils mit dem stabilen Schlüssel, der zum Anwenden nötig ist.
  • Grenze: was ein Cursorwert bedeutet und was ein konsistenter Batch enthält.
  • Konsistenz: wie verwandte Tabellen gelesen werden, ohne unvereinbare Quellzustände zu vermischen.
  • Aufbewahrung: wie lange ein getrennter Consumer von seiner gespeicherten Position aus fortsetzen darf.
  • Reinitialisierung: ein unterstützter vollständiger Snapshot für den Fall, dass diese Position nicht mehr gültig ist.

Ein unterstützter Mechanismus kann alle fünf erfüllen. Die Change-Tracking-Dokumentation von SQL Server ist ein konkretes veröffentlichtes Beispiel: Sie definiert eine minimale gültige Version, zeichnet Deletes ebenso wie Änderungen auf und beschreibt eine Snapshot-Transaktion, die Validierung, Versionserfassung und die zugehörigen Änderungsabfragen umspannt. Es ist ein Beispiel dafür, was man von einer Quelle verlangen sollte, keine Syntax, die Sie in eine andere Engine einfügen können.

Entwerfen Sie den Checkpoint als Teil der Pipeline

Die gespeicherte Position des Consumers ist die eigentliche Zustandsmaschine, also schreiben Sie sie als solche. Der Cursor darf nur durch den Workflow vorrücken, der den Batch bereits angewendet und die Zieltransaktion committet hat.

cursor  = durable_checkpoint()          # last fully applied position
batch   = extract(cursor, upper_bound)  # one consistent batch
apply(batch)                            # idempotent at the target
commit()
checkpoint(batch.upper_bound)           # only now

Wenn die Zustellung auf halbem Weg scheitert, gleichen Sie denselben Batch ab, statt eine neue Quellposition zu erfinden. Erneutes Extrahieren mit einer frischeren Obergrenze ist der Weg, auf dem eine teilweise Anwendung zu einer dauerhaften Lücke wird. Die Spiegelregel gilt auch auf der anderen Seite: idempotentes Anwenden am Ziel verhindert Duplikate und tut nichts für eine Änderung, die die Extraktion nie gesehen hat.

Löschungen brauchen Aufzeichnungen, keine Schlussfolgerungen

Der Vergleich von Zeilen, die noch existieren, kann einem Consumer nicht sagen, welche Schlüssel verschwunden sind. Eine Quelle, die Löschungen nie aufzeichnet, lässt Ihnen zwei unvollkommene Optionen: eine Schlüsselhistorie lange genug für einen Diff halten oder eine ausdrückliche vollständige Abstimmung nach Zeitplan laufen lassen. Beides sind Designentscheidungen mit Kosten, und keine existiert, solange niemand sie verantwortet.

Aufbewahrung ist dieselbe Art Entscheidung. Jeder unterstützte Change Feed hat ein Fenster; nennen Sie es im Vertrag und warnen Sie, wenn sich der Checkpoint eines Consumers ihm nähert. Ein Consumer, der nach einem zweiwöchigen Ausfall aufwacht, sollte einen klaren Fehler "Reinitialisierung erforderlich" lesen, statt still einen teilweisen Batch zu erhalten.

Messen Sie dann, ob das Delta es wert war

Auf den Vergleich jedes Schlüssels und Hashes zurückzufallen ist manchmal richtig, und es lohnt sich zu wissen, was es kostet. Sie übertragen weiterhin Informationen über jede Zeile, Hashes geben Ihnen keinen konsistenten Snapshot über Tabellen hinweg, und ein Hash-Schema bringt eigene Fragen zu Darstellung und Kollisionen mit. Weniger Bytes mit einer prüfbaren Grenze ist das Ziel; weniger Bytes, die still eine committete Änderung fallen lassen, sind überhaupt kein Delta.

Eine Last-Modified-Spalte ist ein Hinweis. Ein Cursor, der einen späten Commit, eine Löschung und einen langen Ausfall übersteht, ist ein Vertrag.

Ähnliche Artikel