Saga – distribuované transakcie v nezávislých databázach

2019/11/23

„Povedzte učiteľovi, že nemám ságy,“ úctivo odpovedal Ostap.

— Iľf-Petrov, Zlaté teľa (1931)

Kedy ságy?

Ak máme bežnú relačnú databázu na jednom stroji a chceme to mať postavené dobre, použijeme transakcie spĺňajúce zásady ACID.

Ak máme bežnú relačnú databázu rozdelenú na viacero strojov (partície) a chceme to mať tiež postavené dobre, použijeme dvojfázový komit (2PC, two-phase commit).

Ak máme viacero nezávislých databáz — napríklad každá mikroslužba má svoju databázu — dvojfázový komit sa nedá realizovať. (V takom prípade možno nemá zmysel ani relačná databáza…)

To je situácia, keď distribuovanú transakciu implementujeme ako ságu.

Sága

Sága je postupnosť lokálnych transakcií v nezávislých službách.

Každá lokálna transakcia, teda aktualizácia dát v konkrétnej nezávislej databáze jednej služby, musí:

Príklad ságy

Predstavme si microservicovú architektúru:

Súc systémom, očakávam, že:

  1. zákazník si objedná tovar
  2. prijmeme od neho platbu
  3. odošleme tovar.

Toto je distribuovaná transakcia, ktorá by sa v bežnej monolitickej databáze riešila s dodržaním pravidiel ACID.

Ako to však riešiť krížom cez viaceré nezávislé databázy?

Architektúry pre ságy

Implementácia má dva typické prístupy:

Event/Choreography: choreografia

Úspešný scenár

Služba Objednávky započne transakciu pre vytvorenie objednávky.

  1. Služba Objednávky publikuje udalosť ORDER_CREATED. Na túto udalosť počúvajú ďalšie služby, ktoré podľa potreby vykonajú svoje transakcie a vypublikujú nové udalosti: 2. Služba Platby vyšle ORDER_BILLED. 2. Služba Doručenie vyšle ORDER_DISPATCHED.
  2. Transakcia končí, keď posledná služba dokončí svoju lokálnu transakciu.
  3. Transakcia končí i v prípade, keď na udalosť nikto nereagoval.

Služba Objednávky vie sledovať všetky udalosti plávajúce v systéme a monitorovať stav.

Rollbacky v neúspešnom scenári

V prípade, že niektorá z lokálnych transakcií zlyhá, treba odvolať všetky predošlé kroky ságy, teda treba spustiť kompenzácie.

Ak služba Doručenie zistí, že tovar nie je na sklade, treba odvolať predošlé kroky:

  1. Služba Platby kompenzuje lokálnu transakciu vrátením peňazí zákazníkovi.
  2. Služba Objednávky povie zákazníkovi, že objednávka zlyhala (a že ju možno treba zopakovať neskôr.)

Výhody choreografie

Choreografia sa ľahké chápe, ľahko kreslí na tabuľu, ľahko sa buduje.

Účastníci sú loosely coupled, pretože vzájomne komunikujú len cez správy.

Odporúča sa ju zvážiť pri jednoduchších ságach, ktoré majú 2 až 4 kroky.

Z technického hľadiska treba implementovať korelačné identifikátory v správach. Všetky správy reprezentujúce udalosti v rámci jednej ságy musia mať identifikátor, aby poslucháči vedeli, na ktorú transakciu reagovať.

Nevýhody choreografie

Ak má sága priveľa krokov, je náročné sledovať, ktorá služba má reagovať na ktoré správy.

Navyše, môžu sa zjaviť neočakávané cyklické závislosti, keď si služby vzájomne počúvajú na udalosti a zacyklia sa.

Testovanie je náročnejšie, pretože na overenie musíme mať spustené všetky služby.

Command / Orchestration

V tomto štýle zavedieme medzi služby dodatočného účastníka: saga orchestrator, ktorý zodpovedá dirigentovi v orchestri.

Orchestrátor

Orchestrátor ovláda jednotlivé kroky na úspešné splnenie transakcie. V prípade zlyhaní vie rollbacknúť ságu, tým, že každej službe pošle rozkaz na odvolanie (undo) príslušnej lokálnej transakcie.

