在這次系統重構中,我們面對了一個非常典型但容易被低估的問題:註冊流程中同步呼叫外部抽獎 API,導致整個 PHP-FPM 在高延遲情境下被拖垮。
表面上這只是一個「註冊後拿 QRCode」的流程,但在實際運行環境中,它是一個高度耦合、強依賴外部系統的同步鏈路。
一、原始架構:看似簡單,但隱含風險
原始流程如下:
使用者註冊
→ Laravel Controller
→ 呼叫 MIRA 抽獎 API(HTTP blocking)
→ 取得 QRCode
→ 回傳前端
這種設計在低流量下沒有問題,但它有一個致命特性:
HTTP request thread 被外部 API 的延遲完全綁死
二、真正的問題不是效能,而是失敗模式
這個架構的問題不在平均效能,而在「尾端延遲與不穩定性」。
當外部 API 出現以下情況:
- 回應時間從 50ms 上升到 500ms+
- 間歇性 timeout
- 網路抖動
- 瞬間流量壓力
Laravel PHP-FPM 會發生:
worker 被長時間占用 → thread pool 被耗盡 → 502 / 504 cascade failure
這種問題的特徵是:
- 平常完全正常
- 一旦出問題就是系統級崩潰
三、重構目標
這次重構的核心目標不是「加速」,而是:
將不可控的外部依賴從 request lifecycle 中移除
具體目標如下:
- API 回應時間 < 50ms
- 外部 API 延遲不影響使用者體驗
- 系統具備削峰能力(burst traffic handling)
- 支援失敗重試與最終一致性
四、新架構:事件驅動 + Queue 解耦
重構後的流程如下:
使用者註冊
→ 寫入 User / Redemption(pending)
→ dispatch Queue Job
→ 立即回傳前端
Queue Worker
→ 呼叫 MIRA API
→ 更新 Redemption 狀態
前端
→ polling / status query
→ 完成後顯示 QRCode
五、核心設計轉變
1. 從「同步結果」變成「狀態機」
系統從:
request → response(必須立即得到 QRCode)
轉為:
pending → processing → completed / failed
這代表一個重要轉變:
系統從即時一致性,轉向最終一致性(eventual consistency)
2. Controller 不再做任何外部 I/O
重構後 Controller 僅負責:
- DB 寫入
- 狀態初始化
- Queue dispatch
完全移除:
- HTTP external call
- long latency operation
- external dependency blocking
3. Queue 成為系統的「緩衝層」
Queue 的角色不只是 background job,而是:
absorbing burst traffic + isolating external instability
它讓系統具備:
- 削峰能力
- 重試能力
- 故障隔離能力
4. 前端改為狀態驅動(Polling)
由於結果變為非同步,前端轉為:
POST /register
→ 200 OK (pending)
GET /redemption/status
→ completed → render QRCode
UI 的本質從:
「拿結果」
變成:
「觀察狀態變化」
六、可靠性設計:三層防護機制
為了確保不重複發券與系統穩定性,架構引入三層保護:
1. DB 層:唯一性約束
user_id UNIQUE
確保一個使用者只會產生一筆 redemption。
2. 狀態鎖:Optimistic Lock
UPDATE ... WHERE status = 'pending'
避免多個 worker 同時處理同一筆任務,防止 race condition。
3. 外部 API:Idempotency Key
使用:
redemption_id → request_id
確保:
- retry 不會重複扣庫存
- timeout 不會造成重複發券
七、失敗模式的轉變
原本系統失敗模式:
MIRA 慢 → request 卡住 → PHP worker 滿 → 全站 502
新系統失敗模式:
MIRA 慢 → queue backlog → 使用者等待變長,但系統不崩潰
八、Queue 設計與系統治理
1. Retry 策略
- exponential backoff
- retry limit
- jitter 避免 thundering herd
2. Dead Letter Queue(DLQ)
避免無限失敗任務卡住系統。
3. Reconciliation Job
定期掃描:
WHERE status = 'processing'
AND updated_at < NOW() - INTERVAL 5 MINUTE
避免 worker crash 導致狀態卡死。
九、Polling 的定位
Polling 在這個架構中不是最佳解,而是:
最穩定的 baseline solution
它的角色是:
- 簡單
- 可控
- 不依賴長連線基礎設施
未來可進一步升級為:
- SSE(中階)
- WebSocket(高互動)
但本質取捨是:
push 是連線成本,polling 是請求成本
十、這次重構的本質
這次架構改造不是效能優化,而是系統設計哲學的轉換:
從「同步獲取結果」
→ 轉為「非同步狀態演進」
結論
當系統開始依賴外部 API 時,真正重要的不是速度,而是:
- 是否可以隔離
- 是否可以重試
- 是否可以削峰
- 是否不會拖垮核心服務
這次重構的核心成果是:
將一個脆弱的同步鏈路,改造成可容錯的事件驅動系統
沒有留言:
張貼留言