# RunBook: Worker HPA — 水平自動擴展設定 > **類型**: 操作型 RunBook > **優先級**: 🔴 P0(Worker 目前單點故障風險) > **建立**: 2026-03-29 12:35 (台北) > **建立者**: Antigravity > **工時預估**: 30–60 分鐘 > **前置條件**: K3s 叢集健康(120/121 皆 Ready) --- ## 背景與現況 ### 🔍 精確現況診斷 **現有 HPA 配置 (`12-hpa.yaml`)**: | Deployment | Min | Max | CPU 閾值 | Memory 閾值 | |-----------|-----|-----|---------|------------| | awoooi-api | 2 | 6 | 70% | 80% | | awoooi-web | 2 | 6 | 70% | 80% | | **awoooi-worker** | ❌ 無 | ❌ 無 | — | — | **Worker 的特殊性**: - Worker 消費 Redis Streams (Event Bus) - 不像 API/Web 依賴 CPU/Memory 觸發,應依賴 **Queue 長度觸發** - 但 K3s 預設沒有安裝 KEDA(Kubernetes Event-driven Autoscaling) - **最保守方案**:設定 min:1 max:3,以 CPU 為指標 --- ## 方案比較 | 方案 | 優點 | 缺點 | 適合性 | |------|------|------|-------| | **A: CPU HPA(立即可行)** | 零依賴,立即部署 | 不直接反應 Queue 長度 | ✅ 推薦(短期) | | B: KEDA Redis Stream HPA | 最精確,按 Queue 長度擴縮 | 需安裝 KEDA operator | 🟡 中期規劃 | | C: 固定 2 副本(無 HPA) | 簡單穩定 | 浪費資源 | ❌ 不推薦 | **決策**:採用方案 A(CPU HPA),並記錄方案 B 的未來路徑。 --- ## Step 1: 確認 Worker 資源設定 ```bash # 查看現有 Worker Deployment 資源限制 kubectl get deployment awoooi-worker -n awoooi-prod -o yaml | grep -A 20 resources # 預期看到: # resources: # requests: # cpu: "100m" # memory: "256Mi" # limits: # cpu: "500m" # memory: "512Mi" ``` **如果沒有設定 resources,HPA 無法正常運作!** 必須先在 `08-deployment-worker.yaml` 加入資源限制。 --- ## Step 2: 更新 k8s/awoooi-prod/12-hpa.yaml 在現有檔案末尾追加 Worker HPA: ```yaml # ============================================================================= # Worker HPA(追加到 12-hpa.yaml 末尾) # ============================================================================= # K-Worker 2026-03-29: Worker HPA(CPU 指標,min:1 max:3) # 注意:Worker 消費 Redis Streams,未來可升級為 KEDA Redis Stream 指標 # 建立者:Antigravity # ============================================================================= --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: awoooi-worker-hpa namespace: awoooi-prod labels: app.kubernetes.io/name: awoooi app.kubernetes.io/component: worker annotations: description: "Worker 水平自動擴展 (1-3 replicas, 70% CPU)" note: "未來可升級為 KEDA Redis Stream 指標,按 Queue 長度動態擴縮" spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: awoooi-worker minReplicas: 1 # 保持最少 1 個處理事件 maxReplicas: 3 # 2 節點叢集的合理上限 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleUp: stabilizationWindowSeconds: 120 # Worker 擴展比 API 保守(120s vs 60s) policies: - type: Pods value: 1 periodSeconds: 120 scaleDown: stabilizationWindowSeconds: 600 # Worker 縮容非常保守,避免事件處理中斷 policies: - type: Pods value: 1 periodSeconds: 300 ``` --- ## Step 3: 確認 Worker Deployment 有資源設定 ```bash # 查看現有設定 kubectl get deployment awoooi-worker -n awoooi-prod -o jsonpath='{.spec.template.spec.containers[0].resources}' ``` 若無資源設定,在 `08-deployment-worker.yaml` 加入: ```yaml # apps/api/src/workers 對應的 K8s Deployment # 在 container spec 加入: resources: requests: cpu: "100m" # Worker 正常負載估算 memory: "256Mi" limits: cpu: "500m" # 防止單 Worker 吃掉所有 CPU memory: "512Mi" ``` --- ## Step 4: 部署 ```bash # 方法 A:直接 apply(推薦,只更新 HPA) kubectl apply -f k8s/awoooi-prod/12-hpa.yaml # 確認 HPA 建立成功 kubectl get hpa -n awoooi-prod # 預期輸出: # NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS # awoooi-api-hpa Deployment/api 5%/70% 2 6 2 # awoooi-web-hpa Deployment/web 3%/70% 2 6 2 # awoooi-worker-hpa Deployment/worker 8%/70% 1 3 1 ← 新增 # 方法 B:透過 CD 觸發(標準流程) git add k8s/awoooi-prod/12-hpa.yaml git commit -m "feat(k8s): add Worker HPA (min:1 max:3 CPU 70%)" git push origin main ``` --- ## Step 5: 壓力測試驗證 HPA 觸發 ```bash # 模擬大量事件涌入(謹慎,在非尖峰時段執行) for i in {1..100}; do curl -s -X POST http://192.168.0.120:32334/api/v1/webhooks/alertmanager \ -H "Content-Type: application/json" \ -d '{ "version": "4", "status": "firing", "alerts": [{"status": "firing", "labels": {"alertname": "LoadTest", "severity": "info"}, "annotations": {}}] }' & done # 觀察 HPA 反應(每 15 秒看一次) watch -n 15 'kubectl get hpa awoooi-worker-hpa -n awoooi-prod' ``` --- ## 中期路線圖:升級 KEDA Redis Stream HPA ```yaml # 未來安裝 KEDA 後,可替換為更精確的 HPA: apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: awoooi-worker-scaledobject namespace: awoooi-prod spec: scaleTargetRef: name: awoooi-worker minReplicaCount: 1 maxReplicaCount: 5 triggers: - type: redis metadata: address: "192.168.0.188:6380" listName: "awoooi:events" # Redis Stream Key listLength: "20" # 每個 Pod 處理最多 20 個待處理事件 ``` KEDA 安裝指令(未來執行): ```bash kubectl apply -f https://github.com/kedacore/keda/releases/download/v2.13.1/keda-2.13.1.yaml ``` --- ## 驗收標準 | 項目 | 通過條件 | |------|---------| | HPA 建立 | `kubectl get hpa -n awoooi-prod` 顯示 `awoooi-worker-hpa` | | 指標正常 | TARGETS 顯示實際 CPU%,非 `` | | Worker 正常運行 | `kubectl get pod -n awoooi-prod -l app=awoooi-worker` 顯示 Running | | 最小副本 | Worker 期望副本數 = 1 | --- ## ⚠️ 架構安全補丁(2026-03-29 更新,部署前必讀) > 來源:`ARCHITECTURAL_RISK_WAR_GAME.md` 深度沙盤推演,代碼確認級別 ### 補丁 1:XCLAIM + Active Sweeper(部署 HPA 的前置條件) **❌ 現況**:`signal_worker.py` 完全沒有 Redis PEL 孤兒任務回收機制。 **影響**:Worker Pod 被 HPA 縮容(或非優雅崩潰)時,正在處理的任務卡在 Redis PEL(Pending Entries List)中永久無人處理。 > 🔴 **HPA 必須在 XCLAIM 機制合併 main 之後才能部署!** 需要在 `signal_worker.py` 加入的兩個機制: ```python # 1. 啟動時接管孤兒(_claim_orphaned_tasks,在 start() 中調用) # 2. 運行中持續掃描(_reclaim_loop,與 _consume_loop 並行) async def _reclaim_loop(self, interval_s: int = 300) -> None: """每 5 分鐘主動掃描 PEL,接管閒置超過 5 分鐘的孤兒任務""" while self._running: await asyncio.sleep(interval_s) claimed = await self._claim_orphaned_tasks(idle_ms=300_000) if claimed > 0: logger.info("active_sweeper_claimed", count=claimed) ``` --- ### 補丁 2:terminationGracePeriodSeconds 三層對齊 **❌ 現況**:`signal_worker.py` 的 `stop()` timeout = **5 秒**,AI 分析任務最長 60 秒。K8s 的 `terminationGracePeriodSeconds` 未設定(預設 30 秒)。兩個值都不夠,且彼此不對齊。 **需要同時修改兩個地方**: ```yaml # k8s/awoooi-prod/08-deployment-worker.yaml spec: template: spec: terminationGracePeriodSeconds: 90 # 🆕 必須設定(比 Python timeout 多 15 秒緩衝) containers: - name: awoooi-worker lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 5"] # 讓 K8s 先更新 Endpoint 再發 SIGTERM ``` ```python # apps/api/src/workers/signal_worker.py async def stop(self) -> None: self._running = False if self._task: try: await asyncio.wait_for(self._task, timeout=75.0) # 🆕 從 5 秒改為 75 秒 except (TimeoutError, asyncio.CancelledError): self._task.cancel() logger.info("signal_worker_stopped") ``` **三層數值關係**: ``` preStop sleep: 5s Python timeout: 75s ← 比 K8s grace period 少 15s 緩衝 K8s grace period: 90s ← terminationGracePeriodSeconds ``` --- ### 合規確認指令(部署後必須執行) ```bash # 確認 terminationGracePeriodSeconds 已生效 kubectl get deployment awoooi-worker -n awoooi-prod \ -o jsonpath='{.spec.template.spec.terminationGracePeriodSeconds}' # 預期:90 # 模擬縮容,確認優雅關機 kubectl scale deployment awoooi-worker -n awoooi-prod --replicas=0 kubectl logs -n awoooi-prod -l app=awoooi-worker --tail=20 # 預期看到:shutdown_signal_received → signal_worker_shutting_down → signal_worker_stopped # 整個流程在 90 秒內完成 ```