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

29 KiB
Raw Blame History

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 三階段)

# .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() 方法)

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

全域災難冷卻期實作

# 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:411TIER_ACTIONS[1] 包含 restart_podrestart_container。評估邏輯只檢查 severity <= P2RiskLevel <= MEDIUM完全沒有服務類型白名單

最小可行修復(加入服務黑名單)

# 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 需要確認當前設定。

// 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 → 無法收到告警 → 統帥無法操作 → 完全失聯

緩解策略(非根治)

# 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 已有!):

# 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:289call_openclaw_analyzer() 只有 httpx.AsyncClient(timeout=60.0)沒有 Circuit Breaker

場景OpenClaw 高負載GPU 過熱、記憶體壓力),每個 Sentry/Alertmanager 呼叫都等待 60 秒才 timeout。大量 FastAPI 背景任務積壓,最終導致 API Pod 記憶體耗盡 OOM Kill。

最小可行修復

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

# .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,這些計數器可能被靜默淘汰。

需要立即確認

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

# 設定為 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

最小防護

# .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 工作之前,必須先完成以下確認:

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

# 現況(無任何 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 防禦性包裝

# 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與心跳循環並行

# 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 時的強制規格

# 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 前必須建立)

# 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 期間生產爆炸時):

# 從 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原子性部署不可拆分

「真正的架構師不是設計完美的系統,而是設計在任何極端狀況下都能優雅降級的系統。」 🦞