2010. április 22., csütörtök

Java Servlet-ek futási idejének limitálása

Már régóta keresek olyan megoldást Java-ban, mint PHP esetén a max_execution_time, amivel a Servlet-ek futási idejét lehetne korlátozni. Hosszú keresgélés után arra jutottam, hogy a problémának egyszerű frappáns megoldása nem létezik, ugyanis az alkalmazásszerverek szálakban futtatják a Servlet-eket, és mivel a szálak leállítása (Thread.stop) már jó ideje depricated, nincs olyan metódus, amivel kilőhetnénk egy szálat. Röviden tehát nincs értelmes megoldás arra, hogy kivédjünk egy Servlet-ben keletkező végtelen ciklust. Ugyanakkor van 1-2 áthidaló módszer, ami nem szép ugyan, de valamilyen módon megoldja a fenti problémát. Ezekből írnék le néhányat:

A legegyszerűbb megoldás, hogy a depricated megjelölés ellenére használjuk a stop metódust. Ez több szempontból is veszélyes. Először is ha jól tudom a depricated metódusok későbbi Java verziókból eltűnhetnek, így az alkalmazásunk Java verzióhoz lesz kötve. Ezen kívül a stop-nak vannak egyéb nem kívánatos hatásai is, nem véletlen, hogy depricated lett. Az a gond a stop-al, hogy ha megölünk egy szálat, bizonyos erőforrások lefoglalva maradhatnak. Tipikus példa, hogy ha létrehozunk egy átmeneti fájlt a szálban, majd kilőjük azt, az átmeneti fájl ottmarad. Ez mondjuk áthidalható oly módon, hogy nem engedünk ilyen erőforrásokat használni a szálban. Például fájlok helyett adatbázis használunk. Ez utóbbi esetben ugyanis megtehetjük, hogy a szálnak adunk egy adatbázis kapcsolatot, indítunk egy tranzakciót, és ha az idő lejárt, csinálunk egy rollback-et, vagy egyszerűen kirántjuk alóla a kapcsolatot (az adatbázis szerver jó eséllyel ez utóbbi esetben is rollback-el magától). Így nem marad inkonzisztens állapot. Tulajdonképpen egy servlet-en belül más külső erőforrást nagyon nem is használunk, tehát a fájlrendszer adatbázisra történő lecserélése szinte teljes egészében megoldja a problémánkat. Ezt amúgy is érdemes megfontolni, ugyanis a fájlrendszerrel ellentétben az adatbázis viszonylag egyszerűen klaszterezhető, így igény esetén könnyebb lesz több gépen szétkenni az alkalmazást. Ennek ellenére azért a Thread.stop függvény depricated mivolta kicsit kérdésessé teszi az egész eljárást.

A másik megoldás amit találtam, hogy a Servlet-ek kiszolgálását nem szálakban, hanem különálló process-ekben végezzük el. Ez kicsit hasonlít a CGI végrehajtáshoz, és a hátrányai is ugyanazok. Minden esetben újra inicializálni kell az egész környezetet, aminek jelentős overheadje van. A megoldás ugyanaz lehet, mint amit CGI esetén a FastCGI csinál. Elindítunk néhány Servlet kiszolgáló process-t, amit végrehajtás után újrahasznosítunk. Így nincs inicializálási overhead, és bár az erőforrásigény nagyobb (pl. minden process-nek külön heap kell), azért az esetek többségében elviselhető. A futási időben nem lesz különösebb változás, hiszen alacsony szinten a natív szálak és a process-ek közötti váltás hasonlóan történik. Ebben az esetben tehát úgy néz ki a dolog, hogy indítunk valamennyi servlet kiszolgáló process-t, és ha kérés jön, keresünk egy szabadot, lekezeltetjük a kérést, majd a felhasználónak visszadobjuk a választ. Ha a process túl sokat időzik a válaszadással (pl. végtelen ciklus miatt), egyszerűen kilőjük az adott process-t, létrehozunk egy újat, és minden megy tovább úgy, ahogy eddig. A kérdés már csak annyi, hogy milyen módon érdemes kilőni a process-t? Az egyik módszer, hogy egyszerűen kill-el kilőjük. Ami hatásos ugyan, de elég durva, hiszen a Servlet futtató környezetnek esélye sincs, hogy normálisan lezárja az erőforrásokat (pl. SQL kapcsolat lezárása). Ennél sokkal enyhébb, ha a kérés feldolgozását külön szálba indítjuk, és indítunk mellé egy timeout szálat, ami ha lejár, ellenőrzi, hogy volt-e eredménye a feldolgozásnak. Ha igen, visszaadja azt, ha nem, felszabadítja az erőforrásokat, és futtat egy System.exit()-et. Ezzel a megoldással tulajdonképpen belülről vesszük rá öngyilkosságra a túl sok erőforrást felhasználó process-t.

