ADR 變更: - ADR-039 (gitea-cicd-migration) 保留給 Gitea CI/CD 遷移 - 原 ADR-039 (global-autorepair-governance) 改為 ADR-040 LOGBOOK: - 新增 Phase 19.4 Terminal Service API 整合記錄 - 更新當前狀態 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
7.4 KiB
7.4 KiB
ADR-040: 全域自動修復熔斷機制
ADR-040: Global Auto-repair Governance Strategy
狀態: 已批准 日期: 2026-03-30 01:10 (台北時間) 決策者: 統帥 + Antigravity (首席架構師) 觸發事件: 沙盤推演發現跨資源 Poison Pill 可導致 AI 骨牌修復死循環
問題陳述
場景:跨資源修復骨牌效應
流量激增
→ AI 執行 scale_up (擴容 API Pod)
→ API Pod 數量增加,連線池耗盡 PostgreSQL
→ AI 收到 DB 告警,執行 restart_api_pod (釋放 DB 連線)
→ API 重啟導致 502 激增
→ AI 再次執行 scale_up
→ 死循環,資源榨乾
核心問題:現有 max_repairs_per_resource: 3 只限單一資源,無法防止骨牌效應。
決策:雙層全域保護機制
機制一:全域修復冷卻期(Global Action Cooldown)
當系統整體在過去 15 分鐘內自動修復超過 5 次(不論對象),強制凍結所有 Auto-Repair,轉為 AWAITING_APPROVAL。
機制二:StatefulSet 硬禁令(Stateful Service Blacklist)
有狀態服務(PostgreSQL、Redis、ClickHouse、MinIO 等)永遠不允許自動重啟,強制人工介入。
實作規範
全域計數器(Redis 實作)
# apps/api/src/services/global_repair_cooldown.py
"""
全域修復熔斷機制
================
ADR-039:防止跨資源循環修復
設計原則:
- Redis TTL 滑動窗口(15 分鐘)
- 失敗降級:Redis 故障時保守跳過自動修復,強制人工確認
"""
import structlog
from src.core.redis_client import get_redis
logger = structlog.get_logger(__name__)
GLOBAL_COOLDOWN_KEY = "global:auto_repair:count"
GLOBAL_COOLDOWN_TTL = 900 # 15 分鐘窗口
GLOBAL_COOLDOWN_THRESHOLD = 5 # 超過 5 次強制凍結
STATEFUL_SERVICE_BLACKLIST = frozenset({
"postgres", "postgresql", "awoooi-postgres",
"redis", "awoooi-redis", "redis-stack",
"clickhouse", "signoz-clickhouse",
"elasticsearch", "etcd",
"minio", "awoooi-minio",
})
async def check_global_repair_cooldown(
incident_id: str,
affected_services: list[str],
) -> tuple[bool, str]:
"""
檢查是否允許自動修復
Returns:
(can_repair: bool, reason: str)
"""
redis = get_redis()
# === 硬禁令:有狀態服務黑名單 ===
for service in affected_services:
if any(bl in service.lower() for bl in STATEFUL_SERVICE_BLACKLIST):
reason = f"服務 {service} 為有狀態服務,禁止自動重啟,請統帥手動介入"
logger.warning(
"stateful_service_blocked",
service=service,
incident_id=incident_id,
)
return False, reason
# === 全域冷卻期:Redis 計數 ===
try:
count_raw = await redis.get(GLOBAL_COOLDOWN_KEY)
current_count = int(count_raw) if count_raw else 0
if current_count >= GLOBAL_COOLDOWN_THRESHOLD:
reason = (
f"系統在過去 15 分鐘內已自動修復 {current_count} 次,"
f"超出安全閾值 {GLOBAL_COOLDOWN_THRESHOLD},"
"強制轉為人工審核模式"
)
logger.warning(
"global_repair_cooldown_active",
current_count=current_count,
threshold=GLOBAL_COOLDOWN_THRESHOLD,
incident_id=incident_id,
)
return False, reason
return True, "允許自動修復"
except Exception as e:
# Redis 故障 → 保守策略:禁止自動修復
logger.error(
"global_repair_cooldown_redis_error",
error=str(e),
fallback="blocking_auto_repair_for_safety",
)
return False, f"Redis 連線異常,保守禁止自動修復(原因:{e})"
async def record_global_repair_action() -> None:
"""
記錄一次全域修復動作
使用 INCR + EXPIRE 實現滑動窗口計數
注意:INCR 是原子操作,多個 Worker 並發安全
"""
try:
redis = get_redis()
count = await redis.incr(GLOBAL_COOLDOWN_KEY)
# 只在第一次設定 TTL(避免頻繁重設導致窗口延長)
if count == 1:
await redis.expire(GLOBAL_COOLDOWN_KEY, GLOBAL_COOLDOWN_TTL)
logger.info(
"global_repair_action_recorded",
count=count,
threshold=GLOBAL_COOLDOWN_THRESHOLD,
)
except Exception as e:
# Redis 故障:靜默失敗
logger.warning("global_repair_record_failed", error=str(e))
整合到 auto_repair_service.py
# auto_repair_service.py - evaluate_auto_repair() 加入前置檢查
from src.services.global_repair_cooldown import check_global_repair_cooldown
async def evaluate_auto_repair(self, incident: Incident) -> AutoRepairDecision:
# === 最優先:全域熔斷檢查(在所有其他邏輯之前)===
can_repair, cooldown_reason = await check_global_repair_cooldown(
incident_id=incident.incident_id,
affected_services=incident.affected_services or [],
)
if not can_repair:
return AutoRepairDecision(
can_auto_repair=False,
reason=cooldown_reason,
blocked_by="GLOBAL_GUARDRAIL",
)
# === 現有邏輯:Severity 檢查 ===
if incident.severity and incident.severity.value in ["P0", "P1"]:
...
# ... 後續現有邏輯 ...
# auto_repair_service.py - execute_auto_repair() 執行後記錄
async def execute_auto_repair(self, incident, playbook) -> AutoRepairResult:
# ... 現有執行邏輯 ...
# === 執行成功後,記錄全域計數 ===
if result.success:
from src.services.global_repair_cooldown import record_global_repair_action
await record_global_repair_action()
return result
全域熔斷狀態視覺化(未來)
Dashboard 首頁 → AI 自治指標面板 → 顯示:
「⚠️ 系統保護模式:過去 15 分鐘修復 5 次,暫停自動修復」
→ 統帥點擊解除 → POST /api/v1/repair/cooldown/reset(Tier 1 操作)
閾值設計依據
| 參數 | 值 | 理由 |
|---|---|---|
GLOBAL_COOLDOWN_THRESHOLD |
5 | 正常運作時每天 < 5 次修復;5 次以上代表異常模式 |
GLOBAL_COOLDOWN_TTL |
900s(15 分鐘) | 大多數骨牌效應在 10 分鐘內完成;15 分鐘提供緩衝 |
模組化合規驗證
| 項目 | 說明 | 合規狀態 |
|---|---|---|
| 層次 | services/ 獨立服務層 |
✅ 合規 |
| 依賴 | 只依賴 core/redis_client |
✅ 合規 |
| Redis 降級 | 故障時保守處理(禁止自動修復) | ✅ 合規 |
| 原子操作 | 使用 INCR(Redis 原生原子性) | ✅ 合規 |
驗收標準
| 項目 | 通過條件 |
|---|---|
| 有狀態服務保護 | PostgreSQL/Redis 的 Incident 永遠返回 GLOBAL_GUARDRAIL |
| 全域計數 | 第 6 次修復請求返回 can_auto_repair=False |
| Redis 降級 | Redis 故障時 check_global_repair_cooldown 返回 (False, "Redis 連線異常...") |
| Dashboard 可見 | (未來)保護模式顯示在 AI 自治指標面板 |
相關文件
docs/proposals/ARCHITECTURAL_RISK_WAR_GAME.md:風險沙盤推演apps/api/src/services/auto_repair_service.py:自動修復服務- ADR-028:Failure Auto-Repair Loop
- ADR-030:Intelligent Auto-Remediation
- ADR-038:OpenClaw Concurrency Governance(Semaphore + Circuit Breaker)