Files
awoooi/docs/runbooks/RUNBOOK-WORKER-HPA.md
OG T 89e05e6ea2 docs: ADR-037 + 監控架構提案 + Runbooks
- ADR-037 監控增強架構
- MONITORING_MASTER_PLAN 主計畫
- MASTER_EXECUTION_SCHEDULE 執行排程
- Phase D/E/Worker HPA Runbooks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-29 16:04:08 +08:00

9.3 KiB
Raw Blame History

RunBook: Worker HPA — 水平自動擴展設定

類型: 操作型 RunBook
優先級: 🔴 P0Worker 目前單點故障風險)
建立: 2026-03-29 12:35 (台北)
建立者: Antigravity
工時預估: 3060 分鐘
前置條件: 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 預設沒有安裝 KEDAKubernetes Event-driven Autoscaling
  • 最保守方案:設定 min:1 max:3以 CPU 為指標

方案比較

方案 優點 缺點 適合性
A: CPU HPA立即可行 零依賴,立即部署 不直接反應 Queue 長度 推薦(短期)
B: KEDA Redis Stream HPA 最精確,按 Queue 長度擴縮 需安裝 KEDA operator 🟡 中期規劃
C: 固定 2 副本(無 HPA 簡單穩定 浪費資源 不推薦

決策:採用方案 ACPU HPA並記錄方案 B 的未來路徑。


Step 1: 確認 Worker 資源設定

# 查看現有 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"

如果沒有設定 resourcesHPA 無法正常運作! 必須先在 08-deployment-worker.yaml 加入資源限制。


Step 2: 更新 k8s/awoooi-prod/12-hpa.yaml

在現有檔案末尾追加 Worker HPA

# =============================================================================
# Worker HPA追加到 12-hpa.yaml 末尾)
# =============================================================================
# K-Worker 2026-03-29: Worker HPACPU 指標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 有資源設定

# 查看現有設定
kubectl get deployment awoooi-worker -n awoooi-prod -o jsonpath='{.spec.template.spec.containers[0].resources}'

若無資源設定,在 08-deployment-worker.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: 部署

# 方法 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 觸發

# 模擬大量事件涌入(謹慎,在非尖峰時段執行)
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

# 未來安裝 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 安裝指令(未來執行):

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%,非 <unknown>
Worker 正常運行 kubectl get pod -n awoooi-prod -l app=awoooi-worker 顯示 Running
最小副本 Worker 期望副本數 = 1

⚠️ 架構安全補丁2026-03-29 更新,部署前必讀)

來源:ARCHITECTURAL_RISK_WAR_GAME.md 深度沙盤推演,代碼確認級別

補丁 1XCLAIM + Active Sweeper部署 HPA 的前置條件)

現況signal_worker.py 完全沒有 Redis PEL 孤兒任務回收機制。

影響Worker Pod 被 HPA 縮容(或非優雅崩潰)時,正在處理的任務卡在 Redis PELPending Entries List中永久無人處理。

🔴 HPA 必須在 XCLAIM 機制合併 main 之後才能部署!

需要在 signal_worker.py 加入的兩個機制:

# 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)

補丁 2terminationGracePeriodSeconds 三層對齊

現況signal_worker.pystop() timeout = 5 秒AI 分析任務最長 60 秒。K8s 的 terminationGracePeriodSeconds 未設定(預設 30 秒)。兩個值都不夠,且彼此不對齊。

需要同時修改兩個地方

# 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
# 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

合規確認指令(部署後必須執行)

# 確認 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 秒內完成