A minap belefutottam (János küldte nekem) egy igen érdekes cikkbe egy hosting szolgáltató részéről. A szolgáltatót Wix-nek hívják, a cikk pedig ez: Scaling to 100M: MySQL is a Better NoSQL, azaz magyarul valami ilyesmi: 100M-ra skálázva: A MySQL a jobb NoSQL.

Azt sajnos nem tudjuk, hogy a 100M az rekord vagy tranzakció, de ez most nem is annyira lényeges. A cikk ugyan 2015 decemberében született, azonban rámutat néhány érdekességre. Emellett arra is, hogy egy kis hype mennyire el tudja vinni az újdonságokra, kihívásokra és új tudásra kiéhezett fejlesztőket. Mielőtt itt tovább mennénk, javaslom, hogy olvasd el a linkelt cikket. Nem fogom az egészet idézni, csupán néhány szerintem fontos és/vagy hasznos gondolatra reagálok.

Az alap gondolat

A Wix cikkének alap gondolata, hogy ügyesen használva a MySQL (és valójában bármelyik SQL) szervert, a manapság nagyon divatos NoSQL megoldásokhoz hasonló performanciát lehet elérni. Ezzel a gondolattal tökéletesen egyetértek, egy kis kitétellel: ha hajlandók vagyunk elfogadni ugyanazokat a kompromisszumokat amiket a NoSQL rendszerek használatával elfogadunk:

  • általában lemondunk a konzisztenciáról
  • általában lemondunk a mezőszintű kereshetőségről
  • általában lemondunk a struktúráltság egy részéről vagy egészéről
  • általában denormalizálunk
  • általában lemondunk a tranzakcionalitásról

A NoSQL megoldások performancia előnye egy relációs adatbázishoz képest általában ezen funkciókról való teljes vagy részleges lemondáson alapul.

Mire használják

A Wix-nél hoszting szolgáltató lévén weboldalak laknak, minden weboldalnak megvan a maga domainje ami lehet saját, vagy ingyenes oldal esetén egy Wix-es (user.wix.com/site). Egy weboldalnak több címe is lehet (ezeket a Wix route-nak hívja), illetve minden site-nak különbözö beállításai vannak. A weboldalak amolyan programozói tudás nélkül létrehozható dolgok, a felhasználó végig kattintgat egy sor beállítást: kiválasztja a stílust, beállítja az oldal nevét, hátterét, objektumokat dobál fel, stb.

Amikor valaki meg akar látogatni egy Wix oldalt, a kérés beesik az egyik szerverükre, ahonnan az URL alapján az adatbázisból kivadásszák az oldal beállításait, majd renderelik és prezentálják az oldalt a kinyert információk alapján. (Most tekintsünk el a mindenféle gyorsírótárazásoktól).

Hogyan oldották meg

Ahogy az eredeti cikk is említi, MySQL relációs adatbázis motorra esett a választásuk, nem valamelyik NoSQL megoldásra. Azt sajnos nem említik meg, hogy pontosan melyik verzió vagy fork.

Alapvetően a cikkben említett terhelés mellett elsősorban a konkurenciát akadályozó tényezőktől kell megszabadulni: hosszú tranzakciók, illetve hosszú ideig jelen lévő lockok (zárolások).

Nincs adatbázis lock nélkül. A kérdés nem az, hogy meg tudunk-e szabadulni teljesen a lockoktól egy relációs adatbázisban, inkább az, hogy mennyi ideig vannak életben és pontosan milyen típusúak azok a lockok. Még READ UNCOMMITTED izolációs szint mellett is kerül egy struktúra változását akadályozó zárolás minden érintett táblára. (Ha pedig mégis írnánk az adatbázist az említett szint mellett, az adatbázis motor automatikusan szigorúbb szintet léptet életbe.)

Magyarán a célunk az, hogy a lehető legkevesebb objektumot, minél rövidebb ideig zároljunk, ennek elérése érdekében a Wix-nél a következőket követték el:

  • Megszabadultak az idegen kulcsoktól
  • Megszabadultak az adatbázis által generált azonosítóktól
  • Drasztikusan denormalizálták az adatbázist
    • Amit lehetett egy táblába zsúfoltak
    • Azon információkat, ami alapján nem szűrnek, egyszerűen egy nagy szöveges mezőbe száműzték
  • Leegyszerűsítették a lekérdezéseket

MySQL-ben a zárolás szintje függ a tárolás módjától. A MYISAM motor csak tábla szintű zárolásra képes, vagyis a sok írással rendelkező (write-heavy) rendszer esetén nem a legjobb választás, amíg például az InnoDB motor már rekord szinten is képes lockolni, cserébe kicsit lassabb olvasással kell számolnunk.

Idegen kulcs vagy nem idegen kulcs

