關於Tornado5.1:到底是真實的異步和還是虛假的異步

原文轉載自「劉悅的技術博客」https://v3u.cn/a_id_107

我們知道Tornado 優秀的大併發處理能力得益於它的 web server 從底層開始就自己實現了一整套基於 epoll 的單線程異步架構,其他 web 框架比如Django或者Flask的自帶 server 基本是基於 wsgi 寫的簡單服務器,並沒有自己實現底層結構。而tornado.ioloop 就是 tornado web server 最底層的實現。

ioloop 的實現基於 epoll ,那麼什麼是 epoll? epoll 是Linux內核爲處理大批量文件描述符而作了改進的 poll / select 。
那麼到底什麼是 poll / select ? socket 通信時的服務端,當它接受( accept )一個連接並建立通信後( connection )就進行通信,而此時我們並不知道連接的客戶端有沒有信息發完。 這時候我們有兩種選擇:

一直在這裏等着直到收發數據結束;

每隔一會兒來看看這裏有沒有數據;

第一種辦法雖然可以解決問題,但我們要注意的是對於一個線程進程同時只能處理一個 socket 通信,其他連接只能被阻塞。 顯然這種方式在單進程情況下不現實。

第二種辦法要比第一種好一些,多個連接可以統一在一定時間內輪流看一遍裏面有沒有數據要讀寫,看上去我們可以處理多個連接了,這個方式就是 poll / select 的解決方案。 看起來似乎解決了問題,但實際上,隨着連接越來越多,輪詢所花費的時間將越來越長,而服務器連接的 socket 大多不是活躍的,所以輪詢所花費的大部分時間將是無用的。爲了解決這個問題, epoll 被創造出來,它的概念和 poll 類似,不過每次輪詢時,他只會把有數據活躍的 socket 挑出來輪詢,這樣在有大量連接時輪詢就節省了大量時間。

具體說說select:select最早於1983年出現在4.2BSD中,它通過一個select()系統調用來監視多個文件描述符的數組,當select()返回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程可以獲得這些文件描述符從而進行後續的讀寫操作。

while true {
    select(streams[])
    for i in streams[] {
        if i has data
        read until unavailable
    }
}

select的優點是支持目前幾乎所有的平臺,缺點主要有如下2個:

1)單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般爲1024,不過可以通過修改宏定義甚至重新編譯內核的方式提升這一限制。

2)select 所維護的存儲大量文件描述符的數據結構,隨着文件描述符數量的增大,其複製的開銷也線性增長。同時,由於網絡響應時間的延遲使得大量TCP連接處於非活躍狀態,但調用select()會對所有socket進行一次線性掃描,所以這也浪費了一定的開銷。

poll則在1986年誕生於System V Release 3,它和select在本質上沒有多大差別,但是poll沒有最大文件描述符數量的限制。

epoll是Linux 2.6 開始出現的爲處理大批量文件描述符而作了改進的poll,是Linux下多路複用IO接口select/poll的增強版本,它能顯著提高程序在大量併發連接中只有少量活躍的情況下的系統CPU利用率。另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。
在select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。

while true {
    active_stream[] = epoll_wait(epollfd)
    for i in active_stream[] {
        read or write till
    }
}

兩相對比,可以看出來,epoll只輪詢數據活躍的socket,性能自然就比較高了。

而Tornado其實默認是同步阻塞機制的,爲了能夠實現異步,你就必須使用異步的寫法纔可以,這裏有一個簡單的demo:

from  tornado.web import RequestHandler
import tornado.ioloop
import tornado.httpclient
import tornado.web
import requests


#異步任務
class AsyncHandler(RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        http_client = tornado.httpclient.AsyncHTTPClient()
        http_client.fetch("http://baidu.com",
                          callback=self.on_fetch)

    def on_fetch(self, response):
        print(response)
        self.write('done')
        self.finish()


#同步任務
class SyncHandler(RequestHandler):
    def get(self):
        response = requests.get("http://baidu.com")
        print(response)
        self.write('done')


def make_app():
    return tornado.web.Application(handlers=[
        (r'/async_fetch', AsyncHandler),
        (r'/sync_fetch', SyncHandler),
    ],debug=True)


if __name__ == '__main__':
    app = make_app()
    app.listen(8000)
    tornado.ioloop.IOLoop.current().start()

可以看到異步任務我們使用了(回調)和@tornado.web.asynchronous

@tornado.web.asynchronous 並不能將一個同步方法變成異步,所以修飾在同步方法上是無效的,只是告訴框架,這個方法是異步的,且只能適用於HTTP verb方法(get、post、delete、put等)。@tornado.web.asynchronous 裝飾器適用於callback-style的異步方法,對於用@tornado.web.asynchronous 修飾的異步方法,需要主動self.finish()來結束該請求,普通的方法(get()等)會自動結束請求在方法返回的時候。

對比下效率:使用ab命令發送500個請求,每秒50個 ab -n 500 -c 50

結果顯而易見,異步效率更高,15秒完成了同步需要50秒的任務。

但是,要想達到異步效果,就必須使用異步寫法,讓io操作變成異步io,而異步寫法對於後臺研發的綜合素質要求比較高,那麼能不能用同步的寫法達成異步效果呢?當然可以,就是使用celery+tornado

最後總結一下:

Tornado的異步原理: 單線程的torndo打開一個IO事件循環, 當碰到IO請求(新鏈接進來 或者 調用api獲取數據),由於這些IO請求都是非阻塞的IO,都會把這些非阻塞的IO socket 扔到一個socket管理器,所以,這裏單線程的CPU只要發起一個網絡IO請求,就不用掛起線程等待IO結果,這個單線程的事件繼續循環,接受其他請求或者IO操作,如此循環。

說人話:poll/select: 在一個育嬰室內,護士會對育嬰室內所有的嬰兒挨個check一遍,如此往復。epoll:護士會使用高科技設備對嬰兒進行監聽,並且只會check生命體徵有問題(活躍)的嬰兒,如此往復。

另外,對於如果面對超高的併發請求(qps上萬),僅僅採用 epoll 模型是不夠的,我們還必須使用多進程多線程等方式來充分利用系統資源,這就引出了nginx反向代理tornado進行負載均衡

原文轉載自「劉悅的技術博客」 https://v3u.cn/a_id_107

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