Ú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!