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.