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

314 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 資源設定
```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"
```
**如果沒有設定 resourcesHPA 無法正常運作!** 必須先在 `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 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 有資源設定
```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%,非 `<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` 加入的兩個機制:
```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)
```
---
### 補丁 2terminationGracePeriodSeconds 三層對齊
**❌ 現況**`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 秒內完成
```