博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

tornado 异步任务1

Posted on 2017-10-24 16:33  bw_0927  阅读(607)  评论(0)    收藏  举报

https://piaosanlang.gitbooks.io/faq/tornado/tornadoyi-bu-bi-ji-ff08-yi-ff09-yi-bu-ren-wu.html

https://piaosanlang.gitbooks.io/faq/tornado/tornadoyuan-ma-fen-xi-ff08-er-ff09-yi-bu-shang-xia-wen-guan-li-ff08-stackcontext.html

https://piaosanlang.gitbooks.io/faq/tornado/tornadoyuan-ma-yue-du-ff08-yiff09-ioloop-zhi-chuang-jian-ioloop.html

http://www.jianshu.com/p/31fae7dd05ba

http://tornado-zh.readthedocs.io/zh/latest/ioloop.html 

http://www.cnblogs.com/shijingjing07/p/6555439.html

 

同步请求:

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        client=tornado.httpclient.HTTPClient()
        response=client.fetch("http://test.com/list")
        self.write("success")

异步请求:

class IndexAsyncHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        client=tornado.httpclient.AsyncHTTPClient()
        client.fetch("http://test.com/list",callback=self.on_response)
    def on_response(self,response):
        self.write("success")
        self.finish()

 

同步请求在请求完毕后,自动关闭连接。
异步请求保持连接开启,需要手动关闭连接。
tornado中使用@tornado.web.asynchronous装饰器作用是保持连接一直开启,
回调函数执行完毕后,调用finish方法来主动关闭连接。

 

http://tornado-zh.readthedocs.io/zh/latest/guide/coroutines.html?highlight=ThreadPoolExecutor

包含了 yield 关键字的函数是一个 生成器(generator). 所有的生成器都是异步的; 当调用它们的时候,会返回一个生成器对象,而不是一个执行完的结果. 

 

5.异步生成器
上例中,是使用回调函数来做业务处理及关闭连接的。
回调函数的缺点是,可能引起回调深渊,系统将难以维护。如回调中调用回调。

def get(self):
    client = AsyncHTTPClient()
    client.fetch("http://example.com", callback=on_response)

def on_response(self, response):
    client = AsyncHTTPClient()
    client.fetch("http://another.example.com/", callback=on_response2)

def on_response2(self, response):
    client = AsyncHTTPClient()
    client.fetch("http://still.another.example.com/", callback=on_response3)

def on_response3(self, response):
    [etc., etc.]

tornado2.1引入了tornado.gen模块,可以更整洁地执行异步请求。

@gen.coroutine 装饰器通过 yield 表达式和生成器进行交流, 而且通过返回一个 Future 与协程的调用方进行交互.

class IndexGenHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    @tornado.gen.engine
    def get(self):
        client=tornado.httpclient.AsyncHTTPClient()
        response=yield tornado.gen.Task(client.fetch,"http://test.com/list")
        self.write("success")
        self.finish()

 

tornado.gen是一个生成器(不是因为@gen.coroutine, 而是因为yield关键字,带有yield的函数就是生成器)(关于生成器参见“python生成器,函数,数组” ),
yield关键字的作用是返回控制,异步任务执行完毕后,程序在yield的地方恢复。
可以看到使用生成器,异步后业务处理不是在回调函数中完成的,看起来像同步处理一样,代码逻辑更清晰。

使用生成器和回调函数异步请求是一样的

 

6.异步请求的适用场景
请求处理逻辑复杂耗时,或长时间请求数据库的时候,异步请求可以大幅提升并发请求效率。
同时综合考虑缓存,业务逻辑放在客户端等手段,来缓解服务器压力。

 参考资料:http://docs.pythontab.com/tornado/introduction-to-tornado/ch5.html

  

  

 

=======================================================================================

异步装饰器, 还有AsyncHTTPClient都是用在tornado的web应用中的, 例如def MainHandler(tornado.web.RequestHandler), 必须启动一个io_loop, 否则不会运行

例如

 

from concurrent.futures import ThreadPoolExecutor

import tornado.ioloop
import Queue
import pdb

def download_finish(resp):
    print resp 
    print "finish"

def download(url):
    http_client = AsyncHTTPClient()
    http_client.fetch(url, callback = download_finish)

url = "http://10.11.78.64/"
download(url)
#loop = tornado.ioloop.IOLoop.current()
#loop.start()

 

上面并不会启动异步的客户端进行下载,你需要取消掉注释,启动loop  

 

 

http://blog.chinaunix.net/uid-26000296-id-4699999.html

前言

刚开始使用用tornado时都很迷惑:tornado不是标榜异步非阻塞解决10K问题的嘛?
但为什么我在handler中有一个步骤是耗时时,整体的响应就慢下了呢?是不是torando根本就不好用。其实是你用错了,因为你没有将你的耗时任务使用tornado的异步功能实现。

下面就各种torndo响应请求,进行耗时任务处理时的各种异步场景一一总结

一、异步HTTP请求的实现

tornado是一个异步web framework,说是异步,是因为tornado server与client的网络交互是异步的,底层基于io event loop。  

所有的异步思想都是一致的,python是基于linux c实现的,所以python的loop也就对外提供了定时器,添加异步任务等之类的接口。
但是如果client请求server处理的handler里面有一个阻塞的耗时操作,那么整体的server性能就会下降。
比如: 访问一个耗时的网站请求 www.douban.com/search, 这个结果要在5秒后才返回值。 
当我访问的话,肯定是要等5秒钟,这时候,要是有别的客户要连接的别的页面,不堵塞的页面,你猜他能马上显示吗?不能的。。。 他也是要等当前这个5秒延迟过后,才能访问的。

