一個請求被誰處理
HTTP server 收到請求,需要有個執行單元去跑 handler。有兩種主流模型:
Thread-per-request(同步):每個請求分配一個 thread。Thread 在等 I/O(查資料庫、呼叫外部 API)時是阻塞的,那個 thread 什麼都不能做。
Event Loop(非同步):一個(或少數幾個)thread 管理所有請求。等 I/O 時,event loop 去處理其他請求,I/O 完成後回來繼續。
這個底層選擇,決定了框架的「實例模型」——同一個框架的 sync 和 async 版本,寫法、效能特性、適用場景都不一樣。
Python 的 WSGI vs ASGI
Python web 的並發模型分水嶺是 WSGI(同步)和 ASGI(非同步)。
WSGI(Web Server Gateway Interface):Django、Flask 的底層協議。每個請求是一個同步的函式呼叫:
# Django view — 同步函式
def user_detail(request, id):
user = User.objects.get(pk=id) # ← 阻塞,等資料庫
return JsonResponse({'name': user.name})Gunicorn 用多個 worker process 處理並發——每個 worker 同時只能跑一個請求。你要 100 QPS,就需要足夠多的 worker process,記憶體用量線性增長。
ASGI(Asynchronous Server Gateway Interface):FastAPI、Starlette 的底層協議。Handler 是 coroutine,等 I/O 時釋放 event loop:
# FastAPI view — 非同步函式
async def user_detail(id: int):
user = await db.get(User, id) # ← 非阻塞,await 期間處理其他請求
return {'name': user.name}Uvicorn 用 asyncio event loop,一個 process 可以同時等待數千個 I/O 操作。記憶體效率遠優於 WSGI 的 multi-process 模型。
Django 的 async 支援(Django 3.1+):Django 現在允許 async view,但 ORM 還是部分同步的——需要用 sync_to_async() 包裝,寫法比 FastAPI 繁瑣。這是框架設計歷史包袱的代價。
Node.js:天生 Event Loop
Node.js 的 Event Loop 是它的核心設計,Express 和所有 Node.js framework 都跑在上面:
┌─────────────────────────────────┐
│ Event Loop │
│ │
│ poll → check → timers → I/O │
│ │
│ 等 I/O 時,跑其他 callback │
└─────────────────────────────────┘
Single Thread
// Express handler — 看起來同步,底層是 event loop
app.get('/users/:id', async (req, res) => {
const user = await userRepo.findById(req.params.id); // ← await 讓出 event loop
res.json(user);
});Node.js 的單 thread event loop 在 I/O 密集場景(等資料庫、等 API)表現優秀,因為「等」的時候可以處理其他請求。
Node.js 的限制:CPU 密集任務(大量計算、圖片處理)會佔住 event loop,阻塞所有其他請求。解法是 Worker Threads 或把計算搬到 background job。
Java / Spring 的演化
Spring MVC(傳統):基於 Servlet,thread-per-request 模型,Tomcat 有 thread pool。
// Spring MVC — 同步,blocking
@GetMapping("/users/{id}")
public User show(@PathVariable Long id) {
return userRepo.findById(id).orElseThrow(); // ← blocking,thread 等待
}Spring WebFlux(reactive):基於 Netty + Project Reactor,non-blocking event loop 模型。
// Spring WebFlux — 非同步,non-blocking
@GetMapping("/users/{id}")
public Mono<User> show(@PathVariable Long id) {
return userRepo.findById(id); // ← Mono = 非同步包裝
}Kotlin + Coroutine + Ktor 讓這個寫法更自然:
// Ktor — Kotlin coroutine,寫起來像同步
get("/users/{id}") {
val id = call.parameters["id"]!!.toLong()
val user = userRepo.findById(id) // ← 底層是 coroutine,non-blocking
call.respond(user)
}為什麼同框架的 sync/async 寫法差很多
以 Django 為例:
# Django 同步 view
def user_list(request):
users = User.objects.all() # ← 同步 ORM
return JsonResponse({'users': list(users.values())})
# Django 非同步 view(需要 async ORM)
async def user_list(request):
users = await User.objects.all() # ← 需要 Django 4.1+ 的 async ORM
# 或用 sync_to_async
users = await sync_to_async(list)(User.objects.all())
return JsonResponse({'users': list(users)})Django 的 ORM 從同步設計出發,async 是事後加上去的,所以有些操作必須用 sync_to_async() 包裝——這是歷史包袱。
FastAPI 從一開始就是 async-first,所以寫法很自然:
# FastAPI — 一致的 async 體驗
async def user_list(db: Session = Depends(get_db)):
users = await db.execute(select(User))
return users.scalars().all()什麼時候選哪個模型
I/O 密集的 API(查資料庫、呼叫外部 API):async / event loop 模型更有效率。FastAPI、Node.js(Express / Hono)、Ktor、Spring WebFlux 在這個場景表現好。
CPU 密集的後端(大量計算、資料轉換):multi-thread / multi-process 更合適,因為 event loop 會被 CPU 佔住。Django(multi-worker)或 Java(thread pool)反而是對的選擇。
標準 CRUD API:老實說,除非你真的碰到效能瓶頸,不然框架的 sync vs async 差異對 95% 的 API 沒有感覺。真正的差異在高並發(1000+ QPS)場景才會出現。
混合場景:很多框架現在支援 sync/async 混用——FastAPI 可以宣告同步 handler(讓框架跑在 threadpool)、Django 可以有 async view。在遷移期間這很有用,但長期最好統一。
