FastAPI经常被提到的一个特点就是支持异步。
但实际用的时候,很多人容易产生一个误解:
只要把def改成async def,程序就会自动变快。
其实不是这样。
异步真正有价值的地方,是处理大量I/O等待。
什么是I/O等待
一个API请求可能需要:
调用第三方API
↓
等网络
↓
返回结果或者:
查询数据库
↓
等数据库
↓
返回结果等待期间CPU实际上没多少事要做。
如果程序能在等待的时候处理其他请求,就能提高资源利用率。
async def
FastAPI可以直接定义异步接口:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {
"message": "hello"
}如果里面调用的也是异步API:
@app.get("/data")
async def get_data():
result = await some_async_request()
return result那整个调用链就能保持异步。
多个请求可以并发
比如要请求三个互不依赖的服务:
a = await get_a()
b = await get_b()
c = await get_c()这里实际上是:
get_a
↓
等待
get_b
↓
等待
get_c
↓
等待如果三个请求可以同时发,那更适合:
import asyncio
a, b, c = await asyncio.gather(
get_a(),
get_b(),
get_c()
)这样三个任务可以并发执行。
并发不等于无限制并发
这里有个很容易踩的坑。
比如有10,000个URL:
await asyncio.gather(
*[fetch(url) for url in urls]
)理论上看起来很漂亮。
但实际上可能出现:
连接数暴涨
内存上涨
目标服务器压力过大
本地文件描述符耗尽
请求大量失败所以实际项目里通常要限制并发数量。
比如用Semaphore:
semaphore = asyncio.Semaphore(10)
async def fetch_with_limit(url):
async with semaphore:
return await fetch(url)这样最多同时处理10个任务。
我在学校做的“我的珠科小程序”项目的后端其实就是大量爬取学校教务系统、OA系统的各种信息后整合,小程序用户的一个请求→我们后端并发几个请求到学校系统(doge
async函数里别执行长时间同步操作
比如:
@app.get("/test")
async def test():
time.sleep(10)
return {"ok": True}这里虽然函数是:
async def但:
time.sleep()仍然是阻塞操作。
它会影响事件循环。
如果确实要执行同步阻塞任务,需要考虑线程池或者其他后台任务方案。
HTTP客户端也需要支持异步
如果FastAPI本身是异步接口,但里面用的是同步HTTP客户端:
requests.get(...)那网络等待仍然会把当前流程卡住。
如果需要异步请求,可以选支持异步的HTTP客户端。
比如:
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(url)这样请求链路就能保持异步。
数据库也一样
如果API用异步数据库驱动,那么:
FastAPI
↓
async database
↓
await整个过程更符合异步模型。
但如果:
FastAPI async
↓
同步数据库驱动那异步优势可能会被同步I/O抵消掉。
什么情况下不需要async
并不是所有FastAPI接口都必须写:
async def如果接口本身:
- 计算非常简单
- 没有异步I/O
- 用同步库
- 业务逻辑很简单
那普通:
def也完全够用。
没必要为了"看着现代"把所有代码都改成异步。
我对FastAPI异步的理解
真正值得理解的不是:
async
await这两个关键字。
而是:
什么时候需要等?
等的时候能不能干别的?
调用链是不是保持异步?
要不要限制并发?如果只是:
async def然后里面全是同步阻塞操作,那实际上并没有获得真正的异步收益。
FastAPI的异步能力真正适合的是:
API
↓
网络请求
↓
数据库
↓
Redis
↓
第三方服务这种大量I/O的应用。
理解这一点之后,再去设计FastAPI服务时就不会简单地把"async"当成性能开关,而是会根据实际业务来选择同步还是异步。