Memória alapú adatbázisok

A napokban ismertem meg egy új adatbázis szervezési filozófiát, az Object Prevalence -t, amit jobb híján memória alapú adatbázisnak fordítottam (tudom, hogy a szó szerinti fordításnak semmi köze ehhez, de ez adja vissza legjobban magyarul, hogy miről is van szó). A filozófia alapgondolata, hogy napjainkban a memória nem túl drága erőforrás, és megfelelő mennyiségben rendelkezésre is áll ahhoz, hogy egy rendszer teljes adatbázisát ott tároljuk. Gondoljunk csak bele, ha mondjuk egy felhasználó tárolása 5K memóriát igényel (és 5000 karakterbe aztán tényleg rengeteg adat belefér), akkor 1000 felhasználó mindössze 5Mb-ot, 10 000 pedig 50Mb-ot igényel. 10 000 felhasználó már egész jelentősnek mondható, ugyanakkor 50 Mb a jelenlegi RAM méretek mellett elenyésző. Ez alapján szinte adja magát a gondolat, hogy a teljes adatbázist tartsuk a memóriában, célszerűen az adott programozási nyelv jól megszokott eszközeivel (pl. Java esetén sima Map-ek és List-ek). Kicsit olyan érzésem van a dologgal kapcsolatban, mintha a relációs adatbázis kezelők csak mindössze a nyakunkon maradtak volna azokból az időkből, mikor még a memória és a tároló kapacitás drága volt.

Más szempontból vizsgálva azt is mondhatjuk, hogy a memóriába ágyazott adatbázisok a jelenlegi rendszerek optimalizálásának logikus következő lépései. Gondoljunk csak egy általános vállalati rendszerre, ahol az adatbázist valamilyen ORM rétegen keresztül (pl. Hibernate) érjük el. Egy entitás objektum módosítása esetén módosítjuk a memóriában lévő lokális változatot, amit az ORM engine SQL utasításokra alakít, átpaszírozza őket egy socketen, ott az adatbázis szerver értelmezi az SQL utasításokat, és módosítja a winchesteren található fájlokat. Ha adatot olvasunk, ugyanez megy visszafelé. Az az ironikus a dologban, hogy a winchesteren lévő adatbázis fájlokat a gyakori használat miatt az operációs rendszer jó eséllyel felcache-eli a memóriába, így végső soron ugyanúgy a memóriából dolgozunk, csak ehhez a szükséges erőforrások sokszorosát használjuk el. Memória tekintetében rossz esetben legalább 2x megvan az adat (az ORM által kezelt memória reprezentáció, valamint az SQL adatbázis felcache-elt része), és itt még nem beszéltünk a közbe iktatott mindenféle cache-ekről, amit az adattáblák és a Java alkalmazás közé pakolunk, hogy gyorsítsuk az elérést. Számítási erőforrást tekintve megint csak elég rossz a helyzet, hiszen az adatok módosítása egy rakás SQL-t eredményez, amit az adatbázis szervernek értelmeznie kell, majd végrehajtania a változásokat, újraindexelni a táblákat, stb. A memória alapú adatbázisok segítségével ezek a felesleges köztes lépések kiküszöbölhetőek.

Jelenleg két implementációt találtam Java nyelven a memóriába ágyazott adatbázisokra. Az egyik a Prevayler a másik pedig a valamivel okosabbnak tűnő Space4j.

Persze az első kérdés, ami felmerül, hogy ha minden adatot a memóriában tartunk, mi történik egy esetleges rendszer leállás/áramszünet, stb. esetén. A Prevlayer/Space4j megoldása a problémára, hogy minden adatmanipuláló műveletet un. Command-okba csomagolunk és a memória objektumok manipulálását csak és kizárólag ezeken keresztül végezhetjük. Minden Command végrehajtását log-oljuk, így ha leáll a rendszer, nincs gond, hiszen a Command-okat újra lefuttatva visszakapjuk az eredeti állapotot. A gondolat maga nem új keletű. Így működnek az adatbázis rendszerek tranzakciói, vagy éppen a HSQLDB, ami egy ugyancsak memóriában működő, de SQL alapú adatbázis. Hogy a log fájl ne nőjön túl nagyra, bizonyos időközönként a rendszer csinál egy memória snapshot-ot, amikor a teljes állapotot lemezre menti. Ez kicsit erőforrás igényesebb művelet, de általában ez is viszonylag gyorsan megtörténik, és csak ritkán kerül rá sor.  Egy ilyen adatbázisban tehát az olvasás extrém gyorsan történik, hiszen egyszerű memória műveletekről van szó, és a módosítás is nagyságrendekkel gyorsabb, mint egy relációs adatbázis esetén, hiszen mindössze csak a Command-ot kell serializálni, és letenni a log-ba, nem kell SQL-eket értelmezni, indexeket karbantartani, fájlokat átrendezni, stb.

