Tornado的高性能服务器开发常用方法

11 篇文章 0 订阅
9 篇文章 1 订阅

最近一直开发AI人脸识别相关的项目,需要提供给客户一些服务,所以我需要开发一些服务端程序。由于AI算法都是用python3写的,所以我就索性用起了python开发服务端,毕竟速度也快,以前用过Flask、Django,这次决定有Tornado来做,对该框架做了一系列的调用,痴迷于他的异步非阻塞的功能,项目开发完之后有了一些经验,特此对以前的资料查询做一个总结,以便后面可以复用。

高性能源于Tornado基于Epoll(unix为kqueue)的异步网络IO。因为tornado的单线程机制,一不小心就容易写出阻塞服务[block]的代码。不但没有性能提高,反而会让性能急剧下降。因此,探索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')

此时耗时动作将严重阻塞系统的性能,导致并发量很小,因为处理一个请求的时间就好几秒。

一、我们将以上代码改成异步的,使用回调函数

from tornado.ioloop import IOLoop
class AsyncHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    @tornado.gen.coroutine
    def get(self, *args, **kwargs):
        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 www.google.com")
        return 'after'

 

这种写法就使耗时的任务在后台运行了,从而显著提高并发,但是此时,我们有两个知识点需要了解:

1、装饰器

@tornado.web.asynchronous

@tornado.gen.coroutine

两个问题:

为什么要使用这两个装饰器?

为什么要先用asynchronous在用coroutine呢?或着说为什么要用这种调用顺序?

这两个装饰器的作用:

1.1、@tornado.web.asynchronous

首先我们要明白同步和异步的作用

同步的情况下,web请求到来之后必须处理完成之后在返回,这是一个阻塞的过程。也就是说当一个请求被处理时,服务器进程会被挂起直至请求完成。而这会影响服务器的并发能力。

异步的情况下,web服务器进程在等待请求处理的时候,会将IO循环打开,继续来接受请求。而拿到处理结果之后会调用回调函数,将结果返回。这要既不影响处理请求,也不影响接受请求,能够显著的提升并发能力。

我们必须要明白,在同步的情况下,web服务进程,接受请求,处理请求,然后返回结果,最后自己来关闭连接。这个关闭的动作是自动的。

而异步的情况下,因为在处理一个请求的时候还没有的到结果,所以需要保持连接的打开,最后返回结果之后,关闭连接,这个关闭动作必须要手动关闭。也就是必须手动调用self.finish.

tornado中使用@tornado.web.asynchronous装饰器作用是保持连接一直开启,

上面的例子中使用的回调函数的缺点是,可能引起回调深渊,系统将难以维护,比如回调中调用回调等。

 

因为实现异步需要保持连接一直打开,而不能在handler执行完毕的时候关掉。

所以总的来说,@tornado.web.asynchronous的作用就是:把http连接变成长连接,直到调用self.finish,连接都在等待状态。

1.2、@tornado.gen.coroutine

这个函数的作用就是简化异步编程,让代码的编写更像同步代码,同时实现的确实异步的。这样避免了写回调函数。而且使用的是协程的方式来来实现异步编程。最新版的tornado,其实不一定需要写@tornado.web.asynchronous。

 

1.3、顺序

@asynchronous会监听@gen.coroutine的返回结果(Future),并在@gen.coroutine装饰的代码段执行完成后自动调用finish。从Tornado 3.1版本开始,只使用@gen.coroutine就可以了。

 

2、函数

IOLoop.instance().add_timeout()

functools.partial()

 

2.1、IOLoop.instance().add_timeout()

首先我们需要了解IOLoop,以及IOLoop.instance()也就是实例化动作。

IOLoop 是基于 epoll 实现的底层网络I/O的核心调度模块,用于处理 socket 相关的连接、响应、异步读写等网络事件。每个 Tornado 进程都会初始化一个全局唯一的 IOLoop 实例,在 IOLoop 中通过静态方法 instance() 进行封装,获取 IOLoop 实例直接调用此方法即可。

Tornado 服务器启动时会创建监听 socket,并将 socket 的 file descriptor 注册到 IOLoop 实例中,IOLoop 添加对 socket 的IOLoop.READ 事件监听并传入回调处理函数。当某个 socket 通过 accept 接受连接请求后调用注册的回调函数进行读写。接下来主要分析IOLoop 对 epoll 的封装和 I/O 调度具体实现。

epoll是Linux内核中实现的一种可扩展的I/O事件通知机制,是对POISX系统中 select 和 poll 的替代,具有更高的性能和扩展性,FreeBSD中类似的实现是kqueue。Tornado中基于Python C扩展实现的的epoll模块(或kqueue)对epoll(kqueue)的使用进行了封装,使得IOLoop对象可以通过相应的事件处理机制对I/O进行调度。

IOLoop模块对网络事件类型的封装与epoll一致,分为READ / WRITE / ERROR三类。

functools模块用于高阶函数:作用于或返回其他函数的函数。一般而言,任何可调用对象都可以作为本模块用途的函数来处理。

functools.partial返回的是一个可调用的partial对象,使用方法是partial(func,*args,**kw),func是必须要传入的,而且至少需要一个args或是kw参数。

在这里就是添加一个回调函数的partial对象。

 

上面的这种写法不能获取返回值。需要获取返回值需要使用yield挂起函数,并根据函数的return获取返回值。

二、带返回值的,同时使用协程来实现

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'

可以看到结果值也被返回了。有时候这种协程处理,未必就比同步快。在并发量很小的情况下,IO本身拉开的差距并不大。甚至协程和同步性能差不多。但是在大并发量的情况下就不一样了,因为并发请求很多,越来越多的请求如果被耗时的处理阻塞,将会长时间得不到结果。

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

想要返回值也很容易。再切换一下使用方式接口。使用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'

具体使用何种方式,更多的依赖业务,不需要返回值的往往需要处理callback,回调太多容易出错,当然如果需要很多回调嵌套,首先优化的应该是业务或产品逻辑。yield的方式很优雅,写法可以异步逻辑同步写,快是快了一些,但也会损失一定的性能。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值