Files
awoooi/docs/adr/ADR-038-openclaw-concurrency-governance.md
OG T bf06737eed docs: ADR-038/039 + LOGBOOK 更新
- ADR-038: OpenClaw 併發治理架構
- ADR-039: 全域自動修復熔斷
- LOGBOOK: 今日進度記錄

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-29 15:48:09 +08:00

7.8 KiB
Raw Blame History

ADR-038: OpenClaw 推理引擎併發治理架構

狀態: 已批准 日期: 2026-03-29 14:05 (台北時間) 決策者: 統帥 + Antigravity (首席架構師) 觸發事件: 沙盤推演發現 Thundering Herd 可導致 Ollama OOM 崩潰


問題陳述

場景:網路閃斷後的告警雪崩

.188 網路閃斷 3 分鐘 → 恢復後:
  Alertmanager 積壓 N 個告警同時倒入 AWOOOI Webhook
  Sentry 積壓 N 個錯誤同時倒入 AWOOOI Webhook
  Signal Worker 拉起 N 個 asyncio.Task 同時呼叫 OpenClaw
  Ollama/GPU 收到 N 個並發 LLM 推理請求
  → VRAM/RAM OOM → Ollama 進程崩潰
  → Circuit Breaker 觸發,但系統大腦已死

核心問題Circuit Breaker 只防「失敗」,不防「超載」。


決策Semaphore + Circuit Breaker 雙層保護

架構設計

告警事件
  │
  ▼
Signal Worker (asyncio.Task)
  │
  ▼
┌─────────────────────────────────────────┐
│  Layer 1: Circuit Breaker               │
│  - 5 次連續失敗 → OPEN60 秒冷卻)     │
│  - OPEN 狀態:立即返回 None不等待      │
└──────────────────┬──────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────┐
│  Layer 2: Concurrency Semaphore         │
│  - 全域最多 3 個並發 LLM 推理           │
│  - 超過限制的請求:排隊等待(非拒絕)   │
└──────────────────┬──────────────────────┘
                   │
                   ▼
             OpenClaw / Ollama

為何 max_concurrent = 3

考量 說明
Ollama CPU 模式 .188 純 CPU無 GPU每次推理佔 1-2 CPU Core
.188 主機規格 共享 CPU同時跑 PostgreSQL + Redis + SigNoz
安全邊界 3 = 不超過 .188 可用 CPU 的 60%(保留 40% 給其他服務)
排隊不拒絕 超出的請求排隊等待,確保不遺失重要告警

實作規範

