Python的Tornado架構的非同步任務與AsyncHTTPClient

來源:互聯網
上載者:User
高效能伺服器Tornado
Python的web架構名目繁多,各有千秋。正如光榮屬於希臘,偉大屬於羅馬。Python的優雅結合WSGI的設計,讓web架構介面實現千秋一統。WSGI 把應用(Application)和伺服器(Server)結合起來。Django 和 Flask 都可以結合 gunicon 搭建部署應用。

與 django 和 flask 不一樣,tornado 既可以是 wsgi 應用,也可以是 wsgi 服務。當然,選擇tornado更多的考量源於其單進程單線程非同步IO的網路模式。高效能往往吸引人,可是有不少朋友使用之後會提出疑問,tornado號稱高效能,實際使用的時候卻怎麼感受不到呢?

實際上,高效能源於Tornado基於Epoll(unix為kqueue)的非同步網路IO。因為tornado的單線程機制,一不小心就容易寫出阻塞服務(block)的代碼。不但沒有效能提高,反而會讓效能急劇下降。因此,探索tornado的非同步使用方式很有必要。

Tornado 非同步使用方式
簡而言之,Tornado的非同步包括兩個方面,非同步服務端和非同步用戶端。無論服務端和用戶端,具體的非同步模型又可以分為回調(callback)和協程(coroutine)。具體應用情境,也沒有很明確的界限。往往一個請求服務裡還包含對別的服務的用戶端非同步請求。

服務端非同步方式
服務端非同步,可以理解為一個tornado請求之內,需要做一個耗時的任務。直接寫在商務邏輯裡可能會block整個服務。因此可以把這個任務放到非同步處理,實現非同步方式就有兩種,一種是yield掛起函數,另外一種就是使用類線程池的方式。請看一個同步例子:

class SyncHandler(tornado.web.RequestHandler):  def get(self, *args, **kwargs):    # 耗時的代碼    os.system("ping -c 2 www.google.com")    self.finish('It works')

使用ab測試一下:

ab -c 5 -n 5 http://127.0.0.1:5000/sync

