Iată o defecțiune pe care am văzut-o de două ori, în două motoare diferite. Un export rulează la 10:00 și înregistrează cea mai mare marcă de timp a ultimei modificări pe care a văzut-o. O tranzacție începută la 09:58 este încă deschisă, face commit la 10:03 și poartă o marcă de timp din momentul în care a început. Următorul export cere doar rânduri mai noi de 10:00 și nu o vede niciodată. Dimensiunea transferului a fost excelentă, iar datele erau greșite.

Urmează exportul care a pierdut un rând

Urmărește ordinea evenimentelor, nu SQL-ul. Aplicația scrie timpul de modificare al rândului la începutul tranzacției, nu la commit. Exportatorul citește un marcaj de apă de ceas de perete în timp ce tranzacția este încă deschisă. Apoi tranzacția face commit cu un timp de modificare mai vechi decât marcajul salvat. Următorul lot al consumatorului începe după marcaj, așa că rândul rămâne în urma cursorului și nu va mai fi selectat niciodată.

Nimic nu înregistrează o eroare, pentru că nimic nu a eșuat. Asta face această clasă de defecte costisitoare: fluxul delta este rapid, numerele de rânduri par plauzibile, iar rândul lipsă apare săptămâni mai târziu ca un tichet de reconciliere pe care nimeni nu îl poate reproduce.

Numerele de secvență au aceeași formă de defect dintr-un alt motiv. Un ID monoton îți spune ordinea în care au fost alocate valorile, nu ordinea în care tranzacțiile au făcut commit. O tranzacție care a luat un număr mic poate face commit după una care a luat un număr mare, așa că un cursor bazat pe ordinea alocării poate lăsa și el o gaură.

Cinci promisiuni pe care fluxul trebuie să le facă

Înainte de a alege un transport, cere sistemului sursă un contract cu cinci clauze. Dacă lipsește vreuna, ai o euristică, nu un flux de modificări.

  • Acoperire: inserări, actualizări și ștergeri, fiecare cu cheia stabilă necesară pentru aplicare.
  • Graniță: ce înseamnă o valoare de cursor și ce conține un lot coerent.
  • Consistență: cum se citesc tabelele înrudite fără a amesteca stări sursă incompatibile.
  • Retenție: cât timp poate relua un consumator deconectat din poziția salvată.
  • Reinițializare: un instantaneu complet acceptat pentru când acea poziție nu mai este validă.

Un mecanism acceptat poate satisface toate cinci. Documentația SQL Server despre urmărirea modificărilor este un exemplu publicat concret: definește o versiune minimă validă, înregistrează ștergerile precum și modificările și descrie o tranzacție de instantaneu care acoperă validarea, captura versiunii și interogările de modificări asociate. Este un exemplu despre ce să ceri unei surse, nu o sintaxă pe care o lipești în alt motor.

Proiectează punctul de control ca parte a pipeline-ului

Poziția salvată a consumatorului este adevărata mașină de stări, așa că scrie-o ca atare. Cursorul poate înainta doar prin fluxul de lucru care a aplicat deja lotul și a făcut commit tranzacției țintă.

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

Dacă livrarea eșuează la jumătatea drumului, reconciliază același lot în loc să inventezi o poziție sursă nouă. Reextragerea cu o limită superioară mai recentă este calea prin care o aplicare parțială devine o gaură permanentă. Regula oglinzii se aplică și pe cealaltă parte: aplicarea idempotentă la țintă previne duplicatele și nu face nimic pentru o modificare pe care extracția nu a văzut-o niciodată.

Ștergerile au nevoie de înregistrări, nu de concluzii

Compararea rândurilor care încă există nu poate spune unui consumator ce chei au dispărut. O sursă care nu înregistrează niciodată ștergeri îți lasă două opțiuni imperfecte: să păstrezi un istoric de chei suficient de lung pentru diff sau să rulezi o reconciliere completă explicită conform unui calendar. Ambele sunt decizii de proiectare cu un cost și niciuna nu există dacă nimeni nu o deține.

Retenția este același tip de decizie. Fiecare flux de modificări acceptat are o fereastră; declară-o în contract și avertizează când punctul de control al unui consumator se apropie de ea. Un consumator care se trezește după o întrerupere de două săptămâni ar trebui să citească o eroare clară „este necesară reinițializarea” în loc să primească în tăcere un lot parțial.

Apoi măsoară dacă delta a meritat

Revenirea la compararea fiecărei chei și a fiecărui hash este uneori corectă și merită să știi cât costă. Transferi în continuare informații despre fiecare rând, hash-urile nu îți dau un instantaneu coerent între tabele, iar o schemă de hash aduce propriile întrebări despre reprezentare și coliziuni. Mai puțini octeți cu o graniță verificabilă este scopul; mai puțini octeți care lasă să cadă în tăcere o modificare confirmată nu este deloc delta.

O coloană de ultima modificare este un indiciu. Un cursor care supraviețuiește unui commit târziu, unei ștergeri și unei întreruperi lungi este un contract.

Articole similare