核心實作(apps/api/src/core/circuit_breaker.py

"""
OpenClaw 推理引擎保護機制
=========================
ADR-038: 雙層保護策略
- Layer 1: Circuit Breaker防失敗傳播
- Layer 2: Concurrency Semaphore防 Thundering Herd

遵循 leWOOOgo 積木化鐵律:
- 此模組屬於 core/ 基礎設施層
- 不依賴任何 Service 層
- 透過 Singleton 提供全域狀態
"""
import asyncio
import time
from enum import Enum
from dataclasses import dataclass, field

import structlog

logger = structlog.get_logger(__name__)


class CircuitState(Enum):
    CLOSED = "closed"       # 正常運作
    OPEN = "open"           # 斷路(快速失敗)
    HALF_OPEN = "half_open" # 試探性恢復


@dataclass
class CircuitBreakerConfig:
    failure_threshold: int = 5       # 連續失敗次數觸發斷路
    timeout_s: float = 60.0          # 斷路後冷卻時間(秒)
    max_concurrent: int = 3          # 最大並發 LLM 推理數


class OpenClawGuard:
    """
    OpenClaw 雙層推理保護門衛

    使用方式:
        guard = get_openclaw_guard()

        if guard.is_circuit_open():
            return None  # 快速失敗

        async with guard.semaphore:  # 排隊等待
            try:
                result = await call_openclaw(...)
                guard.record_success()
                return result
            except Exception:
                guard.record_failure()
                raise
    """

    def __init__(self, config: CircuitBreakerConfig | None = None):
        self.config = config or CircuitBreakerConfig()
        self.state = CircuitState.CLOSED
        self.failure_count = 0
        self._opened_at: float | None = None
        # Semaphore 必須在 event loop 中建立
        self._semaphore: asyncio.Semaphore | None = None

    @property
    def semaphore(self) -> asyncio.Semaphore:
        if self._semaphore is None:
            self._semaphore = asyncio.Semaphore(self.config.max_concurrent)
        return self._semaphore

    def is_circuit_open(self) -> bool:
        if self.state == CircuitState.OPEN:
            if time.time() - self._opened_at > self.config.timeout_s:
                self.state = CircuitState.HALF_OPEN
                logger.info("circuit_breaker_half_open")
                return False
            return True
        return False

    def record_success(self) -> None:
        self.failure_count = 0
        if self.state != CircuitState.CLOSED:
            logger.info("circuit_breaker_closed")
        self.state = CircuitState.CLOSED

    def record_failure(self) -> None:
        self.failure_count += 1
        if self.failure_count >= self.config.failure_threshold:
            self.state = CircuitState.OPEN
            self._opened_at = time.time()
            logger.warning(
                "circuit_breaker_opened",
                failure_count=self.failure_count,
                cooldown_s=self.config.timeout_s,
            )

    def get_metrics(self) -> dict:
        return {
            "state": self.state.value,
            "failure_count": self.failure_count,
            "max_concurrent": self.config.max_concurrent,
        }


# 全域 Singleton
_guard: OpenClawGuard | None = None


def get_openclaw_guard() -> OpenClawGuard:
    """取得全域 OpenClaw 保護門衛"""
    global _guard
    if _guard is None:
        _guard = OpenClawGuard()
    return _guard

呼叫端整合(sentry_webhook.py + signoz_webhook.py

from src.core.circuit_breaker import get_openclaw_guard

async def call_openclaw_analyzer(error_context: dict) -> ErrorAnalysisResult | None:
    guard = get_openclaw_guard()

    # Layer 1: Circuit Breaker 快速失敗
    if guard.is_circuit_open():
        logger.warning("openclaw_circuit_open_skip", metrics=guard.get_metrics())
        return None

    # Layer 2: Semaphore 排隊(最多 3 個並發,超出排隊等待)
    async with guard.semaphore:
        try:
            async with httpx.AsyncClient(timeout=60.0) as client:
                response = await client.post(
                    f"{settings.OPENCLAW_BASE_URL}/analyze",
                    json=error_context,
                )
                response.raise_for_status()
                guard.record_success()
                return ErrorAnalysisResult(**response.json())
        except Exception as e:
            guard.record_failure()
            logger.exception("openclaw_call_failed", error=str(e))
            return None

模組化合規驗證

項目 說明 合規狀態
層次 core/ 基礎設施層 合規
依賴 只依賴 stdlib + structlog 合規
Singleton get_openclaw_guard() 工廠函數 合規
協議介面 OpenClawGuard 直接使用(無需 Protocol因為非跨模組 合規
測試 可透過 _guard = None 重置 合規

驗收標準

項目 通過條件
Semaphore 排隊 第 4 個請求在前 3 個完成前不呼叫 OpenClaw
Circuit Breaker 觸發 5 次失敗後 is_circuit_open() 返回 True
冷卻恢復 60 秒後切換到 HALF_OPEN1 次成功恢復 CLOSED
Graceful Degrade Circuit OPEN 時呼叫返回 None不拋例外

相關文件

  • docs/proposals/ARCHITECTURAL_RISK_WAR_GAME.md:風險沙盤推演
  • apps/api/src/api/v1/sentry_webhook.pySentry Webhook 整合
  • apps/api/src/api/v1/signoz_webhook.pySignOz Webhook 整合
  • ADR-028Failure Auto-Repair Loop
  • ADR-039全域自動修復熔斷機制