Úspešný scenár

  1. Služba Objednávky požiada orchestrátora o začatie transakcie
  2. Orchestrátor pošle Platbám príkaz vykonaj platbu!
    1. Platby odvetia platba vykonaná!
  3. Orchestrátor pošle Doručeniam príkaz doručte tovar!
    1. Doručenia odpovedia tovar doručený!

Neúspešný scenár

  1. Služba Objednávky požiada orchestrátora o začatie transakcie.
  2. Orchestrátor pošle Platbám príkaz vykonaj platbu!
    1. Platby odvetia platba vykonaná!
  3. Orchestrátor pošle Doručeniam príkaz doručte tovar!
    1. Doručenia odpovedia nemáme tovar!
  4. Orchestrátor vie, že uspela len jedna lokálna transakcia (v službePlatby) a prikáže službe Platby spustiť kompenzáciu, t. j. refundovať zákazníka.

Výhody orchestrácie

Nevýhody orchestrácie

Implementácia

Implementácia správ

Podobne ako v choreografii je treba používať korelačné identifikátory v správach prináležiacim k jednej transakcii.

Adresáta pre odpovede je dobré vložiť do samotnej správy a nepoužívať centrálne napečenú adresu orchestrátora.

Treba sa vyhnúť synchrónnym volaniam v štýle RPC, napr. postaviť orchestrátora a mikroslužbu na synchrónnom HTTP. Mnoho transakcií totiž vie pokračovať aj v prípade, že je druhá strana vypnutá/havarovaná/nedostupná. Na druhej strane, asynchrónny model je náročnejší na implementáciu.

Odporúčaná architektúra na komunikáciu využíva message brokery.

Keďže typické message brokery využívajú doručenie správ v duchu at least once (správa bude doručená aspoň raz, teda napríklad aj 3x v prípade opakovaných pokusov), treba zaručiť idempotentnosť operácií v službách i orchestrátorovi.

Implementácia orchestrátora

Orchestrátor sa dá implementovať ako konečnostavový automat (state machine).

Ak orchestrátor posiela požiadavku, vyvolá príslušnú službu, uloží stav entity (napr. Objednávky) v rámci ságy (napr. Pending, Created) a čaká na odpoveď.

Pri odpovedi orchestrátor zistí stav entity z databázy, potom rozhodne, ktorého účastníka zavolá, aktualizuje stav entity.

Implementácia ságy

Asynchrónna komunikácia a message broker

Účastníci ságy komunikujú asynchrónne. Buď odosielajú udalosti a reagujú na ne (v prípade choreografie) alebo používajú asynchrónny štýl request/response (v prípade orchestrácie).

Asynchrónny spôsob sa dá dosiahnuť použitím message brokera (RabbitMQ, Kafka a pod.)

Potrebujeme totiž nutne garantovať, že sága skončí, aj keď je niektorý účastník vyradený. Na to potrebujeme:

Databázy

Lokálna transakcia v rámci jednej služby musí atomicky:

  1. Aktualizovať databázu
  2. A zároveň odoslať správu s udalosťou, či notifikáciou o úspechu či zlyhaní.

Inými slovami, musíme sa vyhnúť situáciám:

To vieme zabezpečiť dvoma návrhovými vzormi pre mikroslužby:

Kompenzácie: ako sa vysporiadať s lokálnou transakciou, ktorá zlyhala

Každá lokálna transakcia skôr či neskôr zlyhá — obvykle kvôli narušeniu pravidiel biznisovej logiky. Chceme vydať tovar, ktorý nie je na sklade? Lokálna transakcia musí zlyhať a sága sa musí zrušiť.

Služba podieľajúca sa na ságe musí explicitne deklarovať kompenzujúcu transakciu, ktorá zodpovedá undo kroku, teda odvolá, či zvráti účinky lokálnej transakcie.

Implementácia kompenzujúcej transakcie má nasledovné výzvy:

Škodlivé prístupy k dátam

Škodlivé prístupy k dátam, ktoré adresujú zásady ACID v bežných transakciách, sa môžu prejaviť aj v ságach.

Dirty Read

Klasický príklad je spracovanie objednávky. Ak je sága A pomalá, sága B môže v rámci kompenzácie objednávku zrušiť skôr než ju sága A úspešne dokončí („komitne“). Takto dôjde k dokončeniu už zrušenej objednávky, čo je rozhodne neželaný stav. Ukazuje fenomén dirty read, teda čítanie nekomitnutých dát, ktorý vieme ošetriť použitím sémantických zámkov (viď nižšie).

