14.Tornado高性能的祕密:ioloop對象分析 (副標題:IOLoop是個事件循環)

網上都說nginx和lighthttpd是高性能web服務器,而tornado也是著名的高抗負載應用,它們間有什麼相似處呢?上節提到的ioloop對象是如何循環的呢?往下看。

首先關於TCP服務器的開發上節已經提過,很明顯那個三段式的示例是個效率很低的(因爲只有一個連接被端開新連接才能被接受)。要想開發高性能的服務器,就得在這accept上下功夫。

首先,新連接的到來一般是經典的三次握手,只有當服務器收到一個SYN時才說明有一個新連接(還沒建立),這時監聽fd是可讀的可以調用accept,此前服務器可以乾點別的,這就是SELECT/POLL/EPOLL的思路。而只有三次握手成功後,accept纔會返回,此時監聽fd是讀完成狀態,似乎服務器在此之前可以轉身去幹別的,等到讀完成再調用accept就不會有延遲了,這就是AIO的思路,不過在*nix平臺上好像支持不是很廣。。。再有,accept得到的新fd,不一定是可讀的(客戶端請求還沒到達),所以可以等新fd可讀時在read()(可能會有一點延遲),也可以用AIO等讀完後再read就不會延遲了。同樣類似,對於write,close也有類似的事件。

總的思路就是,在我們關心的fd上註冊關心的多個事件,事件發生了就啓動回調,沒發生就看點別的。這是單線程的,多線程的複雜一點,但差不多。nginx和lightttpd以及tornado都是類似的方式,只不過是多進程和多線程或單線程的區別而已。爲簡便,我們只分析tornado單線程的情況。

關於ioloop.py的代碼,主要有兩個要點。一個是configurable機制,一個就是epoll循環。先看epoll循環吧。IOLoop 類的start是循環所在,但它必須被子類覆蓋實現,因此它的start在PollIOLoop裏。略過循環外部的多線程上下文環境的保存與恢復,單看循環:

