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

脚本评测结果:

上述左侧是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是阻塞函数,所以不会并发处理请求

左侧第一次请求耗时为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

脚本测试结果:

可以看到仍然是排队进行处理。因此如果使用协程实现了异步,并不一定就能获得异步非阻塞的性能,得需要看阻塞函数是不是异步的。下面将对异步非阻塞函数进行测试

异步非阻塞

  • 异步非阻塞不需要返回结果

看一个样例,程序休眠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内部计时器:

从ab测试的结果可以看出,两个请求并发的总耗时为两个请求的总和。

如果单从ab这个结果,可以看出tornado对于返回结果和不返回结果的处理时间是不一样的。并且执行过程仍然是阻塞的,但真的是这样吗?

那么接下来看一下,脚本执行的结果:

同样左侧是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,相当于排队处理请求,没有实现并发。

而在脚本测试中表现除了截然相反的结果。

脚本测试结果:

可以看到多线程的方式,也真正实现了异步非阻塞,tornado可以实现并发处理。

在多线程测试中可以验证两件事情:

  1. 由脚本测试的耗时可以推出:程序发送请求,以及服务发送结果到接收这两部分的时间延迟大约在10-30ms左右;
  2. 本次测试可以看出,ab确实有问题。由于本人时间有限,至于到底有什么问题,希望有知道的小伙伴可以告知~

Tornado 结合 Celery

太高级了,还用不到。

总结

简单总结一下:

  1. tornado实现异步的方式主要有两种:协程+第三方异步aio库,线程池的方式,还有Celery;至于其原理,建议移步:另一篇博客
posted @ 2023-01-09 21:28  快乐的拉格朗日  阅读(51)  评论(0编辑  收藏  举报