Lost Update

Iné očividné riešenie kompenzujúcej transakcie navádza na postup:

  1. Pred lokálnou transakciou si zapamätáme stav entity. Stav účtu je 20 korún.

  2. Vykonáme lokálnu transakciu. Stav účtu po zaplatení je 10 korún.

  3. Paralelná transakcia nastaví stav účtu na 5 korún.

  4. Okolnosti nás prinútia kompenzovať, čo dosiahneme pôvodnými hodnotami. Nastavíme stav účtu na 20 korún.

  5. Práve sme dali zákazníkovi viac peňazí než má právo.

    Tento prístup zjavne nefunguje a je ukážkou fenoménu lost update (stratené zmeny)

Jedným z riešení je použitie komutatívnych zmien (viď nižšie.)

Dirty Read

Dvakrát pričítame ku kreditu zákazníka, raz z jednej, raz z druhej transakcie, hoci druhé pričítanie nie je komitnuté a bude odvolané. Medzi týmito operáciami si zákazník môže dokončiť ságu, ktorá je nad jeho reálne financie.

Rodiny lokálnych transakcií

Vedecké články deklarujú tri rodiny lokálnych transakcií:

V príklade rezervácie stola v reštaurácii: ak Autorizácia platby uspeje (pivot), potom Rezervácia stola musí tiež uspieť (retriable).

Iný príklad ságy navrhnutej nasledovne:

  1. Vytvorenie objednávky (a kompenzácia cez zamietnutie objednávky).
  2. Rezervácia kreditu (pivot), ktorá môže zlyhať, kompenzáciu nepotrebuje (pozri nižšie.)
  3. Schválenie objednávky (retriable): nemôže zlyhať, nepotrebuje kompenzáciu.

Keďže Schválenie nemôže zlyhať, rezervácia kreditu nepotrebuje kompenzáciu.

Rady a odporúčania

Kroky ságy vyslovenie závisia od činnosti, ktorú chceme dosiahnuť. Je teda vysoko aplikačne-špecifická.

Ak krok ságy závisí na predošlom kroku, je to prirodzené. Niekedy si však môžeme vybrať poradie krokov a preusporiadať ho. Ideálne je, ak môžeme aktualizácie dát (updates) presunúť až za pivot, pretože tým sa zbavíme povinnosti tvoriť kompenzačné transakcie.

Opatrenia proti ACID fenoménom

Ľiteratúra deklaruje nasledovné oblastí, kde vieme zabrániť fenoménom z ACID zásad:

Semantic Locks — sémantické zámky

Pomocou sémantického zámku oddelíme špinavé dáta (zmenené, ale nekomitnuté) od ostatných dát a zabránime fenoménu dirty read.

Na to sa odporúča, aby každá biznisová entita (napr. Objednávka) mala svoj evidovaný stav v rámci ságy.

Napríklad započatá Objednávka bude mať stav Pending (nevyriešená). Vybavená objednávka sa po dokončení transakcie prepne do stavu Approved (analógia komitu) alebo Rejected (analógia abortu).

Predstavme si, že paralelne bežiaca lokálna transakcia sa rozhodne zrušiť nevyriešenú objednávku. Toto nie je povolený stav a preto táto paralelná transakcia oznámi klientovi, že musí skúsiť neskôr.

Tieto opakované pokusy:

Commutative Updates — komutatívne zmeny

Komutatívne zmeny sú ochranou proti fenoménu lost updates, teda keď v rámci transakcie sa načítajú dáta, uložia do pamäte, následne zapíšu, ale medzičasom dáta v databáze upravila iná transakcia. Zmeny z inej transakcie sa tak stratili.

  1. Transakcia A načíta stav Heleninho účtu, ktorý je 100 korún. Poznačí si to do premennej.
  2. Transakcia B zistí, že Helena má na účte stále 100 korún.
  3. Transakcia B vyberie z Heleninho účtu 40 korún, zakúpi čln, nastaví stav účtu na 60 a komitne.
  4. Transakcia A príde na rad. Od stavu účtu v premennej odoberie 20 korún, kúpi si luk a výsledok (80 korún) zapíše do databázy a komitne.