01 while True:
02    poll_timeout = 3600.0
03  
04    # Prevent IO event starvation by delaying new callbacks
05    # to the next iteration of the event loop.
06    with self._callback_lock:
07        callbacks = self._callbacks
08        self._callbacks = []
09    for callback in callbacks:
10        self._run_callback(callback)
11  
12    if self._timeouts:
13        now = self.time()
14        while self._timeouts:
15            if self._timeouts[0].callback is None:
16                # the timeout was cancelled
17                heapq.heappop(self._timeouts)
18            elif self._timeouts[0].deadline <= now:
19                timeout = heapq.heappop(self._timeouts)
20                self._run_callback(timeout.callback)
21            else:
22                seconds = self._timeouts[0].deadline - now
23                poll_timeout = min(seconds, poll_timeout)
24                break
25  
26    if self._callbacks:
27        # If any callbacks or timeouts called add_callback,
28        # we don't want to wait in poll() before we run them.
29        poll_timeout = 0.0
30  
31    if not self._running:
32        break
33  
34    if self._blocking_signal_threshold is not None:
35        # clear alarm so it doesn't fire while poll is waiting for
36        # events.
37        signal.setitimer(signal.ITIMER_REAL, 00)
38  
39    try:
40        event_pairs = self._impl.poll(poll_timeout)
41    except Exception as e:
42        # Depending on python version and IOLoop implementation,
43        # different exception types may be thrown and there are
44        # two ways EINTR might be signaled:
45        # * e.errno == errno.EINTR
46        # * e.args is like (errno.EINTR, 'Interrupted system call')
47        if (getattr(e, 'errno'None== errno.EINTR or
48            (isinstance(getattr(e, 'args'None), tupleand
49             len(e.args) == 2 and e.args[0== errno.EINTR)):
50            continue
51        else:
52            raise
53  
54    if self._blocking_signal_threshold is not None:
55        signal.setitimer(signal.ITIMER_REAL,
56                         self._blocking_signal_threshold, 0)
57  
58    # Pop one fd at a time from the set of pending fds and run
59    # its handler. Since that handler may perform actions on
60    # other file descriptors, there may be reentrant calls to
61    # this IOLoop that update self._events
62    self._events.update(event_pairs)
63    while self._events:
64        fd, events = self._events.popitem()
65        try:
66            self._handlers[fd](fd, events)
67        except (OSError, IOError) as e:
68            if e.args[0== errno.EPIPE:
69                # Happens when the client closes the connection
70                pass
71            else:
72                app_log.error("Exception in I/O handler for fd %s",
73                              fd, exc_info=True)
74        except Exception:
75            app_log.error("Exception in I/O handler for fd %s",
76                          fd, exc_info=True)

首先是設定超時時間。然後在互斥鎖下取出上次循環遺留下的回調列表(在add_callback添加對象),把這次列表置空,然後依次執行列表裏的回調。這裏的_run_callback就沒什麼好分析的了。緊接着是檢查上次循環遺留的超時列表,如果列表裏的項目有回調而且過了截止時間,那肯定超時了,就執行對應的超時回調。然後檢查是否又有了事件回調(因爲很多回調函數裏可能會再添加回調),如果是,則不在poll循環裏等待,如註釋所述。接下來最關鍵的一句是event_pairs = self._impl.poll(poll_timeout),這句裏的_impl是epoll,在platform/epoll.py裏定義,總之就是一個等待函數,當有事件(超時也算)發生就返回。然後把事件集保存下來,對於每個事件,self._handlers[fd](fd, events)根據fd找到回調,並把fd和事件做參數回傳。如果fd是監聽的fd,那麼這個回調handler就是accept_handler函數,詳見上節代碼。如果是新fd可讀,一般就是_on_headers 或者 _on_requet_body了,詳見前幾節。我好像沒看到可寫時的回調?以上,就是循環的流程了。可能還是看的糊里糊塗的,因爲很多對象怎麼來的都不清楚,configurable也還沒有看。看完下面的分析,應該就可以了。

Configurable類在util.py裏被定義。類裏有一段註釋,已經很明確的說明了它的設計意圖和用法。它是可配置接口的父類,可配置接口對外提供一致的接口標識,但它的子類實現可以在運行時進行configure。一般在跨平臺時由於子類實現有多種選擇,這時候就可以使用可配置接口,例如select和epoll。首先注意 Configurable 的兩個函數: configurable_base 和 configurable_default, 兩函數都需要被子類(即可配置接口類)覆蓋重寫。其中,base函數一般返回接口類自身,default返回接口的默認子類實現,除非接口指定了__impl_class。IOLoop及其子類實現都沒有初始化函數也沒有構造函數,其構造函數繼承於Configurable,如下:

01 def __new__(cls**kwargs):
02     base = cls.configurable_base()
03     args = {}
04     if cls is base:
05         impl = cls.configured_class()
06         if base.__impl_kwargs:
07             args.update(base.__impl_kwargs)
08     else:
09         impl = cls
10     args.update(kwargs)
11     instance = super(Configurable, cls).__new__(impl)
12     # initialize vs __init__ chosen for compatiblity with AsyncHTTPClient
13     # singleton magic.  If we get rid of that we can switch to __init__
14     # here too.
15     instance.initialize(**args)
16     return instance

當子類對象被構造時,子類__new__被調用,因此參數裏的cls指的是Configurabel的子類(可配置接口類,如IOLoop)。先是得到base,查看IOLoop的代碼發現它返回的是自身類。由於base和cls是一樣的,所以調用configured_class()得到接口的子類實現,其實就是調用base(現在是IOLoop)的configurable_default,總之就是返回了一個子類實現(epoll/kqueue/select之一),順便把__impl_kwargs合併到args裏。接着把kwargs併到args裏。然後調用Configurable的父類(Object)的__new__方法,生成了一個impl的對象,緊接着把args當參數調用該對象的initialize(繼承自PollIOloop,其initialize下段進行分析),返回該對象。

所以,當構造IOLoop對象時,實際得到的是EPollIOLoop或其它相似子類。另外,Configurable 還提供configure方法來給接口指定實現子類和參數。可以看的出來,Configurable類主要提供構造方法,相當於對象工廠根據配置來生產對象,同時開放configure接口以供配置。而子類按照約定調整配置即可得到不同對象,代碼得到了複用。

解決了構造,來看看IOLoop的instance方法。先檢查類是否有成員_instance,一開始肯定沒有,於是就構造了一個IOLoop對象(即EPollIOLoop對象)。以後如果再調用instance,得到的則是已有的對象,這樣就確保了ioloop在全局是單例。再看epoll循環時注意到self._impl,Configurable 和 IOLoop 裏都沒有, 這是在哪兒定義的呢? 爲什麼IOLoop的start跑到PollIOLoop裏,應該是EPollIOLoop纔對啊。 對,應該看出來了,EPollIOLoop 就是PollIOLoop的子類,所以方法被繼承了是很常見的哈。

從上一段的構造流程裏可以看到,EPollIOLoop對象的initialize方法被調用了,看其代碼發現它調用了其父類(PollIOLoop)的它方法, 並指定了impl=select.epoll(), 然後在父類的方法裏就把它保存了下來,所以self._impl.poll就等效於select.epoll().poll().PollIOLoop裏還有一些註冊,修改,刪除監聽事件的方法,其實就是對self._impl的封裝調用。就如上節的 add_accept_handler 就是調用ioloop的add_handler方法把監聽fd和accept_handler方法進行關聯。

IOLoop基本是個事件循環,因此它總是被其它模塊所調用。而且爲了足夠通用,基本上對回調沒多大限制,一個可執行對象即可。事件分發就到此結束了,和IO事件密切相關的另一個部分是IOStream,看看它是如何讀寫的。

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