Ve škole mě učili ACID vlastnosti databázových systémů, které dnes vládnou světu. Nebo ne? Potřebujeme garantované transakční vlastnosti například pro internetový obchod? Čím dál častěji bude odpověď znít: oželíme, protože naše konkurenceschopnost je lepší bez nich. Jak to?
Overbooking v letecké přepravě
Ještě, než se pustíme do srovnávání ACID a Eventuální konzistence, uveďme, že jejím výsledkem může být, že vzácně dojde k nekonzistentní transakci – tedy například prodáme jedno sedadlo v letadle dvěma lidem, protože si ho náhodou koupili v úplně stejný čas. Vadí to?
Pokud má letadlo 200 míst, letecké společnosti už mnoho let nezastaví prodej po dosažení 200 vydaných letenek. Statisticky vědí, že na konkrétním typu letu se s velkou pravděpodobností někdo odhlásí, onemocní či z jiných důvodů let nenastoupí. Tato místa (třeba pět) pak zůstanou neobsazena. Potenciální zájemci měli smůlu a letěli s konkurencí. Letecká společnost je tedy ochotna prodat letenek 205, což ji statisticky umožní mít plně obsazené letadlo, tedy dosáhnout maximální efektivitu a zisk. Může se občas stát, že statistický model selže – možná už se vám to stalo. Společnost nabídne například 100 EUR tomu, kdo se vzdá místa v letadle a poletí o pár hodin později. Je to pakatel v porovnání s tím, kolik strategie overbookingu přináší.
Jinak řečeno – dočasná nekonzistence může vypadat technicky hrozivě, ale její řešení může být obchodní, snadné a levné (v porovnání se situací s vynucenou konzistencí). Co když máte na skladě poslední pračku a ve stejnou vteřinu si ji koupí dva zákazníci, což vám ACID systém ošetří, ale eventuálně konzistentní nemusí? Jednomu z nich jednoduše nabídnete lepší model stejné značky bez navýšení ceny. Rád to přijme a ještě vám vytvoří velmi pozitivní referenci o chování vaší firmy, když dojde k takové situaci. Stojí to pár kaček – technická úspora za rozvnolnění ACID může znamenat ušetřené miliony za hardware i licence.
ACID
Vraťme se do učebnic – co je ACID vlastnost transakcí? Atomicity říká, že je nelze roztrhnout v půlce (odečíst peníze z jednoho účtu, ale už nepřipsat na druhý). Consistency znamená, že celý systém je v každé milisekundě svého života v konzistentním stavu. Můžete dělat cokoli, číst, zapisovat, odkudkoli, jakkoli, kdykoli – vždy bude stav konzistentní v tom smyslu, že bude dodržovat omezující podmínky v systému (sloupec, který musí mít hodnotu, nebude NULL) – což samozřejmě nezaručuje logickou konzistenci (aplikace klidně může správně zapsat hlouposti). Transakce jsou Isolated, takže se nemohou vzájemně ovliňovat (to je složitější, než se zdá a u jednoduchých systémů to má mouchy – například udržení konzistence může znamenat zámky na záznamy a vznik stavu, kdy na sebe dvě transakce donekonečna čekají, tzv. deadlock – řešit to lze, ale tak jednoduché to není). Výsledky transakce jsou Durable, takže i kdyby se po jejich skončení stalo něco nečekaného, třeba se restartoval server, zápisy tam musí trvale zůstat.
Vraťme se ještě k požadavku na izolaci, který je velmi obtížný a vede na masivní dopady ve výkonu. Zejména pokud nebudete mít systém na jednom serveru, ale hned na několika v různých lokalitách – natáhnout zabezpečovací mechanismy mezi nimi často snižuje výkonu hluboko pod možnosti jediného serveru (už jen pro dodržení Atomicity s využitím dvoufázového commitu nebo Paxos vás čeká velká latence navíc po přidání druhého serveru v synchronním režimu). Skutečná izolace je na úrovni obvykle označované jako Serializable. To znamená v mnoha implementacích dát zámečky na zápis, ale i na čtení a dokonce i uzamčení celého range (SELECT s nějakou WHERE klauzulí). Dopady na výkon jsou brutální a držet tohle v distribuovaném sytém+u je sebevražda. Pokud vám nevadí menší izolace, můžete použít Repeatable read, tedy garanci, že čtení v rámci jedné transakce dopadne pokaždé stejně (že se hodnota nezmění) – nicméně už nedáváte range zámeček, takže se může objevit fantomové čtení (SELECT s WHERE vrátí jiný počet záznamů). Obvykle se systémy “vyrelaxují” ještě víc a použije se Read commit. Nikdo vám negarantuje, že pokud budete během transakce číst dvakrát to samé, dostanete pokaždé stejnou hodnotu (nedávají se read zámky nebo se uvolní hned po každém SELECT, ne až na konci transakce). Dokonce i write zámek se dá vypnout a pak následují čtení uncommited hodnot (dirty read), ale to už je z pohledu ACID opravdu špinavé.
Obvykle tedy ACID trochu ošidíte…a možná ještě víc. ACID se dá docela dobře udělat v jednom serveru, ale na více uzlech po přidání latence dostáváte horší parametry, než na jednom (čili nám to trochu anti-škáluje). Distribuované řešení mnohdy vede (z důvodu vyřešení jinak problematické škálovatelnosti) na Sharding, kdy si DB rozdělíte na řezy a každý je na jiném serveru (ACID uvnitř řezu je OK, ale pokud chcete ACID napříč hodnotami z různých serverů, máte problém – když nic jiného tak výkonnostní). Mimochodem to vede ke strategii mít “řezy” podle logických celků (faktura, zákazník, …) místo klasického relačního přístupu (no a už máte nakročeno k document oriented NoSQL, což je jeden z typů, který rozebereme na cloudsvet.cz jindy). Pokud už takhle slevujete, co to udělat jinak a nasadit systém s vlastnostmi eventuální konzistence?
Kde použít ACID? Tam, kde transakční vlastnosti jsou klíčem a i sebemenší riziko jejich porušení má fatální následky neopravitelého charakteru – lidské životy nebo hoooodně velké peníze.
Eventuální konzistence
Ponechme dnes stranou CAP teorém (z didaktických důvodů – používá podobná slova jako ACID, ale se zcela jiným významem, navíc ACID je o datové transakci i na jednom stroji, CAP je teorie distribuovaných systémů – o tom příště) a zaměřme se rovnou na eventuální konzistenci. Dnes existuje velké množství software pro datovou vrstvu postavených na těchto vlastnostech (často se označují jako NoSQL). Co je tedy eventuální konzistence?
ACID implikuje, že na konci transakce je vše trvale zapsané a konzistentní. Pokud v ten okamžik bude kdokoli číst, vždy dostane aktuální (před chviličkou zapsaný) výsledek. Eventuální konzistence dává přednost masivní škálovatelnosti a velkému výkonu, ale na oplátku trochu šidí jiné věci. Tak například po zápisu je systém ve stavu, kdy se nová informace šíří a může trvat nějakou dobu, než doputuje ke všem relevantním uzlům (obvykle definujete, že položka musí být zapsána na X uzlech v Y zónách dostupnosti z celkového počtu Z uzlů – typicky v mém 10 nodovém systému budu chtít data zapsaná na 3 uzlech tak, že ty jsou minimálně ve 2 počítačových sálech, třeba 2 v jednom racku a 1 v jiném místě). Těsně po zapsání tedy můžete přečíst novou hodnotu (ze systému, který zápis inicioval), ale také starou hodnotu (ze vzdáleného nodu, kam změna ještě nedorazila). To se vám v ACID nestane. Pokud nastavíte “commit” po zápisu třeba na dva systémy, máte dobrou Durabilitu (takže to není o tom, že vaše zápisy jsou nějak fatálně nejisté), ale i tak bude nějakou krátkou dob trvat, než se dosáhne plné konzistence – systém je tedy eventuálně konzistentní (= po nějakém čase pokud nebudete do hodnoty šťouchat).
Eventuální konzistence je jen jeden z aspektů a NoSQL systémy mohou mít například velmi odlišné přístupy k řešení k roztržení clusteru – uvedu jednoduchý příklad. Pokud máte 3 nody a vypadne vám síť, takže jeden z nich nevidí ostatní. Co teď? Mají být všechny nody nadále dostupné, protože to zajišťuje stále stejný výkon, latenci a dostupnost? OK, pak ale ve dvou vzájemně nemluvících částech musí vzniknout konflikty a problém s konzistencí. Chcete konzistenci udržet? Pak se musí ten uzel, který je najednou sám, zcela odmlčet a nepřijímat žádné požadavky – ovšem na úkor výkonu a latence systému. Tohle ale také můžete volit ne per-systém, ale podle typu operace nebo typu dat.
Na konci tisíciletí došlo k myšlenkovému obohacení a svět si uvědomil, že ACID je jen jeden z možných designů, a že v ráci CAP omezení (příště) lze mít i systémy jiných vlastností. V příštích článcích se podíváme na CAP teorii, CP a AP systémy (vs. CA), vysvětlíme si BASE a rozřadíme si některé NoSQL systémy podle přístupu k těmto omezením.
Mimochodem – pro svoje vlastnosti (horizontální škálovatelnost na menších obyčejných serverech) se krásně hodí do prostředí OpenStack IaaS (včetně toho, že OpenStack trove pro DBaaS mnohé z nich podporuje) a PaaS.
Minimalizace rizika
Dopady případné dočasné nekonzistece můžete velmi výrazně omezit způsobem, jak jsou vaše aplikace napsané. Například asi není dobrý nápad načíst skladové zásoby v okamžiku vložení zboží do košíku a nezkontrolovat je za deset minut, kdy se zákazník odebírá k elektronické pokladně. Aplikace by měla tušit, že když něco zapíše a za vteřinu to samé chce přečíst, možná to tam ještě nebude (protože load balancing osloví jiný node, kde systém ještě nedoběhl do konzistence) – ale i s tím se dá počítat. O některých strategiích budeme na cloudsvet.cz ještě psát.