因爲最近做項目,一直接觸tornado,所以抽空把源碼的一些原理搞一搞。
tornado採用多進程+異步+epoll的模型,可以提供比較強大的網絡響應性能。通過Nginx+tornado一起部署,可以同時支持多個實例的運行,從而支持加倍的請求響應,可達數千併發連接。
模塊分析
tornado服務器主要三大處理模塊,IOLoop,IOStream,HTTPConnection。
1. 先說說IOLoop初始化:
在Tornado服務器中,IOLoop是調度的核心模塊,Tornado服務器把所有的socket描述符都註冊到IOLoop, 註冊的時候指明回調處理函數,IOLoop內部不斷的監聽IO事件, 一旦發現某個socket可讀寫, 就調用其註冊時指定的回調函數。 IOLoop使用了單例模式。
在Tornado運行的整個過程中,只有一個IOLoop實例,僅需一個 IOLoop實例, 就可以處理全部的IO事件。上文中多次用到了ioloop.IOLoop.instance()這個方法。它會返回ioloop的一個單例。下面這段代碼,可以看到python是怎麼定義一個單例的。代碼中使用了cls,這不是一個關鍵字,和self一樣,cls是python的一個built-in變量,self表示類的實例,而cls表示類。大概代碼如下(中間有所省略):
class IOLoop(object):
def instance(cls):
if not hasattr(cls, "_instance"):
cls._instance = cls()
return cls._instance
def initialized(cls):
return hasattr(cls, "_instance")
上文說到tornado是基於epoll事件驅動模型,也不完全正確,tornado實際上是根據平臺選擇底層驅動。請看IOLoop類的configurable_default
方法:
@classmethod
def configurable_default(cls):
if hasattr(select, "epoll"):
from tornado.platform.epoll import EPollIOLoop
return EPollIOLoop
if hasattr(select, "kqueue"):
# Python 2.6+ on BSD or Mac
from tornado.platform.kqueue import KQueueIOLoop
return KQueueIOLoop
from tornado.platform.select import SelectIOLoop
return SelectIOLoop
這裏的IOLoop實際上是個通用接口,根據不同平臺選擇:linux->epoll,BSD->kqueue,如果epoll和kqueue都不支持則選擇select(性能要差些)。
class IOLoop(Configurable):
IOLoop 繼承了Configurable類,Configurable類的__new__
方法調用了configured_class
方法:
def __new__(cls, *args, **kwargs):
base = cls.configurable_base()
init_kwargs = {}
if cls is base:
impl = cls.configured_class()
if base.__impl_kwargs:
init_kwargs.update(base.__impl_kwargs)
else:
impl = cls
init_kwargs.update(kwargs)
instance = super(Configurable, cls).__new__(impl)
# initialize vs __init__ chosen for compatibility with AsyncHTTPClient
# singleton magic. If we get rid of that we can switch to __init__
# here too.
instance.initialize(*args, **init_kwargs)
return instance
configured_class
方法又調用了configurable_default
方法:
@classmethod
def configured_class(cls):
# type: () -> type
"""Returns the currently configured class."""
base = cls.configurable_base()
if cls.__impl_class is None:
base.__impl_class = cls.configurable_default()
return base.__impl_class
所以當初始化一個IOLoop實例的時候就給IOLoop做了配置,根據不同平臺選擇合適的驅動。
2. 說說epoll的接口(只分析Linux平臺)
class _EPoll(object):
_EPOLL_CTL_ADD = 1 # 添加一個新的epoll事件
_EPOLL_CTL_DEL = 2 # 刪除一個epoll事件
_EPOLL_CTL_MOD = 3 # 改變一個事件的監聽方式
def __init__(self):
self._epoll_fd = epoll.epoll_create()
def fileno(self):
return self._epoll_fd
def register(self, fd, events):
epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_ADD, fd, events)
def modify(self, fd, events):
epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_MOD, fd, events)
def unregister(self, fd):
epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_DEL, fd, 0)
def poll(self, timeout):
return epoll.epoll_wait(self._epoll_fd, int(timeout * 1000))
三種操作(添加,刪除,改變)分別對應tornado.IOLoop裏面的三個函數:add_handler
, remove_handler
, update_handler
def add_handler(self, fd, handler, events):
fd, obj = self.split_fd(fd)
self._handlers[fd] = (obj, stack_context.wrap(handler))
self._impl.register(fd, events | self.ERROR)
def update_handler(self, fd, events):
fd, obj = self.split_fd(fd)
self._impl.modify(fd, events | self.ERROR)
def remove_handler(self, fd):
fd, obj = self.split_fd(fd)
self._handlers.pop(fd, None)
self._events.pop(fd, None)
try:
self._impl.unregister(fd)
except Exception:
gen_log.debug("Error deleting fd from IOLoop", exc_info=True)
上面的self._impl
就是 select.epoll()
,使用方法可以參考epoll接口。
3. 說說事件驅動
IOLoop的start()
方法用於啓動事件循環(Event Loop)。
def start(self):
if self._stopped:
self._stopped = False
return
self._running = True
while True:
poll_timeout = 0.2
callbacks = self._callbacks
self._callbacks = []
for callback in callbacks:
self._run_callback(callback)
try:
event_pairs = self._impl.poll(poll_timeout)
except Exception, e:
if (getattr(e, 'errno', None) == errno.EINTR or
(isinstance(getattr(e, 'args', None), tuple) and
len(e.args) == 2 and e.args[0] == errno.EINTR)):
continue
else:
raise
if self._blocking_signal_threshold is not None:
signal.setitimer(signal.ITIMER_REAL,
self._blocking_signal_threshold, 0)
self._events.update(event_pairs)
while self._events:
fd, events = self._events.popitem()
self._handlers[fd](fd, events)
self._stopped = False
if self._blocking_signal_threshold is not None:
signal.setitimer(signal.ITIMER_REAL, 0, 0)
大致的思路是:有連接進來(client端請求),就丟給epoll,順便註冊一個事件和一個回調函數,我們主線程還是繼續監聽請求;然後在事件循環中,如果發生了某種事件(如socket可讀,或可寫),則調用之前註冊的回調函數去處理。
4. 其他有關tornado 異步也可以研究下tornado-celery。