Relációs adatbázisokban az egyes táblák közötti kapcsolatokat idegen kulcsokkal (FOREIGN KEY) lehet definiálni. Az ilyen típusú kényszerek elsődleges célja, hogy ne kerülhessen árva rekord az adatbázisba, amit úgy ér el, hogy ha egy lekérdezés módosítani kíván egy kulcsot, akkor az adatbázis motor ellenőrzi, hogy a modosítás konzisztens állapothoz vezet-e vagy sem. Ez természetesen extra adatelérést eredményez (általában egy kulcs menti keresést), illetve az érintett objektumok részleges zárolásával jár.

Egyértelmű, ha nincs idegen kulcs, akkor ezek az extra feladatok nem terhelik az adatbázist.

DE, és ez egy nagy de: ezzel lehetővé válik, hogy olyan módosításokat hajtsunk végre, amik értelmezhetetlen állapothoz vezetnek. Képzeljük el, hogy egy fórum adatbázisában a hozzászólások tábla teljesen független a felhasználók táblájától és törlünk egy felhasználót, akinek rengeteg hozzászólása van. Nem nagy para, kiírjuk, hogy a felhasználó nem létezik. Most képzeljük el, hogy egy elírás miatt beszúrunk egy felhasználót egy régen törölt azonosítóval. A következő lépés, hogy cseréljük ki a hozzászólás szót számlára, a felhasználót pedig ügyfélre. A fórumot most már banknak is hívhatjuk. Rábíznád a pénzed?

A legtöbb esetben a NoSQL megoldások hasonlóan lazán kezelik az egyes entitások közötti kapcsolatokat. Vagyis leginkább nem foglalkoznak a kérdéssel.

A Wix-nél azért tehették ezt meg, mert az említett adatbázis nagyon limitált adathalmazzal dolgozik és csak egy specifikus célt szolgál. Meg merem kockáztatni, hogy a cikkben említett adatbázisnak van egy normalizáltabb és sokkal bővebb változata.

Generált azonosítók

A rekordok azonosítása megérne egy külön cikket, most röviden arról van szó, amikor új rekord létrehozásakor az adatbázis szerver automatikusan generál egy azonosítót. MySQL-ben ezt például az AUTO_INCREMENT tulajdonsággal érhetjük el. Ezzel a probléma, hogy az adatbázis szervernek folyamatosan szemmel kell tartania a számláló értékét, illetve, hogy a konkurens írás lehetetlen, hiszen két rekord nem kaphatja ugyan azt az azonosítót.

A GUID vagy UUID egy 128bites azonosító, amit implementációtól függően bárhol generálhatunk, és nagyon nagy biztonsággal – az adott rendszerben – egy még nem létező értéket kapunk.

Rendben, hallom ahogy most jól felhorkan mindenki, hogy de akkor nem tudjuk numerikus típusban tárolni az azonosítónkat. Ide akkor most két gondolat:

  1. DECIMAL(39,0), ha nagyon ragaszkodunk a számokhoz
  2. MySQL-en kívül létezik más RDBMS is. (Postgre az UUID adattípussal oldja meg, SQL Server-ben uniqueidentifier-nek hívják, Oracle pedig egyszerűen egy RAW(16) típust használ erre a célra.)

Valójában a GUID egy fix hosszúságú azonosító, így megszabadulhatunk a változó hosszúságú szövegekkel járó problémáktól. (CHAR vs VARCHAR).

Ha a GUID sok, létezik neki egy rövidebb, 64 bites változata, amit már pl egy BIGINT is elbír. (Lásd pl MySQL-ben az UUID_SHORT()) függvényt.

Általánosságban elmondható, hogy ez a megoldás levesz némi terhet az adatbázis kiszolgáló válláról, azonban nem szabad elfelejteni, hogy például az egyediséget így is ellenőrzini fogja. A UNIQUE/PRIMARY KEY kényszerek elhagyása pedig senkinek se jusson eszébe. Nagyon rossz ötlet és ha megtörténik a baj, közel sem biztos, hogy rendbe lehet rakni a rekordokat. (És nem, az alkalmazásból – pl a PHP kódból – nem tudod biztosítani az egyediséget, elég csak például ennyit megemlíteni: race condition.)

Hasonló azonosítókat több NoSQL megoldás is használ az értékek, vagy a saját objektumai azonosítására.

Denormalizáció

Ez egy gyakran használt módszer a performancia növelésére, gondoljunk csak előre generált összegző táblákra, több szinten is szereplő azonosítókra.

