運算與編排¶
運算層分為兩個平面:資料平面(ECS on EC2)運行長期存活的 API 伺服器和逐使用者 Worker 程序,以及控制平面(Lambda + SQS)負責編排 Worker 生命週期、背景任務和維護作業。這種分離意味著編排器沒有狀態可丟失,而 Worker 沒有管理開銷。
運維保證
透過 ECS 滾動式更新搭配斷路器實現零停機部署(100% 最低健康、200% 最大容量)。新 Task 若未通過健康檢查則自動回滾。控制平面無單點故障 — Lambda 函式天生具備高可用性,由 AWS 管理重試。資料平面透過維護 Lambda 在 60 秒內從任何單一容器故障中恢復。
ECS 叢集¶
叢集使用兩個容量提供者 (Capacity Provider),分別由獨立的 Auto Scaling Group 支撐,各自針對其工作負載特性最佳化:
為什麼需要兩個容量提供者
API Task 需要 CPU 餘裕來處理請求(運算密集型)。Worker Task 需要記憶體來維持券商 SDK 連線,但在閒置期間幾乎不使用 CPU(記憶體密集型)。將它們混合在同一個執行個體類型上會在兩個方向都浪費資源。
API 容量提供者¶
| 參數 | 值 |
|---|---|
| 執行個體類型 | m6i.large(2 vCPU、8 GB) |
| 每個執行個體的 Task 數 | 2 |
| 每個 Task 的 CPU | 1024 單元(0.5 vCPU) |
| 每個 Task 的記憶體 | 1536 MB 軟性 / 3072 MB 硬性 |
| 網頁伺服器 | Gunicorn 搭配 8 個 Uvicorn Worker |
| 網路模式 | bridge(動態主機連接埠) |
| 部署方式 | 滾動式更新,啟用斷路器 (circuit breaker) |
| 最低健康百分比 | 100%(零停機部署) |
| 最大百分比 | 200%(部署期間雙倍容量) |
自動擴展策略¶
API 服務使用目標追蹤擴展 (target-tracking scaling),基於兩個維度:
| 策略 | 目標 | 擴展冷卻時間 | 縮減冷卻時間 |
|---|---|---|---|
| CPU 使用率 | 70% 平均 | 60s | 300s |
| 請求數量 | 每目標 1000 req/min | 60s | 300s |
縮減刻意放慢(300s 冷卻時間),以避免在開盤/收盤前後的間歇性流量尖峰中產生震盪 (thrashing)。
滾動式部署¶
部署使用 ECS 滾動式更新搭配斷路器:
- 註冊新的 Task 定義
- ECS 啟動新 Task(最多 200% 容量)
- ALB 健康檢查確認新 Task 健康
- 舊 Task 排空連線(300s 註銷延遲)
- 舊 Task 停止
如果新 Task 未通過健康檢查,斷路器會自動回滾到上一個 Task 定義——無需人工介入。
Worker 容量提供者¶
| 參數 | 值 |
|---|---|
| 執行個體類型 | r6i.large(2 vCPU、16 GB) |
| 每個執行個體的 Task 數 | 30(保守值) |
| 每個 Task 的 CPU | 64 單元 |
| 記憶體軟性限制 | 384 MB |
| 記憶體硬性限制 | 1024 MB |
| 網路模式 | bridge(共享主機 ENI) |
| 期望數量 | 由 Lambda 編排器管理 |
容量計算¶
每個 r6i.large 提供 2048 CPU 單元和 16,384 MB RAM:
| 資源 | 可用量 | 每個 Task | 最大 Task 數 | 是否瓶頸? |
|---|---|---|---|---|
| CPU | 2048 單元 | 64 單元 | 2048 / 64 = 32 | 否 |
| 記憶體 | 16,384 MB | 384 MB 軟性 | 16384 / 384 = 42 | 是(軟性) |
| 記憶體(硬性) | 16,384 MB | 1024 MB 硬性 | 16384 / 1024 = 16 | 最差情況 |
每個執行個體 30 個 Task 的目標是保守的——預留了以下餘裕:
- 作業系統和 ECS 代理開銷(約 512 MB)
- 券商 API 呼叫期間的暫時性記憶體尖峰
- 容器執行期開銷
OOM 防護
如果 Worker 超過 1024 MB 硬性限制,Linux OOM Killer 只會終止該容器。EC2 執行個體和所有其他 Worker 不受影響。維護 Lambda 會在 60 秒內偵測到缺失的 Worker,編排器會自動重新啟動它。
Bridge 網路模式¶
Worker 使用 bridge 模式而非 awsvpc:
| 特性 | awsvpc | bridge |
|---|---|---|
| 每個 Task 一個 ENI | 是(各一個) | 否(共享) |
| 最大 Task 數(m/r large) | 約 3 | 30+ |
| 逐 Task 安全群組 | 是 | 否(主機安全群組) |
| 連接埠對應 | 靜態 | 動態 |
| 成本影響 | ENI 限制需要更多執行個體 | 高密度,更少執行個體 |
這個取捨是可以接受的,因為 Worker 只需要對外存取券商 API。它們不接收入站連線——所有通訊都透過 Redis 流轉。
Lambda 編排器¶
五個 Lambda 函式構成無伺服器控制平面:
| 函式 | 觸發方式 | 逾時 | 記憶體 | 並行數 | 用途 |
|---|---|---|---|---|---|
| worker_control | SQS FIFO (worker-control) | 60s | 256 MB | 50-500 | 啟動、停止、認領 Worker。Pool 分配和 RunTask 備援。 |
| order_tasks | SQS FIFO (order-tasks) | 120s | 256 MB | 50-500 | 背景成交確認。執行後向券商查詢訂單狀態。 |
| maintenance | EventBridge(每 60 秒) | 300s | 256 MB | 1 | 扇出協調器。掃描 Redis 中的所有 Worker 標記,分區工作,平行呼叫 maintenance_worker。 |
| maintenance_worker | Lambda invoke(來自 maintenance) | 30s | 256 MB | 100 | 處理個別孤立偵測批次。檢查 ECS Task 狀態,清理過期標記,停止孤立 Task。 |
| pool_manager | EventBridge(每 5 分鐘) | 60s | 256 MB | — | 計算池中 Worker 數量,與目標比較,啟動或終止以達到期望的池大小。 |
編排器流程¶
FIFO 保證
worker-control.fifo 佇列使用 user_id 作為訊息群組 ID。這確保同一使用者的多個啟動/停止命令按順序處理,防止停止命令在啟動完成之前到達的競態條件。
SQS 佇列¶
| 佇列 | 類型 | 可見性逾時 | 保留期 | DLQ | DLQ 最大接收次數 | 用途 |
|---|---|---|---|---|---|---|
| worker-control.fifo | FIFO | 90s | 1 天 | worker-control-dlq.fifo | 3 | Worker 生命週期命令(啟動、停止、認領)。訊息群組:user_id。 |
| order-tasks.fifo | FIFO | 180s | 1 天 | order-tasks-dlq.fifo | 3 | 成交確認、延遲訂單檢查。訊息群組:order_id。 |
| pool-claim | Standard | 10s | 5 分鐘 | — | — | 一次性認領訊息,發送給池中 Worker。保留期短,因為未認領的訊息已過時。 |
死信佇列 (Dead Letter Queue)¶
兩個 FIFO 佇列都設有 DLQ,捕捉在 3 次處理嘗試後仍然失敗的訊息。兩個 Lambda 處理器皆使用 ReportBatchItemFailures,因此只有特定失敗的記錄會被重試——同一批次中已成功處理的記錄不會被重新投遞,其接收次數也不會被錯誤累加。
| DLQ | 保留期 | CloudWatch 警報 | 儀表板 |
|---|---|---|---|
| worker-control-dlq.fifo | 14 天 | {env}-orchestrator-dlq-has-messages(> 0) |
有 |
| order-tasks-dlq.fifo | 14 天 | {env}-order-tasks-dlq-has-messages(> 0) |
有 |
兩個警報均發送至 orchestrator-alerts SNS 主題(電子郵件通知)。CloudWatch 儀表板並排顯示兩個 DLQ 的訊息數量。
DLQ 訊息代表真正的故障
由於使用了 ReportBatchItemFailures,只有真正連續失敗 3 次的訊息才會進入 DLQ——不會有因批次污染的誤報。常見原因:Redis 連線中斷、ECS 容量耗盡、券商 API 持續逾時。處理方式:檢查 CloudWatch Logs 中對應的 Lambda 錯誤,修復根本原因後,將訊息從 DLQ 重新驅動回主佇列。