Óvatosan a Hibernate-el

Nemrégiben egy projekt kapcsán felmerült egy elég csúnya hiba. Nagyobb megterhelés esetén a rendszer processzor használata extrém magasra pörgött fel, olyannyira, hogy képtelen volt a webes kérések kiszolgálására, így újra kellett indítani. A rendszer fejlesztésével egyre instabilabbá vált a rendszer, aztán egy ponton elérte a teljes használhatatlanság fokát. Sokáig kerestük a hiba okát, mire rájöttünk, hogy mi okozta.

A Hibernate egy rendkívül kényelmes JPA alapú ORM rendszer. Az ember létrehoz néhány felcímkézett osztályt, és ide tárolja az adatokat. A Hibernate pedig szinte láthatatlanul elvégzi az adatbázis leképzést, és a kapcsolódó adatbázis műveleteket. A hibát ott követtük el, hogy kihasználva ezt a kényelmességet úgy használtuk ezeket a perzisztens objektumokat, mintha a memóriában lennének. Ciklusok futkároztak keresztül kasul a listákon, olvasgattuk/írogattuk az elemeket, mindeközben fittyet hányva arra, hogy milyen komoly adatbázis műveletek futnak a háttérben. Mint később kiderült, annyira komolyak, hogy néhány száz felhasználó taccsra tudott vágni egy 8 processzoros gépet. Mikor bekapcsoltuk az SQL-ek loggolását, elborzadva láttuk az eredményt, egy-egy webes kérés kiszolgálásakor százával pörögtek a 3-4x-es join-olt lekérdezések. Nem csoda, hogy az alkalmazás hamar felzabálta a gépet. Végül is az adatbázis használat optimalizálásával, és cache-ek használatával hamar úrrá lettünk a problémán. Ezt a kis affért tekinthetnénk akár egyszerű programozási hibának is, de engem gondolkodóba ejtett.

Egy régi kollégámmal annak idején sokat vitatkoztunk a Python és a Java nyelv kapcsán. Ő Python párti volt, én viszont nem nagyon tudtam megbarátkozni a nyelvvel. Ez azóta persze megváltozott. Script nyelvként nagyon megszerettem a Pythont, de komolyabb projektbe nem mernék belefogni vele. A vitánk alapja az volt, hogy szerintem a Python nagyon megengedő nyelv. Nem típusos, és szinte bármit felül lehet benne definiálni. Ez egyfelől nagyon kényelmes, ugyanakkor véleményem szerint egyben veszélyes is. Sokan egy programnyelv típusosságára vagy az operator overloading hiányára úgy tekintenek, mint koloncra, ami gúzsba köti a fejlesztőt, nem adja meg a kellő szabadságot, hogy kényelmes és elegáns kódot készítsünk. Én ugyanakkor ezeket a megkötéseket segítségnek tekintem, amelyek segítenek abban hogy hibátlan és optimális kódot készítsünk. A barátom érve ezzel szemben az volt, hogy ezeknek az eszközöknek a használata nem kötelező. Aki nem tud programozni, az ne használjon ilyesmit, aki jó fejlesztő, nem követ el szarvas hibákat. A probléma csak az, hogy az ember hamar elcsábul, vagy a felhasznált programkönyvtárak miatt belekényszerül abba, hogy éljen ezekkel a lehetőségekkel. A másik probléma, hogy ezek a nyelvek általában nem is adnak lehetőséget arra, hogy határok közé szorítsuk magunkat. Python esetén például ha akarnánk sem tudnánk típusos függvényeket írni, ami azért néha hasznos tud lenni, hiszen segít abban, hogy ne rontsuk el a paraméterezést (két paraméter véletlen felcserélése komoly galibát tud okozni). Van egy harmadik ok is, amiért a szigorú nyelvek mellett tettem le a voksomat. Egy komoly fejlesztés sokszor igen feszített tempóban történik. Kemények a határidők, sok a feature igény, kevés az ember. Ilyen körülmények között nem ér rá az ember gondolkodni, hogy vajon szép-e ez a kód, nem okoz-e majd később galibát, stb. Egyszerűen csak darálja a funkciókat, hogy elkészüljön a szoftver. Hasonló hibába estünk bele mi is a Hibernate kapcsán. Kihasználtuk az eszköz által nyújtott kényelmet, daráltuk a kódot, és nem gondoltuk végig, hogy ennek milyen következménye lehet. És ez a kis programozási nyelvekkel kapcsolatos kitérő itt kapcsolódik vissza az eredeti témához. Kényelem vs. biztonság.

