tornado原理介紹及非同步非阻塞實現方式

快樂的拉格朗日發表於2023-01-09

tornado原理介紹及非同步非阻塞實現方式

以下內容根據自己實操和理解進行的整理,歡迎交流~

在tornado的開發中,我們一般會見到以下四個組成部分。

  • ioloop:

同一個ioloop例項執行在一個單執行緒環境下。

tornado.ioloop.IOLoop.current().start()
  • app

可以有多個app,一般使用一個。會掛接一個或多個服務端套接字埠對外提供服務。

app = tornado.web.Application([
                                (r"/api/predict", PredictHandler),
                                (....) 
                                ], debug=False
                                )
  • 路由表

將服務url與handler對應起來,形成一個路由對映表。當請求到來時,根據請求的訪問url查詢路由對映表來找到相應的業務handler。

[
    (r"/api/predict", PredictHandler),
    (....) 
]
  • handler

開發時編寫的業務邏輯。可以有多個handler,為了可以透過不同的url訪問handler,增加一個handler就需要增加一個路由定址。

class Handler_1(RequestHandler):
    def get(self, *args, **kwargs):
                time.sleep(5)
                self.write("done")

class Handler_2(RequestHandler):
    def get(self, *args, **kwargs):
                time.sleep(5)
                self.write("done")

app = tornado.web.Application([
                            (r"/api/predict_1", Handler_1),
                            (r"/api/predict_2", Handler_2)
                            ], debug=False)

tornado的請求處理流程

  1. 當一個請求到來時,ioloop讀取這個請求,解包成一個http請求物件;
  2. tornado找到該請求物件中對應app的路由表,透過路由表查詢掛接的handler;
  3. 執行handler。handler方法執行後一般會返回一個物件;
  4. ioloop負責將物件包裝成http響應物件序列化傳送給客戶端;
tornado原理介紹及非同步非阻塞實現方式

非同步

  • 什麼是非同步

    tornado的非同步非阻塞是針對另一請求來說的,本次的請求如果沒有執行完,那依然需要繼續執行。

    python程式碼的非同步和tornado的非同步差異?

    Python裡面的非同步是針對程式碼段來講,非同步的程式碼段相當於放進後臺執行,CPU接著執行非同步程式碼段下一行的程式碼,這樣可以充分利用CPU,畢竟IO的速度可比不上CPU的執行速度,一直等著IO結束多浪費時間。

  • 為什麼需要非同步?

    提高CPU利用和服務併發

    在傳統的同步web伺服器中,網路為了實現多併發的功能,就需要和每一個使用者保持長連線,這就需要向每一個使用者分配一個執行緒,這是非常昂貴的資源支出。而在進行IO操作時,CPU是處於閒置的狀態。

    為了解決上述問題,tornado採用了單執行緒事件迴圈,減少併發連線的成本。一次只有一個執行緒在工作。要想用單執行緒實現併發,這就要求應用程式是非同步非阻塞的。

    需要注意的是tornado的高效能源於Tornado基於Epoll的非同步網路IO。但是因為tornado的單執行緒機制,很容易寫出阻塞服務(block)的程式碼。不但沒有效能提高,反而會讓效能急劇下降。

  • tornado實現非同步的方式

    在一個tornado請求之內,需要做一個I/O耗時的任務。直接寫在業務邏輯裡可能會block整個服務(也就是其他請求無法訪問此服務)。因此可以把這個任務放到非同步處理,實現非同步的方式就有兩種,一種是yield掛起函式,另外一種就是使用類執行緒池的方式。

    一般網上都會說‘yield生成器方式’,‘使用協程方法的非同步非阻塞’,但是都沒有提及到特別重要的一點,那就是yield掛起的函式必須是非阻塞函式。如果寫了使用了非同步方法,但是寫了阻塞函式,那麼處理請求的方式仍然是同步阻塞的。

同步阻塞:

併發請求多路由地址:

  1. 若handler裡面沒有耗時IO操作,則會立馬返回,多個並行訪問感覺上是並行的,實際上由於tornado單執行緒事件迴圈機制,實際上是序列處理請求。
  2. 若handler裡面有耗時IO操作,不會立馬返回,會阻塞在耗時IO操作裡面;這時併發請求會阻塞住(因為在排隊),遲遲返不回結果。

產生上述的原因是由於taonado的單執行緒事件迴圈,每次只有一個執行緒執行操作。如果執行緒正在處理阻塞函式,就不能重新獲得一個連線,處理併發的請求。

所以tornado要求裡面的業務邏輯是非同步非阻塞。

非同步非阻塞——協程

協程/生成器

tornado推薦使用協程實現非同步的方法。python 關鍵字 yield來實現非同步。

