Inside FastAPI · بخش 1/1
Englishوقتی یک درخواست HTTP به FastAPI میرسد چه اتفاقی میافتد؟
یک درخواست GET از سوکت و سرور ASGI و روتر و گراف وابستگیها و سریالسازی میگذرد. این مقاله نشان میدهد هر لایه چه کاری انجام میدهد و کجا میتواند شکست بخورد.
با دستور uvicorn app.main:app --port 8000 سرویس را بالا میآورید و با curl localhost:8000/items/42 پاسخ JSON را میگیرید. بین این دو دستور، چند مؤلفه بهترتیب کار میکنند. بیشتر غافلگیریهای محیط تولید در سرویسهای FastAPI از اینجا شروع میشود که نمیدانیم کدام لایه مالک کدام بخش از این مسیر است.
سوکت در اختیار سرور است، نه فریمورک
کلاینت اتصال TCP باز میکند و Uvicorn آن را میپذیرد. Uvicorn خط درخواست و هدرها و بدنه را طبق قواعد HTTP/1.1 تجزیه میکند و سپس scope را میسازد: دیکشنریای شامل متد، مسیر، query string، هدرها، آدرس کلاینت و مشخصات پروتکل. FastAPI نه سوکت میبیند و نه HTTP را تجزیه میکند. همین جدایی است که اجازه میدهد همان شیء اپلیکیشن روی Uvicorn یا Hypercorn یا سرور ASGI دیگری اجرا شود.
مرز ASGI
ASGI یک فراخوانی سهجزئی تعریف میکند: اپلیکیشن scope را میگیرد، یک تابع async به نام receive برای رویدادهای ورودی، و یک تابع async به نام send برای رویدادهای خروجی. برای درخواست HTTP، سرور با receive پیامهای http.request را تحویل میدهد و اپلیکیشن با send پیامهای http.response.start و یک یا چند http.response.body را میفرستد. middlewareها همین رابط را میپوشانند؛ middlewareی که بدنه را میخواند باید آن را برای لایه بعدی برگرداند.
مسیریابی کار Starlette است
دکوراتور @app.get در نهایت یک APIRoute را در روتر Starlette ثبت میکند. روتر فهرست مسیرها را به ترتیب طی میکند و مسیر را تطبیق میدهد. مسیر ثابت باید پیش از مسیر پارامتری که با آن هم مطابقت دارد تعریف شود، چون اولین تطابق برنده است. پس از تطابق، Starlette handler مربوط را صدا میزند؛ برای APIRoute این handler همان handler درخواست FastAPI است که حل وابستگی و اعتبارسنجی ورودی را انجام میدهد.
وابستگیها پیش از تابع شما اجرا میشوند
امضای اندپوینت هنگام ثبت مسیر بررسی میشود. هر Depends(...) یک گره در درخت وابستگی میشود، از جمله وابستگیهای تودرتو. در هر درخواست FastAPI این درخت را حل میکند، ورودیهای مسیر و query و هدر و بدنه را اعتبارسنجی میکند و تنها بعد از آن اندپوینت شما را صدا میزند. مقاله بعدی این مجموعه همین درخت را با جزئیات دنبال میکند.
from fastapi import Depends, FastAPI
app = FastAPI()
def get_settings() -> dict[str, str]:
return {"region": "eu-west-1"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, settings: dict = Depends(get_settings)):
return {"item_id": item_id, "region": settings["region"]}
اندپوینت، سریالسازی و پاسخ
اندپوینت async def مستقیماً روی event loop اجرا میشود. اندپوینت def در یک thread از thread pool محدود اجرا میشود، چون ممکن است blocking باشد. خروجی در صورت تعریف response_model در برابر آن اعتبارسنجی میشود، به داده سازگار با JSON تبدیل میشود و کلاس پاسخ آن را رندر میکند. در پایان اپلیکیشن send را صدا میزند و Uvicorn بایتها را روی سوکت مینویسد.
| مرحله | مالک | خطای معمول |
|---|---|---|
| پذیرش و تجزیه | Uvicorn | درخواست بدشکل، هدر بیش از حد بزرگ |
| تطبیق مسیر | روتر Starlette | 404، ترتیب اشتباه مسیرها |
| وابستگیها | FastAPI | 401، 422، timeout استخر اتصال |
| اندپوینت | کد شما | استثنا، فراخوانی blocking |
| سریالسازی | Pydantic | خطای اعتبارسنجی پاسخ |
زیر بار چه تغییری میکند
این مراحل بهتنهایی ارزاناند و در کنار هم میتوانند گران شوند. یک فراخوانی blocking داخل اندپوینت async def همه درخواستهای آن event loop را متوقف میکند، چون loop تا پایان آن فراخوانی نمیتواند کار دیگری را زمانبندی کند. اندپوینت def این مشکل را ندارد، اما برای thread pool محدود رقابت میکند. وابستگیای که session پایگاه داده را بدون بستن باز میکند، مشکل تأخیر را زیر بار همزمانی به مشکل کمبود اتصال تبدیل میکند.
چه چیزی را با خود ببریم
مسیر درخواست زنجیرهای از قراردادهای باریک است: HTTP به ASGI، ASGI به روتر، روتر به گراف وابستگی، گراف به اندپوینت و اندپوینت به serializer. وقتی چیزی خراب میشود، اول مرحله را پیدا کنید و بعد کد را تغییر دهید. بیشتر گزارشهای «FastAPI کند است» در واقع blocking در event loop، اشباع thread pool یا وابستگیای است که بیشتر از نامش کار میکند.