共计 5953 个字符,预计需要花费 15 分钟才能阅读完成。
前言
目前用到的都是同步请求。当遇到查数据库等耗时操作的时候,请求会一直被阻塞。
实验
我们使用 sql 语句 cmds = "select sleep(1)"
来模拟长时间的数据库查询。
使用 siege 做压测。
另外,还可以用 ab 压测:ab -c 10 -n 10 http://10.9.40.173:8888/asynctask?t=1
同步 V.S. Tornado-MySQL 异步
只贴出 handler 部分的代码。
其中 IndexHandler 为同步请求,路由为‘/’
AsynHandler 为异步请求,路由为‘/asyn’
cmds = "select sleep(1)" | |
MYSQL_POOL = pools.Pool(dict(host=host, port=port, user=usr, passwd=passwd, db=db) , | |
max_idle_connections=5, max_open_connections=10) | |
class IndexHandler(tornado.web.RequestHandler): | |
def get(self): | |
conn = MySQLdb.connect(host, usr, passwd, db, port) | |
cur = conn.cursor() | |
cur.execute(cmds) | |
rep = cur.fetchall() | |
self.write({'res': rep}) | |
class AsynHandler(tornado.web.RequestHandler): | |
@tornado.web.asynchronous | |
@tornado.gen.coroutine | |
def get(self): | |
cur = yield MYSQL_POOL.execute(cmds) | |
self.write({'res': cur.fetchall()}) | |
self.finish() |
在同步的情况下,浏览器发起请求后会被阻塞,一直等到上一条查询完了,才能接受下一个请求。假设数据库请求耗时 1s,同时发起调用的话,第 1 个请求会在大概 1s 后得到响应,第 2 个请求会在大概 2s 后得到响应。。。
在异步的情况下,则不需要等待数据库查询结束,可以在得到结果前接收下一个请求。假设数据库请求耗时 1s,同时发起调用的话,理想情况下所有请求都会在 1s 后得到响应。
我们参照官网使用工具 siege 进行测试
模拟 10 个用户同时发出 1 次请求:
同步请求的测试结果是:
siege 10.9.40.173:8888 -c10 -r1 | |
** SIEGE 3.0.8 | |
** Preparing 10 concurrent users for battle. | |
The server is now under siege.. done. | |
Transactions: 10 hits | |
Availability: 100.00 % | |
Elapsed time: 10.06 secs | |
Data transferred: 0.00 MB | |
Response time: 5.12 secs | |
Transaction rate: 0.99 trans/sec | |
Throughput: 0.00 MB/sec | |
Concurrency: 5.09 | |
Successful transactions: 10 | |
Failed transactions: 0 | |
Longest transaction: 10.05 | |
Shortest transaction: 1.00 |
总共用时 10.06 secs,平均响应时间是 5.12 secs,跟我们预想的一样,最快的响应是 1 secs(第一个请求),最慢的响应是 10 secs(最后一个请求)。平均耗时大概是(1+2+3+…+10)/10=5.5 secs
而异步请求的测试结果是:
siege 10.9.40.173:8888/asyn -c10 -r1 | |
** SIEGE 3.0.8 | |
** Preparing 10 concurrent users for battle. | |
The server is now under siege.. done. | |
Transactions: 10 hits | |
Availability: 100.00 % | |
Elapsed time: 2.04 secs | |
Data transferred: 0.00 MB | |
Response time: 1.03 secs | |
Transaction rate: 4.90 trans/sec | |
Throughput: 0.00 MB/sec | |
Concurrency: 5.04 | |
Successful transactions: 10 | |
Failed transactions: 0 | |
Longest transaction: 1.06 | |
Shortest transaction: 1.01 |
总共用时 2.04 secs,平均响应时间是 1.03 secs,达到了异步请求的理论速度。最快的响应是 1.01 secs,最慢的响应也才 1.06 secs。
不需要返回结果的异步请求
class AsynPingNoResHandler(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.baidu.co$ | |
# do something others | |
self.finish('It works') | |
@tornado.gen.coroutine | |
def ping(self, url): | |
os.system("ping -c 2 {}".format(url)) | |
return 'after' |
需要返回结果的异步请求
再来一个利用 tornado.gen.coroutine
的例子,我参考伯乐在线的文章做的实验,但是没有得到文章的结果:
class AsyncTaskHandler(tornado.web.RequestHandler): | |
@tornado.web.asynchronous | |
@tornado.gen.coroutine | |
def get(self): | |
t = self.get_argument('t') | |
response = yield tornado.gen.Task(self.query, t) | |
#print 'response', response | |
self.write({'res': response}) | |
self.finish() | |
@tornado.gen.coroutine | |
def query(self, t): | |
conn = MySQLdb.connect(host, usr, passwd, db, port) | |
cur = conn.cursor() | |
cur.execute(cmdsn % t) | |
rep = cur.fetchall() | |
return rep |
这个代码的测试结果是:
siege 10.9.40.173:8888/asynctask?t=1 -c10 -r1 | |
** SIEGE 3.0.8 | |
** Preparing 10 concurrent users for battle. | |
The server is now under siege.. done. | |
Transactions: 10 hits | |
Availability: 100.00 % | |
Elapsed time: 10.07 secs | |
Data transferred: 0.00 MB | |
Response time: 9.45 secs | |
Transaction rate: 0.99 trans/sec | |
Throughput: 0.00 MB/sec | |
Concurrency: 9.39 | |
Successful transactions: 10 | |
Failed transactions: 0 | |
Longest transaction: 10.06 | |
Shortest transaction: 9.05 |
不知道为啥,这种代码的表现是,10 个并发全部查询完才有一次性返回。可以说这个代码根本没有达到异步的效果。甚至比同步还差!
同步是 1s 的时候返回第 1 个请求,2s 的时候返回第 2 个请求…
这种方式,在最后第 10s 的时候一次性返回 10 个请求,每个请求的响应时间都是 10s。求高手指点一下这是为啥。
伯乐在线的文章的作者解释:
使用 yield task 的方式,准确来说是协程。只是在网络 IO 中 tornado 的主线程不至于被 handler 里的代码 block,单个 handler 还是被网络耗时任务所 block 的。如果并发连接比较小的情况下,很有可能其性能还不如同步的写法好。
具体以你自己的测试结果为准吧,在底层上,tornado 这样的做法在 IO 层面上是异步的。
个人认为,使用协程跳到另一个函数去执行 ping/sql 操作,就阻塞在另一个函数里了,只是没有阻塞在 handler 的 get 中,对于整个程序而言,那还是被 block 了。。
fetch
如果 tornado.gen.coroutine
如此不堪的话,那它还有什么用?参照官网的例子,官网例子是用来 fetch 的。
同步 fetch
class OfficialSynHandler(tornado.web.RequestHandler): | |
def get(self): | |
print '# in get', datetime.datetime.now() | |
client = tornado.httpclient.HTTPClient() | |
response = client.fetch("http://www.csdn.net/") | |
print 'response', response | |
self.write('ok') |
siege 测试结果
siege 10.9.40.173:8888/officialsyn -c10 -r1 | |
** SIEGE 3.0.8 | |
** Preparing 10 concurrent users for battle. | |
The server is now under siege.. done. | |
Transactions: 10 hits | |
Availability: 100.00 % | |
Elapsed time: 2.05 secs | |
Data transferred: 0.00 MB | |
Response time: 0.51 secs | |
Transaction rate: 4.88 trans/sec | |
Throughput: 0.00 MB/sec | |
Concurrency: 2.47 | |
Successful transactions: 10 | |
Failed transactions: 0 | |
Longest transaction: 1.03 | |
Shortest transaction: 0.15 |
ab 压测
Document Path: /officialsyn/ | |
Document Length: 306 bytes | |
Concurrency Level: 10 | |
Time taken for tests: 0.016 seconds | |
Complete requests: 10 | |
Failed requests: 0 | |
Non-2xx responses: 10 | |
Total transferred: 4450 bytes | |
HTML transferred: 3060 bytes | |
Requests per second: 626.25 [#/sec] (mean) | |
Time per request: 15.968 [ms] (mean) | |
Time per request: 1.597 [ms] (mean, across all concurrent requests) | |
Transfer rate: 272.15 [Kbytes/sec] received |
异步 fetch
class OfficialAsynHandler(tornado.web.RequestHandler): | |
def get(self): | |
print '# in get', datetime.datetime.now() | |
client = tornado.httpclient.AsyncHTTPClient() | |
response = yield tornado.gen.Task(client.fetch, "www.csdn.net") | |
print 'response', response | |
self.write('ok') |
siege 测试结果
siege 10.9.40.173:8888/officialasyn -c10 -r1 | |
** SIEGE 3.0.8 | |
** Preparing 10 concurrent users for battle. | |
The server is now under siege.. done. | |
Transactions: 10 hits | |
Availability: 100.00 % | |
Elapsed time: 1.03 secs | |
Data transferred: 0.00 MB | |
Response time: 0.01 secs | |
Transaction rate: 9.71 trans/sec | |
Throughput: 0.00 MB/sec | |
Concurrency: 0.06 | |
Successful transactions: 10 | |
Failed transactions: 0 | |
Longest transaction: 0.02 | |
Shortest transaction: 0.00 |
ab 压测
Document Path: /officialasyn/ | |
Document Length: 306 bytes | |
Concurrency Level: 10 | |
Time taken for tests: 0.016 seconds | |
Complete requests: 10 | |
Failed requests: 0 | |
Non-2xx responses: 10 | |
Total transferred: 4450 bytes | |
HTML transferred: 3060 bytes | |
Requests per second: 640.78 [#/sec] (mean) | |
Time per request: 15.606 [ms] (mean) | |
Time per request: 1.561 [ms] (mean, across all concurrent requests) | |
Transfer rate: 278.46 [Kbytes/sec] received |
可见,使用异步 fetch 的话每个请求都响应得比较快,平均响应时间短。
个人总结
如果要在 tornado 中实现非阻塞的 mysql 查询,不要自己折腾去写 yield/coroutine/gen 这些异步模块,直接用 Tornado-MySQL 就好了。还有可以考虑用 celery 等任务队列。
如果是不需要返回结果的异步操作,通过 tornado 的 IO 循环,把可以把耗时的任务放到后台异步计算,请求可以接着做别的计算。
如果是 fetch 的话使用 yield 异步模块可以有较好的效果,减少了平均响应时间,因此可以用来做 REST API 的通信。
本文永久更新链接地址:http://www.linuxidc.com/Linux/2017-02/140855.htm