Tornado的非同步條件:要使用到非同步,就必須把IO操作變成非阻塞的IO。這一點非常重要,否則就達不到非同步的效果。透過非同步,可以釋放執行緒,執行緒從連線佇列獲取一個新的連線請求,從而可以處理其他請求。

當採用協程+非阻塞函式進行非同步處理時,不管這個IO操作是否有返回結果,當前路由不會跳過耗時函式執行下一行程式碼。前面說過了,tornado的非同步與python的非同步不是一回事,或者說針對的物件不一樣。但是執行緒不會一直乾等著,在等待的時候可以幹別的事情,於是就去重新連線了一個請求,與新的請求打的火熱。原來的IO操作執行完畢了,會通知執行緒返回繼續執行下一行程式碼,

此種方式的嚴重缺點:

使用 coroutine 方式嚴重依賴第三方庫(需要支援非同步)的實現,如果庫本身不支援 Tornado 的非同步操作,再怎麼使用協程也依然會是阻塞的;或者可以參考內建非同步客戶端,藉助tornado.ioloop.IOLoop封裝一個自己的非同步客戶端,但開發成本太高。

基於協程的程式設計

class SleepHandler(BaseHandler):
    """
    非同步的延時10秒
    """
    @gen.coroutine
    def get(self):
        yield gen.sleep(10) # 這裡必須是非同步函式,I/O操作,否則仍然會阻塞
        self.write("when i sleep 5s")

對於不支援非同步的耗時操作,如何使服務不阻塞,可以繼續處理其他請求呢?

那就是:基於執行緒的非同步程式設計

非同步非阻塞——執行緒池非同步

由於python直譯器使用GIL,多執行緒只能提高IO的併發能力,不能提高計算的併發能力。因此可以考慮透過子程式的方式,適當增加提供服務的程式數,提高整個系統服務能力的上限

基於執行緒池的方式,能讓tornado的阻塞過程變成非阻塞,其原理是在tornado本身這個執行緒之外啟動一個執行緒執行阻塞程式,從而變成非阻塞。

執行緒池為RequestHandler持有,請求處理邏輯中的耗時/阻塞任務可以提交給執行緒池處理,主迴圈邏輯可以繼續處理其他請求,執行緒池內的任務處理完畢後,會透過回撥註冊callback到ioloop,ioloop可以透過執行callback恢復掛起的請求處理邏輯。

需要新增的程式碼:

  1. 建立執行緒池:executor = ThreadPoolExecutor(10)
  2. @tornado.gen.coroutine # 使用協程排程 + yield
  3. @tornado.concurrent.run_on_executor

優點:

非同步非阻塞服務

小負載的工作,可以起到很好的效果

缺點:

如果大量使用執行緒化的非同步函式做一些高負載的活動,會導致Tornado程式效能低下響應緩慢;

from concurrent.futures import ThreadPoolExecutor

class Executor(ThreadPoolExecutor):
    """ 單例模式
    """
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not getattr(cls, '_instance', None):
            thred_num = 10 # 執行緒池數量
            cls._instance = ThreadPoolExecutor(max_workers=thred_num)
        return cls._instance

class PredictHandler(RequestHandler):
        # 1.使用單例模式
    executor = Executor()
        # 2.直接建立
        # executor = ThreadPoolExecutor(10)

        @tornado.gen.coroutine  # 使用協程排程
    def get(self, *args, **kwargs):
            result = yield self.main_process(url)
                self.write(....)

        @tornado.concurrent.run_on_executor 
    def main_process(self,url):
                #  do something
                # 會讓tornado阻塞的行為
        return sa_result

def create_app():
    return tornado.web.Application([
        (r"/api/predict", PredictHandler),
    ], debug=False) # 開啟多程式後,一定要將 debug 設定為 False

app = create_app()
app.listen(8501)
tornado.ioloop.IOLoop.current().start()

如果函式做的是高負載該怎麼辦?

使用:Tornado 結合 Celery

Tornado 結合 Celery tornado-celery

Celery 是一個簡單、靈活且可靠的,處理大量訊息的分散式系統,它是一個專注於實時處理的任務佇列, 同時也支援任務排程。

它是一個分散式的實時處理訊息佇列排程系統,tornado接到請求後,可以把所有的複雜業務邏輯處理、資料庫操作以及IO等各種耗時的同步任務交給celery,由這個任務佇列非同步處理完後,再返回給tornado。這樣只要保證tornado和celery的互動是非同步的,那麼整個服務是完全非同步的。

參考:

https://segmentfault.com/a/1190000015619549

https://www.jianshu.com/p/de7f04e65618

https://juejin.cn/post/6844904179564183565

https://blog.csdn.net/permike/article/details/51783528

https://blog.csdn.net/qq_16912257/article/details/78705587

https://segmentfault.com/a/1190000016610210

https://blog.csdn.net/iin729/article/details/109908963

相關文章