A fentiek alapján már nagyjából összeállt a fejemben az architektúra. Mivel általában így is - úgy is Apache-ot használunk a request-ek feldolgozására (pl. mod_jk-n keresztül), ezért célszerű ezzel legyártani a process-eket. Erre ideális megoldás lehet a FastCGI, ahol beállíthatjuk, hogy hány élő process legyen, és a webszerverrel történő kommunikáció is adott. Minden egyes Java process saját Servlet futtató környezettel rendelkezik. A lényeg annyi, hogy egy process egyetlen request feldolgozását végezze egyszerre. Ha jön egy kérés, a FastCGI mechanizmus kiosztja azt valamelyik process-nek, ami megpróbálja kiszolgálni a kérést. Ha a Servlet nem végez az adott időn belül, a process saját magát öli meg, amit a FastCGI elvileg kezel, tehát a halott process-t új változattal pótolja. A megoldás szépsége, hogy tulajdonképpen bármire általánosítható, tehát ugyanezt pl. meg lehet valósítani Python-ra is, így lesz egy időkorlátos Python futtatókörnyezetünk. A FastCGI-nak köszönhetően az inicializálást elég egyszer elvégezni, a process-en belül működik a connection pool-ing, vagy bármilyen más "újrahasznosító" technológia, így a végrehajtásnak nem lesz sokkal nagyobb az overhead-je, mint ha szálakkal dolgoznánk. Az egyetlen probléma a külön allokált heap, de ezzel lehet takarékoskodni, és ha jobban meggondoljuk, ez akár előny is lehet, hiszen egy felszálazott környezetben ha egyetlen szálnak elszáll a memória felhasználása, az az egész alkalmazás OutOfMemory-val történő lehalását eredményezi, egy ilyen rendszerben viszont minden végrehajtásnak külön memória korlátja van, így legrosszabb esetben is csak az aktuális request száll el.

Összességében tehát bár nem próbáltam még ki, de úgy érzem ez egy hatékony megoldás lehet biztonságos keretbe zárt Servletek futtatására, és egy nagy rendelkezésre állást biztosító, külső fejlesztők számára nyitott (a PHP-hoz hasonlóan megvalósítható rajta Java hosting, hisz a felhasználók nem tudják tönkretenni a futtatókörnyezetet) Google App Engine-hez hasonló rendszer kialakítására.

