Files
awoooi/docs/adr/ADR-040-global-autorepair-governance.md
OG T 998b5a7b5f docs: ADR-039 重編號為 ADR-040 + LOGBOOK 更新
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>
2026-03-30 01:20:50 +08:00

247 lines
7.4 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.
# 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 實作)
```python
# 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
```python
# 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"]:
...
# ... 後續現有邏輯 ...
```
```python
# 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/resetTier 1 操作)
```
---
## 閾值設計依據
| 參數 | 值 | 理由 |
|------|-----|------|
| `GLOBAL_COOLDOWN_THRESHOLD` | 5 | 正常運作時每天 < 5 次修復5 次以上代表異常模式 |
| `GLOBAL_COOLDOWN_TTL` | 900s15 分鐘)| 大多數骨牌效應在 10 分鐘內完成15 分鐘提供緩衝 |
---
## 模組化合規驗證
| 項目 | 說明 | 合規狀態 |
|------|------|---------|
| 層次 | `services/` 獨立服務層 | ✅ 合規 |
| 依賴 | 只依賴 `core/redis_client` | ✅ 合規 |
| Redis 降級 | 故障時保守處理(禁止自動修復)| ✅ 合規 |
| 原子操作 | 使用 INCRRedis 原生原子性)| ✅ 合規 |
---
## 驗收標準
| 項目 | 通過條件 |
|------|---------|
| 有狀態服務保護 | 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-028Failure Auto-Repair Loop
- ADR-030Intelligent Auto-Remediation
- ADR-038OpenClaw Concurrency GovernanceSemaphore + Circuit Breaker