python高性能服务器编写,Tornado的高性能服务器开发常用方法

最近一直开发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的方式很优雅,写法可以异步逻辑同步写,快是快了一些,但也会损失一定的性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值