A Hibernate-es probléma kapcsán tehát elgondolkodtam, hogy milyen elvek szerint kellene vajon felépíteni egy ORM rendszert úgy, hogy ne futhasson bele az ember ilyen hibákba, hasonlóan ahhoz, ahogyan a Java típusossága segít abban, hogy ne cserélhessük fel a paramétereket úgy, ahogy Pythonban. Az első gondolatom az volt, hogy talán vissza kellene térni a tiszta SQL-hez. Így rá vagyunk kényszerítve, hogy explicit módon adjuk meg a lekérdezéseket, és rá vagyunk szorítva arra, hogy ezek a lekérdezések optimálisak legyenek. (Legalábbis a nem optimális lekérdezések kibökik a szemünket.) Egy ilyen rendszerben SQL-el kérdeznénk le az adatokat, leképeznénk őket objektumokba, elvégeznénk a szükséges műveleteket, majd SQL-el pakolnánk őket vissza az adatbázisba. Ez tipikus DAO filozófia, és pont azért alakultak ki az ORM rendszerek, mert ez így azért elég kényelmetlen. E mellett azt se felejtsük el, hogy a manuális leképzés megint csak sok hibalehetőséget rejt magában. Az ORM rendszerek automatikus leképzése, és az objektum szintű típusosság sok gondot levesz a vállunkról, ráadásul az alkalmazás adatbázis rendszer független lesz, ami megint csak nagy előny. Tehát az ORM-től nem olyan könnyű megszabadulni, és valószínűleg nem is kell, de nagyon oda kell rá figyelni, ami nem jó, és mint látható, komoly hibákat szülhet.

Ami mostanában körvonalazódik a szemeim előtt, az valami olyan megoldás, ami ötvözi a JPA és a DAO-k előnyeit. A durva SQL-ek amiatt születtek, mert a rendszer felhúzott sok csatolt entitást is (ez okozta a sok join-t). Éppen ezért én a saját ORM rendszeremből kihagynám a kapcsolatokat, vagy legalábbis nem tenném annyira transzparenssé, ahogyan a Hibernate. Például egy entitáshoz kapcsolódó további entitásokat egy entitás osztályban definiált függvény szippantaná fel. Tehát az entitások tábla szintűek lennének, a kapcsolatok pedig explicit módon jelennének meg lekérdezések formájában. Bár nem okozott akkora galibát, de némi fejtörést igen, hogy bonyolultabb objektum manipulációk esetén az objektum struktúrát több request/response szakaszig a session-ben kell tartani, és csak ezután szabad visszarakni az adatbázisba. Mivel az entityManager csak 1-1 request alatt él, hamar problémát okozott, hogy egy session-ben lévő perzisztens objektumot (ami közben már elveszítette perzisztens jellegét) vissza akartunk rakni az adatbázisra (Lazy...Exception). Ennek kiküszöbölésére valamilyen long transaction mechanizmust tudnék elképzelni, ahol több request-en átívelő kérésekhez lehetne kérni egy long transaction-t. A long transaction session szintű objektum lenne, és ha felolvasunk/módosítunk valamit, ide kerülne. Végül egy commit-al a végén kerülne vissza az összes objektum az adatbázisra. Itt lehetne valamilyen ütközés figyelés (pl. verzionáljuk az objektumokat), ami konkurens írás esetén exception-el jelez. Végül pedig kellene valamilyen transzparens cache az adatbázis és az alkalmazás közé, hogy ha valamit felolvastunk, azt ne kelljen még egyszer felszedni. Valahogy így nézne ki egy ideális ORM rendszer.  Ami még kicsit zavaró, az a sok dinamikus proxy, ezek teljesen feleslegesen terhelik a processzort, ezek helyett hatékonyabb lenne statikus (fordítási idejű) kódgenerálást alkalmazni.

Találkoztam még egy érdekes Space4j nevű projekttel, ami az egész dolgot megfordította. Nem az adatbázist képezi memóriára, ehelyett az objektumokat a memóriában tartja, ami nem kell, azt perzisztensen kipakolja onnan, és e mellett mindent megold. Itt tehát nem a perzisztens objektumok cache-elése a feladat, hanem a cache (memória) perzisztens mentése. Talán pont egy ilyen megoldás lenne a leginkább ideális, persze kérdés, hogy mindezt hogy lehetne integrálni a már meglévő rendszerekkel, amennyiben hostolt rendszerben szeretnénk azt használni (pl. Google App Engine). Emellett még nem próbáltam a rendszert, tehát nem tudok nyilatkozni róla, hogy milyen rejtett hátrányai lehetnek egy ilyen megoldásnak.