用Python Asyncio加快API端点速度的方法

511 阅读8分钟

如何使用Python Asyncio

作为一个开发者,你希望你编写的API尽可能快。那么,如果我告诉你,通过一个技巧,你可能会将你的API的速度提高2倍、3倍,甚至4倍呢?在这篇文章中,你将学习如何利用PythonasyncioHTTPX库和Flask微框架来优化你的API的某些部分。

在本教程中,你将:

  1. 使用流行的Python框架编写一个小型HTTP API。Flask
  2. 使用HTTPX,这是一个很棒的现代Python HTTP客户端,支持异步。
  3. 熟悉Pythonasyncio库的一个小子集。

请抓紧时间,因为你即将使用Pythonasyncio 来加速你的Flask API端点 !

目录

一个应用,两个端点

为了开始这项壮举,你将写一个简单的Flask应用,有两个端点。一个将是异步的,另一个则不是。

注意:为了获得最大的兼容性,请确保你使用的是Python3.8或更新版本。

首先,创建一个目录来存放你的代码,并在其中创建一个虚拟环境。

mkdir asyncapi
cd asyncapi
python3 -m venv env/

激活虚拟环境并安装支持async的Flask。

source env/bin/activate
python -m pip install "Flask[async]"

接下来,将以下代码放在一个名为app.py 的文件中。

from flask import Flask

app = Flask(__name__)

@app.route('/get_data')
def get_data():
    return 'ok'

@app.route('/async_get_data')
async def async_get_data():
    return 'ok'

现在你已经有了一个完全工作的Flask API,有两个端点。

请注意,第二个端点,/async_get_data ,使用async def 语法来定义它的方法。不过,这个端点的功能与普通的/get_data 端点完全相同,只是你可以在其中运行异步代码。然而,按照现在的写法,它的速度并不快。你可以通过运行我们的API并使用cURL进行一些调用来证明这一点。

启动Flask开发服务器。

flask run

在你的开发服务器启动并运行后,你可以在一个新的终端中使用time ,以显示
,向两个端点发出的cURL请求的执行时间是差不多的。

注意:根据你使用的操作系统和shell,下面的命令输出可能看起来有所不同。只要你有time 命令,你就应该能看到这个结果。

首先,测量同步端点。

time curl "http://localhost:5000/get_data"
ok
________________________________________________________
Executed in    7.28 millis    fish           external
   usr time    5.86 millis  248.00 micros    5.61 millis
   sys time    0.03 millis   32.00 micros    0.00 millis

注意那行写着 "在7.28毫秒内执行"。那是相当快的。

现在再次使用time 来测量异步端点。

time curl "http://localhost:5000/async_get_data"
ok
________________________________________________________
Executed in   21.77 millis    fish           external
   usr time    2.48 millis    0.00 micros    2.48 millis
   sys time    2.77 millis  293.00 micros    2.48 millis

你是否惊讶地发现,异步版本的速度更慢?

异步请求比同步请求多花了大约3倍的时间!7毫秒和21毫秒之间的差异对人的眼睛来说并不明显。这仍然是一个很好的证明,使用Pythonasyncio,可能会有开销,所以并不是在所有情况下都更快。

两个端点,一个快,一个慢

为了看到async_get_data 端点变得比它的同步端点快,你必须让端点实际做一些工作。API的一个常见情况是,它们需要调用其他API,例如,从第三方服务中获取特定地点的天气信息。

你可以使用httpxFlash的组合向你的API添加HTTP请求,这是一种故意返回缓慢HTTP响应的服务。为什么慢?因为你希望能够模拟大型和或缓慢的外部API,这将有助于夸大使用asyncio 的效果。

首先,安装httpx 库。

python -m pip install httpx

修改app.py ,使其看起来像这样。

from flask import Flask
import asyncio
import httpx

app = Flask(__name__)

@app.route('/get_data')
def get_data():
    response_1 = httpx.get('https://flash.siwalik.in/delay/1000/')
    response_2 = httpx.get('https://flash.siwalik.in/delay/1000/')

    return {
        'response_1': response_1.status_code,
        'response_2': response_2.status_code
    }

@app.route('/async_get_data')
async def async_get_data():
    async with httpx.AsyncClient() as client:
        coroutine_1 = client.get('https://flash.siwalik.in/delay/1000/')
        coroutine_2 = client.get('https://flash.siwalik.in/delay/1000/')
        results = await asyncio.gather(coroutine_1, coroutine_2)

        return {
            'response_1': results[0].status_code,
            'response_2': results[1].status_code
        }

现在两个端点都向flash.siwalik.in/delay/1000/ 发出两个GET请求一秒钟后返回一个简单的响应。第一个函数get_data() 对于使用过Pythonrequests 库的人来说应该很熟悉。response_1 包含第一次API调用的结果,response_2 包含第二次API调用的结果。然后该方法返回每个响应的状态代码。