Server Software:    TornadoServer/4.3Server Hostname:    127.0.0.1Server Port:      5000Document Path:     /syncDocument Length:    5 bytesConcurrency Level:   5Time taken for tests:  5.076 secondsComplete requests:   5Failed requests:    0Total transferred:   985 bytesHTML transferred:    25 bytesRequests per second:  0.99 [#/sec] (mean)Time per request:    5076.015 [ms] (mean)Time per request:    1015.203 [ms] (mean, across all concurrent requests)Transfer rate:     0.19 [Kbytes/sec] received

qps 僅有可憐的 0.99,姑且當成每秒處理一個請求吧。

下面祭出非同步大法:

class AsyncHandler(tornado.web.RequestHandler):  @tornado.web.asynchronous  @tornado.gen.coroutine  def get(self, *args, **kwargs):    tornado.ioloop.IOLoop.instance().add_timeout(1, callback=functools.partial(self.ping, 'www.google.com'))    # do something others    self.finish('It works')  @tornado.gen.coroutine  def ping(self, url):    os.system("ping -c 2 {}".format(url))    return 'after'

儘管在執行非同步任務的時候選擇了timeout 1秒,主線程的返回還是很快的。ab壓測如下:

Document Path:     /asyncDocument Length:    5 bytesConcurrency Level:   5Time taken for tests:  0.009 secondsComplete requests:   5Failed requests:    0Total transferred:   985 bytesHTML transferred:    25 bytesRequests per second:  556.92 [#/sec] (mean)Time per request:    8.978 [ms] (mean)Time per request:    1.796 [ms] (mean, across all concurrent requests)Transfer rate:     107.14 [Kbytes/sec] received

上述的使用方式,通過tornado的IO迴圈,把可以把耗時的任務放到後台非同步計算,請求可以接著做別的計算。可是,經常有一些耗時的任務完成之後,我們需要其計算的結果。此時這種方式就不行了。車道山前必有路,只需要切換一非同步方式即可。下面使用協程來改寫:

class AsyncTaskHandler(tornado.web.RequestHandler):  @tornado.web.asynchronous  @tornado.gen.coroutine  def get(self, *args, **kwargs):    # yield 結果    response = yield tornado.gen.Task(self.ping, ' www.google.com')    print 'response', response    self.finish('hello')  @tornado.gen.coroutine  def ping(self, url):    os.system("ping -c 2 {}".format(url))    return 'after'

可以看到非同步在處理,而結果值也被返回了。

Server Software:    TornadoServer/4.3Server Hostname:    127.0.0.1Server Port:      5000Document Path:     /async/taskDocument Length:    5 bytesConcurrency Level:   5Time taken for tests:  0.049 secondsComplete requests:   5Failed requests:    0Total transferred:   985 bytesHTML transferred:    25 bytesRequests per second:  101.39 [#/sec] (mean)Time per request:    49.314 [ms] (mean)Time per request:    9.863 [ms] (mean, across all concurrent requests)Transfer rate:     19.51 [Kbytes/sec] received

qps提升還是很明顯的。有時候這種協程處理,未必就比同步快。在並發量很小的情況下,IO本身拉開的差距並不大。甚至協程和同步效能差不多。例如你跟博爾特跑100米肯定輸給他,可是如果跟他跑2米,鹿死誰手還未定呢。

yield掛起函數協程,儘管沒有block主線程,因為需要處理傳回值,掛起到響應執行還是有時間等待,相對於單個請求而言。另外一種使用非同步和協程的方式就是在主線程之外,使用線程池,線程池依賴於futures。Python2需要額外安裝。

下面使用線程池的方式修改為非同步處理:

from concurrent.futures import ThreadPoolExecutorclass FutureHandler(tornado.web.RequestHandler):  executor = ThreadPoolExecutor(10)  @tornado.web.asynchronous  @tornado.gen.coroutine  def get(self, *args, **kwargs):    url = 'www.google.com'    tornado.ioloop.IOLoop.instance().add_callback(functools.partial(self.ping, url))    self.finish('It works')  @tornado.concurrent.run_on_executor  def ping(self, url):    os.system("ping -c 2 {}".format(url))

再運行ab測試:

Document Path:     /futureDocument Length:    5 bytesConcurrency Level:   5Time taken for tests:  0.003 secondsComplete requests:   5Failed requests:    0Total transferred:   995 bytesHTML transferred:    25 bytesRequests per second:  1912.78 [#/sec] (mean)Time per request:    2.614 [ms] (mean)Time per request:    0.523 [ms] (mean, across all concurrent requests)Transfer rate:     371.72 [Kbytes/sec] received

qps瞬間達到了1912.78。同時,可以看到伺服器的log還在不停的輸出ping的結果。
想要傳回值也很容易。再切換一下使用方式介面。使用tornado的gen模組下的with_timeout功能(這個功能必須在tornado>3.2的版本)。

class Executor(ThreadPoolExecutor):  _instance = None  def __new__(cls, *args, **kwargs):    if not getattr(cls, '_instance', None):      cls._instance = ThreadPoolExecutor(max_workers=10)    return cls._instanceclass FutureResponseHandler(tornado.web.RequestHandler):  executor = Executor()  @tornado.web.asynchronous  @tornado.gen.coroutine  def get(self, *args, **kwargs):    future = Executor().submit(self.ping, 'www.google.com')    response = yield tornado.gen.with_timeout(datetime.timedelta(10), future,                         quiet_exceptions=tornado.gen.TimeoutError)    if response:      print 'response', response.result()  @tornado.concurrent.run_on_executor  def ping(self, url):    os.system("ping -c 1 {}".format(url))    return 'after'

線程池的方式也可以通過使用tornado的yield把函數掛起,實現了協程處理。可以得出耗時任務的result,同時不會block住主線程。

Concurrency Level:   5Time taken for tests:  0.043 secondsComplete requests:   5Failed requests:    0Total transferred:   960 bytesHTML transferred:    0 bytesRequests per second:  116.38 [#/sec] (mean)Time per request:    42.961 [ms] (mean)Time per request:    8.592 [ms] (mean, across all concurrent requests)Transfer rate:     21.82 [Kbytes/sec] received

qps為116,使用yield協程的方式,僅為非reponse的十分之一左右。看起來效能損失了很多,主要原因這個協程返回結果需要等執行完畢任務。

好比打魚,前一種方式是撒網,然後就完事,不聞不問,時間當然快,後一種方式則撒網之後,還得收網,等待收網也是一段時間。當然,相比同步的方式還是快了千百倍,畢竟撒網還是比一隻只釣比較快。

具體使用何種方式,更多的依賴業務,不需要傳回值的往往需要處理callback,回調太多容易暈菜,當然如果需要很多回調嵌套,首先最佳化的應該是業務或產品邏輯。yield的方式很優雅,寫法可以非同步邏輯同步寫,爽是爽了,當然也會損失一定的效能。

非同步多樣化
Tornado非同步服務的處理大抵如此。現在非同步處理的架構和庫也很多,藉助redis或者celery等,也可以把tonrado中一些業務非同步化,放到後台執行。

此外,Tornado還有用戶端非同步功能。該特性主要是在於 AsyncHTTPClient的使用。此時的應用情境往往是tornado服務內,需要針對另外的IO進行請求和處理。順便提及,上述的例子中,調用ping其實也算是一種服務內的IO處理。接下來,將會探索一下AsyncHTTPClient的使用,尤其是使用AsyncHTTPClient上傳檔案與轉寄請求。

非同步用戶端
前面瞭解Tornado的非同步任務的常用做法,姑且歸結為非同步服務。通常在我們的服務內,還需要非同步請求第三方服務。針對HTTP請求,Python的庫Requests是最好用的庫,沒有之一。官網宣稱:HTTP for Human。然而,在tornado中直接使用requests將會是一場惡夢。requests的請求會block整個服務進程。

上帝關上門的時候,往往回開啟一扇窗。Tornado提供了一個基於架構本身的非同步HTTP用戶端(當然也有同步的用戶端)--- AsyncHTTPClient。

AsyncHTTPClient 基本用法
AsyncHTTPClient是 tornado.httpclinet 提供的一個非同步http用戶端。使用也比較簡單。與服務進程一樣,AsyncHTTPClient也可以callback和yield兩種使用方式。前者不會返回結果,後者則會返回response。

如果請求第三方服務是同步方式,同樣會殺死效能。

class SyncHandler(tornado.web.RequestHandler):  def get(self, *args, **kwargs):    url = 'https://api.github.com/'    resp = requests.get(url)    print resp.status_code    self.finish('It works')

使用ab測試大概如下:

Document Path:     /syncDocument Length:    5 bytesConcurrency Level:   5Time taken for tests:  10.255 secondsComplete requests:   5Failed requests:    0Total transferred:   985 bytesHTML transferred:    25 bytesRequests per second:  0.49 [#/sec] (mean)Time per request:    10255.051 [ms] (mean)Time per request:    2051.010 [ms] (mean, across all concurrent requests)Transfer rate:     0.09 [Kbytes/sec] received

效能相當慢了,換成AsyncHTTPClient再測:

class AsyncHandler(tornado.web.RequestHandler):  @tornado.web.asynchronous  def get(self, *args, **kwargs):    url = 'https://api.github.com/'    http_client = tornado.httpclient.AsyncHTTPClient()    http_client.fetch(url, self.on_response)    self.finish('It works')  @tornado.gen.coroutine  def on_response(self, response):    print response.code

qps 提高了很多

Document Path:     /asyncDocument Length:    5 bytesConcurrency Level:   5Time taken for tests:  0.162 secondsComplete requests:   5Failed requests:    0Total transferred:   985 bytesHTML transferred:    25 bytesRequests per second:  30.92 [#/sec] (mean)Time per request:    161.714 [ms] (mean)Time per request:    32.343 [ms] (mean, across all concurrent requests)Transfer rate:     5.95 [Kbytes/sec] received

同樣,為了擷取response的結果,只需要yield函數。

class AsyncResponseHandler(tornado.web.RequestHandler):  @tornado.web.asynchronous  @tornado.gen.coroutine  def get(self, *args, **kwargs):    url = 'https://api.github.com/'    http_client = tornado.httpclient.AsyncHTTPClient()    response = yield tornado.gen.Task(http_client.fetch, url)    print response.code    print response.body

AsyncHTTPClient 轉寄
使用Tornado經常需要做一些轉寄服務,需要藉助AsyncHTTPClient。既然是轉寄,就不可能只有get方法,post,put,delete等方法也會有。此時涉及到一些 headers和body,甚至還有https的waring。

下面請看一個post的例子, yield結果,通常,使用yield的時候,handler是需要 tornado.gen.coroutine。

headers = self.request.headersbody = json.dumps({'name': 'rsj217'})http_client = tornado.httpclient.AsyncHTTPClient()resp = yield tornado.gen.Task(  self.http_client.fetch,   url,  method="POST",   headers=headers,  body=body,   validate_cert=False)

AsyncHTTPClient 構造請求
如果業務處理並不是在handlers寫的,而是在別的地方,當無法直接使用tornado.gen.coroutine的時候,可以構造請求,使用callback的方式。

body = urllib.urlencode(params)req = tornado.httpclient.HTTPRequest( url=url,  method='POST',  body=body,  validate_cert=False) http_client.fetch(req, self.handler_response)def handler_response(self, response):  print response.code

用法也比較簡單,AsyncHTTPClient中的fetch方法,第一個參數其實是一個HTTPRequest執行個體對象,因此對於一些和http請求有關的參數,例如method和body,可以使用HTTPRequest先構造一個請求,再扔給fetch方法。通常在轉寄服務的時候,如果開起了validate_cert,有可能會返回599timeout之類,這是一個warning,官方卻認為是合理的。

AsyncHTTPClient 上傳圖片
AsyncHTTPClient 更進階的用法就是上傳圖片。例如服務有一個功能就是請求第三方服務的圖片OCR服務。需要把使用者上傳的圖片,再轉寄給第三方服務。

@router.Route('/api/v2/account/upload')class ApiAccountUploadHandler(helper.BaseHandler):  @tornado.gen.coroutine  @helper.token_require  def post(self, *args, **kwargs):    upload_type = self.get_argument('type', None)    files_body = self.request.files['file']    new_file = 'upload/new_pic.jpg'    new_file_name = 'new_pic.jpg'    # 寫入檔案    with open(new_file, 'w') as w:      w.write(file_['body'])    logging.info('user {} upload {}'.format(user_id, new_file_name))    # 非同步請求 上傳圖片    with open(new_file, 'rb') as f:      files = [('image', new_file_name, f.read())]    fields = (('api_key', KEY), ('api_secret', SECRET))    content_type, body = encode_multipart_formdata(fields, files)    headers = {"Content-Type": content_type, 'content-length': str(len(body))}    request = tornado.httpclient.HTTPRequest(config.OCR_HOST,                         method="POST", headers=headers, body=body, validate_cert=False)    response = yield tornado.httpclient.AsyncHTTPClient().fetch(request)def encode_multipart_formdata(fields, files):  """  fields is a sequence of (name, value) elements for regular form fields.  files is a sequence of (name, filename, value) elements for data to be  uploaded as files.  Return (content_type, body) ready for httplib.HTTP instance  """  boundary = '----------ThIs_Is_tHe_bouNdaRY_$'  crlf = '\r\n'  l = []  for (key, value) in fields:    l.append('--' + boundary)    l.append('Content-Disposition: form-data; name="%s"' % key)    l.append('')    l.append(value)  for (key, filename, value) in files:    filename = filename.encode("utf8")    l.append('--' + boundary)    l.append(        'Content-Disposition: form-data; name="%s"; filename="%s"' % (          key, filename        )    )    l.append('Content-Type: %s' % get_content_type(filename))    l.append('')    l.append(value)  l.append('--' + boundary + '--')  l.append('')  body = crlf.join(l)  content_type = 'multipart/form-data; boundary=%s' % boundary  return content_type, bodydef get_content_type(filename):  import mimetypes  return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

對比上述的用法,上傳圖片僅僅是多了一個圖片的編碼。將圖片的位元據按照multipart 方式編碼。編碼的同時,還需要把傳遞的相關的欄位處理好。相比之下,使用requests 的方式則非常簡單:

files = {}f = open('/Users/ghost/Desktop/id.jpg')files['image'] = fdata = dict(api_key='KEY', api_secret='SECRET')resp = requests.post(url, data=data, files=files)f.close()print resp.status_Code

總結
通過AsyncHTTPClient的使用方式,可以輕鬆的實現handler對第三方服務的請求。結合前面關於tornado非同步使用方式。無非還是兩個key。是否需要返回結果,來確定使用callback的方式還是yield的方式。當然,如果不同的函數都yield,yield也可以一直傳遞。這個特性,tornado的中的tornado.auth 裡面對oauth的認證。

大致就是這樣的用法。

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.