Files
awoooi/docs/proposals/ARCHITECTURAL_RISK_WAR_GAME.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

841 lines
29 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.
# AWOOOI 架構風險全維度沙盤推演
# Architectural Risk Full-Spectrum War Game
> **文件類型**: 架構決策基礎(必讀,優先於所有 RunBook
> **建立**: 2026-03-29 13:37 (台北)
> **定位**: 本文件是所有執行計畫的上位文件,任何實施步驟必須以此為準
---
## 第零章:代碼確認的真實現況
在產出任何計畫前,必須先誠實面對代碼層面的真實狀況:
| 風險項目 | 代碼位置 | 真實現況 |
|---------|---------|---------|
| Worker SIGTERM | `signal_worker.py:450-455` | ✅ 已實作 SIGTERM 攔截 |
| Worker stop() timeout | `signal_worker.py:147` | ❌ **只有 5 秒**AI 任務最長 60 秒 |
| XCLAIM孤兒任務回收| `signal_worker.py` 全文 | ❌ **完全缺失**PEL 孤兒無人處理 |
| StatefulSet 硬阻斷 | `auto_repair_service.py` 全文 | ❌ **完全缺失**,只有 severity 和 risk 檢查 |
| TIER_ACTIONS Tier 1 | `auto_repair_service.py:411` | ❌ `restart_pod` 在 Tier 1DB/Redis 無例外 |
| OpenClaw Circuit Breaker | `sentry_webhook.py:289` | ❌ **只有 60s httpx timeout無斷路保護** |
| ESLint i18n plugin | `.eslintrc.js:20-22` | ❌ **只有 TODO 注解,未安裝** |
| Redis AOF 確認 | — | ⚠️ 未確認,需立即核查 |
| Visual Regression baseline | — | ❌ 未建立Mac 環境 ≠ CI 環境 |
---
## 第一章:已識別的 6 大致命衝突(首席架構師版)
首席架構師已準確識別的 6 個衝突,代碼確認加深嚴重度:
### 衝突一CI/CD 監控鐵幕導致部署癱瘓
**嚴重度**: 🔴 P0 | **類型**: 流程衝突
**代碼確認**`service-registry.yaml` 已有 60+ 服務,但 `validate_coverage.py` 尚未整合至 `cd.yaml`
**正確方案Soft Launch 三階段)**
```yaml
# .github/workflows/cd.yaml
# 階段一即時Warn-Only
- name: Service Registry Coverage Check
run: |
python ops/scripts/validate_coverage.py --warn-only
# exit 0 即使有缺失,只發 Telegram 警告
continue-on-error: true # ← 關鍵:不阻擋部署
# 階段二(待 Registry 完整後):正式 Block
# 將 continue-on-error 改為 false
```
---
### 衝突二Worker HPA + Redis PEL 孤兒任務
**嚴重度**: 🔴 P0 | **類型**: 邏輯衝突
**代碼確認**`signal_worker.py` 完全沒有 XCLAIM 邏輯。`start()` 方法只有 `_ensure_consumer_group()`,沒有 pending 任務回收。
**最小可行修復(加入 `start()` 方法)**
```python
# signal_worker.py
async def start(self) -> None:
if self._running:
return
await self._ensure_consumer_group()
# 🆕 關鍵:啟動時先接管已死亡 Worker 的孤兒任務
await self._claim_orphaned_tasks()
self._running = True
self._task = asyncio.create_task(self._consume_loop())
async def _claim_orphaned_tasks(self, idle_ms: int = 60000) -> int:
"""
XCLAIM 機制:接管超過 idle_ms 未 ACK 的 Pending 任務
場景:前一個 Worker Pod 在處理任務途中被 K8s 砍掉,
此任務卡在 PEL 中,新 Worker 啟動時必須接管。
idle_ms: 任務閒置超過此毫秒數才接管(預設 60 秒)
"""
redis_client = get_redis()
claimed_count = 0
try:
# 查詢 PEL 中所有 Pending 任務
pending = await redis_client.xpending_range(
STREAM_KEY, CONSUMER_GROUP,
min='-', max='+', count=100
)
for entry in pending:
# 只接管超過 idle_ms 未被處理的任務
if entry['time_since_delivered'] > idle_ms:
claimed = await redis_client.xclaim(
STREAM_KEY, CONSUMER_GROUP, CONSUMER_NAME,
min_idle_time=idle_ms,
message_ids=[entry['message_id']]
)
if claimed:
claimed_count += len(claimed)
logger.info(
"orphaned_task_claimed",
message_id=entry['message_id'],
original_consumer=entry['consumer'],
idle_ms=entry['time_since_delivered'],
)
except Exception as e:
# XCLAIM 失敗不應阻擋 Worker 啟動
logger.warning("xclaim_failed", error=str(e))
if claimed_count > 0:
logger.info("orphaned_tasks_recovered", count=claimed_count)
return claimed_count
```
**與 HPA 的部署順序**XCLAIM 必須先合併到 main才能部署 Worker HPA。
---
### 衝突三:告警風暴重疊(跨源 Incident 爆炸)
**嚴重度**: 🔴 P0 | **類型**: 資料流衝突
**代碼確認**`incident_service.py` 中的聚合邏輯是基於 `fingerprint` 字串匹配Sentry 和 Alertmanager 的 fingerprint 格式不同,無法跨源聚合。
**根本原因場景**
```
PostgreSQL 掛掉 →
Alertmanager: 1 個 PostgreSQLDown 告警
Sentry: 200 個 ConnectionRefused 告警(所有 API 請求)
→ 201 個獨立 Incident
→ 201 次 OpenClaw 分析Token 爆炸)
```
**全域災難冷卻期實作**
```python
# apps/api/src/services/incident_service.py 新增
GLOBAL_INCIDENT_DEBOUNCE_TTL = 300 # 5 分鐘全域冷卻期
P0_INFRASTRUCTURE_SERVICES = {
"postgres", "postgresql", "redis", "k8s-api", "etcd"
}
async def _check_global_incident_storm(self, signal_data: dict) -> str | None:
"""
檢查是否有活躍的 P0 基礎設施災難
若有 → 返回主 Incident ID關聯事件
若無 → 返回 None正常建立新 Incident
"""
redis = get_redis()
storm_key = "global:incident_storm:active"
# 判斷是否是 P0 基礎設施告警(優先處理,不關聯)
alert_name = signal_data.get("alert_name", "")
service = signal_data.get("target", "")
is_infra_p0 = any(svc in service.lower() for svc in P0_INFRASTRUCTURE_SERVICES)
if is_infra_p0 and signal_data.get("severity") == "critical":
# 設定全域風暴旗幟
main_incident_id = f"storm-{uuid.uuid4().hex[:8]}"
await redis.setex(storm_key, GLOBAL_INCIDENT_DEBOUNCE_TTL, main_incident_id)
logger.warning("global_incident_storm_detected", main_id=main_incident_id)
return None # P0 本身正常建立 Incident
# 非 P0 告警:檢查是否在風暴期間
active_storm_id = await redis.get(storm_key)
if active_storm_id:
logger.info(
"alert_correlated_to_storm",
storm_id=active_storm_id,
alert=alert_name,
)
return active_storm_id.decode() # 關聯到主 Incident不單獨分析
return None
```
---
### 衝突四Auto-Repair 誤殺有狀態服務
**嚴重度**: 🔴 P0 | **類型**: 架構衝突
**代碼確認**`auto_repair_service.py:411``TIER_ACTIONS[1]` 包含 `restart_pod``restart_container`。評估邏輯只檢查 `severity <= P2``RiskLevel <= MEDIUM`**完全沒有服務類型白名單**。
**最小可行修復(加入服務黑名單)**
```python
# auto_repair_service.py 新增常數與防護
# 🚨 不可自動重啟的服務(有狀態服務)
STATEFUL_SERVICE_BLACKLIST = frozenset({
"postgres", "postgresql", "awoooi-postgres",
"redis", "awoooi-redis", "redis-stack",
"clickhouse", "signoz-clickhouse",
"elasticsearch", "etcd",
"minio", "awoooi-minio",
})
async def evaluate_auto_repair(self, incident: Incident) -> AutoRepairDecision:
# ... 現有檢查 ...
# 🆕 新增:有狀態服務硬阻擋(必須在 Playbook 匹配之前)
affected_services = incident.affected_services or []
for service in affected_services:
if any(bl in service.lower() for bl in STATEFUL_SERVICE_BLACKLIST):
logger.warning(
"auto_repair_blocked_stateful_service",
incident_id=incident.incident_id,
service=service,
)
return AutoRepairDecision(
can_auto_repair=False,
reason=f"服務 {service} 為有狀態服務,禁止自動重啟,請統帥手動介入",
blocked_by="STATEFUL_SERVICE_GUARDRAIL",
)
# ... 後續現有邏輯 ...
```
---
### 衝突五:前端「合併地獄」(時序衝突)
**嚴重度**: 🟠 P1 | **類型**: 時序衝突
**正確鎖定主幹策略**
```
git flow 前端主權計畫:
Week 1: Feature Freeze
main 分支鎖定(禁止非 i18n 相關 PR 合併前端代碼)
Week 1: i18n 清零 PR唯一允許的前端 PR
branch: fix/i18n-zero-violation
→ 一次性修復所有 40+ 違規
→ 同步安裝 eslint-plugin-i18next先 warn 模式)
→ Merge to main
Week 1 完成後: Feature Unfreeze
→ 開始 Storybook PR
→ 開始 Omni-Terminal SSE Event Sourcing PR
→ ESLint 切換為 error 模式
```
---
### 衝突六Playwright HTTPS 憑證與網路盲區
**嚴重度**: 🟠 P1 | **類型**: 基礎設施衝突
**代碼確認**`playwright.config.ts` 需要確認當前設定。
```typescript
// playwright.config.ts 必要修改
export default defineConfig({
use: {
baseURL: process.env.BASE_URL || 'http://192.168.0.120:32335',
ignoreHTTPSErrors: true, // 🆕 自簽憑證必須忽略
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1, // 防止 Retina 差異
},
expect: {
toHaveScreenshot: {
threshold: 0.05,
maxDiffPixelRatio: 0.05,
},
},
});
```
---
## 第二章:被遺漏的 6 個更深層致命風險
首席架構師的 6 個衝突是正確的,但以下 6 個更深層的風險同樣在系統中存在:
### 深層風險 A.188 節點是單點故障SPOF——系統大腦失憶
**位置**: 192.168.0.188AI+Web 中心)
**影響**: .188 掛掉 = Ollama + OpenClaw + Redis + PostgreSQL + SigNoz **全部同時失效**
這是整個系統最致命的單點:
```
.188 掛掉時的連鎖崩潰:
Redis 失效 → Signal Worker 無法消費 → 告警全部積壓
PostgreSQL 失效 → K3s 控制面失去 Datastore → K3s 可能崩潰
OpenClaw 失效 → 所有 AI 分析停止 → Sentry/Alertmanager Webhook 排隊
SigNoz 失效 → 可觀測性盲區
K3s 崩潰 → AWOOOI API/Web Pod 全滅
沒有 AWOOOI → 無法收到告警 → 統帥無法操作 → 完全失聯
```
**緩解策略(非根治)**
```yaml
# OpenClaw 和 Worker 必須有 .188 失效時的降級模式
# 最低標準Telegram Bot 直接發送「.188 疑似失效」告警
# (繞過 AWOOOI API直接 curl Telegram API
# k8s/monitoring/alert-rules.yaml 新增
- alert: AIWebCenterDown
expr: probe_success{job="blackbox", target="http://192.168.0.188:8089/health"} == 0
for: 2m
annotations:
summary: ".188 AI 中心失聯,系統進入降級模式"
runbook: "docs/runbooks/RUNBOOK-188-FAILOVER.md"
```
---
### 深層風險 B可觀測性循環依賴觀測者的盲點
**這是架構上最諷刺的問題**
```
當 AWOOOI API 本身崩潰:
Alertmanager 想發 webhook 給 AWOOOI → AWOOOI 掛了webhook 失敗
Sentry 想發 webhook 給 AWOOOI → 同上
Telegram 通知要透過 AWOOOI → 同上
告警鏈路的最後一哩Telegram 通知)依賴於被監控對象本身!
```
**現有防護**ADR-035 已有!):
```yaml
# Alertmanager 有直接 Telegram 通知(繞過 AWOOOI
# 但需要確認alertmanager.yml 是否有 backup receiver
receivers:
- name: 'openclaw-api' # 主路徑(透過 AWOOOI
...
- name: 'direct-telegram' # 備援路徑(直接打 Telegram
webhook_configs:
- url: 'https://api.telegram.org/bot{TOKEN}/sendMessage'
```
**需要驗證**ADR-035 的三層防護機制是否真的覆蓋了「AWOOOI API 本身掛掉」的場景。
---
### 深層風險 COpenClaw 呼叫無 Circuit Breaker後端 AI 癱瘓傳播)
**代碼確認**`sentry_webhook.py:289``call_openclaw_analyzer()` 只有 `httpx.AsyncClient(timeout=60.0)`**沒有 Circuit Breaker**。
**場景**OpenClaw 高負載GPU 過熱、記憶體壓力),每個 Sentry/Alertmanager 呼叫都等待 60 秒才 timeout。大量 FastAPI 背景任務積壓,最終導致 API Pod 記憶體耗盡 OOM Kill。
**最小可行修復**
```python
# apps/api/src/core/circuit_breaker.py新建
import asyncio
from enum import Enum
from collections import deque
class CircuitState(Enum):
CLOSED = "closed" # 正常
OPEN = "open" # 斷路(直接失敗)
HALF_OPEN = "half_open"# 試探性恢復
class SimpleCircuitBreaker:
"""
簡單 Circuit Breaker不依賴 NVIDIA 的實作)
狀態機:
CLOSED → OPEN連續 5 次失敗)
OPEN → HALF_OPEN冷卻 60 秒後)
HALF_OPEN → CLOSED1 次成功)
HALF_OPEN → OPEN1 次失敗)
"""
def __init__(self, failure_threshold=5, timeout_s=60):
self.state = CircuitState.CLOSED
self.failure_count = 0
self.threshold = failure_threshold
self.timeout_s = timeout_s
self._opened_at: float | None = None
def is_open(self) -> bool:
if self.state == CircuitState.OPEN:
import time
if time.time() - self._opened_at > self.timeout_s:
self.state = CircuitState.HALF_OPEN
return False
return True
return False
def record_success(self):
self.failure_count = 0
self.state = CircuitState.CLOSED
def record_failure(self):
self.failure_count += 1
if self.failure_count >= self.threshold:
import time
self.state = CircuitState.OPEN
self._opened_at = time.time()
# 全域 OpenClaw Circuit Breaker
_openclaw_cb = SimpleCircuitBreaker(failure_threshold=5, timeout_s=60)
def get_openclaw_circuit_breaker() -> SimpleCircuitBreaker:
return _openclaw_cb
```
```python
# sentry_webhook.py 修改 call_openclaw_analyzer()
async def call_openclaw_analyzer(error_context: dict) -> ErrorAnalysisResult | None:
cb = get_openclaw_circuit_breaker()
# 斷路保護:直接失敗,不等待
if cb.is_open():
logger.warning("openclaw_circuit_open_skip_analysis")
return None
try:
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(...)
if response.status_code == 200:
cb.record_success()
return ErrorAnalysisResult(**response.json())
else:
cb.record_failure()
return None
except Exception as e:
cb.record_failure()
logger.exception("openclaw_call_failed", error=str(e))
return None
```
---
### 深層風險 DK8s Rolling Update 資料庫遷移衝突
**場景**CD 執行 rolling update舊版和新版 API Pod 同時存在Kubernetes rolling strategy。若新版本有 `ALTER TABLE` 遷移,舊版 Pod 會因為欄位不存在而報錯;若先跑遷移,新舊結構衝突。
**現況確認需要**:確認 Alembic 遷移策略。
**防護機制(必須檢查現有 CD**
```yaml
# .github/workflows/cd.yaml 確認是否有 migration 步驟
# 若無,必須加入:
- name: Run DB Migration
run: |
kubectl exec -n awoooi-prod \
$(kubectl get pod -n awoooi-prod -l app=awoooi-api -o name | head -1) \
-- python -m alembic upgrade head
# 遷移必須是向後兼容的(不能刪除欄位,只能新增)
```
---
### 深層風險 ERedis 記憶體壓力下的靜默資料丟失
AnomalyCounter 的滑動窗口使用 Redis Sorted Set / Counter如果 Redis 記憶體緊張觸發 `maxmemory-policy`,這些計數器可能被靜默淘汰。
**需要立即確認**
```bash
ssh root@192.168.0.188 'docker exec awoooi-redis redis-cli CONFIG GET maxmemory-policy'
# 如果是 allkeys-lru 或 allkeys-lfuAnomalyCounter 的計數器會被淘汰!
# 正確設定應為volatile-ttl 或 noeviction搭配記憶體告警
```
**修復**AnomalyCounter 的 Redis key 必須用帶 TTL 的 key已有確保 eviction policy 不會誤殺有 TTL 的 key
```bash
# 設定為 volatile-ttl只淘汰有 TTL 的 key從 TTL 最短的開始)
docker exec awoooi-redis redis-cli CONFIG SET maxmemory-policy volatile-ttl
# AnomalyCounter 計數器有 TTL → 可能被淘汰
# 解法:增加 Redis maxmemory 設定或改用 noeviction + 主動監控
```
---
### 深層風險 FGitHub Actions Runner 安全隔離問題
`.110` 的 self-hosted runner 在每次 CD 時執行 `kubectl patch secret`,這代表:
- Runner 必須有 K8s 集群的 admin 權限
- 任何能合 PR 進 main 的人,都能觸發有 K8s admin 權限的 Job
**最小防護**
```yaml
# .github/workflows/cd.yaml
# 所有 kubectl 操作的 Job 必須加上環境保護
jobs:
deploy:
environment: production # ← 必須設定 GitHub Environment 審核
```
---
## 第三章終極安全執行定序12 波)
整合所有 6+6 衝突分析後,以下是**唯一正確的執行順序**
```
🛡️ Wave 0: 即時止血(當天,不需部署,純配置)
0.1 確認 Redis maxmemory-policy5min
0.2 確認 Redis appendonly5min
0.3 確認 Alertmanager 備援 Telegram 路徑10min
🔴 Wave 1: 底層安全網Week 1必須串行執行
依序:
1.1 開發 XCLAIM 機制2h
1.2 開發 StatefulSet Guardrail1h
1.3 開發 OpenClaw Circuit Breaker2h
1.4 開發 Global Incident Debounce2h
1.5 以上四項合為單一 PR測試後 Merge1h
→ 此 PR 絕對不能拆分!四個修復互相依賴。
🔴 Wave 2: Worker 升級Wave 1 完成後)
2.1 Worker terminationGracePeriodSeconds 90s30min
2.2 Worker stop() timeout 75s30min
2.3 部署 Worker HPA30min
🟠 Wave 3: 前端主幹鎖定(與 Wave 1 同時啟動,但獨立分支)
3.1 宣佈 Frontend Feature Freeze
3.2 i18n 閃電清零4h
3.3 安裝 eslint-plugin-i18nextWarn 模式1h
3.4 Merge i18n PR → 解除 Frontend Freeze
3.5 ESLint 切換 Error 模式
🟠 Wave 4: CI 基礎設施Wave 3 完成後)
4.1 playwright.config.tsignoreHTTPSErrors + threshold
4.2 Docker Visual Baseline 初始建立
4.3 E2E Weekly Schedule YAMLWarn-Only
4.4 CD validate_coverage.pyWarn-Only
🟠 Wave 5: 告警後端完整Wave 1 完成後)
5.1 Sentry SENTRY_AUTH_TOKEN 配置Phase D
5.2 SignOz 告警規則部署到 .188Phase E
🟡 Wave 6: 可觀測性統合Wave 5 完成後)
6.1 Prometheus Federation.110 → .188
6.2 AI Autonomy Index Metrics 建立
6.3 Redis AOF + Sentinel 評估與啟用
🟡 Wave 7: 前端能力擴充Wave 4 完成後)
7.1 Storybook 10 核心組件
7.2 Omni-Terminal SSE Event Sourcing
7.3 監控 GenUI 卡片7 張)
7.4 Nexus AI 自治率 UI
⚪ Wave 8: DB HA 根本解決
8.1 CloudNativePG 評估報告
8.2 決策後執行Patroni / CloudNativePG / 維持現狀+備份)
⚪ Wave 9: 業務指標層
9.1 FinOps Dashboard API + UI
9.2 SLO / MTTR API 端點
⚪ Wave 10: 安全主權
10.1 Kali → MCP Tool → SecurityAgent
10.2 SBOM 生成整合
⚪ Wave 11: CI 硬阻擋切換
11.1 Visual Regression CI: 從 warn → block
11.2 Coverage validation: 從 warn → block
11.3 ESLint: 確認已為 error 模式
⚪ Wave 12: Phase 4 視覺靈魂注入
12.1 品牌 3D 資產 + Q 版 OpenClaw
12.2 全站微動畫升級
```
---
## 第四章:執行前的強制確認清單
在開始任何 Wave 1 工作之前,必須先完成以下確認:
```bash
#!/bin/bash
# ops/scripts/pre-execution-checklist.sh
echo "=== AWOOOI 執行前強制確認清單 ==="
# 1. Redis AOF 確認
APPENDONLY=$(docker exec awoooi-redis redis-cli CONFIG GET appendonly | tail -1)
echo "Redis AOF: $APPENDONLY" # 必須是 yes
# 2. Redis maxmemory-policy 確認
POLICY=$(docker exec awoooi-redis redis-cli CONFIG GET maxmemory-policy | tail -1)
echo "Redis eviction policy: $POLICY" # 不能是 allkeys-lru
# 3. K3s 叢集狀態確認
kubectl get nodes -n awoooi-prod
kubectl get pod -n awoooi-prod
# 4. Alertmanager 備援 Telegram 路徑確認
curl -s http://192.168.0.120:30093/api/v1/receivers | python3 -m json.tool | grep name
# 5. 確認 .110 → .120 網路路由Playwright E2E 需要)
ping -c 3 192.168.0.120
echo "=== 確認完成,可以開始執行 ==="
```
---
## 附錄:尚未解決的開放問題(需要統帥決策)
| 問題 | 選項 A | 選項 B | 影響 |
|------|--------|--------|------|
| PostgreSQL HA | CloudNativePGK8s 原生)| Patroni+keepalivedVM 層)| Q2 重大決策 |
| Redis HA 層級 | Sentinel主動故障轉移| AOF+手動恢復(保守)| 月度決策 |
| .188 備援節點 | 購置第二台 AI 主機 | Cloud GPU 熱備 | 季度預算 |
| GitHub Runner 安全隔離 | GitHub Environments 審核 | 拆分 CI唯讀和 CD需要 K8s admin| 安全策略 |
---
---
## 第五章:四個最終深水區(代碼確認級別)
### 5.1 Redis 崩潰 → AnomalyCounter 連鎖炸毀 → 告警永久丟失
**代碼確認**`anomaly_counter.py:147`
```python
# 現況(無任何 try/except
await self.redis.zadd(timeline_key, {str(timestamp): timestamp}) # ← Redis 掛了 = 直接 throw
await self.redis.zremrangebyscore(...)
await self.redis.zcount(...) # ← 全部爆炸
# → 呼叫端 sentry_webhook.py → 整個 background task 失敗 → 告警丟失!
```
**修復Graceful Degradation 防禦性包裝**
```python
# anomaly_counter.py 修改 record_anomaly()
async def record_anomaly(self, anomaly_signature: dict) -> AnomalyFrequency:
"""記錄異常Redis 失敗時優雅降級(不拋例外)"""
try:
return await self._record_anomaly_impl(anomaly_signature)
except Exception as e:
# Redis 連線失敗 → 降級:返回最小化頻率物件,讓主流程繼續執行
logger.warning(
"anomaly_counter_redis_degraded",
error=str(e),
reason="Returning default frequency to allow alert chain to continue"
)
# 不拋例外!告警鏈路必須繼續!
return AnomalyFrequency(
anomaly_key=self.hash_signature(anomaly_signature),
count_1h=1, count_24h=1, count_7d=1, count_30d=1,
first_seen=datetime.now(), last_seen=datetime.now(),
auto_repair_count=0, permanent_fix_applied=False,
escalation_level=None, # 無法升級判斷,保守處理
)
async def _record_anomaly_impl(self, anomaly_signature: dict) -> AnomalyFrequency:
"""原始實作邏輯(從 record_anomaly 提取)"""
# ... 原有的所有 Redis 操作 ...
```
**原則**「記不住」不能導致「發不出」。Redis 是輔助系統,不是核心路徑。
---
### 5.2 Worker 非優雅崩潰 → PEL 孤兒任務永久卡死
**代碼確認**`signal_worker.py` 全文無 `reclaim_loop` 或定期 XPENDING 掃描。
現有 `_claim_orphaned_tasks()` 只在 `start()` 時執行一次,解決不了**運行中 Pod 崩潰**的場景:
```
場景2 個 Worker 穩定運行中
Worker A 處理任務途中 → Segfault / OOM Kill非優雅關機
Worker B 正在運行 → start() 不再觸發 → XCLAIM 永遠不執行
孤兒任務卡在 PEL → 直到下次 HPA 觸發新 Pod 才救回
可能等待 600+ 秒HPA stabilizationWindowSeconds
```
**修復Active Sweeper Loop與心跳循環並行**
```python
# signal_worker.py 新增 _reclaim_loop()
async def start(self) -> None:
await self._ensure_consumer_group()
await self._claim_orphaned_tasks() # 啟動時一次
self._running = True
self._task = asyncio.create_task(self._consume_loop())
self._reclaim_task = asyncio.create_task(self._reclaim_loop()) # 🆕 持續掃描
async def _reclaim_loop(self, interval_s: int = 300) -> None:
"""
Active Sweeper每 5 分鐘主動掃描 PEL接管閒置超過 5 分鐘的孤兒任務
與 _consume_loop 並行執行,不阻擋正常消費
"""
while self._running:
try:
await asyncio.sleep(interval_s)
if not self._running:
break
claimed = await self._claim_orphaned_tasks(idle_ms=300_000) # 5 分鐘
if claimed > 0:
logger.info("active_sweeper_claimed", count=claimed)
except asyncio.CancelledError:
break
except Exception as e:
logger.warning("active_sweeper_error", error=str(e))
async def stop(self) -> None:
self._running = False
# 同時取消 reclaim_loop
if hasattr(self, '_reclaim_task') and self._reclaim_task:
self._reclaim_task.cancel()
if self._task:
try:
await asyncio.wait_for(self._task, timeout=75.0) # 已校正
except (TimeoutError, asyncio.CancelledError):
pass
```
---
### 5.3 SSE Event Store Redis 記憶體炸彈
**代碼確認**`terminal.py:114` 使用 SSE Publisher/Subscribe 模式(`publisher.subscribe(topics)`**不是 Redis List 模式**。
這是一個**重要的架構現況修正**
- 現有 terminal.py 使用 `src.core.sse.SSEPublisher` 作為事件分發機制
- **Event SourcingRedis RPUSH尚未實作**,這是未來要加的功能
- 因此 Redis 記憶體炸彈風險存在於**未來實作時**,需要在設計階段就預防
**未來實作 Event Sourcing 時的強制規格**
```python
# terminal.py 未來的 stream_with_persistence()
MAX_PAYLOAD_BYTES = 50 * 1024 # 50KB 上限tool_result 超出截斷)
MAX_EVENTS_PER_SESSION = 50 # 每個 session 最多 50 個事件LTRIM
SESSION_TTL_SECONDS = 3600 # 1 小時 TTL
async def stream_with_persistence(command_id: str, event_type: str, data: dict):
redis = get_redis()
key = f"terminal:events:{command_id}"
# 🚨 必要的 Payload 保護
payload_json = json.dumps(data)
if len(payload_json) > MAX_PAYLOAD_BYTES:
payload_json = json.dumps({
"truncated": True,
"original_size": len(payload_json),
"preview": payload_json[:1024],
"message": f"Payload {len(payload_json)//1024}KB 過大,已截斷"
})
event = {"type": event_type, "data": json.loads(payload_json)}
await redis.rpush(key, json.dumps(event))
await redis.ltrim(key, -MAX_EVENTS_PER_SESSION, -1) # 只保留最後 50 個
await redis.expire(key, SESSION_TTL_SECONDS)
```
---
### 5.4 Frontend Feature Freeze → Hotfix 死鎖
**代碼確認**:無 `release/` 分支策略main 是唯一的長期分支。
**修復Git Flow 三分支策略(啟動 Freeze 前必須建立)**
```bash
# Week 1 開始 i18n 清零前,執行:
# Step 1: 從當前 main 建立穩定的 release 基準
git checkout main
git pull origin main
git checkout -b release/v1.x
git push origin release/v1.x
# Step 2: 在 GitHub 設定 release/v1.x 為 Protected Branch
# → 只有 Hotfix PR 可以合併到此分支
# Step 3: 開始 i18n 清零(在 main/develop 進行)
git checkout main
git checkout -b fix/i18n-zero-violation
# ... 執行 i18n 清零 ...
git push origin fix/i18n-zero-violation
# → PR 合併到 main
```
**緊急 Hotfix 流程**Freeze 期間生產爆炸時):
```bash
# 從 release 分支切 hotfix
git checkout release/v1.x
git checkout -b hotfix/critical-approval-button-fix
# ... 最小化修復 ...
git push origin hotfix/critical-approval-button-fix
# PR 合併到 release/v1.x → 立即部署
# 然後 cherry-pick 到 maini18n 重構進行中的分支)
git checkout main
git cherry-pick <hotfix-commit-hash>
```
**Hotfix 觸發條件定義**(須寫入 HARD_RULES.md
- 統帥無法使用核心功能(簽核按鈕失效、登入無法使用)
- P0 級 Sentry Error 每分鐘 > 10 次
- 服務 availability < 99%
---
## 第六章Wave 1 最終實作清單(可立即授權執行)
經過全面代碼確認Wave 1 的四個修復需要修改以下精確位置:
| 修復項目 | 修改檔案 | 位置 | 預估工時 |
|---------|---------|------|---------|
| XCLAIM + Active Sweeper | `signal_worker.py` | `start()`, `stop()`, 新增 `_reclaim_loop()`, `_claim_orphaned_tasks()` | 2h |
| StatefulSet Guardrail | `auto_repair_service.py` | `evaluate_auto_repair()` 開頭新增服務黑名單 | 1h |
| AnomalyCounter Redis 降級 | `anomaly_counter.py` | `record_anomaly()` 包裝 try/except + 降級回傳 | 1h |
| OpenClaw Circuit Breaker | `core/circuit_breaker.py`(新建)→ `sentry_webhook.py`, `signoz_webhook.py` | `call_openclaw_analyzer()` 包裝斷路保護 | 2h |
| Global Incident Debounce | `services/incident_service.py` | `process_signal()` 前加全域冷卻檢查 | 1.5h |
**Wave 1 執行條件**
1. Wave 0.1-0.3 手動確認完成Redis AOF/eviction、Alertmanager 備援)
2. Git Flow建立 `release/v1.x` 穩定分支(防止 Freeze 期間 Hotfix 死鎖)
3. 所有修改捆綁為一個 PR原子性部署不可拆分
*「真正的架構師不是設計完美的系統,而是設計在任何極端狀況下都能優雅降級的系統。」* 🦞