Nielenže má Helena na účte 80 korún, ale pribudol jej čln a luk v celkovej cene 60 korún. Stratila sa 40korunová zmena.

Databázové zmeny sú komutatívne, ak sa dajú vykonať v ľubovoľnom poradí. Matematické operácie odčítania a kompenzácie v podobe čítania sú prirodzene komutatívne. Ak nebudeme zapisovať finálne stavy účtov, ale len rozdiely, vyhneme sa lost updateom.

Operácie sú jednoduché: odčítam 20 v transakci A, odčítam 40 v transakcii B sa dajú vykonať aj v opačnom poradí a výsledok sa zachová.

To platí aj pre kompenzácie. Ak zažijeme kombináciu

A: mínus 20, B: mínus 40, kompenzácia B: plus 40

je to to isté ako

B: mínus 40, A: mínus 20, kompenzácia B: plus 40.

Bežné operácie sú rezervuj kredit-odomkni kredit, alebo prirátaj sumu-odrátaj sumu.

Pessimistic View — pesimistické pohľady

Pesimistické pohľady chránia pred fenoménom dirty read, teda keď modrá transakcia číta zmenené, ale nekomitnuté dáta z červenej transakcie.

V tomto prípade preusporiadame kroky ságy vhodným spôsobom.

Typické riziko je dlhodobé držanie dát, ktoré by sa mali v jednoduchej databáze zamykať. Učebnicový príklad je kapacita voľných miest vo vlaku, kde medzi počiatočným získaním obsadenosti a predaním lístka môže uplynúť pridlhá doba a dôjde k overbookingu.

Riešením sú:

Reread Value – znovunačítanie hodnoty

Znovunačítanie hodnôt chráni pred fenoménom lost updates, keď sa zmeny stratia.

Pred aktualizáciou riadku načítame jeho dáta a porovnáme ich s dátami v cache, či premenných. Ak zistíme rozdiely, ságu abortneme a spustíme kompenzácie.

Toto je variant princípu optimistického uzamykania (optimistic offline lock).

Version File – verzovací súbor

Verzovací súbor sa používa v kombinácii s komutatívnymi zmenami.

Do verzovacieho súboru sa v rámci transakcie ukladajú záznamy s nasledovnou štruktúrou:

Paralelne bežiace operácie sa zaznamenajú do verzovacieho súboru, a následne sa preusporiadajú do správneho poradia.

V prípade viacerých zmien nad tou istou entitou môžeme rozhodnúť poslednú platnú zmenu pomocou časovej pečiatky.

V prípade duplicitných zmien môžeme optimalizovať vykonávanie krokov.

Ak máme nasledovný sled krokov:

  1. Červená transakcia autorizuje Ireninu kartu.
  2. Modrá transakcia vytvorí novú Ireninu objednávku.
  3. Červená transakcia zistí, že Irenina karta nebola autorizovaná.
  4. Modrá transakcia autorizuje Ireninu kartu.

Ak sa tieto operácie uložia do verzovacieho súboru, môžeme odstrániť duplicitnú autorizáciu karty.

Učebnicový príklad používa príklad banky, kde sa zmeny na účtoch ukladali do verzovacieho súboru. Finálny balans účtu sa vypočítaval periodicky (raz za deň) alebo na požiadanie. Keďže zmeny na účte sú komutatívne, prepočítavalo sa len v prípade zmeny balansu z nekomutatívnej transakcie.

Ak používame režim s typom transakcie a zmenovými parametrami, môžeme to použiť na opakovateľné i kompenzovateľné transakcie, pretože stav databázového riadku môžeme rekalkulovať pomocou starej verzie a aplikovaním príslušných zmenových parametrov.

Odporúčania pre ságy

Kedže ságy sú vysoko aplikačne špecifické, nie je nutné používať ich vždy a všade.

V prípade kritických operácií (prevod veľkých peňazí) je stále možné navrhnúť architektúru s použitím klasických distribuovaných transakcií. Ságy je zase možné využiť pri výkonných, ale menej riskantných prípadoch.

Jednotlivé kroky ságy zároveň môžeme navrhnúť tak, aby vysoko rizikové operácie so šancou na pád sa vykonali ako prvé a s postupujúcimi krokmi riziko narastá až po transakcie za pivotom, ktoré sú opakovateľné.

Zdroje

>> Home