幸运的是,tornado提供了一套异步机制,方便我们实现自己的异步操作。
当handler处理需要进行其余的网络操作的时候,tornado提供了一个AsyncHTTPClient来支持异步。
示例代码如下:

import json
import tornado.web
import tornado.gen

def MainHandler(tornado.web.RequestHandler):
     def initialize(self):  
         ... 


     @tornado.web.asynchronous
     @tornado.gen.coroutine
     def get(self):
         url = "www.douban.com/search/"
         data = {}
         data['title'] = 'fury'
         ...
         client = tornado.httpclient.AsyncHTTPClient()
         response = yield client.fetch(url, method=POST, body=json.dumps(data))
         self.on_response(response)

     def on_response(self, resp):
         body = json.loads(resp.body)
         if body == None:
             self.write('error')
         else:
             self.write(body)
         return

注意:
1. 只在每个响应处理的函数(如get, post, put,delete)前加修饰器@tornado.web.asynchronous 和  @tornado.gen.coroutine, 不要在初始函数 initialize(self)前加修饰器,不然会造成各种程序错误
2. 在 on_response(self, resp)回调函数中,如果使用了 self.wirte()或self.render()函数,就不要使用 self.finish()函数, 不然,会报异常:
       raise RuntimeError("finish() called twice.  May be caused "
       RuntimeError: finish() called twice.  May be caused by using async operations without the @asynchronous decorator.
   如果没有用self.wirte()或self.render()函数,就要使用self.finish()函数

上面的例子,主要有几个变化:
.  使用asynchronous decorator,它主要设置_auto_finish为false,   这样handler的get函数返回的时候tornado就不会关闭与client的连接
.  使用AsyncHttpClient,fetch的时候提供callback函数,  这样当fetch http请求完成的时候才会去调用on_response,而不会阻塞。
.  on_response调用完成之后通过finish结束与client的连接

==================================================== 

高性能服务器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
#其中-n代表请求数,-c代表并发数

Server Software:        TornadoServer/4.3
Server Hostname:        127.0.0.1
Server Port:            5000

Document Path:          /sync
Document Length:        5 bytes

Concurrency Level:      5
Time taken for tests:   5.076 seconds
Complete requests:      5
Failed requests:        0
Total transferred:      985 bytes
HTML transferred:       25 bytes
Requests 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:          /async
Document Length:        5 bytes

Concurrency Level:      5
Time taken for tests:   0.009 seconds
Complete requests:      5
Failed requests:        0
Total transferred:      985 bytes
HTML transferred:       25 bytes
Requests 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循环,把可以把耗时的任务放到后台异步计算,请求可以接着做别的计算。

偏函数:functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

 

可是,经常有一些耗时的任务完成之后,我们需要返回其计算的结果。此时这种方式就不行了。车道山前必有路,只需要切换一异步方式即可。下面使用协程来改写

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.3
Server Hostname:        127.0.0.1
Server Port:            5000

Document Path:          /async/task
Document Length:        5 bytes

Concurrency Level:      5
Time taken for tests:   0.049 seconds
Complete requests:      5
Failed requests:        0
Total transferred:      985 bytes
HTML transferred:       25 bytes
Requests 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 ThreadPoolExecutor       #线程池

class 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:          /future
Document Length:        5 bytes

Concurrency Level:      5
Time taken for tests:   0.003 seconds
Complete requests:      5
Failed requests:        0
Total transferred:      995 bytes
HTML transferred:       25 bytes
Requests 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._instance


class 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:      5
Time taken for tests:   0.043 seconds
Complete requests:      5
Failed requests:        0
Total transferred:      960 bytes
HTML transferred:       0 bytes
Requests 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上传文件与转发请求。

 

========================

http://blog.willdx.me/2017/05/25/Tornado-%E5%8D%8F%E7%A8%8B-%E5%BC%82%E6%AD%A5-%E9%9D%9E%E9%98%BB%E5%A1%9EIO/#

在 Tornado 中两个装饰器:

tornado.web.asynchronous: 让请求变成长连接的方式,必须手动调用 self.finish() 才会响应
tornado.gen.coroutine: 改请求为协程模式,说明白点就是能使用 yield 配合 Tornado 编写异步程序


Tornado 的协程异步函数有两个特点:

1.需要使用 coroutine 装饰器
2.返回值需要使用 raise gen.Return() 当做异常抛出 
注释: 返回值作为异常抛出是因为在 Python 3.2 之前生成器是不允许有返回值的

 

import time
import logging
import tornado.ioloop
import tornado.web
import tornado.options
from tornado import gen
tornado.options.parse_command_line()
class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        self.write("Hello, world")
        self.finish()
class NoBlockingHnadler(tornado.web.RequestHandler):
    @gen.coroutine
    def get(self):
        yield gen.sleep(10)
        self.write('Blocking Request')
class BlockingHnadler(tornado.web.RequestHandler):
    def get(self):
        time.sleep(10)
        self.write('Blocking Request')
def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
        (r"/block", BlockingHnadler),
        (r"/noblock", NoBlockingHnadler),
    ], autoreload=True)
if __name__ == "__main__":
    app = make_app()
    app.listen(8000)
    tornado.ioloop.IOLoop.current().start()

当我们使用 yield gen.sleep(10) 这个异步的 sleep 时候其他请求是不阻塞的:

当使用 time.sleep(10) 时候会阻塞其他的请求, 因为time.sleep方法并不支持Tornado的异步方式; 所以这种实现异步的方式有一定的局限性;

注释:
这里的异步非阻塞是针对另一请求来说的,本次的请求该是阻塞的仍然是阻塞的
gen.coroutine 在 Tornado 3.1 后会自动调用 self.finish() 结束请求,可以不使用asynchronous 装饰器