tornado异步非阻塞测试
测试不同的异步实现方式:协程+第三方库;线程池
测试工具
最近使用tornado 6.1版本进行服务开发,毕竟纸上得来终觉浅,于是针对tornado异步非阻塞的功能进行了详细的验证和测试,并在测试过程中发现了关于压测工具ab的一个特别有意思的事情。
如果想获得有关异步非阻塞的测试结果,请忽略ab的测试结果。因为ab的测试结果不准,以脚本测试的结果为准~
在本次测试中,使用了两种工具测试tornado是否真正的实现了并发。
一个是ab,安装方式为: yum -y install httpd-tools
。
另一个为自己写的脚本代码。代码如下,具体逻辑为统计一次请求的总耗时,即发送请求到结果返回这一段时间。并使用shell脚本运行两次,模拟两个并发请求。
import time
t1 = time.time()
cmd = "curl http://127.0.0.1:50000/api/predict"
process = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr = subprocess.STDOUT)
res = process.stdout.read().decode('utf-8')
t2 = time.time()
print("cost time:",(t2-t1))
使用shell脚本运行上述py文件两次:
#!/bin/bash
for i in 1 2
do
nohup python test.py >log${i}.log 2>&1 &
done
shell脚本运行 nohup
这一行代码极快,可以认为这两次请求是同时发生的,即对服务产生了2次并发请求。
下面将按照同步阻塞,异步阻塞,异步非阻塞,多线程这四个模块进行测试。
同步阻塞
同步阻塞完成一个请求,再去完成另一个请求。假设并发数为2,那么第二个 请求就必须等第一个请求完成,才能轮到自己执行业务逻辑。第二个任务的耗时应该为第一个耗时的二倍,这几乎没有悬念。具体看测试结果
测试代码:
# 同步阻塞代码
class PredictHandler(RequestHandler):
def get(self, *args, **kwargs):
try:
# 耗时函数
time.sleep(10)
self.write("done!")
except BaseException as e:
self.write("error")
ab测试结果:
# ab -c 2 -n 2 http...
# 并发数量为2,可以看到总耗时为20s,二者是串行执行
ab -c 2 -n 2 http://127.0.0.1:50000/api/predict
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient).....done
Server Software: TornadoServer/6.1
Server Hostname: 127.0.0.1
Server Port: 50000
Document Path: /api/predict
Document Length: 5 bytes
Concurrency Level: 2
Time taken for tests: 20.026 seconds
Complete requests: 2
Failed requests: 0
Write errors: 0
Total transferred: 394 bytes
HTML transferred: 10 bytes
Requests per second: 0.10 [#/sec] (mean)
Time per request: 20025.896 [ms] (mean)
Time per request: 10012.948 [ms] (mean, across all concurrent requests)
Transfer rate: 0.02 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 10013 10013 0.1 10013 10013
Waiting: 10013 10013 0.1 10013 10013
Total: 10013 10013 0.1 10013 10013
脚本评测结果:
![](https://img2023.cnblogs.com/blog/1950727/202301/1950727-20230109212238606-7991114.png)
上述左侧是tornado内部计时器,可以看到业务逻辑的执行时长;右侧一栏是两次并发请求各自的总耗时。
由上面可以看出同步阻塞的tornado服务无法实现并发执行,一次只能执行一个请求,如果有多个请求只能排队。
异步阻塞——协程
异步就一定可以并发执行吗?并不是,得具体看异步执行的方法。在协程实现异步的方法中,要求耗时函数为非阻塞函数。如果是非阻塞函数,tornado服务性能等同于同步阻塞。
首先看一个下载音频的例子,使用requrests下载音频,并将音频保存到本地。
import requests
class PredictHandler(RequestHandler):
@tornado.gen.coroutine
def get(self, *args, **kwargs):
try:
audio_url = "...wav"
r = yield requests.get(audio_url)
with open("test1.wav","wb") as f:
f.write(r.content)
self.write("done!\n")
except BaseException as e:
self.write("error\n")
由于requrests是阻塞函数,所以不会并发处理请求
![](https://img2023.cnblogs.com/blog/1950727/202301/1950727-20230109212324933-1982830683.png)
左侧第一次请求耗时为66ms左右,右侧第一次请求总耗时为79ms左右;
右侧第二次请求总耗时为129ms左右,为两次请求的耗时总和。说明第二次请求有一部分时间在等第一个请求结束,也就是在排队。异步阻塞实际上是同步阻塞。
再来看一个更明显的例子,在这个例子里面,程序休眠10s,总耗时在10s左右
import requests
class PredictHandler(RequestHandler):
@tornado.gen.coroutine
def get(self, *args, **kwargs):
try:
# 耗时函数
yield time.sleep(10)
self.write("done!\n")
except BaseException as e:
self.write("error\n")
ab测试结果:
# ab -c 2 -n 2 http://
Server Software: TornadoServer/6.1
Server Hostname: 127.0.0.1
Server Port: 50000
Document Path: /api/predict
Document Length: 5 bytes
Concurrency Level: 2
Time taken for tests: 20.028 seconds
Complete requests: 2
Failed requests: 0
Write errors: 0
Total transferred: 394 bytes
HTML transferred: 10 bytes
Requests per second: 0.10 [#/sec] (mean)
Time per request: 20028.276 [ms] (mean)
Time per request: 10014.138 [ms] (mean, across all concurrent requests)
Transfer rate: 0.02 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 0
Processing: 10013 10014 1.2 10015 10015
Waiting: 10013 10014 1.2 10015 10015
Total: 10013 10014 1.1 10015 10015
脚本测试结果:
![](https://img2023.cnblogs.com/blog/1950727/202301/1950727-20230109212505638-1589734067.png)
可以看到仍然是排队进行处理。因此如果使用协程实现了异步,并不一定就能获得异步非阻塞的性能,得需要看阻塞函数是不是异步的。下面将对异步非阻塞函数进行测试
异步非阻塞
- 异步非阻塞不需要返回结果
看一个样例,程序休眠10s,不需要返回结果的异步非阻塞
class PredictHandler(RequestHandler):
@tornado.gen.coroutine
def get(self, *args, **kwargs):
try:
audio_url = self.get_argument("url","")
# 异步非阻塞操作
yield gen.sleep(10)
self.write("done!")
except BaseException as e:
self.write("error")
ab测试结果,可以看到总耗时0.006s,几乎立马就返回了结果
Server Software: TornadoServer/6.1
Server Hostname: 127.0.0.1
Server Port: 50000
Document Path: /api/predict
Document Length: 5 bytes
Concurrency Level: 2
Time taken for tests: 0.006 seconds
Complete requests: 2
Failed requests: 0
Write errors: 0
Total transferred: 394 bytes
HTML transferred: 10 bytes
Requests per second: 318.27 [#/sec] (mean)
Time per request: 6.284 [ms] (mean)
Time per request: 3.142 [ms] (mean, across all concurrent requests)
Transfer rate: 61.23 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 2 3 1.5 4 4
Waiting: 2 3 1.4 4 4
Total: 2 3 1.5 4 4
ab的总耗时极其短,小于1s。
脚本测试结果:
每个请求的返回结果均为10s,这里没有把图片贴上来(忘记保存了_)
脚本测试的结果说明,两次请求耗时相同,均为10s左右,可以推出服务实现了并发。
这里ab的测试结果与脚本测试的结果便出现了分歧,可能是由于ab测试原理与脚本测试原理不同导致。
- 异步非阻塞需要返回结果
这里的测试用例业务逻辑为:异步非阻塞下载文件。
测试过程中发现异步下载的文件不能过大,120M的文件便无法下载。
测试代码:
class PredictHandler(RequestHandler):
@tornado.gen.coroutine
def get(self, *args, **kwargs):
try:
http_client = AsyncHTTPClient()
audio_url = ""
response = yield http_client.fetch(audio_url)
# 根据返回结果保存音频
with open("test.wav","wb") as f:
f.write(response.body)
self.write("done!\n")
except BaseException as e:
self.write("error\n")
ab测试结果:
# ab -c 2 -n 2 htt...
Server Software: TornadoServer/6.1
Server Hostname: 127.0.0.1
Server Port: 50000
Document Path: /api/predict
Document Length: 6 bytes
Concurrency Level: 2
Time taken for tests: 0.147 seconds
Complete requests: 2
Failed requests: 0
Write errors: 0
Total transferred: 396 bytes
HTML transferred: 12 bytes
Requests per second: 13.57 [#/sec] (mean)
Time per request: 147.349 [ms] (mean)
Time per request: 73.675 [ms] (mean, across all concurrent requests)
Transfer rate: 2.62 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 66 74 10.6 81 81
Waiting: 66 73 10.6 81 81
Total: 66 74 10.7 81 81
tornado内部计时器:
![](https://img2023.cnblogs.com/blog/1950727/202301/1950727-20230109212535560-1532125293.png)
从ab测试的结果可以看出,两个请求并发的总耗时为两个请求的总和。
如果单从ab这个结果,可以看出tornado对于返回结果和不返回结果的处理时间是不一样的。并且执行过程仍然是阻塞的,但真的是这样吗?
那么接下来看一下,脚本执行的结果:
![](https://img2023.cnblogs.com/blog/1950727/202301/1950727-20230109212602370-2145707557.png)
同样左侧是tornado的内部计时函数,右侧是脚本的计时函数。
可以看出左侧两次下载分别耗时55ms和100ms,右侧的耗时为70ms和112ms。考虑到发送请求和接收请求这两个时间也会有时间延迟,大约为10-20ms左右(可以从同步阻塞中的计时函数服务中推断出来~)。可以看出协程实现的异步非阻塞实现了真正的并发。
可以看出,由于ab内部的实现原理(大概率)的原因,导致与实际测试的结果不一致。本人更倾向于脚本测试的结果,因为这跟实际调用服务时的情况是完全一致的呀~
在线程池实现的异步操作中,ab也表现出了 ‘不可思议的结果’。
线程池的异步非阻塞
在线程池中的业务逻辑为:阻塞函数休眠10s。在异步阻塞函数的测试中可以知道,这种情况下跟同步阻塞是一样的,并没有实现真正的异步。那在线程池的实现中会怎么样呢?
测试代码:
class PredictHandler(RequestHandler):
executor = ThreadPoolExecutor(2)
@tornado.gen.coroutine
def get(self, *args, **kwargs):
try:
r=yield self.main_process(audio_url)
self.write("done!\n")
except BaseException as e:
self.write("error\n")
@tornado.concurrent.run_on_executor
def main_process(self,audio_url):
time.sleep(10)
return 0
ab测试结果:
# ab -c 2 -n 2
Server Software: TornadoServer/6.1
Server Hostname: 127.0.0.1
Server Port: 50000
Document Path: /api/predict
Document Length: 6 bytes
Concurrency Level: 2
Time taken for tests: 20.027 seconds
Complete requests: 2
Failed requests: 0
Write errors: 0
Total transferred: 396 bytes
HTML transferred: 12 bytes
Requests per second: 0.10 [#/sec] (mean)
Time per request: 20027.190 [ms] (mean)
Time per request: 10013.595 [ms] (mean, across all concurrent requests)
Transfer rate: 0.02 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 10013 10014 0.4 10014 10014
Waiting: 10013 10013 0.3 10014 10014
Total: 10013 10014 0.4 10014 10014
ERROR: The median and mean for the waiting time are more than twice the standard
deviation apart. These results are NOT reliable.
在ab测试结果中,我们可以看到测试总耗时为20.027s,相当于排队处理请求,没有实现并发。
而在脚本测试中表现除了截然相反的结果。
脚本测试结果:
![](https://img2023.cnblogs.com/blog/1950727/202301/1950727-20230109212627274-1906181117.png)
可以看到多线程的方式,也真正实现了异步非阻塞,tornado可以实现并发处理。
在多线程测试中可以验证两件事情:
- 由脚本测试的耗时可以推出:程序发送请求,以及服务发送结果到接收这两部分的时间延迟大约在10-30ms左右;
- 本次测试可以看出,ab确实有问题。由于本人时间有限,至于到底有什么问题,希望有知道的小伙伴可以告知~
Tornado 结合 Celery
太高级了,还用不到。
总结
简单总结一下:
- tornado实现异步的方式主要有两种:协程+第三方异步aio库,线程池的方式,还有Celery;至于其原理,建议移步:另一篇博客。