結論先講
FastAPI 快在「框架本身輕」,Django 慢在「框架本身重」,但到了高 VU 兩者都死在 GIL + bcrypt。 真正的差距不在 async vs sync,而在 DB Pool 配置(Django 10 vs FastAPI 50)和 ORM overhead(Django ORM 比 SQLAlchemy 多做了很多事)。
配置對比
| 項目 | Django | FastAPI |
|---|---|---|
| Runtime | Python / uvicorn | Python / uvicorn |
| Framework | Django 4.x | FastAPI |
| ORM | Django ORM | SQLAlchemy |
| Worker | gunicorn + UvicornWorker × 4 | uvicorn —workers 4 |
| DB Pool | 10 | 50 |
| Hash | bcrypt (pyca, C binding) | bcrypt (pyca, C binding) |
注意 DB Pool 的差異:Django 只有 10,FastAPI 有 50。這不是我們刻意偏袒——這是兩個框架的「預設合理值」。Django 的 CONN_MAX_AGE 和 pool 機制和 FastAPI 不同。
Per-VU 數據
| VU | Django Avg | Django RPS | FastAPI Avg | FastAPI RPS |
|---|---|---|---|---|
| 10 | 228ms | 17 | 64ms | 24 |
| 50 | 3,272ms | 13 | 666ms | 47 |
| 100 | 堵塞 (6.4s) | 13 | 1,716ms | 47 |
| 500 | 堵塞 (34.8s) | 1 | 堵塞 (9.8s) | 48 |
| 1K | Err 99.7% | 17 | Err 26.7% (5xx) | 24 |
低 VU:FastAPI 64ms vs Django 228ms
FastAPI 在 10 VU 時 avg 64ms,是 全場 9 個框架最快的(和 Laravel 並列)。Django 228ms,是倒數第二(.NET Core 216ms 差不多)。
差距 3.5 倍。為什麼?
- 框架啟動成本: FastAPI 是輕量級的——一個 ASGI app,幾乎沒有 middleware。Django 有 CSRF、Session、Auth、Messages 等一堆 middleware
- ORM 查詢效率: SQLAlchemy 的 query builder 比 Django ORM 生成的 SQL 更精簡
- 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 團隊
- 調高 DB Pool:
CONN_MAX_AGE = None+ 用django-db-connection-pool設定 pool max - 考慮 Django Ninja 替代 DRF: 更輕量、更快的 API 框架,語法接近 FastAPI
- bcrypt 沒辦法用 async,但可以用
run_in_executor丟到 thread pool 避免阻塞 event loop - Django 的優勢不在效能: Admin panel、ORM migration、生態完整性是選 Django 的理由
FastAPI 團隊
- uvicorn worker 數 = CPU 核數: 不要開超過 CPU 核數的 worker
- 監控 worker 崩潰: FastAPI 在高 VU 時會出 5xx,要設好 health check 和 auto-restart
- 考慮
asyncio.to_thread()包 bcrypt: 至少不阻塞 event loop - 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 的企業級對決