Python Web框架Tornado

Python Web框架Tornado的異步處理代碼示例


Tornado是一個輕量級但高性能的Python web框架,與另一個流行的Python web框架Django相比,tornado不提供操作數據庫的ORM接口及嚴格的MVC開發模式,但可以提供基本的web server功能,故它是輕量級的;它藉助non-blocking and event-driven的I/O模型(epoll或kqueue)實現了一套異步網絡庫,故它是高性能的。

Tornado的輕量級+高性能特性使得它特別適用於提供web api的場合,使用合理的話,其非阻塞+異步能力可以應對C10K問題。

需要特別注意的是,由於Python的GIL導致多線程總是單核執行的”特點”,tornado處理http請求時,若某個請求的後端響應有阻塞現象(如從DB或磁盤讀數據導致處理時間很長),則會導致其他http請求也被block,這會嚴重拖累tornado在高併發場景下的性能。

幸運的是,tornado提供了異步處理請求的能力,在異步模式下,我們可以通過傳入回調函數或藉助tornado提供的tornado.gen.coroutine裝飾器,使得tornado內部的io loop在等待當前請求響應結果的同時,仍然可以接受其它的http請求,這樣就避免了某個耗時操作影響tornado的處理能力。

2. 如何在tornado框架下編寫異步處理代碼

Tornado官網文檔給出了幾個簡單的異步代碼示例,不過說實話,代碼太過簡單(都是在某個uri的handler類的get或post函數中展現了基本的異步語法),沒有多大的實戰意義。

在實際項目中,複雜的處理邏輯不可能都堆在get或post函數中,而是會封裝在其它class中供handler類的get或post函數調用。所以,本文給出一個稍複雜的實例,旨在說明如何在其它class的函數中實現異步處理邏輯,以實現http請求異步化處理的目的。

假設現在的需求是用tornado實現一個web server,支持名爲cityhotel的uri方法,當client通過http GET請求訪問該uri時,web server根據query參數指定的城市,去請求存放hotel詳細數據的另一個後端api,進行業務處理後返回某個連鎖hotel在該城市的所有門店給client。 
假設client GET請求的url格式爲:
http://host/api/hotel/cityhotel?city=xxx 
再假設存放hotel詳細數據的後端api接口爲:
http://hotel_backend/getCityHotels?city=xxx

根據上面的場景,由於我們用tornado實現的web server接到client的請求後,還要去另一個API接口請求基礎數據,而後者在返回前,tornado會block,所以,這種場景下,tornado最好以異步方式請求那個提供基礎數據的API,避免不可控的後端拖累tornado的響應性能。

根據上面描述的業務需求,下面的代碼示範瞭如何通過異步方式處理業務處理。

模塊入口文件(main.py):


#!/bin/env python

import tornado.ioloop
import tornado.web
import tornado.gen
import hotelcore


class CityHotelHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def get(self):
        ## parse query params
        params = {}
        keys = ['city']
        for key in keys:
            value = self.get_query_argument(key)
            params[key] = value
        (status, rsp) = yield hotelcore.HotelApiHandler.get_city_hotel(params['city'])
        if 200 == status:
            self.set_header('content-type', 'application/json')
            self.finish(rsp)
        else:
            self.set_status(404)
            self.finish()



def main():
    app_inst = tornado.web.Application([
        (r'/api/hotel/cityhotel', CityHotelHandler),
    ], compress_response = True)

    app_inst.listen(8218)
    tornado.ioloop.IOLoop.current().start()


if '__main__' == __name__:
    main()




處理業務邏輯的module封裝在hotelcore.py文件中,代碼如下:


#!/bin/env python
#-*- encoding: utf-8 -*-

import json

from tornado import gen
from tornado import httpclient


class HotelApiHandler(object):
    _cfg_dict = {
        'api_host' : 'api.hotelbackend.com',
    }


    @classmethod
    @gen.coroutine
    def get_city_hotel(cls, city):
        ret = yield cls._parallel_fetch_city_hotel(city)
        raise gen.Return((200, ret))


    @classmethod
    @gen.coroutine
    def _parallel_fetch_city_hotel(cls, city):
        base_url = 'http://%s/v1/getCityHotel' % (cls._cfg_dict['api_host'])
        ## hote type: 1=normal room; 2=deluxe room
        hotel_type = {'normal': 1, 'deluxe': 2}
        urls = []
        for v in hotel_type.values():
            api_url = '%s?city=%s&level=%s' % (base_url, city, v)
            urls.append(api_url)
        ## issue async http request
        http_clt = httpclient.AsyncHTTPClient()
        rsps_dict = yield dict(normal_room = http_clt.fetch(urls[0]), deluxe_room = http_clt.fetch(urls[1]))
        city_hotel_info = cls._parse_city_hotel(rsps_dict, city)
        ret = { }
        if len(city_hotel_info):
            ret['errno']  = 0
            ret['errmsg'] = 'SUCCESS'
            ret['data']   = city_hotel_info
        else:
            ret['errno']  = 1
            ret['errmsg'] = 'Service Not Found at This City'
            ret['data']   = ''
        raise gen.Return(ret)


    @classmethod
    def _parse_city_hotel(cls, rsp_dict, city):
        city_hotel_info = {}
        for hotel_level, rsp in rsp_dict.items():
            rsp_json = json.loads(rsp.body)
            datas = rsp_json['data']
            for city_id, city_detail in datas.items():
                name = city_detail['name']
                if city in name:
                    city_hotel_info[hotel_level] = city_detail
                    break
        return city_hotel_info




說明:
編寫tornado異步處理代碼需要對Python的decorator語法和generator/yield語法比較熟悉
tornado提供的裝飾器@gen.coroutine表明被裝飾函數是個異步處理函數,
該函數的調用不會block tornado主線程被@gen.coroutine裝飾的函數中,
需要異步執行的耗時函數用yield來調用,yield本身返回的是個generator,
結合@gen.coroutine後,它返回一個tornado定義的Future類型的對象
yield調用的函數在執行過程中,進程控制權會返給主線程,
故即使該函數需要較長運行時間,tornado的主線程也可以繼續處理其它請求
在Python 2.x版本的語法中,generator中不允許用return返回函數的返回值,
必須用tornado提供的raise gen.Return(ret)達到返回的目的,
這是個比較tricky的方法yield返回的Future對象可以通過調用body屬性來獲取
通過yield調用的函數的返回值
只要結合上述幾點理解了
@gen.coroutine和yield在tornado異步編程中的語法意義,
那麼,寫出複雜的異步調用代碼與編寫實現相同功能
但tornado整體性能無法保證的同步調用代碼相比,實現難度就幾乎不存在了。

參考資料

  1. Tornado Doc: User’s guide

  2. Book: Introduction to tornado chapter 5. asynchronous web services


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