tornado 异步 mysql_Tornado 异步性能分析

Tornado 异步性能分析

Tornado 是一个 Python web 框架和异步网络库,起初由 FriendFeed 开发。通过使用非阻塞网络 I/O,Tornado 可以支撑上万级的连接,处理长连接, WebSockets ,和其他需要与每个用户保持长久连接的应用。

本文介绍如何正确的使用 tornado 异步特性以及对其进行压力测试, 查看 tornado 的性能到底有多强悍。

本文使用 tornado 5.0.2 版本。

一、最简单的脚手架

创建 server.py 文件,写入以下代码

# -*- coding: utf-8 -*-

import tornado.ioloop

import tornado.web

class Index(tornado.web.RequestHandler):

def get(self):

self.write("hello world")

def make_app():

return tornado.web.Application([

(r"/", Index)

])

if __name__ == '__main__':

app = make_app()

app.listen(8888)

tornado.ioloop.IOLoop.current().start()

最简单的代码,可以先将服务启动. 在浏览器中输入 http://127.0.0.1:8888 即可看到 hello world 页面

3554b45bd8dbc481b018efc915b3f676.png

我使用 jmeter 进行压测,可以达到 1200,可以说是非常不错

d4a16e9f114048d9b132520faf187f77.png

二、同步的困扰

但是在开发过程中并不是所有的接口都什么都不做就返回个简单的字符串或者 json, 更多的时候是进行一些数据库的操作或者本地的 IO 操作之后再返回相应的数据。

我们知道,程序在进行 IO 操作时,无论是网络 IO 或者文件的读写 IO,与内存计算相比,耗时是非常大的,此时我们如果模拟一个比较耗时的操作,那么这个接口 QPS 还会那么好吗?

class Index(tornado.web.RequestHandler):

def get(self):

name = self.get_argument('name', 'get')

time.sleep(0.5)

self.write("hello {}".format(name))

我们在 get 请求的时候加入了 time.sleep(0.5) 来模拟接口处理数据的耗时操作

我们在 chrome 的开发者工具中看到这个请求耗时 503ms,

055068c1463f3ba10e06b6ea3b5b6ffc.png

我们再使用 jmeter 来进行压测, 居然降到惊人的 2, 之前还 1200,现在却降到了 2, 很悲催

90614335c5f23bdeb4b0f93bb7b4a992.png

响应时间最短 503, 最长 50240, 50 秒钟才将结果返回给用户, 这种响应速度的网站用户早就跑光了

d1358139d4424ea847dce3cdc0b91353.png

三、为什么同步的性能会这么差?

在 python 中 time.sleep() 函数是个阻塞函数, 会阻塞 python 的运行, 使用 tornado 做 web 项目,你不可能只为一个用户服务,当有多个用户访问同一个接口时,python 在处理 sleep 操作时会阻塞运行,这时后来的请求也要等待前的请求,只有当前面的请求结束以后再处理,这也就是为什么上面的压测最短是 503ms,最长却需要 50240ms 的原因.因为大家是阻塞进行的. 不光 time.sleep 是这样的阻塞函数, python 中绝大多数函数都是这样的阻塞函数,包括文件 IO 操作, 网络请求操作等都是阻塞函数,所以我们需要异步的处理网络请求。

四、异步的解决方案

4.1 使用多线程

谈到异步,我们首先应该想到的是用多线程来处理,将耗时的操作放到多线程中处理, 修改上面的代码

class Index(tornado.web.RequestHandler):

def get(self):

name = self.get_argument('name', 'get')

t = threading.Thread(target=time.sleep, args=(0.5,))

t.start()

self.write("hello {}".format(name))

这种多线程方案可以实现不阻塞,但是缺点也很明显,一个是每个请求都开启一个线程,对于资源来说很浪费,线程的开销也是很浪费服务器资源的

第二是,我们并没有获取在函数的结果,比如说 target 函数是一个数据库的查询操作,此时如果我们想要获取到数据库的值是获取不到的。

4.2 使用线程池来执行耗时操作 from tornado.concurrent import run_on_executor

class Index(tornado.web.RequestHandler):

executor = ThreadPoolExecutor(1)

@tornado.web.asynchronous

@tornado.gen.coroutine

def get(self):

name = self.get_argument('name', 'get')

rst = yield self.work(name)

self.write("hello {}".format(rst))

self.finish()

def post(self):

name = self.get_argument('name', 'post')

self.write("hello {}".format(name))

@run_on_executor

def work(self, name):

time.sleep(0.5)

return "{} world".format(name)

这里在 Index 类中初始化了一个线程池, executor, 这有一个线程, 将耗时的操作放到 work 函数中, 并且使用 @run_on_executor 装饰器进行装饰, 这时我们就可以获取到耗时操作的返回值。

