From 07618fb1b348cd412d6d2ae3bacd69922d2fa69f Mon Sep 17 00:00:00 2001 From: Hank Date: Thu, 11 Jun 2026 23:15:55 +0800 Subject: [PATCH] =?UTF-8?q?perf(docker):=20OPcache=20revalidate=20?= =?UTF-8?q?=E8=AA=BF=E6=A0=A1=20+=20composer=20=E9=87=8D=E8=A3=9D=E6=A2=9D?= =?UTF-8?q?=E4=BB=B6=E4=BF=AE=E6=AD=A3=20+=20nginx=20healthcheck=20?= =?UTF-8?q?=E4=BF=AE=E5=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 實測修正根因:fpm 的 OPcache 本來就啟用(初版診斷是 CLI 假象), 真凶是 revalidate_freq=2 在 Windows bind mount 上每 2 秒重新 stat 全部腳本——窗口內 0.23s、窗口外 2.5s。 - local.ini:revalidate_freq 2→30、memory 192M、max_files 20000 (程式碼變更最多 30s 生效,立即生效用 kill -USR2 1 平滑重載) - entrypoint:composer 重裝改用 lock 內容比對,git 分支操作不再 觸發開機重裝(原 mtime 比對造成數分鐘 502 窗口) - healthcheck:localhost→127.0.0.1(busybox wget 先解析 ::1 而 nginx 只 listen IPv4,導致永遠 unhealthy) 驗收:穩態 0.20~0.27s(修復前 2.3~2.5s,約 10 倍);nginx healthy。 量測數據已寫回優化計畫文檔。 Co-Authored-By: Claude Fable 5 --- docker-compose.yml | 3 +- docker/php/docker-entrypoint.sh | 7 +- docker/php/local.ini | 13 ++- ...026-06-11-performance-optimization-plan.md | 98 +++++++------------ 4 files changed, 53 insertions(+), 68 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7d32d4a..dc877de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,7 +60,8 @@ services: app: condition: service_healthy healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + # busybox wget 對 localhost 會先解析到 IPv6 ::1,但 nginx 只 listen IPv4,須用 127.0.0.1 + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/"] interval: 30s timeout: 10s retries: 3 diff --git a/docker/php/docker-entrypoint.sh b/docker/php/docker-entrypoint.sh index db9ca67..1399ccc 100644 --- a/docker/php/docker-entrypoint.sh +++ b/docker/php/docker-entrypoint.sh @@ -22,9 +22,12 @@ php -r " file_put_contents('/var/www/.env', \$env); " -# Composer 依賴(vendor 不存在時才裝,通常已存在於 volume) -if [ -f "composer.json" ] && { [ ! -d "vendor" ] || [ "composer.json" -nt "vendor/autoload.php" ]; }; then +# Composer 依賴(vendor 不存在或 lock 內容變更時才裝) +# 用內容比對而非 mtime:git checkout/pull 會更新 composer.json 的 mtime, +# mtime 比對會讓每次分支操作後的開機都重跑 autoload 生成(bind mount 上耗時數分鐘、期間全站 502) +if [ -f "composer.lock" ] && { [ ! -d "vendor" ] || ! cmp -s composer.lock vendor/.composer.lock.installed; }; then composer install --no-scripts --optimize-autoloader + cp composer.lock vendor/.composer.lock.installed fi # 背景執行:等 MySQL → migration → cache clear → storage link → swagger diff --git a/docker/php/local.ini b/docker/php/local.ini index 4f8b444..578537b 100644 --- a/docker/php/local.ini +++ b/docker/php/local.ini @@ -2,4 +2,15 @@ upload_max_filesize=40M post_max_size=40M memory_limit=512M max_execution_time=600 -max_input_vars=10000 \ No newline at end of file +max_input_vars=10000 + +; ── OPcache 調校(2026-06-11 效能優化 O1.1)── +; 預設 revalidate_freq=2 在 Windows bind mount 上每 2 秒就重新 stat 全部 +; 已快取腳本(~850 檔),實測使每次閒置後的請求從 0.23s 暴增至 2.5s。 +; 拉長至 30 秒:程式碼變更最多 30 秒後生效;需要立即生效時執行 +; docker compose exec app kill -USR2 1(php-fpm 平滑重載,約 1 秒) +opcache.enable=1 +opcache.memory_consumption=192 +opcache.max_accelerated_files=20000 +opcache.validate_timestamps=1 +opcache.revalidate_freq=30 diff --git a/docs/analysis/2026-06-11-performance-optimization-plan.md b/docs/analysis/2026-06-11-performance-optimization-plan.md index 9b2be70..c40ce3a 100644 --- a/docs/analysis/2026-06-11-performance-optimization-plan.md +++ b/docs/analysis/2026-06-11-performance-optimization-plan.md @@ -2,98 +2,68 @@ > 撰寫日期:2026-06-11 > 背景:使用者反映本機操作體驗差、回應慢。本文以實測數據定位根因,並依投資報酬率排序優化項目。 +> **狀態更新(2026-06-11)**:Phase 1 已執行完成,穩態延遲 2.5s → **0.2s**(約 10 倍改善)。執行結果見第三節,根因分析已依實測修正。 --- -## 一、實測基準(2026-06-11,本機 Docker 環境) +## 一、實測基準(2026-06-11 上午,修復前) | 量測項目 | 數值 | 備註 | |---------|------|------| | `GET /api/diving-offers`(冷) | **7.98 秒** | 經 nginx:8080 → php-fpm | -| 同端點第 2 次 | 6.07 秒 | | -| 同端點第 3 次(Redis 快取命中) | **2.49 秒** | 快取命中仍要 2.5 秒 → 瓶頸不在查詢 | -| 容器內全測試套件 | 64.4 秒 | 同套件本機 PHP 僅 4.5 秒(**14 倍差距**) | +| 同端點(Redis 快取命中、間隔數秒) | **2.3~2.5 秒** | 快取命中仍要 2.5 秒 → 瓶頸不在查詢 | +| 同端點(2 秒內連發) | 0.23 秒 | 關鍵線索:短窗口內其實很快 | +| 容器內全測試套件 | 64.4 秒 | 同套件本機 PHP 僅 4.5 秒 | -**判讀**:Redis 快取命中後仍需 2.5 秒,代表時間幾乎都花在「PHP 啟動 + 載入框架程式碼」,而非業務邏輯或資料庫。瓶頸在環境層,不在程式碼。 +## 二、根因分析(依實測修正) -## 二、根因分析(已驗證) +> ⚠️ 初版文檔判定「OPcache 被停用」是**錯誤診斷**——當時用 `php -r`(CLI)量測,而 `opcache.enable_cli` 預設關閉造成假象。php-fpm 的 OPcache 一直是啟用的(851 個腳本在快取中)。正確根因如下: -1. **OPcache 已載入但被停用**(容器內實測 `opcache_get_status` 回報 disabled;`docker/php/local.ini` 無任何 opcache 設定)。每個 HTTP 請求都重新編譯 Laravel 全框架數千個 PHP 檔。 -2. **Windows bind mount `./:/var/www`**(docker-compose.yml:16)。每次檔案讀取都跨越 Windows↔Linux 檔案系統邊界(Docker Desktop 的檔案橋接是已知效能黑洞),與根因 1 疊加:每請求數千次跨界 I/O。 -3. 次要因素: - - `cfdive-nginx` healthcheck 持續失敗(`wget http://localhost/` connection refused),雖不影響服務但遮蔽真正的健康狀態。 - - `SESSION_DRIVER=database`、`QUEUE_CONNECTION=database`:queue worker 輪詢 MySQL;API 多為 stateless 影響小,列為觀察項。 - - `APP_DEBUG=true`:開發環境正常設定,影響相對小。 +1. **`opcache.revalidate_freq=2`(預設值)× Windows bind mount**:每隔 2 秒,下一個請求就要跨 Windows↔Linux 檔案系統邊界重新 stat 全部 ~850 個已快取腳本。實測鐵證:2 秒窗口內 0.23s,窗口過期後 2.3~2.5s。一般操作節奏(點一下、看一下、再點)幾乎每次都踩在窗口外,所以「每個操作都慢」。 +2. **Entrypoint 的 composer 重裝條件用 mtime 比對**:`composer.json -nt vendor/autoload.php`——git checkout/pull 會更新 composer.json 的 mtime,導致每次分支操作後的容器啟動都重跑 `composer install`(bind mount 上 autoload 生成耗時數分鐘,**期間 php-fpm 未啟動、全站 502**)。 +3. **nginx healthcheck 誤報**:busybox wget 對 `localhost` 先解析 IPv6 `::1`,nginx 只 listen IPv4 → 永遠 connection refused → 永遠 unhealthy。 +4. 觀察項(未處理):cron 每分鐘 `schedule:run` 以 CLI 啟動 PHP(CLI 無 OPcache + bind mount),造成偶發 ~2 秒突波;`SESSION/QUEUE=database`;`APP_DEBUG=true`。 -## 三、優化計畫 +## 三、Phase 1 執行紀錄(2026-06-11 完成,branch perf/phase1-opcache) -### Phase 1 — 立即(0.5 天內,預期把 API 壓到 0.5 秒以下) +| 項目 | 修改 | 結果 | +|------|------|------| +| O1.1 OPcache 調校 | `docker/php/local.ini`:`revalidate_freq` 2→**30**、memory 128→192、max_files 10000→20000 | 穩態 **0.20~0.27s**(前:2.3~2.5s);每 30 秒閒置後僅一次 ~1.2s revalidate | +| O1.2 healthcheck 修復 | docker-compose.yml:`http://localhost/` → `http://127.0.0.1/` | nginx 顯示 **healthy** | +| O1.3 composer 重裝條件 | entrypoint 改為 composer.lock **內容比對**(`cmp` vs `vendor/.composer.lock.installed` 標記檔) | git 分支操作不再觸發開機重裝與 502 窗口 | -**O1.1 啟用 OPcache(投報率最高的一項)** +### 修復後量測(同一端點、每 5 秒一次) -在 `docker/php/local.ini` 加入開發友善設定後 rebuild app 容器: - -```ini -opcache.enable=1 -opcache.enable_cli=0 -opcache.memory_consumption=192 -opcache.max_accelerated_files=20000 -; 開發環境:仍偵測檔案變更,存檔即生效 -opcache.validate_timestamps=1 -opcache.revalidate_freq=0 +``` +#1: 1.96s(cron 突波) #2: 0.22s #3: 0.21s #4: 0.20s #5: 0.27s #6: 0.26s +閒置 35 秒後:1.19s(revalidate,每 30 秒至多一次)→ 下一次 0.30s ``` -驗收:`GET /api/diving-offers` 快取命中請求 < 500ms(對照基準 2.49s)。 +### 開發注意事項 -**O1.2 修復 nginx healthcheck** +- 後端程式碼變更最多 **30 秒**後生效;要立即生效:`docker compose exec app kill -USR2 1`(php-fpm 平滑重載,約 1 秒) +- `local.ini` 與 entrypoint 是 build 進 image 的,改動需 `docker compose build app && docker compose up -d app` +- 此設定對 VPS 同樣適用且有益(Linux 原生 stat 便宜,revalidate=30 純賺) -診斷 `wget http://localhost/` 為何 connection refused(常見原因:nginx 只 listen 特定 server_name、或 healthcheck 應改打 `127.0.0.1`),修正 docker-compose.yml:63 的 healthcheck。驗收:`docker compose ps` 顯示 nginx healthy。 +## 四、後續階段(依新數據重新評估) -**O1.3 `composer dump-autoload -o`** +### Phase 2 — 結構性(暫緩) -寫入 Dockerfile / entrypoint,減少 autoload 時的檔案系統探測。 - -### Phase 2 — 結構性(若 Phase 1 後仍 > 1 秒才執行,0.5~1 天) - -**O2.1 消除 Windows bind mount I/O** - -選項(擇一,建議 a 先試): -- (a) **vendor/ 改用 named volume**:`- vendor-data:/var/www/vendor`,框架程式碼(I/O 大宗)留在 Linux 原生檔案系統,原始碼仍可即時編輯。需在容器內跑 `composer install`。 -- (b) **整個 repo 搬入 WSL2 檔案系統**(`\\wsl$\Ubuntu\...`),IDE 透過 WSL remote 開啟。I/O 改善最徹底(10~50 倍),但改變現有 laragon 工作流程,遷移成本較高。 - -**O2.2 SESSION/QUEUE driver 評估** - -Redis 已在跑,`SESSION_DRIVER=redis`、`QUEUE_CONNECTION=redis` 可移除 queue worker 對 MySQL 的輪詢壓力。屬順手改,不是主瓶頸。 +原計畫的 vendor named volume / WSL2 遷移以「Phase 1 後仍 >1s」為觸發條件——實測穩態 0.2s,**未達觸發條件,暫緩**。若未來 cron 突波造成困擾,優先處理觀察項 4(如 schedule:run 改由長駐 worker、或縮減無任務時的 bootstrap)。 ### Phase 3 — 應用層體感(與環境無關,部署後使用者也受益,1~2 天) -**O3.1 課程封面圖未壓縮** +**O3.1 課程封面圖未壓縮**:聊天圖片已有 `scaleDown(2048) + toJpeg(85)`(`BookingMessageController.php:115`),但 `CourseImageController` 原檔直存。建議套用相同縮圖管線 + 前端 `loading="lazy"`。應開 openspec change(影響 `course-image-upload` 規格)。 -聊天圖片已有 `scaleDown(2048) + toJpeg(85)` 處理(`BookingMessageController.php:115`),但 `CourseImageController` 上傳的封面/圖庫**原檔直存**。手機拍攝的 5~10MB 原圖會直接進課程列表頁。建議:套用與聊天圖片相同的縮圖管線,列表用圖另出 ~600px 縮圖;前端 `` 加 `loading="lazy"`。 +**O3.2 無分頁的列表端點**:`/api/member/bookings`、`/api/provider/bookings`、`/api/admin/bookings` 皆 `->get()` 全量。資料量成長後線性變慢,建議金流上線前補分頁。 -**O3.2 無分頁的列表端點** - -`GET /api/member/bookings`、`/api/provider/bookings`、`/api/admin/bookings` 皆為 `->get()` 全量撈取。目前資料量小無感,預約數成長後會線性變慢。建議在金流上線前(資料開始累積)補分頁。 - -**O3.3 前端載入體感** - -- 路由已全面 lazy-load(已驗證,不需處理)。 -- 列表頁加 skeleton 載入佔位,消除「白屏等待」體感。 -- 搜尋輸入加 debounce(300ms),減少連發請求。 +**O3.3 前端載入體感**:列表頁 skeleton 佔位、搜尋 debounce(300ms)。路由已全面 lazy-load(已驗證,不需處理)。 ### 生產環境註記(部署時順手做) - `php artisan optimize`(config / route / view cache) -- 生產 OPcache 改 `validate_timestamps=0`(部署時重啟 php-fpm 刷新) +- 生產 OPcache 可進一步 `validate_timestamps=0`(部署時 `kill -USR2 1` 重載刷新) -## 四、執行順序與驗收 +## 五、驗收方式 -``` -第 1 步 O1.1 OPcache → 重測基準(預期 2.49s → <0.5s) -第 2 步 O1.2 + O1.3 順手修 -第 3 步 視重測結果決定是否做 O2.1(>1s 才做) -第 4 步 O3.1 圖片壓縮(開 openspec change,影響 course-image-upload 規格) -第 5 步 O3.2 / O3.3 排入一般 backlog -``` - -每步驗收一律以第一節的 curl 量測方式對照,數據寫回本文件。 +每步以同一 curl 量測對照(`curl -s -o /dev/null -w "%{time_total}" http://127.0.0.1:8080/api/diving-offers`),數據寫回本文件。