第二个函数,async_get_data() ,看起来有点不同,尽管最终结果是一样的。按部就班,这就是正在发生的事情。

async with httpx.AsyncClient() as client:

该代码创建了一个异步上下文管理器,使client 对象可用。这和普通的上下文管理器是一样的,只是它允许执行异步代码。client 是进行实际的HTTP调用。

接下来,你将两个变量分配给调用client.get() 对Flash API的两个API调用的结果。

coroutine_1 = client.get('https://flash.siwalik.in/delay/1000/')
coroutine_2 = client.get('https://flash.siwalik.in/delay/1000/')

起初你可能会认为API调用的结果会被存储在这些变量中,但实际上coroutine_1coroutine_2 是联合程序,而不是HTTP响应。

results = await asyncio.gather(coroutine_1, coroutine_2)

现在,神奇的异步发生了。在这里,asyncio.gather() 的返回值被分配到result 变量中,并被等待着。当你看到await 关键字时,这意味着代码将在这里阻塞执行,直到对一个联合程序的调用完成。.gather() 方法本身就是一个联合程序,它将同时执行其他联合程序的序列(如[coroutine_1, coroutine_2] ),然后返回一个结果列表。

最后,该方法通过访问结果数组中的每个HTTP响应的状态代码来返回。

return {
    'response_1': results[0].status_code,
    'response_2': results[1].status_code
}

调用get_data()async_get_data() 应该会得到完全相同的结果,但async_get_data() 会完成得更快。你认为它的完成速度会快多少?

对结果进行计时

现在你有一个API,有两个做同样事情的端点,只是一个是异步的,一个不是,你应该回到使用cURL来计时。

**注意:**在这一点上,你可能需要重新启动Flask开发服务器来接收你的变化。

get_data

time curl "http://localhost:5000/get_data"
{"response_1":200,"response_2":200}

________________________________________________________
Executed in    3.56 secs      fish           external
   usr time    0.29 millis  295.00 micros    0.00 millis
   sys time    5.00 millis   48.00 micros    4.96 millis

你可以看到,两个响应都返回了HTTP 200,而你的端点总共花了大约3.5秒来返回。这是有道理的:Flash提供的外部端点每个暂停了一秒钟,额外的1.5秒的其他开销可以在DNS查询、TCP连接、缓慢的Comcast互联网和其他互联网相关的面条中计算出来。

接下来,尝试对async_get_data 终端进行计时。

time curl "http://localhost:5000/async_get_data"
{"response_1":200,"response_2":200}

________________________________________________________
Executed in    1.81 secs      fish           external
   usr time    6.21 millis  342.00 micros    5.87 millis
   sys time    0.06 millis   59.00 micros    0.00 millis

如果在上一节中,你猜测异步版本的速度大约是两倍,你是正确的!为什么?为什么呢?因为Flask并发地运行了对外部API的调用。这意味着,在这种情况下,从Flash API中检索结果的整个行为只和最慢的调用一样慢。

事实上,你可以尝试给每个方法添加第三个调用。第一个端点将花费大约1.5秒的时间,而异步版本仍将在大约1.8秒内执行。你可以继续向异步版本添加HTTP调用,它应该继续以大致相同的时间返回,直到你开始遇到各种硬件、网络和操作系统层面的限制。

Python Asyncio的一个真实用例

HTTP调用并不是Python asyncio能够发挥作用的唯一地方。事实上,你可以在Python模块的名称中看到其他的用例:asyncio 代表异步输入/输出。

许多API是由数据库支持的,从数据库服务器获取SQL查询的结果往往受到I/O的约束。你可以把我们应用中对Flash API的调用改为对数据库的调用。想象一下,这个数据库调用需要500毫秒的时间来返回。

同步端点get_data ,大约需要1.5秒来返回结果:

  • 1秒的HTTP调用

  • 0.5秒的数据库调用异步端点async_get_data ,大约需要1秒来返回结果。

  • HTTP调用1秒

  • 0.5秒的数据库调用

尽管单个调用花费的时间相同,但它们都是并发执行的。这意味着总的时间只相当于最慢的操作返回结果的时间。你刚刚通过使用asyncio ,节省了0.5秒!

请记住,如果你需要一个异步调用的结果用于下一个异步调用,这将没有什么帮助。在这种情况下,你只能在第一个调用完成后开始运行第二个调用,这将使你的总时间回到同步执行时间。

总结

在这篇文章中,你学到了Pythonasyncio 如何在你的代码等待多个输入/输出实例的情况下大大加快你的应用程序。你还了解到asyncio 是如何有效和容易地与Flask网络框架和HTTPX库一起使用的。

asyncio 这并不总是能让你的API更快,但在某些情况下,它能带来巨大的变化,就像本教程中所展示的那样。在未来编写代码时,请记住你在这里学到的东西,你可能会轻松获得一些性能上的胜利!