Tornado實現多程序/多執行緒的HTTP服務
用Tornado Web服務的基本流程
1.實現處理請求的Handler,該類繼承自tornado.web.RequestHandler,實現用於處理請求的對應方法如:get、post等。返回內容用self.write方法輸出。
2.例項化一個Application。建構函式的引數是一個Handlers列表,通過正則表示式,將請求與Handler對應起來。通過dict將Handler需要的其他物件以引數的方式傳遞給Handler的initialize方法。
3.初始化一個tornado.httpserver.HTTPServer物件,建構函式的引數是上一步的Application物件。
4.為HTTPServer物件繫結一個埠。
5.開始IOLoop。
需要用到的特性
由於tornado的亮點是非同步請求,所以這裡首先想到的是將所有請求都改造為非同步的。但是這裡遇到一個問題,就是非同步函式內一定不能有阻塞調用出現,否則整個IOLoop都會被卡住。這就要求徹底地去改造服務,將所有IO或是用時較長的請求都改造為非同步函式。這個工程量是非常大的,需要去修改已有的程式碼。因此,我們考慮用執行緒池的方式去實現。當一個執行緒阻塞在某個請求或IO時,其他執行緒或IOLoop會繼續執行。
另外一個瓶頸就是GIL限制了CPU的併發數量,因此考慮用子程序的方式增加程序數,提高服務能力上限。
綜合上面的分析,大致用以下方案:
1.通過子程序的方式複製多個程序,使子程序中的只讀頁指向同一個物理頁。
2.執行緒池。迴避非同步改造的工作量,增加IO的併發量。
測試程式碼
首先測試執行緒池,測試用例為:
對sleep頁面同時發出兩個請求:
1.線上程池中執行的函式(這裡是self.block_task)能夠同時執行。表現為在控制檯交替打印出數字。
2.兩個get請求幾乎同時返回,在瀏覽器上顯示返回的內容。
執行緒池的測試程式碼如下:
import os
import sys
import time
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.gen
from tornado.concurrent import run_on_executor
from concurrent.futures import ThreadPoolExecutor
from tornado.options import define, options
class HasBlockTaskHandler(tornado.web.RequestHandler):
executor = ThreadPoolExecutor(20) #起執行緒池,由當前RequestHandler持有
@tornado.gen.coroutine
def get(self):
strTime = time.strftime("%Y-%m-%d %H:%M:%S")
print "in get before block_task %s" % strTime
result = yield self.block_task(strTime)
print "in get after block_task"
self.write("%s" % (result))
@run_on_executor
def block_task(self, strTime):
print "in block_task %s" % strTime
for i in range(1, 16):
time.sleep(1)
print "step %d : %s" % (i, strTime)
return "Finish %s" % strTime
if __name__ == "__main__":
tornado.options.parse_command_line()
app = tornado.web.Application(handlers=[(r"/sleep", HasBlockTaskHandler)], autoreload=False, debug=False)
http_server = tornado.httpserver.HTTPServer(app)
http_server.bind(8888)
tornado.ioloop.IOLoop.instance().start()
整個程式碼裡有幾個位置值得關注:
1.executor = ThreadPoolExecutor(20)。這是給Handler類初始化了一個執行緒池。其中concurrent.futures不屬於tornado,是Python的一個獨立模組,在python3中是內建模組,python2.7需要自己安裝。
2.修飾符@run_on_executor。這個修飾符將同步函式改造為在executor(這裡是執行緒池)上執行的非同步函式,內部實現是將被修飾的函式submit到executor,返回一個Future物件。
3.修飾符@tornado.gen.coroutine。被這個修飾符修飾的函式,是一個以同步函式方式編寫的非同步函式。原本通過callback方式編寫的非同步程式碼,有了這個修飾符,可以通過yield一個Future的方式來寫。被修飾的函式在yield了一個Future物件後將會被掛起,Future物件的結果返回後繼續執行。
執行程式碼後,在兩個不同瀏覽器上訪問sleep頁面,得到了想要的效果。這裡有一個小插曲,就是如果在同一瀏覽器的兩個tab上進行測試,是無法看到想要的效果。第二個get請求會被block,直到第一個get請求返回,服務端才開始處理第二個get請求。這讓我一度覺得多執行緒沒有生效,用了半天時間查了很多資料,才看到是瀏覽器把相同的第二個請求block了,具體連結參考這裡。
由於tornado很方便地支援多程序模型,多程序的使用要簡單很多,在以上例子中,只需要對啟動部分稍作改動即可。具體程式碼如下所示:
if __name__ == "__main__":
tornado.options.parse_command_line()
app = tornado.web.Application(handlers=[(r"/sleep", HasBlockTaskHandler)], autoreload=False, debug=False)
http_server = tornado.httpserver.HTTPServer(app)
http_server.bind(8888)
print tornado.ioloop.IOLoop.initialized()
http_server.start(5)
tornado.ioloop.IOLoop.instance().start()
需要注意的地方有兩點:
app = tornado.web.Application(handlers=[(r"/sleep", HasBlockTaskHandler)], autoreload=False, debug=False),在生成Application物件時,要將autoreload和debug兩個引數至為False。也就是需要保證在fork子程序之前IOLoop是未被初始化的。這個可以通過tornado.ioloop.IOLoop.initialized()函式來跟。
http_server.start(5)在啟動IOLoop之前通過start函式設定程序數量,如果設定為0表示每個CPU都啟動一個程序。
最後的效果是可以看到n+1個程序在執行,且公用同一個埠。
Linux公社的RSS地址 :https://www.linuxidc.com/rssFeed.aspx
本文永久更新連結地址:https://www.linuxidc.com/Linux/2019-04/158006.htm