A Wix egészen extrém szinten műveli ezt, a legtöbb információt bezsúfolja egy JSON struktúrába és ezt tárolja, ami igen hatékony tud lenni, de:

  • Egészen a legfrissebb MySQL verzióig a teljes mező lekérdezése (és hálózaton utaztatása nélkül), nem lehet hozzáférni az adott struktúrában tárolt értékekhez. Ha ez a struktúra szép nagy, de csak mondjuk egy objektum nevére van szükségünk belőle, akkor is utaztatnunk kell a teljes struktúrát a hálózaton.
  • Semmilyen kényszer sem tudunk definiálni, azaz nem mondhatjuk, hogy egy weboldalnak rendelkeznie kell egy címmel, amit a <title> elembe írhatunk.
  • Nem tudjuk indexelni, ebből adódóan nem tudunk a benne szereplő információ alapján hatékonyan keresni
  • A tárolt adat struktúráját nem tudjuk kikényszeríteni, valamint minden lekérdezésnél észben kell tartani, hogy azt nem is ismerhetjük

Lekérdezések egyszerűsítése

Ez egy bonyolult téma és nem is csak egy cikket lehetne vele foglalkozni. Az egész eredménye nagyon nagy mértékben függ az adatbázis szervertől, az alkalmazástól, az adattól és a kiindulásként használt lekérdezéstől. Egyelőre csak annyit tartsunk észben, hogy a az eredeti cikkben bemutatott esetben mindig csak egyetlen egy rekordhoz akarnak hozzáférni, azaz nincsenek rendezett listák, ajánlók, listák, miegymás.

A legtöbb adatbázis szerver nagyon gyorsan eléri a keresett rekordot, ha lehetősége van az clustered (magyarul fürtözött, de ez nagyon bután hangzik) kulcs mentén kivadászni azt (MySQL-ben ez mindig az elsődleges kulcs, ha létezik ilyen). Ennek oka, hogy vagy közvetlenül az index fa levelein tárolja az adatot, vagy onnan nagyon könnyen elérhető helyen és módon.

Nem clustered indexek esetén a teljes rekord adattartalmát egy extra kereséssel érik el. Egy sima index mindig csak az indexben definiált adatokat tartalmazza, valamint egy hivatkozást a rekordra (MySQL-ben ez a hivatkozás az elsődleges kulcs értéke).

Az igazság az, hogy több kis lekérdezés futtatása általában erőforrás igényesebb, mint egy bonyolultabb, de jól megfogalmazott és indexekkel jól megtámogatott lekérdezésé. Ez a sebességkülönbség természetesen nagyobb eredményhalmazok esetében érhető tetten.

A linkelt cikk szerzői a denormalizáció miatt szinte soha sem nyúlnak két táblához, azaz nincs is szükségük JOIN használatára.

wix

Összegzés

Ahogy én látom, az eredeti cikkben leírtak jól működhetnek az ő esetükben, és a hozzájuk hasonló helyzetekben. Másképp fogalmazva, az ott dolgozó szakemberek jól átgondolták, jól megtervezték és jól kivitelezték a rendszerüket, aminek meg is lett az eredménye. Személyesen úgy gondolom, hogy egy nagyon szép mérnöki munkáról van szó.

A választott megoldás elsősorban azért működik, mert egy jól körülhatárolható problémára ad választ, valamint csak kevés mező mentén kell keresni az adatbázisban, majdnem mindig csak egy rekordra van szükség az alkalmazás működéséhez és elsősorban olvasás történik (read-heavy).

Mit ajánlok? SQL vagy NoSQL?

A válasz röviden: az attól függ.

Olyan adatot, ami nem nagy gond ha elveszik, vagy megsérül (pl fórum session), mehet valami memóriában dolgozó Key-Value tárolóba. A memória elérési ideje mindig jobb lesz, mint a merevlemezé, vagy akár SSD-é.

Ha számít a konzisztenicia, akkor nem kérdés: relációs adatbázis. Vagyis legalább valami, ami lehetővé teszi, hogy precízen definiáljuk az adatstruktúránkat és a szabályainkat következetesen betartassa.

Ismerned kell az adatod, annak felhasználási módját és a lehetőségeidet. Tesztelj, de ne ülj fel az első hype vonatra amit meglátsz.

A linkelt cikkből néhány kulcsfontosságú információt hiányolok, mint például az adatbázis szerver típusa és verziója, a használt tároló motor, teszteltek-e hasonló terhelés mellett más megoldásokat, a hálózati forgalmat hogy érintette a változtatás, de leginkább, hogy milyen vason fut az adatbázis szerver, hiszen a számok így önnmagukban semmit sem mondanak.

Hogy gyorsabb-e egy SQL szerver, mint egy NoSQL megoldás? Nem tudom, felhasználás, megvalósítás és hozzáértés függő.

Zárszó

Ha úgy döntesz, hogy NoSQL megoldást használsz, bizonyosodj meg róla, hogy minden kritériumnak megfelel. Sok közülük például nem támogatja a jogosultságok beállítását.

Ha pedig egy relációs adatbázisban gondolkodsz, javaslom, hogy nézd meg a piacon elérhető alternatívákat is.