結論先講

跨框架效能比較,方法論比數據重要。 沒有控制變因的 benchmark 是 marketing,不是 engineering。「Go 比 Python 快 10 倍」這種結論,如果不交代測試條件,就跟「吃了這個藥會瘦」一樣沒有參考價值。


問題:為什麼大部分 benchmark 不可信

網路上常見的框架 benchmark 有幾個問題:

1. 硬體不一致

「Go 跑在 M2 Max,Django 跑在 t2.micro,結論:Go 快 50 倍」——這比較的不是框架,是硬體。

2. 場景太簡單

Hello Worldreturn 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
變因設定說明
CPU2 coresDocker cpus: '2.0'
RAM2 GBDocker memory: 2G
CPU reservation1 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
CPU4 cores比被測框架多,避免 DB 成為瓶頸
RAM4 GBInnoDB buffer pool 1 GB
Max connections500夠所有框架的 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

控制變因的矩陣

把所有控制變因整理成一張表:

類別變因是否統一
計算CPU2 cores統一
計算RAM2 GB統一
資料庫EngineMySQL 8.0統一
資料庫Pool Max20統一
資料庫Pool Min5統一
安全Hash 算法bcrypt統一
安全Hash rounds10統一
快取Redis關閉統一
網路NetworkDocker bridge統一
測試VU 梯度10~10K統一
測試測試時長每階段 5min統一
測試Seed data100 筆統一
語言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 到混合場景