Tornado之源碼解析

因爲最近做項目,一直接觸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_handlerremove_handlerupdate_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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章