Representational State Transfer - a service-ek feletti tranzakciókat ha lehet kerüljük

Találkoztam egy érdekes kérdéssel stackoverflow-on azzal kapcsolatban, hogy hogyan lehet tranzakciókat használni több REST service összehangolására. A válasz elég egyértelmű, sehogy. Azt is leírom, hogy miért, mert nekem is utana kellett kicsit olvasni meg gondolni, hogy kézenfekvő legyen.

Mint már régebben említettem, létezik olyan, hogy stateless megkötés. Hogy egy kicsit felfrissítsem az emlékeket, ez arról szól, hogy a kliens állapotát a kliens tartja karban, míg az erőforrások állapotát a szerver tartja karban. Szóval ha a kliens munkamenetét a szerveren tároljuk, akkor a kommunikáció a kliens és a szerver között nem lesz állapotmentes (stateless), mert a kliens által küldött kérésekből hiányozni fog egy fontos darab, amit a szerveren tárolunk munkamenetben. Lehet rá hivatkozni session id-vel, de nem fogunk tudni ez alapján visszajátszani egy régebbi üzenetet, mert az üzenet nem fogja tartalmazni a szükséges információt ugyanúgy, ahogy a szerveren tárolt munkamenet sem, mert a munkamenet mindig csak az aktuális kliens állapot egy töredékét tárolja, nincs benne history, hogy visszanézzük, hogy az üzenet küldésekor mi volt a munkamenetben. Máshogy fogalmazva egy-egy kéréshez külön kontextus tartozna, ami idővel elveszne, így a kérések nem lennének ismételhetőek. Ha nem ismételhetőek a kérések, akkor a válasz sem ismételhető, tehát nem lehet kesselni sem. Így nehezen skálázható lesz a dolog. Ez a leggyakoribb példa, hogy hogyan lehet megsérteni az állapotmentesség szabályát.

A tranzakciók használata egy sokkal érdekesebb példa a szabályok áthágására. Ugye elviekben használhatnánk multi phase commit-et és így szinkronban tarthatnánk az eltérő REST service-ekhez tartozó erőforrások állapotát. Miért gond ez? Mint már említettem az állapotmentesség jegyében a kliens tartja karban a kliens állapotát és a szerver tartja karban az erőforrások állapotát. Ha a kliens vezényelné le, hogy az egyes szerverek hogyan tartsák karban az erőforrások állapotát, akkor megintcsak nem lenne állapotmentes a kommunikáció, mert egy-egy tranzakció külön kontextusba tenné a kéréseket. A gyakorlat szempontjából ez akkor fontos, ha horizontálisan skálázunk, és több példányt hozunk létre egy-egy REST service-ből. Ilyenkor ezek között a példányok között szinkronban kellene tartani, hogy milyen tranzakciók vannak folyamatban. Ellenkező esetben ha másik példányhoz kerül a commit vagy a rollback, akkor bajban leszünk, mert a másik példány nem tud róla, hogy előzőleg milyen tranzakciót hoztunk létre.

Ha már úgyis le kell tárolni a tranzakciókat, akkor gondolhatunk arra, hogy logikusabb, ha erőforrásokba mentjük őket. Ez még működhet is, de nagyon ronda lenne, ha úgy kellene egy-egy műveletet végrehajtani, hogy POST /transactions/ majd később POST /transactions/1/commit ahelyett, hogy mondjuk PUT /forums/1/messages/23. Azt hiszem érezhető a különbség a két megközelítés között. Nem utolsó sorban megszegnénk nem egy interface constraint-et.

Milyen megoldás lehet hát ezekre a tranzakciós jellegű problémákra?

Nem véletlen, hogy szükség van tranzakciókra, gondoljunk csak bele, ha egyszerre több erőforrást, mint fájlrendszer, adatbázisok kell szinkronban tartanunk, akkor szükség van multi phase commit-ra, hogy hiba esetén tudjunk rollback-et használni, és ne kerüljenek inkonzisztens állapotba ezek az erőforrások. A gond azzal van, hogyha rosszul tagoljuk fel a REST service-einket, akkor megoszlanak közöttük ezek az egyébként összetartozó erőforrások, és kénytelen kelletlen magasabb absztrakciós szintre - a REST service-ek szintjére - kell vinnünk a tranzakcióinkat is.

Az egyik megoldás értelemszerűen, ha visszaállítjuk a megfelelő tagolást a REST service-ek között, és ezzel visszakerül a tranzakció kezelés alacsony absztrakciós szintre.

A másik megoldás, ha a tranzakciók okát, az immediate consistency-t megszüntetjük, és helyette eventual consistency-t használunk. Ilyenkor akár jelezhetjük 202 accepted status header-el a kérőnek, hogy a kérését a rendszer befogadta, és a feldolgozás folyamatban van. Ha egy-egy a feldolgozáskor használt REST service ilyenkor elszáll, akkor amint feléledt folytatja a kérés feldolgozását. Nem kötelező, hogy mindegyik REST service állandóan szinkronban legyen, csak az számít, hogy ne maradjanak le egymástól túlságosan, és a változtatások a megfelelő sorrendben eljussanak mindegyikhez. Ez a megoldás nyilván csak akkor életképes, ha belső REST service-ekről van szó, szóval ha mi írjuk és futtatjuk a belső kliens kódját. Ha nem ez a helyzet, akkor csak az első megoldás életképes. A kettő több-kevésbé kombinálható, ha a megfelelő tagolás visszaállítására építünk egy REST service-t, ami kívülre látható, és mint belső kliens összefogja a rosszul tagolt belső REST service-eket. Szvsz. jobb inkább az első megoldásnál maradni.

Nincsenek megjegyzések:

Megjegyzés küldése