結論先講
跨框架效能比較,方法論比數據重要。 沒有控制變因的 benchmark 是 marketing,不是 engineering。「Go 比 Python 快 10 倍」這種結論,如果不交代測試條件,就跟「吃了這個藥會瘦」一樣沒有參考價值。
問題:為什麼大部分 benchmark 不可信
網路上常見的框架 benchmark 有幾個問題:
1. 硬體不一致
「Go 跑在 M2 Max,Django 跑在 t2.micro,結論:Go 快 50 倍」——這比較的不是框架,是硬體。
2. 場景太簡單
Hello World 或 return JSON 的 benchmark 只能比較框架的 overhead,不能反映真實應用的行為。真實 API 會有:資料庫查詢、密碼 hash、JSON 序列化、驗證邏輯。
3. 配置不統一
- A 框架開了連線池 100,B 框架用預設值 5
- A 框架跑在 production mode,B 框架跑在 debug mode
- A 框架用了 Redis cache,B 框架沒有
4. 沒有交代測試條件
「我測了一下,FastAPI 比 Express 快」——用多少 VU?跑多久?什麼場景?什麼硬體?都沒講。
我們怎麼做:控制變因清單
為了讓 9 個框架的比較有說服力,我們統一了以下所有變因:
1. 計算資源:Docker cpus + memory
# docker-compose.yml — 每個框架都一樣
services:
express-ts:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '1.0'
memory: 1G| 變因 | 設定 | 說明 |
|---|---|---|
| CPU | 2 cores | Docker cpus: '2.0' |
| RAM | 2 GB | Docker memory: 2G |
| CPU reservation | 1 core | 確保最低資源不被其他容器搶走 |
為什麼 2 核 2 GB?
這是一個「夠用但不富裕」的設定。給太多資源(8 核 16 GB),所有框架都跑得很好,差異看不出來。給太少資源(0.5 核 512 MB),所有框架都撐不住,差異也看不出來。2 核 2 GB 剛好在「有些框架開始吃力」的甜蜜點。
2. 資料庫:同一個 MySQL 實例
mysql:
image: mysql:8.0
deploy:
resources:
limits:
cpus: '4.0'
memory: 4G
environment:
MYSQL_ROOT_PASSWORD: benchmark
command: >
--max-connections=500
--innodb-buffer-pool-size=1G| 變因 | 設定 | 說明 |
|---|---|---|
| 版本 | MySQL 8.0 | 所有框架共用同一個 DB |
| CPU | 4 cores | 比被測框架多,避免 DB 成為瓶頸 |
| RAM | 4 GB | InnoDB buffer pool 1 GB |
| Max connections | 500 | 夠所有框架的 connection pool |
為什麼 DB 給 4 核而框架只給 2 核?
因為我們測的是「框架」的效能,不是「資料庫」的效能。如果 DB 資源不足,所有框架都會在 DB 層卡住,你看到的差異是「誰的 DB driver 排隊排得比較好」,不是「框架本身的效能差異」。
為什麼共用同一個 DB 實例?
因為真實世界的部署就是這樣——多個服務共用同一個資料庫。如果每個框架各開一個 MySQL,測的就不是真實場景了。
但這也引入一個風險:A 框架跑壓測時把 DB 打爆,B 框架跟著受影響。 所以我們一次只測一個框架,測完等 DB 冷卻再測下一個。
3. 連線池配置
所有框架統一:
- Pool Min: 5
- Pool Max: 20
- Idle Timeout: 30s
- Acquire Timeout: 60s
連線池大小對效能影響巨大。Pool Max 設 5 和設 100 可以讓同一個框架的表現差 10 倍。統一成 20 是關鍵。
為什麼是 20?
- 太小(5):高併發時連線不夠,大量請求排隊
- 太大(100):MySQL 端的連線管理成為瓶頸
- 20 是大部分框架的建議值,也是我們 2 核環境下的合理數字
4. 密碼 Hash:bcrypt rounds=10
所有框架統一:
- 算法: bcrypt
- Rounds: 10
- 實作: 各語言的主流 bcrypt library
這是我們在壓測中發現的最大的效能影響因素之一。bcrypt 是 CPU-bound 操作,rounds=10 在單一請求時大約 80-120ms,但在高併發時會阻塞 CPU,導致所有請求排隊。
為什麼不用 argon2?
因為 argon2 的效能表現受記憶體配置影響大,不同語言的 argon2 實作差異也大。bcrypt rounds=10 在各語言間的表現比較一致,更適合當控制變因。
為什麼不把 hash 排除?
因為真實的 User API 一定會有密碼 hash。排除 hash 就是在測一個不存在的場景。我們選擇保留 hash,然後在分析中明確標註「bcrypt 是共同瓶頸」。
5. 測試數據
每次壓測前:
1. 清空資料庫
2. Seed 100 筆用戶資料
3. 所有框架用同一份 seed data
| 變因 | 設定 | 說明 |
|---|---|---|
| 初始資料量 | 100 筆用戶 | 模擬小型應用 |
| 資料清理 | 每次壓測前清空 + reseed | 確保起始狀態一致 |
| Seed 資料 | 固定內容 | 不用隨機產生 |
6. 網路
networks:
benchmark:
driver: bridge所有容器在同一個 Docker bridge network。沒有外部網路的不確定性。
7. 快取
壓測時:Redis cache 關閉
如果開 cache,測到的就是「cache hit rate」而非「框架效能」。所有 Read 請求都打到 DB。
8. VU 梯度
標準 VU 梯度: 10, 50, 100, 500, 1K, 5K, 10K
每個 VU 階段:
- Ramp-up: 1 分鐘
- Sustain: 3 分鐘
- Ramp-down: 1 分鐘
7 個 VU 等級讓我們能看到「從健康到崩潰」的完整過程。大部分框架在 2 核 2 GB 的限制下,會在 VU 100-500 之間開始出問題。
無法完全控制的變因
有些變因我們選擇不統一,因為它們是「框架特性的一部分」:
語言 Runtime
- Node.js (Express-TS, Express-JS, NestJS): V8 引擎,single-thread + event loop
- Python (Django, FastAPI): CPython,GIL 限制
- Go: goroutine,天生支援高併發
- Java (Spring Boot): JVM,需要 warm-up
- C# (.NET Core): CLR,類似 JVM
- PHP (Laravel): 每個請求一個 process(傳統模型)
這些差異是「框架選擇」的一部分。統一 runtime 就沒有比較的意義了。
ORM / DB Driver
- Express-TS / NestJS: Sequelize
- Django: Django ORM
- FastAPI: SQLAlchemy
- Go: GORM
- Laravel: Eloquent
- Spring Boot: Spring Data JPA + Hibernate
- .NET Core: Entity Framework Core
ORM 的效能差異也是框架生態的一部分。真實開發中你不會「用 Django 但配 Sequelize」。
HTTP Server
- Express: 基於 Node.js http module
- FastAPI: uvicorn (ASGI)
- Django: gunicorn + wsgi
- Go: net/http(標準庫)
- Spring Boot: Tomcat(內嵌)
- .NET Core: Kestrel
- Laravel: PHP-FPM + Nginx
怎麼驗證控制變因有效
設好控制變因不代表它真的生效了。我們用三種方式驗證:
1. 跑前檢查
# 確認 CPU/RAM 限制生效
docker stats --no-stream --format \
"{{.Name}}: CPU={{.CPUPerc}} MEM={{.MemUsage}}"
# 確認 DB 連線數
mysql -e "SHOW STATUS LIKE 'Threads_connected';"2. 基準線測試
每個框架先跑一次 VU=10 的 health check。如果 health check 的 response time 差異超過 10%,代表環境有問題。
3. 重複性驗證
同一個框架、同一個場景跑兩次。如果兩次結果的 RPS 差異超過 15%,代表環境不穩定,需要排查。
常見的不穩定因素:
- Docker daemon 在做 GC
- 其他容器在跑東西
- macOS 的記憶體壓力
- InfluxDB 在做 compaction
控制變因的矩陣
把所有控制變因整理成一張表:
| 類別 | 變因 | 值 | 是否統一 |
|---|---|---|---|
| 計算 | CPU | 2 cores | 統一 |
| 計算 | RAM | 2 GB | 統一 |
| 資料庫 | Engine | MySQL 8.0 | 統一 |
| 資料庫 | Pool Max | 20 | 統一 |
| 資料庫 | Pool Min | 5 | 統一 |
| 安全 | Hash 算法 | bcrypt | 統一 |
| 安全 | Hash rounds | 10 | 統一 |
| 快取 | Redis | 關閉 | 統一 |
| 網路 | Network | Docker bridge | 統一 |
| 測試 | VU 梯度 | 10~10K | 統一 |
| 測試 | 測試時長 | 每階段 5min | 統一 |
| 測試 | Seed data | 100 筆 | 統一 |
| 語言 | Runtime | 各自 | 不統一(框架特性) |
| 語言 | ORM | 各自 | 不統一(框架生態) |
| 語言 | HTTP Server | 各自 | 不統一(框架選擇) |
「統一」的是基礎設施層面的變因,「不統一」的是框架本身的技術選擇。這樣比較出來的差異,反映的就是「選擇這個框架 + 它的生態」帶來的效能影響。
常見的質疑和回應
「2 核 2 GB 太小了,生產環境不會這麼配」
對。但 benchmark 的目的不是模擬生產環境,而是在受控條件下比較差異。給 32 核 64 GB,所有框架都跑得很好,差異看不出來。受限環境反而能放大框架特性的影響。
「共用 DB 不公平,A 框架測的時候 DB 可能在回收連線」
所以我們一次只測一個框架,測完等 30 秒讓 DB 冷卻。跑前還會檢查 Threads_connected 回到基線。
「bcrypt rounds=10 太重了,生產環境會用 argon2 或降低 rounds」
同意。但如果所有框架都降到 rounds=4,CPU 不再是瓶頸,你看到的就只是「IO 差異」。rounds=10 讓 CPU-bound 的特性被放大,這正是我們想觀察的。
「沒有 warm-up,對 JVM 框架不公平」
我們的測試有 1 分鐘的 ramp-up 期,JVM 有時間做 JIT 編譯。VU=10 的 ramp-up 階段就是 warm-up。
下一篇
k6 腳本怎麼寫 — 帶著這套控制變因,來看 9 個後端框架在最基礎的 CRUD 場景下表現如何。劇透:所有框架的共同瓶頸都是 bcrypt。
本系列文章
完整 68 篇目錄見 系列首頁
← 上一篇:壓測平台架構設計:從 k6 腳本到全自動分析 → 下一篇:k6 腳本怎麼寫:從 Hello World 到混合場景