2026年6月24日 星期三

從同步阻塞到事件驅動:一次外部 API 抽獎系統的架構重構實戰

 在這次系統重構中,我們面對了一個非常典型但容易被低估的問題:註冊流程中同步呼叫外部抽獎 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 時,真正重要的不是速度,而是:

  • 是否可以隔離
  • 是否可以重試
  • 是否可以削峰
  • 是否不會拖垮核心服務

這次重構的核心成果是:

將一個脆弱的同步鏈路,改造成可容錯的事件驅動系統

沒有留言:

張貼留言

從同步阻塞到事件驅動:一次外部 API 抽獎系統的架構重構實戰

 在這次系統重構中,我們面對了一個非常典型但容易被低估的問題: 註冊流程中同步呼叫外部抽獎 API,導致整個 PHP-FPM 在高延遲情境下被拖垮 。 表面上這只是一個「註冊後拿 QRCode」的流程,但在實際運行環境中,它是一個高度耦合、強依賴外部系統的同步鏈路。 一、原...