压测为 500 个用户在 5 秒内请求

5433a0d2827c69541ba680c28333c9bb.png

但是我们看下压测结果

697193374ef12bafd33de0aca0939510.png

依然很糟糕, 每秒也就能处理 2 个请求. 而且最长的请求需要 7 万多 ms, 这也是无法忍受的。

每个请求需要耗时 0.5 秒,现在有一个线程池,它里面有一个线程,也就是说,同时最多也就处理一个请求,我们可以加大线程线中的数量, executor = ThreadPoolExecutor(5) 改为 5 个再进行压测,这时看到是有所增长,可以到达 10, 也依然不是很理想, 最大影响有 4 万多 ms

d8bbbca9835b70f229a257ab77257542.png

既然放到线程中执行与不放到线程中执行,每个请求都需要耗时 0.5 秒,那么放到线程中操作有什么意义呢?

意义在于, 一个 web 项目, 不太可能只提供一个接口, 假设你有 3 个接口, /user, /info, /case , 其中两个接口 ( /user, /case ) 都使用异步处理,放到了线程池中操作, 只有一个接口 ( /info ) 没有,而是直接使用 time.sleep(0.5) 秒, 那么当用户先访问阻塞函数接口 /info 以后, 此时 web 程序处理全局阻塞中,它会卡其它所有请求,即使这时你访问已经异步处理的接口 /user ,也会因为有这样的一个阻塞接口没有处理好而处于阻塞状态。

所以在 web 项目开发过程中,同一个团队不同开发人员在开发接口过程中,不要做那个坑队友的人

很显然, 即使用到的线程池,并且将线程池中的线程设置为 5, 每秒也只能处理 10 个请求,这样的 QPS 也是令人相当着急,一个成熟的系统访问人数成千上万都很正常,接下来我们使用 asyncio 库来异步处理请求

4.3 使用异步处理库 asyncio

很显然, 即使用到的线程池,并且将线程池中的线程设置为 5, 每秒也只能处理 10 个请求,这样的 QPS 也是令人相当着急,一个成熟的系统访问人数成千上万都很正常,接下来我们使用 asyncio 库来异步处理请求。

import asyncio

class Index(tornado.web.RequestHandler):

async def get(self):

name = self.get_argument('name', 'get')

rst = await self.work(name)

self.write("hello {}".format(rst))

self.finish()

def post(self):

name = self.get_argument('name', 'post')

self.write("hello {}".format(name))

async def work(self, name):

await asyncio.sleep(0.5)

return "{} world".format(name)

if __name__ == '__main__':

try:

app = make_app()

app.listen(8888)

tornado.ioloop.IOLoop.current().start()

except:

print(traceback.format_exc())

tornado.ioloop.IOLoop.current().start()

这里我们将 time.sleep 函数换成了 asyncio.sleep 异步的函数, 并且在函数定义 def 将加上 async , 在获取 work 结果时使用了 await 关键词

此时的压测结果为

ee14c65e74deed12a746813944594291.png

可以看到,在 5 秒内就全部执行结束,QPS 可以达到 91, 和之前的 10 或者 2 有着非常大的提高,由于只有 500 个用户, 我们可以模拟更多的用户来访问,使用 1000 个用户来进行压测看下效果。

依然是在 5 秒内完成了请求,并且 QPS 上升到了 181, 继续加大请求用户量为 2000

bc177c8f6c6fe3466a074e60045706c0.png

依然强悍, QPS 继续上升到 305,而且仅多用了 1 秒钟.如果再继续提高用户量,我们来看看它的极限在哪里?

f96d143f05bf803e60c74ac897b3db54.png

4000 的时候,服务端会报 ValueError: too many file descriptors in select() 这个是由于在 windows 中 IOloop 使用的是 selector, 它有 1024 个文件描述符的限制所导致的, 将其放到 linux 中我们看看可以达到多少。

6000 的时候还没有问题,如果 7000 的话会出现异常,这时已经比在多线程中运行性能提高的非常多了,如果考虑用户的增加,就应该考虑水平扩展了,通过负载均衡来解决了。

c940572725a0e87d514a082fa5e282af.png

五、总结

通过上文的对比,我们可以理解,不是因为你使用了 tornado 你的网站性能就能提升多少,而是要正确的使用相应的异步库,只有正确的使用异步特性,才能发挥 tornado 的高性能。

以下列出常用的异步库

功能

同步库

异步库

mysql

PyMySQL

aiomysql

mongodb

pymonogo

motor

redis

redis

aioredis

http

requests

aiohttp

github 上有个组织在维护一些常用的异步库,https://github.com/aio-libs 常用的库都实现的异步, 大家在使用时可以先搜索一下。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值