跳轉到

運算與編排

運算層分為兩個平面:資料平面(ECS on EC2)運行長期存活的 API 伺服器和逐使用者 Worker 程序,以及控制平面(Lambda + SQS)負責編排 Worker 生命週期、背景任務和維護作業。這種分離意味著編排器沒有狀態可丟失,而 Worker 沒有管理開銷。

運維保證

透過 ECS 滾動式更新搭配斷路器實現零停機部署(100% 最低健康、200% 最大容量)。新 Task 若未通過健康檢查則自動回滾。控制平面無單點故障 — Lambda 函式天生具備高可用性,由 AWS 管理重試。資料平面透過維護 Lambda 在 60 秒內從任何單一容器故障中恢復。


ECS 叢集

叢集使用兩個容量提供者 (Capacity Provider),分別由獨立的 Auto Scaling Group 支撐,各自針對其工作負載特性最佳化:

flowchart LR subgraph cluster["ECS 叢集"] subgraph apiCP["API 容量提供者"] API["m6i.large<br/>2 個 API 任務"] end subgraph workerCP["Worker 容量提供者"] W["r6i.large<br/>每執行個體約 30 個 Worker"] end end subgraph control["無伺服器控制平面"] SQS["SQS FIFO x2"] --> L["Lambda x5"] EB["EventBridge"] --> L end L -->|"RunTask / Pool Claim"| W

為什麼需要兩個容量提供者

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 滾動式更新搭配斷路器:

  1. 註冊新的 Task 定義
  2. ECS 啟動新 Task(最多 200% 容量)
  3. ALB 健康檢查確認新 Task 健康
  4. 舊 Task 排空連線(300s 註銷延遲)
  5. 舊 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 數量,與目標比較,啟動或終止以達到期望的池大小。

編排器流程

sequenceDiagram participant API as FastAPI participant SQS as worker-control.fifo participant LC as λ worker_control participant Redis as Valkey participant Pool as 池 Worker participant Claim as pool-claim 佇列 participant ECS as ECS RunTask API->>SQS: 發送 start_worker 訊息 SQS->>LC: 觸發 Lambda LC->>Redis: GET worker:active:{user_id} alt Worker 已活躍 LC-->>SQS: 刪除訊息(無操作) else 無活躍 Worker LC->>Redis: 檢查池 Worker alt 池中有可用 Worker LC->>Claim: 發送認領訊息(user_id, credentials) Claim->>Pool: 池 Worker 接收認領 Pool->>Redis: SET worker:active:{user_id}(TTL 30s) Pool->>Pool: 載入憑證,連線券商 Note over Pool: 約 332ms 就緒 else 池為空 LC->>ECS: RunTask(Worker Task 定義) ECS->>ECS: 在容量提供者上排程 Note over ECS: 約 3103ms 就緒 end end

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 重新驅動回主佇列。