結論先講

FastAPI 快在「框架本身輕」,Django 慢在「框架本身重」,但到了高 VU 兩者都死在 GIL + bcrypt。 真正的差距不在 async vs sync,而在 DB Pool 配置(Django 10 vs FastAPI 50)和 ORM overhead(Django ORM 比 SQLAlchemy 多做了很多事)。


配置對比

項目DjangoFastAPI
RuntimePython / uvicornPython / uvicorn
FrameworkDjango 4.xFastAPI
ORMDjango ORMSQLAlchemy
Workergunicorn + UvicornWorker × 4uvicorn —workers 4
DB Pool1050
Hashbcrypt (pyca, C binding)bcrypt (pyca, C binding)

注意 DB Pool 的差異:Django 只有 10,FastAPI 有 50。這不是我們刻意偏袒——這是兩個框架的「預設合理值」。Django 的 CONN_MAX_AGE 和 pool 機制和 FastAPI 不同。


Per-VU 數據

VUDjango AvgDjango RPSFastAPI AvgFastAPI RPS
10228ms1764ms24
503,272ms13666ms47
100堵塞 (6.4s)131,716ms47
500堵塞 (34.8s)1堵塞 (9.8s)48
1KErr 99.7%17Err 26.7% (5xx)24

低 VU:FastAPI 64ms vs Django 228ms

FastAPI 在 10 VU 時 avg 64ms,是 全場 9 個框架最快的(和 Laravel 並列)。Django 228ms,是倒數第二(.NET Core 216ms 差不多)。

差距 3.5 倍。為什麼?

  1. 框架啟動成本: FastAPI 是輕量級的——一個 ASGI app,幾乎沒有 middleware。Django 有 CSRF、Session、Auth、Messages 等一堆 middleware
  2. ORM 查詢效率: SQLAlchemy 的 query builder 比 Django ORM 生成的 SQL 更精簡
  3. Async vs Sync: FastAPI 的 async handler 讓 uvicorn 在等 DB 時能處理其他請求

高 VU:兩者都死,但死法不同

Django 在 500 VU 時只完成 54 個 request(每秒 1 個),FastAPI 完成 2,502 個(每秒 48 個)。差距 48 倍。

但到 1K VU,FastAPI 開始出 5xx 錯誤(26.7%)——uvicorn worker 直接崩潰。Django 出的是 refused(99.7%)——gunicorn 拒絕連線但 worker 沒崩。

FastAPI 的失敗模式更危險:它接受連線但回 500(用戶看到錯誤頁面),而 Django 直接拒絕連線(用戶看到「無法連線」,比較好處理)。


GIL 的影響

Python 的 GIL(Global Interpreter Lock)讓 CPU-bound 的 multi-threading 沒有加速效果。

Thread 1: [bcrypt 計算中...]  → GIL locked
Thread 2: [等 GIL...........]  → 被擋住
Thread 3: [等 GIL...........]  → 被擋住

兩個框架都用 multi-process(4 workers)來繞過 GIL。每個 worker 是獨立的 Python process,有自己的 GIL。所以 4 workers = 最多 4 個 bcrypt 並行。

但 2 核 CPU 上跑 4 workers,只有 2 個能真正同時算。另外 2 個在等 CPU scheduling。multi-process 繞過了 GIL,但沒繞過 CPU 數量限制。


DB Pool 10 vs 50 的影響

Django 的 DB Pool Max 是 10,FastAPI 是 50。這對高 VU 影響巨大。

100 VU 時:

  • FastAPI:50 個 DB 連線可用,4 workers 每個分 12-13 個,基本夠用
  • Django:10 個 DB 連線,4 workers 每個只分 2-3 個。請求排隊等 DB 連線

如果把 Django 的 Pool Max 也調到 50,高 VU 時的表現會好很多。 但 Django 的預設行為(persistent connections + CONN_MAX_AGE)和 connection pooling 的整合沒有 SQLAlchemy 那麼直接,調整起來比較複雜。

這是一個「框架生態差異」的例子——不是 Django 的程式碼品質差,而是 Django 的預設配置對壓測場景不友善。


FastAPI 的 async 有多大幫助

FastAPI 支援 async handler:

@app.post("/register")
async def register(user: UserCreate):
    # async DB query
    result = await db.execute(insert(User).values(...))
    # bcrypt 仍然是 sync 的!
    hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=10))

async handler 讓 uvicorn 在等 DB response 時能處理其他請求。但 bcrypt 是 sync 的——你不能 await bcrypt.hash()。bcrypt 在算的時候,整個 event loop 被阻塞。

所以 FastAPI 的 async 優勢只在 I/O 部分(DB 查詢、JSON 序列化),bcrypt 那一段仍然是同步阻塞。

這解釋了為什麼 FastAPI 在混合場景(70% Read = I/O)表現比 CRUD 場景(40% Auth = CPU)好很多。


實務建議

Django 團隊

  1. 調高 DB Pool: CONN_MAX_AGE = None + 用 django-db-connection-pool 設定 pool max
  2. 考慮 Django Ninja 替代 DRF: 更輕量、更快的 API 框架,語法接近 FastAPI
  3. bcrypt 沒辦法用 async,但可以用 run_in_executor 丟到 thread pool 避免阻塞 event loop
  4. Django 的優勢不在效能: Admin panel、ORM migration、生態完整性是選 Django 的理由

FastAPI 團隊

  1. uvicorn worker 數 = CPU 核數: 不要開超過 CPU 核數的 worker
  2. 監控 worker 崩潰: FastAPI 在高 VU 時會出 5xx,要設好 health check 和 auto-restart
  3. 考慮 asyncio.to_thread() 包 bcrypt: 至少不阻塞 event loop
  4. SQLAlchemy async: 用 create_async_engine 搭配 asyncpg/aiomysql

下一篇

JVM vs CLR:Spring Boot vs .NET Core — 兩個「企業級」框架的對決。Spring Boot 在 CRUD 場景排第 8,.NET Core 排第 2——但故事沒這麼簡單。JVM 的 warm-up 和 BCrypt.Net 的實作問題讓數據很有意思。


本系列文章

完整 68 篇目錄見 系列首頁

← 上一篇:Node.js 三兄弟 CRUD:Express-TS vs Express-JS vs NestJS → 下一篇:JVM vs CLR:Spring Boot vs .NET Core 的企業級對決