A következő kérdés, ami felmerül egy ilyen rendszerrel kapcsolatban, hogy mi van, ha valahogy mégis olyan hatalmasra nőne az adatbázis, hogy nem fér el a memóriában, esetleg másnak is kell a memória. A Space4j-ben ennek megoldására létezik egy mechanizmus, ami a nem használt adatokat a lemezre "passziválja". Egy adatbázis esetén valószínűleg sok ilyen objektum van, amire épp aktuálisan nincs szükség. A mechanizmus hasonló az operációs rendszerek virtuális memória kezeléséhez, oly annyira, hogy talán nem is érdemes ezzel alkalmazás szinten foglalkozni. Bár gyakorlati tapasztalatom nincs, valószínűleg egy linux rendszer esetén elég lenne annyi, hogy megfelelően nagy swap területet hozunk létre. Bár nem tudom, hogy a kernelnél hol a határ, de ha jól tudom, a mai modern processzorok  legalább 4 Tb memória megcímzésére képesek. Tehát elvileg nem tiltja semmi, hogy akár Tb-os méretű swap használatával gigantikus virtuális memóriát hozzunk létre, amiben már akármilyen adatbázis kényelmesen elfér. Ráadásul az operációs rendszer natív virtuális memória kezelő mechanizmusa biztosan nagyságrendekkel gyorsabban és hatékonyabban működik, mint egy Java-ban programozott megoldás.

A memória alapú adatbázisok működése tehát egyszerű, elegáns, gyors, és erőforrás takarékos, ugyanakkor szinte mindenre képesek, amire egy relációs adatbázis. Például nagy adathalmazok esetén ugyanúgy definiálhatunk indexeket, de itt a megoldás jellegéből adódóan nem kell bonyolult index struktúrákban gondolkodni, elég egy bináris fa, amit az egyetem első félévében minden programozó tanonc fejébe belevernek.

Bár még a gyakorlati tapasztalatom nincs ezekkel a rendszerekkel, az látszik, hogy legnagyobb hátrányuk a Command-ok viszonylagos bonyolultsága lehet, ezekben a rendszerekben ugyanis a Command-ok a módosítás atomi műveletei, amelyek működésébe a rendszer nem lát bele. Az egyetlen dolog, amire a rendszer képes, hogy végrehajtsa a Command-okat. Minden adatellenőrzést, ütközés figyelést, stb. a Command-on belül nekünk kell leprogramozni, ezekhez a rendszer nem nyújt semmilyen támpontot. A másik dolog, ami csak nagyon problematikusan oldható meg, az a konkurens tranzakciók használata. Ez csak oly módon kivitelezhető, hogy másolatot készítünk a tranzakcióban részt vevő objektumokról, majd a tranzakció végeztével a módosításokat végrehajtjuk a memóriában lévő globális objektumokon. A másolást nem tudja nekünk transzparens módon elvégezni a rendszer, hiszen nem lát bele az atomi Command-ok működésébe, így azt sem láthatja, hogy azok milyen adatokat módosítanak. Így az egyetlen lehetőségünk, hogy leprogramozzuk ezeket a másolásokat.

Ami még nagy előnye a rendszernek, hogy nagyon egyszerűen clusterezhető. Minden cluster elem rendelkezik egy saját objektum fával, és bármelyik rendszerben valamilyen módosítás történik az adatbázison, egyszerűen broadcast-olnia kell a módosítást végző command-ot. Persze ilyen elosztott rendszerben a command-okat érdemes timestamp-el ellátni, a hálózati lag-ból eredő elcsúszások kiküszöbölésére (a Command-ok megfelelő sorrendben történő végrehajtása nagyon fontos). Maga a megoldás (módosítások broadcastolása a rendszerben) megint csak nem új keletű. Tulajdonképpen az összes clusterezhető cache (memcached, ehcache, JBossCache, stb.) így működik. Ha valahol változik valami, azt jelszik a többi szerver felé, így tartva konzisztensen a globális állapotot.

Tulajdonképpen a clusterezés gondolatán elindulva jutott eszembe, hogy a Cluster Cache-ek filozófiáját és a memória alapú adatbázisok filozófiáját ötvözve létre lehetne hozni valami perzisztens cache szerű dolgot, ami mentes lenne a Command-ok bonyolultságából eredő hibáktól. A rendszer alapja a JBossCache, vagy valamilyen ahhoz hasonló megoldás lehetne. A JBoss POJO Cache ugyanis tud valami nagyon ügyeset. Komplett Java objektum fákat tárolhatunk benne, mégis gyorsan képes szinkronizálni a cluster egyes elemeit. Ezt úgy éri el, hogy mindenféle proxy-k (és talán AOP) segítségével figyeli, hogy a cache-ben tárolt adatok mikor változnak, és csak a fa változásait küldi szét a többi szervernek. Ez nagyon hasonlít egy cluster-be kötött memória alapú adatbázis működéséhez, ahol a command-ok a JBossCache-ben tárolt objektumok változásai. Nem kellene tehát más tenni, mint a cluster cache-t kiegészíteni egy olyan mechanizmussal, amely folyamatosan log-olja a műveleteket, és néha snapshot-ot készít az aktuális állapotról. Ezt neveztem el én perzisztens cache-nek. Egy ilyen megoldásban nem lenne szükség command-ok írására, így az adatbázis kezelés valóban mindössze memóriában tárolt Java objektumok használatából állna, a rendszer valóban teljesen transzparens lenne. Ami a command-ok egyéb funkcióját illeti (adat ellenőrzés, indexelés, stb.), azt AOP segítségével aggathatnánk rá az adott objektumokra, így megőrizve a rendszer transzparenciáját.