FastAPI 基礎概念:ASGI vs WSGI
簡介
在 Web 框架的世界裡,ASGI(Asynchronous Server Gateway Interface) 與 WSGI(Web Server Gateway Interface) 是兩種最常被提及的介面規範。它們決定了 Python 程式如何與底層的 HTTP 伺服器溝通,直接影響到效能、可擴充性以及開發體驗。
對於想要使用 FastAPI(一個以 ASGI 為核心的現代 API 框架)的開發者而言,了解 ASGI 與 WSGI 的差異不只是理論,而是選擇部署方式、寫程式碼風格、以及未來可否輕鬆加入 WebSocket、背景工作等功能的關鍵。
本文將以 淺顯易懂 的方式說明兩者的概念、實作差異,並提供實用的程式碼範例,協助初學者與中級開發者快速掌握 為什麼 FastAPI 需要 ASGI,以及在什麼情境下仍可能會接觸到 WSGI。
核心概念
1. 什麼是 WSGI?
WSGI 是由 PEP 3333 定義的同步介面,最初設計用來讓 Python Web 應用(如 Django、Flask)與 傳統的 HTTP 伺服器(如 gunicorn、uWSGI)對接。
它的工作流程可以簡化為:
- 伺服器 接收到 HTTP 請求,產生一個
environ(字典)與start_response回呼函式。 - 應用程式 依序處理請求,返回一個可迭代的 bytes 序列作為回應。
- 伺服器 把這些 bytes 送回客戶端。
WSGI 完全是 同步 的,也就是說每一次請求都會佔用一條執行緒或工作程序,直到回應完成。
2. 什麼是 ASGI?
ASGI 是在 WSGI 基礎上延伸出來的 非同步 介面(PEP 3333 的後繼者),支援 同步 與 非同步 兩種模式,同時能處理 HTTP、WebSocket、Server‑Sent Events 等多種協定。
其核心概念:
- Scope:描述連線類型(http、websocket)與相關資訊(method、path、headers)。
- Receive:非同步函式,用來接收客戶端傳來的事件(如訊息、關閉)。
- Send:非同步函式,用來向客戶端發送事件(如
http.response.start、http.response.body)。
ASGI 的設計讓單一執行緒(或單一事件迴圈)即可同時處理大量併發請求,特別適合 I/O 密集(資料庫、外部 API)或 即時通訊(WebSocket)的情境。
3. 同步 vs 非同步:為什麼會影響效能?
| 項目 | WSGI(同步) | ASGI(非同步) |
|---|---|---|
| 請求處理方式 | 每條執行緒/工作程序一次只能處理一個請求 | 單一事件迴圈可同時處理多個 I/O 待命的請求 |
| CPU 使用率 | 高併發時需大量執行緒,CPU 會被切換耗費資源 | 事件驅動,CPU 只在真正計算時使用 |
| 支援協定 | 僅 HTTP | HTTP、WebSocket、SSE 等 |
| 典型框架 | Flask、Django(傳統) | FastAPI、Starlette、Django(ASGI 版) |
在 CPU 密集 的計算任務下,兩者差異不大;但在 I/O 密集(如大量 DB 查詢、外部 API 呼叫)時,ASGI 能顯著降低延遲與資源消耗。
4. FastAPI 為何選擇 ASGI?
FastAPI 的設計目標是 高效能、開發友好,同時支援 自動產生 OpenAPI 文件。使用 ASGI 可以:
- 直接使用
async def端點,讓開發者寫出 非同步程式碼 而不需額外封裝。 - 輕鬆加入 WebSocket 路由,打造即時聊天或推播服務。
- 在同一個應用中混用同步與非同步函式,框架會自動在事件迴圈與執行緒池之間切換。
程式碼範例
以下示範三個常見的比較情境,分別使用 WSGI(Flask)與 ASGI(FastAPI)實作相同的功能,讓你直觀感受差異。
範例 1:單純的 GET API
# Flask (WSGI) - 同步寫法
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/ping")
def ping():
# 同步執行,直接回傳
return jsonify({"msg": "pong"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
# FastAPI (ASGI) - 支援 async
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/ping")
async def ping():
# 即使是 async,也可以寫同步程式碼
return JSONResponse(content={"msg": "pong"})
重點:在 FastAPI 中即使不需要非同步,仍可使用
async def,框架會自動適配。
範例 2:呼叫外部 API(I/O 密集)
# Flask 同步版(會阻塞整個執行緒)
import requests
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/weather")
def weather():
r = requests.get("https://api.example.com/today")
data = r.json()
return jsonify(data)
# FastAPI 非同步版(不阻塞事件迴圈)
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/weather")
async def weather():
async with httpx.AsyncClient() as client:
r = await client.get("https://api.example.com/today")
data = r.json()
return data
使用 httpx.AsyncClient 讓 I/O 在等待回應時釋放 CPU,提升併發效能。
範例 3:WebSocket 即時聊天
# Flask (WSGI) 無法原生支援 WebSocket,需要額外套件
# 這裡示範使用 Flask‑SocketIO(仍基於 WSGI)
from flask import Flask
from flask_socketio import SocketIO, emit
app = Flask(__name__)
socketio = SocketIO(app)
@socketio.on("message")
def handle_message(msg):
emit("reply", {"echo": msg})
if __name__ == "__main__":
socketio.run(app)
# FastAPI 原生支援 WebSocket(ASGI)
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws/chat")
async def chat_socket(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Echo: {data}")
關鍵差異:FastAPI 只需一行程式碼即可開啟 WebSocket,而 Flask 必須依賴額外套件且仍受 WSGI 限制。
範例 4:混用同步與非同步函式
# FastAPI 允許同時存在 sync 與 async
from fastapi import FastAPI
import time
app = FastAPI()
@app.get("/sync")
def sync_endpoint():
time.sleep(2) # 阻塞 2 秒
return {"msg": "sync done"}
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(2) # 非阻塞 2 秒
return {"msg": "async done"}
在同一個應用中,FastAPI 會自動把同步端點交給執行緒池,避免阻塞事件迴圈。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 |
|---|---|---|
| 在 ASGI 程式中使用阻塞函式 | 直接呼叫 requests.get()、time.sleep() 會阻塞整個事件迴圈,導致所有連線卡住。 |
改用 httpx.AsyncClient、asyncio.sleep,或將阻塞程式包在 run_in_threadpool 中。 |
忽略 Scope 的差異 |
處理 WebSocket 時忘了 await websocket.accept(),導致客戶端連線失敗。 |
必須先呼叫 accept(),之後才能 receive / send。 |
| 部署時使用錯誤的伺服器 | 將 FastAPI 放在 gunicorn(WSGI)而非 uvicorn(ASGI)會失去非同步特性。 |
使用 uvicorn、hypercorn 或 gunicorn -k uvicorn.workers.UvicornWorker。 |
| 混用同步 DB 驅動 | 使用傳統的同步 ORM(如 SQLAlchemy core)在 async 端點中直接呼叫,會阻塞。 | 使用 SQLModel、Databases、或將同步呼叫包在 run_in_threadpool。 |
忘記設定 max_concurrency |
在高併發環境下未限制協程數量,可能導致資源耗盡。 | 在部署時使用 uvicorn --workers N --limit-concurrency M,或在程式碼中使用 semaphore 控制。 |
最佳實踐:
- 盡量使用 async:只要外部 I/O 支援非同步,就使用
async def;同步函式僅保留在需要 CPU 密集或第三方庫不支援 async 時。 - 選擇正確的 ASGI 伺服器:開發階段可用
uvicorn,正式環境建議使用uvicorn+gunicorn(Worker 為uvicorn.workers.UvicornWorker)。 - 統一錯誤處理:利用 FastAPI 的
ExceptionHandler於全域捕捉異常,避免因未捕捉的阻塞例外導致伺服器崩潰。 - 監控與日誌:使用
uvicorn --log-level info或結合Prometheus、Grafana監控事件迴圈的延遲與併發數。
實際應用場景
| 場景 | 為何選 ASGI(FastAPI) | 可能仍會涉及 WSGI |
|---|---|---|
| 即時聊天、遊戲伺服器 | 需要 WebSocket 高效能雙向通訊,ASGI 原生支援。 | 若已有大量傳統 Flask API,仍可保留 WSGI 部分,透過反向代理一起部署。 |
| 大量外部 API 呼叫的聚合服務 | 每筆請求會同時向多個第三方服務發起請求,使用 async 可同時等待,縮短總延遲。 | 若某些舊有服務只能以同步方式呼叫,可在 FastAPI 中以 run_in_threadpool 包裝。 |
| 資料分析平台的批次任務 | 主要是 CPU 密集,非同步效益較低,仍可使用 FastAPI 的同步端點或傳統 WSGI 框架。 | 若僅提供報表下載,WSGI 已足夠且部署成本較低。 |
| 微服務架構的 API 閘道 | 需要支援 HTTP/2、WebSocket、長輪詢,ASGI 最適合。 | 某些舊有微服務仍是 WSGI,透過 API 閘道統一轉發即可。 |
總結
- WSGI 是同步、僅支援 HTTP 的傳統規範,適合簡單、CPU 密集的應用。
- ASGI 則是 非同步、協定多元 的新一代規範,讓 FastAPI 能夠以
async def撰寫高效能 API,並原生支援 WebSocket、SSE 等即時功能。 - 在 FastAPI 專案中,應盡可能使用 非同步 程式碼與 ASGI 伺服器(如
uvicorn),同時注意不要把阻塞函式直接帶入事件迴圈。 - 了解兩者的差異,能協助你在 部署、效能優化、以及 架構選型 時作出正確決策,讓應用既 快速 又 可擴充。
掌握 ASGI 與 WSGI 的本質,才能真正發揮 FastAPI 的威力,打造現代化、即時化的 Web 服務。