1. 安装 uvloop 和 httptools
默认情况下,Uvicorn 不自带 uvloop 和 httptools,但它们比默认的 asyncio 事件循环和 HTTP 解析器更快。你可以使用以下命令安装它们:
pip install uvloop httptools
如果环境中已安装,Uvicorn 将自动使用它们。
警告
uvloop 无法在 Windows 上安装。如果您在本地使用 Windows,但在生产环境中使用 Linux,可以使用环境标记来避免在 Windows 上安装 uvloop,例如 uvloop; sys_platform != 'win32'.
- 小心使用non-async functions
在 FastAPI 中使用非同步函数会影响性能。因此,最好使用异步函数。这种影响来自于 FastAPI 将调用 run_in_threadpool,该函数将使用线程池运行。
注意事项
在内部,run_in_threadpool 将使用
anyio.to_thread.run_sync在线程池中运行函数。
提示
线程池中只有 40 个可用线程。如果您使用了所有线程,您的应用程序将被阻塞。
要更改可用线程数,可以使用以下代码:
import anyio
from contextlib import asynccontextmanager
from typing import Iterator
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI) -> Iterator[None]:
limiter = anyio.to_thread.current_default_thread_limiter()
limiter.total_tokens = 100
yield
app = FastAPI(lifespan=lifespan)
您可以在 AnyIO's documentation.中阅读更多相关信息
3. 在 WebSocket 上使用 async for 代替 while True
多数示例都使用 while True 从 WebSocket 读取信息。
我认为使用这种较难看的符号主要是因为 Starlette 文档很久没有显示 async for 符号。
避免使用 while True:
from fastapi import FastAPI
from starlette.websockets import WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
您可以使用 async 来表示:
from fastapi import FastAPI
from starlette.websockets import WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
await websocket.accept()
async for data in websocket.iter_text():
await websocket.send_text(f"Message text was: {data}")
阅读更多相关信息 Starlette documentation.
4. 忽略 WebSocketDisconnect 异常
如果使用 while True ,则需要捕获 WebSocketDisconnect。async for 将为您捕获它。
from fastapi import FastAPI
from starlette.websockets import WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
except WebSocketDisconnect:
pass
如果需要在 WebSocket 断开连接时释放资源,可以使用该异常来完成。
如果使用的是旧版本的 FastAPI,只有接收方法会引发 WebSocketDisconnect 异常。发送方法不会引发该异常。在最新版本中,所有方法都会引发该异常。在这种情况下,您需要在 try 代码块中添加发送方法。
5. 使用 HTTPX 的 AsyncClient 代替 TestClient
由于您要在应用程序中使用异步函数,因此使用 HTTPX 的 AsyncClient 而不是 Starlette 的 TestClient 会更方便。
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"Hello": "World"}
# Using TestClient
from starlette.testclient import TestClient
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"Hello": "World"}
# Using AsyncClient
import anyio
from httpx import AsyncClient, ASGITransport
async def main():
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/")
assert response.status_code == 200
assert response.json() == {"Hello": "World"}
anyio.run(main)
如果使用生命周期事件(on_startup、on_shutdown 或 lifespan 参数),可以使用 asgi-lifespan软件包来运行这些事件。
from contextlib import asynccontextmanager
from typing import AsyncIterator
import anyio
from asgi_lifespan import LifespanManager
from httpx import AsyncClient, ASGITransport
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
print("Starting app")
yield
print("Stopping app")
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def read_root():
return {"Hello": "World"}
async def main():
async with LifespanManager(app, lifespan) as manager:
async with AsyncClient(transport=ASGITransport(app=manager.app)) as client:
response = await client.get("/")
assert response.status_code == 200
assert response.json() == {"Hello": "World"}
anyio.run(main)
6. 使用lifespan state 而不是 app.state
不久前,FastAPI 开始支持生命周期状态(lifespan state),它定义了一种管理对象的标准方法,这些对象需要在启动时创建,并需要在请求-响应循环中使用。
不建议再使用 app.state。您应该改用生命周期状态。
使用 app.state,您可以这样做
from contextlib import asynccontextmanager
from typing import AsyncIterator
from fastapi import FastAPI, Request
from httpx import AsyncClient
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
async with AsyncClient(app=app) as client:
app.state.client = client
yield
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def read_root(request: Request):
client = request.app.state.client
response = await client.get("/")
return response.json()
使用 lifespan state,,可以这样做
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any, TypedDict, cast
from fastapi import FastAPI, Request
from httpx import AsyncClient
class State(TypedDict):
client: AsyncClient
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[State]:
async with AsyncClient(app=app) as client:
yield {"client": client}
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def read_root(request: Request) -> dict[str, Any]:
client = cast(AsyncClient, request.state.client)
response = await client.get("/")
return response.json()
7. 启用 AsyncIO 调试模式
如果想找到阻塞事件循环的端点,可以启用 AsyncIO 调试模式。
启用该模式后,当任务执行时间超过 100ms 时,Python 将打印一条警告信息。
使用 PYTHONASYNCIODEBUG=1 python main.py 运行以下代码:
import os
import time
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
time.sleep(1) # Blocking call
return {"Hello": "World"}
if __name__ == "__main__":
uvicorn.run(app, loop="uvloop")
如果调用端点,会看到如下信息:
INFO: Started server process [19319]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: 127.0.0.1:50036 - "GET / HTTP/1.1" 200 OK
Executing <Task finished name='Task-3' coro=<RequestResponseCycle.run_asgi() done, defined at /uvicorn/uvicorn/protocols/http/httptools_impl.py:408> result=None created at /uvicorn/uvicorn/protocols/http/httptools_impl.py:291> took 1.009 seconds
8. 实施真正 ASGI 中间件,而不是 BaseHTTPMiddleware
BaseHTTPMiddleware 是在 FastAPI 中创建中间件的最简单方法。
注意事项
@app.middleware(“http”) 装饰器是 BaseHTTPMiddleware 的封装器。
BaseHTTPMiddleware 曾经存在一些问题,但最新版本已经修复了大部分问题。尽管如此,使用它仍然会带来性能损失。
要避免性能损失,可以使用真正 ASGI 中间件。缺点是实现起来比较复杂。
请查看 Starlette 文档,了解如何实现真正 ASGI 中间件。 Pure ASGI middleware.
9. 您的依赖项可能在线程上运行
如果函数是非同步的,而你将其作为依赖项使用,那么它将在一个线程中运行。
在下面的示例中,http_client 函数将在一个线程中运行:
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from httpx import AsyncClient
from fastapi import FastAPI, Request, Depends
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, AsyncClient]]:
async with AsyncClient() as client:
yield {"client": client}
app = FastAPI(lifespan=lifespan)
def http_client(request: Request) -> AsyncClient:
return request.state.client
@app.get("/")
async def read_root(client: AsyncClient = Depends(http_client)):
return await client.get("/")
要在事件循环中运行,需要将函数设置为异步:
# ...
async def http_client(request: Request) -> AsyncClient:
return request.state.client
# ...
作为读者的练习,让我们进一步了解如何检查运行中的线程。
你可以用 python main.py 运行下面的代码:
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import anyio
from anyio.to_thread import current_default_thread_limiter
from httpx import AsyncClient
from fastapi import FastAPI, Request, Depends
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, AsyncClient]]:
async with AsyncClient() as client:
yield {"client": client}
app = FastAPI(lifespan=lifespan)
# Change this function to be async, and rerun this application.
def http_client(request: Request) -> AsyncClient:
return request.state.client
@app.get("/")
async def read_root(client: AsyncClient = Depends(http_client)): ...
async def monitor_thread_limiter():
limiter = current_default_thread_limiter()
threads_in_use = limiter.borrowed_tokens
while True:
if threads_in_use != limiter.borrowed_tokens:
print(f"Threads in use: {limiter.borrowed_tokens}")
threads_in_use = limiter.borrowed_tokens
await anyio.sleep(0)
if __name__ == "__main__":
import uvicorn
config = uvicorn.Config(app="main:app")
server = uvicorn.Server(config)
async def main():
async with anyio.create_task_group() as tg:
tg.start_soon(monitor_thread_limiter)
await server.serve()
anyio.run(main)
如果调用端点,会看到如下信息:
❯ python main.py
INFO: Started server process [23966]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Threads in use: 1
INFO: 127.0.0.1:57848 - "GET / HTTP/1.1" 200 OK
Threads in use: 0
将 def http_client 替换为 async def http_client,然后重新运行应用程序。你将不会看到 Threads in use: 因为函数是在事件循环中运行的。