libevent + tornado
Úgy kezdődött a dolog, hogy a facebookos srácok nyílt forrásúvá tették és kiadták a FriendFeed mögött álló python alapú webszervert, a Tornadot. Aranyos dolog, mert nem blokkol, sok klienst képes párhuzamosan kiszolgálni és relatíve gyors mert esemény vezérelt. Feltéve, hogy Linuxon használjuk, mert csak és kizárólag az epollt támogatja (illetve a hagyományos select()-et, nade akkor oda minden előnye).
Viszont én Solaris alatt is szeretném kihasználni a fent említett előnyöket, úgyhogy gyorsan összedobtam egy apró wrappert a libevent köré, ami működik is, és így a Solaris/FreeBSD felhasználók is örülhetnek.
Gyorsan nézzük hogyan történt a dolog.
Először a libeventet kellett letölteni és lefordítani. Szerencsére gond nélkül fordult update 7-es Solaris 10-en.
(jelenleg az 1.4.12-es változat a stabil, azt használtam)
$ ./configure --prefix=/opt/bitnet ... checking port.h usability... yes # solaris event ports, ez kell nekünk ... $ make install
Hogy python alól használni tudjuk, ahhoz szükségünk van a python-libevent csomagra, amit én kézzel tettem fel innen, mert a sajtboltos változat ősrégi.
$ export CFLAGS="-I/opt/bitnet/include" $ export LDFLAGS="-L/opt/bitnet/lib" $ python setup.py install
Ezután jött az érdemi munka, a tornadot rábeszélni, hogy a libevent-et használja. Ehhez egy ugyan olyan wrapper osztályt csináltam, amit a srácok csináltak maguknak a saját epoll, illetve a hagyományos select() támogatásához, ami igazából a standard python Poll objektum által megvalósított interface:
class _LibEvent(object): """A libevent based IOLoop implementation""" def __init__(self): self._eb = libevent.EventBase() self._events = {} self._ready_fds = {} def _fd_ready(self, fd, events, eventObj): eventmask = (IOLoop.READ * (events & libevent.EV_READ) / libevent.EV_READ) | \ (IOLoop.WRITE * (events & libevent.EV_WRITE) / libevent.EV_WRITE) | \ (IOLoop.ERROR * (events & libevent.EV_TIMEOUT) / libevent.EV_TIMEOUT) self._ready_fds[fd] = eventmask def register(self, fd, events): # ezt a reszt is at lehetne alakitani olyanna, mint az elozo fuggveny eventmask = 0 if events & IOLoop.READ: eventmask = eventmask | libevent.EV_READ | libevent.EV_PERSIST if events & IOLoop.WRITE: eventmask = eventmask | libevent.EV_WRITE if events & IOLoop.ERROR: # does libevent has error event type? eventmask = eventmask | libevent.EV_TIMEOUT self._events[fd] = self._eb.create_event(fd, eventmask, self._fd_ready) self._events[fd].add_to_loop() def modify(self, fd, events): self.unregister(fd) self.register(fd, events) def unregister(self, fd): self._events[fd].remove_from_loop() del self._events[fd] def poll(self, timeout=10): self._ready_fds = {} self._eb.loop_exit(timeout) # libevent.EVLOOP_NONBLOCK hasznalatakor a timert sem varja meg, es megeszi a cpu-t self._eb.loop(libevent.EVLOOP_ONCE) return self._ready_fds.items() ... # legvegere, illetve a mar ott levo 'try' blockba: try: import libevent _poll = _LibEvent except: _poll = _Select
Illetve ha nem akarjuk belehackolni a tornadoba a cuccot, akkor "monkey patching" technikával is bedolgozhatjuk, ekkor az alkalmazásunk ioloopjanak indítása előtt kell megmondanunk explicite, hogy libeventet használjon:
import tornado.ioloop tornado.ioloop._poll = _LibEvent tornado.ioloop.IOLoop.instance().start()
(remélem ezt nem kell sokáig megtenni, jeleztem a fejlesztők felé, hogy jó lenne a libevent támogatást a fő fejlesztési vonalon látni)
Csináltam is néhány gyorstesztet az egyik szerverünkön, sok párhuzamos kérés esetén látszik a fejlődés. A teszteket az apache féle ab paranccsal mértem, ahol az 'n' az összes lekérés, a 'c' pedig a konkurenciát (ennyi szálon próbálkozik párhuzamosan) jelenti. A parancsokat háromszor egymás után megismételtem, és a legjobb eredmény került ki ide.
(ulimit -n unlimited)
- _Select:
- n=10000, c=100: 1048.10 [#/sec]
- n=10000, c=1000: 683.96 [#/sec]
- n=10000, c=2000: 672.85 [#/sec] (a háromból egyszer meghalt a lenti hibával)
- n=10000, c=5000: háromszor egymás után: filedescriptor out of range in select()
- _LibEvent:
- n=10000, c=100: 1056.57 [#/sec]
- n=10000, c=1000: 1013.59 [#/sec]
- n=10000, c=2000: 1004.94 [#/sec]
- n=10000, c=5000: 954.52 [#/sec]
- n=10000, c=10000: 781.10 [#/sec]
- n=20000, c=10000: 934.32 [#/sec]
Első körben nekem elég is volt ennyi, hogy lássam, hogy megérte, aztán majd valami egyszerű webalkalmazással meg kéne nézni, hogy a többi megoldáshoz képest (fcgi, cherrypy, esetleg mod_wsgi) hogy muzsikál a cucc.
Használjátok egészséggel!
