結論先講
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 數據
| VU | Avg | P95 | RPS | Error | 狀態 |
|---|---|---|---|---|---|
| 10 | 64ms | 156ms | 24 | 0% | 健康 |
| 50 | 972ms | 2,496ms | 37 | 0% | 警告 |
| 100 | 2,283ms | 5,099ms | 38 | 0% | 警告 |
| 500 | 12,065ms | - | 39 | 0% | 堵塞 |
| 1K | 30,253ms | - | 18 | 0% | 堵塞 |
| 5K | 22,400ms | - | 17 | 0% | 堵塞 |
| 10K | 186ms | - | 2,300 | 99.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 這麼快?
- PHP 的 bcrypt 是 C extension:
password_hash()底層是 C library,速度和 Go 的 native bcrypt 一樣快(~80ms) - Shared-nothing architecture:每個請求是獨立的 process,沒有 GC 壓力、沒有 shared memory contention
- OPcache:PHP 的 opcode cache 讓第二次請求不需要重新解析 PHP 檔案
- 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 篇會詳細講)。
實務建議
- Nginx 是 Laravel 的一部分,不是可選的: 不要直接用
php artisan serve上生產 - pm.max_children 要根據記憶體算:
可用記憶體 ÷ 每個 process 的記憶體÷ 留 30% buffer - OPcache 一定要開: production 環境不開 OPcache 等於慢 5-10 倍
- 考慮 Laravel Octane: 用 Swoole 或 RoadRunner 替代 PHP-FPM,保持 application state 在記憶體中,理論上能大幅提升 RPS
- 不要被 RPS 數字誤導: 看健康 VU 和 error rate,不看 max RPS
Part 3 完結
CRUD 場景的 6 篇到此結束。重點回顧:
- 所有框架的共同天花板是 bcrypt(第 28 篇)
- Go 在低延遲上最好,但 goroutine 幫不了 CPU-bound(第 29 篇)
- TypeScript 比 JavaScript 快,NestJS 的 DI overhead 可接受(第 30 篇)
- Python 的 GIL 在 bcrypt 場景雙重限制(第 31 篇)
- Spring Boot 的 CRUD 弱勢在混合場景翻盤(第 32 篇)
- 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 分但差距其實不大