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的請求處理流程
- 當一個請求到來時,ioloop讀取這個請求,解包成一個http請求物件;
- tornado找到該請求物件中對應app的路由表,透過路由表查詢掛接的handler;
- 執行handler。handler方法執行後一般會返回一個物件;
- ioloop負責將物件包裝成http響應物件序列化傳送給客戶端;
非同步
-
什麼是非同步
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掛起的函式必須是非阻塞函式。如果寫了使用了非同步方法,但是寫了阻塞函式,那麼處理請求的方式仍然是同步阻塞的。
同步阻塞:
併發請求多路由地址:
- 若handler裡面沒有耗時IO操作,則會立馬返回,多個並行訪問感覺上是並行的,實際上由於tornado單執行緒事件迴圈機制,實際上是序列處理請求。
- 若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恢復掛起的請求處理邏輯。
需要新增的程式碼:
- 建立執行緒池:executor = ThreadPoolExecutor(10)
- @tornado.gen.coroutine # 使用協程排程 + yield
- @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