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會繼續執行。html

另一個瓶頸就是GIL限制了CPU的併發數量,所以考慮用子進程的方式增長進程數,提升服務能力上限。python

綜合上面的分析,大體用如下方案:web

  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了,具體連接參考這裏app

因爲tornado很方便地支持多進程模型,多進程的使用要簡單不少,在以上例子中,只須要對啓動部分稍做改動便可。具體代碼以下所示:python2.7

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()

須要注意的地方有兩點:異步

  1. app = tornado.web.Application(handlers=[(r"/sleep", HasBlockTaskHandler)], autoreload=False, debug=False),在生成Application對象時,要將autoreload和debug兩個參數至爲False。也就是須要保證在fork子進程以前IOLoop是未被初始化的。這個能夠經過tornado.ioloop.IOLoop.initialized()函數來跟。
  2. http_server.start(5)在啓動IOLoop以前經過start函數設置進程數量,若是設置爲0表示每一個CPU都啓動一個進程。

最後的效果是能夠看到n+1個進程在運行,且公用同一個端口。

相關文章
相關標籤/搜索