2 megjegyzés:

  1. Egy régi probléma, mely mindenkinél előbb vagy utóbb felmerül... S a gond lehet, hogy nem igazán ott van ahol keressük...



    Az alapvető probléma a "gyilkolással" mindig ugyanaz. A fentiekben említett fájl példa meglehetősen "kedves" ahhoz képest amit okozni lehet. Az igazi gubanc akkor kezdődik amikor osztott erőforrásokkal dolgozunk, s a szálunk épp egypár ilyet birtokol. Kinyírásával lehetetlenné válik minden egyéb szál futása, ami az adott osztott erőforrásra vár. S lássuk be a jó keretrendszerek többsége erősen párhuzamosított, s a trend is erre felé tart. Na meg igazából ez az előnye egy Java Servlet, JEE, vagy egyéb Java keretrendszer egy PHP-val szemben. Amíg a PHP, alap esetben 1 kérés 1 futás 1 zárt halmazon, addig a java erősen támaszkodik osztott erőforrásokra. -- Megjegyzem hasonló a helyzet ha PHP-ban pl. shm segítségével próbálunk osztott tárral dolgozni: kinyírunk egy php folyamatot és inkonzisztencia marad hátra.



    S hogy miért is "deprecated" az a fránya stop... hát pont a fentiek miatt: a szál maga kell tudja, hogyan kell leállnia. Azaz maga a szál kell biztosítsa a leállás lehetőséget. Mert, ki dönti el, hogy mondjuk az adott szemaforon akkor most átmehet a másik szál vagy sem?! - A banki tranzakció átutalásra kerülhet, ha nincs jóváhagyás?!

    A "soft" leállításra több lehetősége is van. Kezdve a szokásos checkpoint módszertől, a mérgező pilulán keresztül egészen odáig, hogy egy exception-t tuszkolunk bele. Akárhogy is csináljuk a szál maga kell rendelkezzen leállásáról. - Amennyiben harmadik fél által gyártott megoldásról van szó, valamilyen AOP jó lehet, igaz, ez nem blackbox megoldás...



    Java Servleteket külön folyamatban futtatni, nem hiszem hogy jó ötlet, vagy előnyt jelentene: nem erre van kitalálva. Talán egy külön erre kihegyezett új Java HTTP megoldás jó lehet.

    VálaszTörlés
  2. A szálakat illetően igazad van, viszont egy átlag webes alkalmazás szerintem nem használ olyan intenzíven osztott erőforrásokat. Ha most hirtelen belegondolok, az adatbázis, cache-ek, meg a session az ami két process között közös lehet. Ezek esetén viszont tranzakciókkal szerintem könnyen megoldható, hogy ne keletkezzen inkonzisztens állapot. Egy request feldolgozása azért alapvetően elszigetelt folyamat. Az sem igaz, hogy az erőforrások lefoglalva maradnak, hiszen ezeket az öngyilkos megoldás esetén kiránthatod a beragadt Servlet alól. Szóval én azért elválasztanám egymástól a klasszikus szálazás, és a Servlet-ek futtatásának problémakörét, az utóbbi esetben ugyanis szerintem sokkal kevesebb az osztott erőforrás, és azok is tudnak tranzakció kezeltek lenni.



    Az AOP megoldáson is gondolkodtam, hogy futásidőben a kódban lévő ugrások elé timeout ellenőrzéseket lövöldözöl be, így nem lehet végtelen ciklust összehozni, de a mellet, hogy baromi bonyodalmas, nem sokkal jobb megoldás, mint kirántani a kód alól az erőforrásokat, aztán System.exit-el eldobatni az egész környezetet. Azt is nézegettem, hogy a debug interfészen csatlakozva kívülről állítod meg a futást, de az erőforrás problémákat ez sem oldja meg. Az út tehát szerintem az, hogy ne engedjünk olyan osztott erőforrásokat használni, amiből gond lehet. Ha ez biztosított, akár nagyon durván is kiölheted a szálat, nem fog fennakadást okozni. Ez a gondolkodás más szempontból is jó lehet, például ha clusterezed a rendszert, akkor egyes részek maguktól is kidögölhetnek (pl. kidöglik egy szerver), és ugyanazzal a problémával kerülsz szembe. Ha úgy vesszük, akkor a felvázolt több process-es rendszert úgy is tekintheted, mint egy virtuális alkalmazás klasztert, ahol minden egyes klaszter node egy process, és egy process egyszerre egy request-el foglalkozhat (nincs szálazás). Innentől kezdve pont ugyanazokkal a problémákkal és megoldásokkal találod szemben magad, mint egy clusterezett rendszer esetén. Tehát szerintem nem olyan vészes a helyzet, és pont a architektúrának köszönhetően egyből jól clusterezhető, és magas rendelkezésre állású rendszered lesz.



    A másik jóság, hogy az architektúra általánosítható, tehát ahogy írtam, alkalmazhatod mondjuk Python-ra, de akár azt is megteheted, hogy a futtatandó Servlet-et behúzás előtt GCJ-vel natív kódra fordítod, amivel memóriát is spórolsz, és gyors lesz mint a villám. De ugyanígy működhet a dolog Python-natív vagy PHP-natív compilerrel is. Szóval azért annyira szerintem nem rossz a dolog.



    Az egészhez mondjuk hozzátartozik, hogy amikor elkezdett foglalkoztatni a téma, arra voltam kíváncsi, hogy hogyan lehetne egy Google App Engine-hez hasonló hosting környezetet megvalósítani. Az App Engine pont egy jó példa arra, hogy a dolog megoldható (bár ott kicsit máshogy van ez megoldva), és még csak nincsenek is olyan éles korlátozások. Szóval szerintem azért érdemes vele foglalkozni.

    VálaszTörlés