並發模型:理解演進的底層語言
在看各時代的演進之前,要先有一個底層認識——「同時來了 1000 個請求,server 怎麼處理?」 這個問題貫穿了整個 web framework 演進史。
主要有三種模型:
Process-based:每個請求分配一個 OS process。Process 有獨立的記憶體空間,完全隔離,但建立成本高(fork 費時)、記憶體用量大(一個 process 幾十 MB)。
Thread-based:每個請求分配一個 thread。Thread 共享 process 的記憶體,建立比 process 快,但有幾個麻煩:
- Context switch 成本(OS 要保存/恢復 thread 狀態)
- 共享記憶體帶來競態條件(race condition)——兩個 thread 同時修改同一個變數,需要鎖(mutex / lock)保護,而鎖本身又是效能瓶頸
- Thread 等 I/O 時什麼都不做,但它佔的資源還在那裡
Event Loop(async I/O):單一 thread 管理所有請求。I/O 等待期間,thread 不阻塞,改去處理其他事情;I/O 完成後觸發 callback 繼續。不是真正的並行,但 I/O 密集場景下吞吐量遠高於 thread-per-request。代價是:CPU 密集任務會卡住整個 event loop,阻塞所有請求。
這三種模型對應了整個演進史的選擇邏輯。
CGI 時代:每個請求都是一個新 process
最早的 web server 處理動態內容的方式是 CGI(Common Gateway Interface,1993):
瀏覽器發請求 → Apache → fork 一個新 process 跑腳本 → 輸出 HTML → process 結束
每個請求都 fork 一個新 process(process-based)。請求量一大,server 就被 fork 的成本拖垮。Perl 和 C 是 CGI 時代的主角,因為它們快。
撞牆點:請求量超過幾十 concurrent 就開始扛不住。process 的記憶體 overhead 太高(每個 process 幾十 MB),fork 的成本太大。
Apache Module 時代:把語言嵌進 server
解法是把語言直接嵌進 web server,不用每次 fork:
- mod_php(1994):PHP 直接跑在 Apache 的 process 裡,不需要 fork
- mod_perl(1996):Perl 也嵌進去
PHP 在這個時代爆炸性成長,因為它讓「在 HTML 裡面直接嵌程式碼」變得容易:
<html>
<body>
<?php echo "Hello, " . $name; ?>
</body>
</html>撞牆點一:效能好了,但程式碼品質爛了。 PHP 讓每個人都可以寫 web,但也讓每個人以不同的方式寫。沒有 MVC、沒有 ORM、資料庫查詢和 HTML 混在一起。專案超過幾千行之後,沒有人知道整個系統怎麼跑的。
撞牆點二:session 管理、路由、安全性每個人都自己實作。 結果是大量的重複和安全漏洞。
Rails 時代:Convention over Configuration
Rails(2004)解的不是效能問題,解的是可維護性問題。
David Heinemeier Hansson 從 Ruby 的動態語言特性出發,做了幾件在當時很激進的事:
- MVC 強制分層:Model / View / Controller 有各自的資料夾,你不能把業務邏輯寫進 View
- Convention over Configuration:你的 User model 自動對應
userstable,不需要任何設定 - ActiveRecord:ORM 直接讓你用物件操作資料庫,SQL 不用手寫
- Generator:
rails generate scaffold User name:string email:string,CRUD 全部生出來
Rails 的影響遠超過 Ruby 生態。Django(Python,2005)、CakePHP(PHP,2005)、Spring MVC(Java)都在同一時期借鑒了這個思路。
撞牆點一:Rails 的 thread 模型。Ruby MRI 有 GIL(Global Interpreter Lock),無法真正並行。Rails 的 Puma / Unicorn 是 multi-process 或 multi-thread,但 context switch 成本和記憶體用量在高並發時成了瓶頸。
撞牆點二:Convention 也是負擔。Rails magic 讓初學者很快上手,但出了問題要 debug 就要讀大量的 DSL 和 meta-programming。而且專案越大,「你以為你改了 A,但其實 B 也動了」這種問題越多。
撞牆點三:JSON API 時代的 overhead。Rails 本來是為了 server-side rendering 設計的。JSON API 不需要 template engine,Rails 帶來的 view layer 整個是多餘的 overhead。
Node.js 時代:Event Loop 解高並發
2009 年,Ryan Dahl 做 Node.js 的出發點是:thread-per-request 模型在高並發下記憶體爆炸。
一個 Apache + PHP 的 server,每個連線會占用一個 thread(或 process),10,000 個並發連線就是 10,000 個 thread——記憶體和 context switch 成本讓 server 撐不住。
Node.js 的 event loop 模型:
單一 thread + non-blocking I/O
→ 一個 process 可以同時「等待」數萬個 I/O 操作
→ 不是真正的並行,但 I/O 等待期間可以處理其他請求
→ CPU-bound 任務(大量計算)仍會卡住 event loop,解法是 Worker Threads 或 background queue
這解釋了為什麼在 Node.js 生態裡,Queue(Bull / BullMQ)這麼重要——CPU 密集或耗時的任務(圖片處理、PDF 生成、大批次匯出)不能放在 event loop 的 request handler 裡跑,要丟到 queue 讓 worker process 另外處理,主 event loop 才不會被卡住。
Express(2010)是 Node.js 生態的第一個主流 framework,極度 minimal:只給你路由和 middleware,其他都自己來。這個設計哲學讓它在 microservice 時代大量被用在「只需要做一件事的 service」。
撞牆點一:Callback hell。早期 Node.js 大量用 callback 處理 async,巢狀 callback 讓程式碼難以閱讀和維護。Promise(2015 ES6)和 async/await(2017 ES2017)緩解了這個問題,但語言本身的 async 模型仍有學習曲線。
撞牆點二:Express 什麼都不給,大專案要自己搭所有東西。每個 Express 專案的結構都長得不一樣,onboarding 成本高。NestJS(2017)用 Angular 的架構理念解這個問題——強制 Module / Injectable / Controller,讓 Node.js 大型後端也有統一結構。
撞牆點三:動態型別的大型系統維護困難。TypeScript 的廣泛採用(2018 後)是為了解決這個問題。現在幾乎所有新的 Node.js 後端都用 TypeScript。
async framework 回潮:ASGI、Ktor、Spring WebFlux
Python 的 Django 是同步框架,底層是 WSGI。2018 年之後,async Python 框架開始崛起:
- FastAPI(2018):基於 Starlette(ASGI),native async/await,Pydantic 自動 validation,OpenAPI 文件自動生成
- Starlette(2018):FastAPI 的底層,輕量 ASGI framework
FastAPI 解的是幾個 Django 的痛點:
- Django ORM 是同步的,在高並發 I/O 場景會因為 thread 阻塞拖慢整體吞吐
- Django REST Framework 的 serializer 寫法太繁瑣
- API 文件要手寫或用插件,不是自動的
JVM 生態也有類似演化:Spring WebFlux(2017)是 Spring 的 reactive 版本,解決 Spring MVC 的 thread-per-request 在高並發下的問題;Kotlin 語言帶來 coroutine,Ktor 以此為基礎成為 Kotlin 原生的 async framework。
Microservice 架構崛起:部署單位的革命
2013–2018 年,另一條平行的演進線改變了整個後端的架構方式——把一個大 monolith 拆成很多小 service。
Monolith 的撞牆點不是效能,是組織規模:
- 部署耦合:一個 bug 修完要把整個 app 重新部署,100 人的工程團隊同時改同一個 codebase,deploy 頻率卻受最慢的那個人限制
- 故障擴散:使用者系統壞了,整個電商後台也跟著壞——因為在同一個 process 裡
- 技術鎖定:你選了 Java,整個系統就是 Java。想用 Python 做 ML pipeline、用 Go 做高並發服務?在 monolith 裡很難
Netflix(2008–2012)、Amazon(2002 年 Bezos 的 mandate)把這個方向推到工業級規模。每個 service 獨立部署、獨立 scale、甚至可以用不同語言寫。
Microservice 對 framework 選擇的影響:一個 microservice 可能只做「處理 payment webhook」或「發送 email notification」這一件事——這個規模的 service 根本不需要 Django 或 Spring Boot 這麼重的框架。Minimal framework(Express / Gin / FastAPI)剛好符合「輕量、只做一件事」的 microservice 定位。
Microservice 帶來的新問題:service 多了,問題也變複雜:
- 網路呼叫取代 function call,latency 和 failure 需要處理
- 分散式資料一致性(跨 service 的 transaction 很難做)
- 服務發現、load balancing、circuit breaker
- 分散式 tracing(一個請求可能跨 5 個 service,出錯怎麼追)
這些問題催生了 Service Mesh(Istio)、API Gateway、分散式追蹤(OpenTelemetry)的需求——但那已經是 infra 層的故事了。
Minimal API 的回頭浪
2022 年之後有一個有趣的現象:輕量 framework 再次流行。
- Hono(2022,Node.js/Bun/Cloudflare Workers):比 Express 更快、typed route、跨 runtime
- Elysia(2023,Bun):利用 Bun 的效能,端對端型別安全
- ASP.NET Minimal API(.NET 6,2021):微軟把 .NET 的 API 寫法簡化,不用 Controller class
這波浪的驅動力是:NestJS / Spring Boot 這類 opinionated 框架,對小型 API service 來說太重了。如果你的 service 就是「接一個請求、查一次資料庫、回傳 JSON」,帶整個 Angular-inspired 架構進來是 overkill。
撞牆點(預測):minimal framework 在大型系統裡,還是會遇到「每個人結構不一樣」的問題。Meta-framework 和 convention-heavy 框架的下一波崛起,可能是因為 minimal framework 的 onboarding 問題又出現了。
演進的核心邏輯
每一代 framework 的出現都是因為:
- 前一代的解法在某個維度撞牆(效能、可維護性、開發速度、型別安全)
- 新的語言特性 / 語言生態創造了新的可能(GIL 的限制、async/await 的普及、TypeScript 的成熟)
- 部署模式改變(monolith → microservice → serverless → edge function)
這些技術不是「新的比較好」,而是「在不同的約束條件下,不同的 trade-off 更合理」。Express 在 2026 還活著,不是因為它沒有替代方案,而是因為它的 trade-off 在特定場景下仍然是最合理的——Express 為什麼還活著 有完整的分析。
延伸閱讀
- Magic vs Explicit 光譜
- Node.js Framework 比較
- Python Framework 比較
- Event Loop)
- Ryan Dahl, Node.js JSConf EU 2009(Node.js 的原始動機,解釋 event loop 為什麼比 thread-per-request 更適合 I/O 密集場景)
- Huli, 从 MVC、SPA 到 SSR(MVC 的 web 應用脈絡,以及 SPA/SSR 的演進關係)
