結論先講

Laravel 的架構(Nginx + PHP-FPM)讓它的失敗模式和其他框架完全不同。 其他框架在高 VU 時「接受連線但處理不完」(服務堵塞),Laravel 是「直接拒絕連線」(快速失敗)。結果是:Laravel 的 RPS 數字在高 VU 時被 refused 的速度灌水,看起來很高但實際上不是在處理請求。


架構差異

大部分框架直接接 HTTP 連線:

Client → Express.js / Kestrel / Tomcat → Application

Laravel 前面有 Nginx:

Client → Nginx → PHP-FPM → Laravel

這個架構差異讓 Laravel 有一個其他框架沒有的保護層:Nginx 的 connection limit

Nginx 的 worker_connections 有上限。超過上限的連線直接被 refuse——不是排隊,不是 timeout,是 立刻被拒絕。拒絕一個連線只要幾微秒。


Per-VU 數據

VUAvgP95RPSError狀態
1064ms156ms240%健康
50972ms2,496ms370%警告
1002,283ms5,099ms380%警告
50012,065ms-390%堵塞
1K30,253ms-180%堵塞
5K22,400ms-170%堵塞
10K186ms-2,30099.2%崩潰

10K VU 的數字解讀

10K VU 時:

  • RPS 2,300(全場最高)
  • avg 186ms(全場最快)
  • Error 99.2%(102,000 個 refused)

看起來矛盾?因為 Nginx refuse 一個連線只要幾微秒:

102,000 個 refused ÷ 45 秒 ≈ 2,266 refused/秒
860 個成功處理 ÷ 45 秒 ≈ 19 成功/秒
總 RPS ≈ 2,285(接近顯示的 2,300)

2,300 RPS 中有 2,266 是 Nginx refuse 的,不是 Laravel 處理的。 實際處理能力只有 19 RPS。


PHP-FPM 的 process 模型

Nginx (1 worker, 1024 connections)
  ↓
PHP-FPM (pm.max_children = 16)
  ↓
Laravel Application

PHP-FPM 的 pm.max_children = 16 表示最多同時處理 16 個請求。第 17 個請求會排隊等到有 process 空出來。

和 Node.js 的 event loop 或 Go 的 goroutine 不同,PHP 的每個請求都是一個獨立的 process。好處是完全隔離——一個請求的記憶體洩漏不會影響其他請求。壞處是併發受限於 process 數量

為什麼不開更多 process

在 2 GB Docker 限制下:

  • 每個 PHP-FPM process 大約佔 30-50 MB
  • 16 processes × 50 MB = 800 MB
  • 加上 MySQL 連線、Nginx、OS overhead ≈ 1.5 GB
  • 剩餘 500 MB 作為緩衝

開到 32 processes 會讓記憶體吃緊,在高 VU 時有 OOM 風險。16 是比較安全的選擇。


低 VU 時 Laravel 為什麼快

Laravel 在 10 VU 時 avg 64ms(和 FastAPI 並列全場最快)。為什麼 PHP 這麼快?

  1. PHP 的 bcrypt 是 C extensionpassword_hash() 底層是 C library,速度和 Go 的 native bcrypt 一樣快(~80ms)
  2. Shared-nothing architecture:每個請求是獨立的 process,沒有 GC 壓力、沒有 shared memory contention
  3. OPcache:PHP 的 opcode cache 讓第二次請求不需要重新解析 PHP 檔案
  4. Eloquent 很輕:雖然 Eloquent 是 Active Record pattern(理論上比 Data Mapper 慢),但 PHP 的 array 操作非常快

PHP 的「每個請求都是全新的」

這是 PHP 最被低估的特性。其他語言的 application server 在請求之間共享記憶體——好處是可以做 cache,壞處是 memory leak 會累積。

PHP 每個請求結束後,整個 process 的記憶體都清掉。不可能有 memory leak。 這讓 Laravel 在 soak test(長時間運行)中比其他框架穩定。


快速拒絕是好事還是壞事

好處:保護後端

Nginx 的 connection limit 保護了 PHP-FPM 不被打爆。在 10K VU 時,PHP-FPM 仍然在正常處理 16 個併發請求——只是其他 9,984 個被 Nginx 擋在門外。

如果沒有 Nginx,10K VU 的連線會直接灌到 PHP-FPM,可能導致 OOM 或 process crash。

壞處:RPS 數字失真

2,300 RPS 的數字會讓人誤以為「Laravel 在高 VU 時表現最好」。實際上它的處理能力只有 19 RPS,剩下的都是 refuse。

在我們的排名系統中,我們用「健康 VU」和「breaking point」來避免這個問題。Laravel 的健康 VU 是 100(不是 10K),因為 error rate 在超過 500 VU 後就不健康了。

其他框架學到的教訓

在 Express、FastAPI、Go 前面加一個 reverse proxy 做 connection buffering,是值得做的事。 Express-TS 在檔案上傳場景因為沒有 connection buffering 而直接被打穿(第 42 篇會詳細講)。


實務建議

  1. Nginx 是 Laravel 的一部分,不是可選的: 不要直接用 php artisan serve 上生產
  2. pm.max_children 要根據記憶體算: 可用記憶體 ÷ 每個 process 的記憶體 ÷ 留 30% buffer
  3. OPcache 一定要開: production 環境不開 OPcache 等於慢 5-10 倍
  4. 考慮 Laravel Octane: 用 Swoole 或 RoadRunner 替代 PHP-FPM,保持 application state 在記憶體中,理論上能大幅提升 RPS
  5. 不要被 RPS 數字誤導: 看健康 VU 和 error rate,不看 max RPS

Part 3 完結

CRUD 場景的 6 篇到此結束。重點回顧:

  1. 所有框架的共同天花板是 bcrypt第 28 篇
  2. Go 在低延遲上最好,但 goroutine 幫不了 CPU-bound第 29 篇
  3. TypeScript 比 JavaScript 快,NestJS 的 DI overhead 可接受第 30 篇
  4. Python 的 GIL 在 bcrypt 場景雙重限制第 31 篇
  5. Spring Boot 的 CRUD 弱勢在混合場景翻盤第 32 篇
  6. Laravel 的 Nginx 快速拒絕讓 RPS 數字失真(本篇)

接下來先看 F2E、DB、Storage、Infra 各層的數據,再回到 B2E 跑更多場景的壓測。


下一篇

前端框架 Lighthouse 排名:React 99 分但差距其實不大 — CRUD 場景看完了後端,接下來看前端。6 個前端框架的 Lighthouse 分數差距只有 6 分——真正有差異的是 bundle size 和 render strategy。


本系列文章

完整 68 篇目錄見 系列首頁

← 上一篇:JVM vs CLR:Spring Boot vs .NET Core 的企業級對決 → 下一篇:前端框架 Lighthouse 排名:React 99 分但差距其實不大