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>
247 lines
7.4 KiB
Markdown
247 lines
7.4 KiB
Markdown
# 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/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)
|