# ADR-006: Sidecar V2 — Provider 池管理与负载均衡架构设计 | 属性 | 值 | |------|-----| | 编号 | ADR-006 | | 日期 | 2026-06-25 | | 版本 | v2.0(评审修订版) | | 状态 | 提议中 | | 作者 | 梁思筑 (architect) | | 关联 | BIZ-48 (父需求), BIZ-51 (本任务) | | 评审 | 陆怀瑾(COO), 沈路明(PM), 严维序(Ops), 徐聪(Dev) | | 类型 | 系统架构设计 | --- ## 1. 背景与目标 ### 1.1 现状分析(Sidecar V1) Sidecar V1 是一个单 Provider(NVIDIA)的限流代理,核心架构: ``` 请求 → 优先级队列 → 令牌桶限流(40 RPM) → httpx → NVIDIA API ``` **V1 局限性**: - 单一 API Key,无多 Provider 管理能力 - 无 429 自动冷却机制 - 配置依赖 OpenClaw 环境变量 / YAML,无 WebUI 管理 - 无 Provider 级负载均衡 - 无成本/用量统计 ### 1.2 V2 目标 将 Sidecar 从"单 Provider 透明代理"升级为"多 Provider 池化网关",实现: 1. **Provider 池体系**:主池 + Fallback 池 + 冷却池 2. **多 Provider 负载均衡**:每 Provider 独立 RPM 流控 3. **429 自动冷却**:检测 429 → 进入冷却池 → 指数退避恢复 4. **完整 WebUI**:配置管理、Dashboard 统计、健康监控 5. **用量统计**:Token 用量、调用次数、费用核算 6. **V1 兼容**:升级不破坏现有部署 --- ## 2. 架构设计 ### 2.1 系统拓扑图 ```mermaid graph TB subgraph "OpenClaw Gateway" OC[OpenClaw 调度器] OC_FB[OpenClaw Fallback
传统配置链路] end subgraph "Sidecar V2 (systemd / Docker)" direction TB subgraph "入口层" GW["API Gateway :9190
FastAPI + 路由匹配"] end subgraph "核心调度层" LB["负载均衡器
Weighted RR 5-10s刷新"] QM["队列管理器
FIFO + 优先级
容量500 + 溢出策略"] end subgraph "Provider 池层" direction LR MP["主池 Main Pool"] FP["Fallback 池"] CP["冷却池
Cooldown Pool"] end subgraph "流控层" RL["Rate Limiter
Per-Provider Token Bucket"] CD["Cooldown Detector
429检测+指数退避
+紧急通道10%RPM"] end subgraph "存储与统计层" MT["Metrics :9191
Prometheus"] ST["统计引擎
Token/费用/调用量"] DB[("SQLite WAL
sidecar_v2.db
+ cron备份")] end subgraph "WebUI 层 :9190" UI["Dashboard
SSE 实时推送"] AP["Admin API
Provider CRUD
Bearer Token 鉴权"] end end OC -->|"X-Sidecar-Pool: main"| GW GW --> LB LB --> QM QM --> RL RL --> MP RL --> FP MP -.->|"429 触发冷却"| CP MP -->|"全部冷却"| FP FP -->|"全部冷却"| OC_FB CP -.->|"冷却结束恢复"| MP RL --> CD CD -.->|"紧急通道 10% RPM"| MP LB --> MT MT --> ST ST --> DB DB --> UI AP --> DB ``` ### 2.2 数据流设计 ```mermaid sequenceDiagram participant OC as OpenClaw participant GW as API Gateway participant LB as 负载均衡器 participant QM as 队列管理器 participant RL as Rate Limiter participant P as Provider participant CD as Cooldown Detector participant ST as 统计引擎 OC->>GW: POST /v1/chat/completions GW->>LB: 路由到目标池 LB->>LB: Weighted RR (5-10s刷新) 选出 Provider A weight=(max_rpm-current_rpm)/max_rpm LB->>RL: BEGIN IMMEDIATE 事务 检查 Provider A RPM + 预占 alt RPM 不足 RL->>QM: 入队等待(超时30s) QM-->>RL: 令牌可用 end RL-->>LB: 允许转发 LB->>P: 转发请求 P-->>LB: 响应 alt 200 OK LB->>ST: INSERT...ON CONFLICT 记录 usage_logs LB-->>GW: 正常响应 else 429 Too Many Requests LB->>CD: 上报429 CD->>P: Provider 移入冷却池 cooldown_until=now+30s×2^n LB->>LB: 重新选择 Provider B alt Provider B 正常 LB->>P: 转发到 Provider B P-->>LB: 200 OK end alt 主池全部冷却 LB->>LB: 降级 Fallback 池 Note over LB: 检查即将恢复的Provider 剩余<10s→等待 alt Fallback 可用 LB->>P: 转发 Fallback Provider P-->>LB: 200 OK (+降级标记) else Fallback 也全冷却 LB->>P: 紧急通道: 1 Provider 10% RPM alt 紧急通道成功 P-->>LB: 200 OK else LB-->>OC: 503 Service Unavailable OC->>OC: OpenClaw 自身 fallback end end end end ``` ### 2.3 部署架构 ```mermaid graph TB subgraph "Ubuntu Server (192.168.1.99)" subgraph "systemd Services" SVC["sidecar-v2.service
MemoryMax=512M
LimitNOFILE=65536
ProtectSystem=strict"] end subgraph "端口规划" P9190[":9190 API + WebUI + Admin"] P9191[":9191 Prometheus Metrics"] end subgraph "存储" DBFILE["/opt/sidecar-v2/data/sidecar_v2.db
WAL模式"] BACKUP["/opt/sidecar-v2/backups/
每日cron .backup + 7天保留"] end subgraph "日志" LOG["/var/log/sidecar-v2/
JSON结构化 + logrotate
10MB/文件 保留30天"] end subgraph "防火墙" FW["UFW: 仅192.168.1.0/24可访问 :9190/:9191"] end end subgraph "可选 Docker" DOCKER["docker run sidecar-v2
-v /opt/sidecar-v2/data:/data
非root用户运行
HEALTHCHECK"] end subgraph "外部" PROM["Prometheus :9090
抓取 :9191 指标"] end PROM -->|"scrape"| P9191 ``` --- ## 3. 技术选型 ### 3.1 核心框架 | 维度 | 方案 A: 扩展现有 V1 | 方案 B: 模块化重构 | **选型** | |------|-------------------|-------------------|---------| | 框架 | FastAPI(保持) | FastAPI(保持) | **FastAPI** | | 优势 | 最小改动,快速交付 | 清晰模块边界 | 折中:分模块开发,渐进替换 | | 风险 | 技术债累积 | 开发周期长 | 按池化层→流控层→统计层分阶段 | | 工作量 | 3-5 天 | 7-10 天 | 6 阶段 69h | ### 3.2 Provider 管理 | 方案 | 描述 | 选型 | |------|------|------| | A: Python 内存 dict + SQLite | 热数据在内存,持久化到 SQLite | ✅ **选用** | | B: Redis + MySQL | 分布式支持但增加运维复杂度 | ❌ 过度设计(当前单机部署) | **多节点迁移触发条件**:活跃 Provider > 50 或管理节点 > 1 时,将 SQLite 迁移至 MySQL。当前单机部署满足需求。 ### 3.3 数据库 | 方案 | 描述 | 选型 | |------|------|------| | A: SQLite(WAL 模式) | 单文件,零配置,事务安全 | ✅ **选用** | | B: MySQL 8.0 | 团队熟悉,但增加运维 | ❌ 备选(Provider > 50 时迁移) | **SQLite 运维配置**: - WAL 模式:`PRAGMA journal_mode=WAL` - WAL 检查点:`wal_autocheckpoint=1000`(1000 页后自动 checkpoint) - 定时 checkpoint:cron 每小时 `PRAGMA wal_checkpoint(TRUNCATE)` - 备份策略:cron 每日 `.backup`,保留 7 天 - 数据库监控:`PRAGMA integrity_check` 每日执行 ### 3.4 负载均衡策略 | 策略 | 描述 | 适用场景 | 选型 | |------|------|---------|------| | Round Robin | 轮询分配 | 所有 Provider 同质 | 默认策略 | | Weighted | 按 RPM 余量加权 | Provider RPM 不同 | ✅ **主池使用** | | Least Connections | 最少活跃连接 | 响应时间差异大 | Fallback 池使用 | **实现**:`weight = (max_rpm - current_rpm_window) / max_rpm`,归一化后加权轮询。权重刷新周期:5-10 秒,平衡性能与响应速度。 **并发安全**:Provider 选择 + RPM 预占使用 `BEGIN IMMEDIATE` 事务包裹,确保原子性。 ### 3.5 429 冷却策略(增强版) ``` 检测到 429 响应 ↓ Provider 移入冷却池 ↓ 冷却时间 = 30s × 2^(连续429次数 - 1) • 首次 429: 30s • 连续 2 次: 60s • 连续 3 次: 120s • 连续 4 次: 240s • 连续 5 次: 480s • 最大: 600s (10 分钟) ↓ 冷却状态持久化到 cooldown_events 表 服务重启后冷却状态可从 DB 恢复 ↓ 冷却结束 → 放回主池(标记"观察中") ↓ 恢复后首次请求成功 → 取消观察,重置冷却计数 恢复后首次仍 429 → 回到冷却池,加倍冷却时间 ``` **紧急通道**:当主池 + Fallback 全部冷却时,选择冷却时间剩余最短的 1 个 Provider,以 10% RPM 继续尝试,避免完全断流。 **冷却预检**:降级到 Fallback 池前,检查主池是否有 Provider 剩余冷却时间 < 10s,可短暂等待其恢复。 ### 3.6 队列控流机制 | 参数 | 配置 | 说明 | |------|------|------| | 队列模型 | FIFO + 优先级 | 继承 V1 的四级优先级(URGENT > HIGH > NORMAL > LOW) | | 最大队列深度 | 500 | 超出后触发溢出策略 | | 令牌等待超时 | 30s(默认) | 可配置 `SIDECAR_QUEUE_TIMEOUT` | | 溢出策略 | REJECT (503) | 队列满时拒绝新请求,返回 503 + Retry-After | **溢出策略选择**: - REJECT (503):返回 503,触发上游 OpenClaw 重试/降级 — **选用** - PASSTHROUGH(直通):绕过限流直接转发 — V1 模式,V2 不推荐(可能导致上游被打爆) - DROP_LOWEST(丢弃最低优先级):丢弃队列中最低优先级的请求 — 复杂度高,V3 考虑 **队列架构**:共享队列,不按 Provider 拆分。请求先入队,出队后由负载均衡器选择 Provider。这样可以用一个队列服务所有 Provider,简化实现。 ### 3.7 池降级链路(增强版) ``` 请求到达 ↓ 主池 Provider 可用? ├─ YES → 转发 └─ NO(全部冷却/禁用) ↓ 主池有 Provider 即将恢复(剩余<10s)? ├─ YES → 等待该 Provider 冷却结束 → 转发 └─ NO ↓ Fallback 池 Provider 可用? ├─ YES → 转发(Header: X-Sidecar-Degraded: fallback) └─ NO(全部冷却/禁用) ↓ 紧急通道:1 Provider 以 10% RPM 尝试 ├─ 成功 → 返回(Header: X-Sidecar-Degraded: emergency) └─ 失败 ↓ 返回 503 + Header: X-Sidecar-Degraded: openclaw_fallback ↓ OpenClaw 收到 503 → 使用自身 fallback 链路 ``` ### 3.8 WebUI 前后端 | 方案 | 描述 | 选型 | |------|------|------| | A: 纯 HTML + Alpine.js(V1 风格) | 轻量,无需构建 | ✅ **选用** | | B: React SPA | 交互更强但增加构建步骤 | ❌ 过度工程 | **WebUI 安全**:Admin API 默认绑定 127.0.0.1,通过 Bearer Token (`SIDECAR_ADMIN_TOKEN`) 鉴权。如需外网访问,通过 nginx 反向代理添加 Basic Auth 或 OAuth2 Proxy。 --- ## 4. Provider 健康检查 ### 4.1 健康判定机制 采用**混合模式**:主动探测 + 被动统计。 #### 主动探测(定时 Health Probe) | 参数 | 值 | |------|-----| | 探测频率 | 每 60s | | 探测方式 | GET /v1/models(轻量 API 调用) | | 超时 | 10s | | 探测对象 | 所有 `status=active` 的 Provider | #### 被动统计(成功率监控) | 参数 | 值 | |------|-----| | 统计窗口 | 滑动 5 分钟 | | 不健康阈值 | 连续 5 次失败 / 5 分钟内成功率 < 50% | | 恢复阈值 | 连续 3 次成功 + 成功率 > 80% | #### 健康状态机 ``` 健康(healthy) ↓ 连续5次失败 或 5min成功率<50% 降级(degraded) — 仍可服务, Dashboard 标黄 ↓ 连续10次失败 或 5min成功率<20% 不健康(down) — 自动禁用, Dashboard 标红 ↓ 连续3次成功 + 5min成功率>80% 恢复(healthy) — 重新激活 ``` ### 4.2 主动探测 vs 被动统计 选择 | 方式 | 优势 | 劣势 | 决策 | |------|------|------|------| | 主动探测 | 及时发现不可用 Provider | 产生额外 API 调用 | 每 60s 轻量探测 | | 被动统计 | 零额外开销 | 发现延迟(依赖实际请求) | 5min 滑动窗口实时统计 | **选型**:两者结合。主动探测覆盖请求低谷期,被动统计覆盖请求高峰期。 --- ## 5. RPM 流控粒度 ### 5.1 决策 | 方案 | 描述 | 选型 | |------|------|------| | A: Provider 级别 | 每个 Provider 一个 RPM 限制 | ✅ **V2 采用** | | B: Provider + Model 级别 | 同一 Provider 下不同 Model 独立 RPM | V3 扩展 | **理由**:当前多 Provider 场景下,RPM 限制是按 API Key 授予的,而非按 Model 授予。同一 API Key 下的不同 Model 共享 RPM 配额是合理的默认行为。如需 Model 级别 RPM,可在 `providers.metadata` JSON 中扩展,不影响当前 Schema。 --- ## 6. Provider 创建时的 Key 验证 创建 Provider 时必须经过 Key 有效性验证: ``` POST /api/v2/providers ↓ 1. 基本信息校验(name, api_key, endpoint_url 必填) ↓ 2. 发送 test call: GET {endpoint_url}/v1/models Header: Authorization: Bearer {api_key} ↓ ├─ 200 OK → 验证通过 → 加密存储 → 返回 201 ├─ 401/403 → Key 无效 → 返回 400 + "API Key 无效" ├─ 超时 → endpoint_url 不可达 → 返回 400 + "无法连接到 Provider" └─ 其他错误 → 返回 400 + 具体错误信息 ↓ 3. RPM 自动探测(可选):验证通过后,读取响应中的 rate limit headers 如无 headers,使用用户配置的 rpm_limit(默认 60) ``` --- ## 7. 数据模型 ### 7.1 ER 图 ```mermaid erDiagram providers ||--o{ provider_usage_logs : has providers ||--o{ cooldown_events : triggers providers ||--o| provider_health : monitors providers { string id PK string name string api_key string endpoint_url string model_prefix string pool string status string source int rpm_limit int tpm_limit float weight float cost_per_1k string cooldown_until string metadata } provider_usage_logs { string id PK string provider_id FK string model int prompt_tokens int completion_tokens int total_tokens float cost int request_count int error_count int avg_latency_ms string hour_bucket } cooldown_events { string id PK string provider_id FK int consecutive_count int cooldown_seconds string response_summary string started_at string ended_at } provider_health { string provider_id PK string state int last_latency_ms int last_status_code float success_rate_5m int consecutive_failures } daily_stats { string id PK string date string pool int total_requests int total_errors int total_tokens float total_cost int unique_providers } system_config { string key PK string value string description } ``` ### 7.2 完整 DDL ```sql -- Provider 配置表(核心) CREATE TABLE providers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, api_key TEXT NOT NULL, -- AES-256-GCM 加密后存储 endpoint_url TEXT NOT NULL, model_prefix TEXT DEFAULT '', -- 如 "nvidia/", "openai/" pool TEXT NOT NULL DEFAULT 'main' CHECK(pool IN ('main','fallback')), status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','disabled','cooldown')), source TEXT NOT NULL DEFAULT 'db' CHECK(source IN ('env','db')), -- 来源标记 rpm_limit INTEGER NOT NULL DEFAULT 60, tpm_limit INTEGER DEFAULT NULL, -- NULL = 不限制 weight REAL DEFAULT 1.0, cost_per_1k_tokens REAL DEFAULT 0.0, cooldown_until TEXT, -- 冷却结束时间戳(ISO 8601),NULL = 非冷却状态 metadata JSON DEFAULT '{}', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- Provider 用量日志(按小时分桶,支持并发安全写入) CREATE TABLE provider_usage_logs ( id TEXT PRIMARY KEY, provider_id TEXT NOT NULL REFERENCES providers(id), model TEXT DEFAULT 'unknown', prompt_tokens INTEGER DEFAULT 0, completion_tokens INTEGER DEFAULT 0, total_tokens INTEGER DEFAULT 0, cost REAL DEFAULT 0.0, request_count INTEGER DEFAULT 0, error_count INTEGER DEFAULT 0, avg_latency_ms INTEGER DEFAULT 0, hour_bucket TEXT NOT NULL, -- YYYY-MM-DD HH:00 按小时分桶 created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE UNIQUE INDEX idx_usage_provider_hour ON provider_usage_logs(provider_id, hour_bucket); -- 冷却事件日志 CREATE TABLE cooldown_events ( id TEXT PRIMARY KEY, provider_id TEXT NOT NULL REFERENCES providers(id), consecutive_count INTEGER NOT NULL DEFAULT 1, cooldown_seconds INTEGER NOT NULL, response_summary TEXT DEFAULT '', -- 触发冷却的 HTTP 响应摘要(前500字符) started_at TEXT NOT NULL DEFAULT (datetime('now')), ended_at TEXT ); CREATE INDEX idx_cooldown_provider_time ON cooldown_events(provider_id, started_at); -- Provider 健康状态 CREATE TABLE provider_health ( provider_id TEXT PRIMARY KEY REFERENCES providers(id), state TEXT NOT NULL DEFAULT 'healthy' CHECK(state IN ('healthy','degraded','down')), last_latency_ms INTEGER DEFAULT 0, last_status_code INTEGER DEFAULT 200, success_rate_5m REAL DEFAULT 1.0, -- 5分钟滑动窗口成功率 consecutive_failures INTEGER DEFAULT 0, last_check_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- 系统配置 KV CREATE TABLE system_config ( key TEXT PRIMARY KEY, value TEXT NOT NULL, description TEXT DEFAULT '', updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- 每日汇总统计(cron 定时聚合,非实时写入) CREATE TABLE daily_stats ( id TEXT PRIMARY KEY, date TEXT NOT NULL, pool TEXT NOT NULL CHECK(pool IN ('main','fallback')), total_requests INTEGER DEFAULT 0, total_errors INTEGER DEFAULT 0, total_tokens INTEGER DEFAULT 0, total_cost REAL DEFAULT 0.0, unique_providers INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE UNIQUE INDEX idx_daily_date_pool ON daily_stats(date, pool); -- 并发安全写入示例(避免竞态条件) -- 用量日志写入使用 UPSERT: -- INSERT INTO provider_usage_logs (...) VALUES (...) -- ON CONFLICT(provider_id, hour_bucket) DO UPDATE SET -- total_tokens = total_tokens + excluded.total_tokens, -- cost = cost + excluded.cost, -- ...; ``` --- ## 8. API 设计 ### 8.1 代理 API(V1 兼容) | 方法 | 路径 | 说明 | |------|------|------| | POST | `/v1/chat/completions` | Chat Completions 代理 | | POST | `/v1/completions` | Completions 代理 | | POST | `/v1/embeddings` | Embeddings 代理 | | POST | `/v1/responses` | Responses 代理(新增) | | GET | `/v1/models` | 模型列表(聚合所有 Provider) | ### 8.2 请求 Headers ``` X-Sidecar-Pool: main | fallback # 指定目标池(默认 main) X-Sidecar-Provider: # 指定 Provider(调试用) X-Priority: normal | high | urgent # 优先级 ``` ### 8.3 Provider 管理 API ```yaml # Provider CRUD GET /api/v2/providers # 列出 Provider ?pool=main&status=active # 筛选 POST /api/v2/providers # 添加 Provider(含 test call 验证) Body: {name, api_key, endpoint_url, pool, rpm_limit, model_prefix, ...} GET /api/v2/providers/{id} # 查看详情(api_key 脱敏显示) PUT /api/v2/providers/{id} # 更新配置 DELETE /api/v2/providers/{id} # 删除 Provider POST /api/v2/providers/{id}/enable # 启用 POST /api/v2/providers/{id}/disable # 禁用 POST /api/v2/providers/{id}/test # 连通性测试 # 池状态 GET /api/v2/pools # 池概览 GET /api/v2/pools/{pool}/providers # 指定池 Provider 列表 GET /api/v2/cooldowns # 当前冷却 Provider # 统计 GET /api/v2/stats/overview # 总览(今日/本周/本月) GET /api/v2/stats/providers/{id} # 单 Provider 统计 GET /api/v2/stats/daily # 按日汇总 ?days=7 GET /api/v2/stats/tokens # Token 趋势 # 配置 GET /api/v2/config # 系统配置 PUT /api/v2/config # 更新配置 # 健康 GET /health # 存活检查 (liveness) GET /health/ready # 就绪检查 (readiness — DB + Provider池) GET /status # 调试状态(限流器+队列+冷却) # Dashboard GET /api/dashboard # Dashboard HTML GET /api/dashboard/stream # SSE 实时推送 ``` ### 8.4 关键 API 请求/响应示例 **POST /api/v2/providers(添加 Provider)** 请求: ```json { "name": "NVIDIA Key 2", "api_key": "nvapi-xxxxxxxxxxxx", "endpoint_url": "https://integrate.api.nvidia.com/v1", "pool": "main", "rpm_limit": 40, "model_prefix": "nvidia/", "cost_per_1k_tokens": 0.001 } ``` 响应(成功): ```json { "id": "p_abc123", "name": "NVIDIA Key 2", "api_key": "nvapi-*****-xxxx", // 脱敏显示 "pool": "main", "status": "active", "rpm_limit": 40, "test_result": "ok", "test_latency_ms": 234 } ``` 响应(Key 无效): ```json { "error": "api_key_invalid", "message": "API Key 验证失败:上游返回 401 Unauthorized" } ``` **GET /api/v2/pools** 响应: ```json { "main": { "total": 5, "active": 3, "cooldown": 1, "disabled": 1, "total_rpm_capacity": 150, "current_load_rpm": 87 }, "fallback": { "total": 2, "active": 2, "cooldown": 0, "disabled": 0, "total_rpm_capacity": 40, "current_load_rpm": 0 } } ``` --- ## 9. Dashboard 指标清单 ### 9.1 实时状态面板 | 面板 | 指标 | 展示方式 | |------|------|----------| | Provider 状态卡片 | 名称、池、状态(绿/黄/红)、RPM利用率、成功率 | 卡片网格 | | 池概览 | 各池 Provider 数量、总 RPM 容量、当前负载 | 数字仪表 | | 冷却列表 | 冷却中 Provider、剩余冷却时间、历史次数 | 列表 + 倒计时 | ### 9.2 趋势图表 | 图表 | 数据源 | 展示方式 | |------|--------|----------| | Token 用量趋势 | provider_usage_logs | 折线图(按小时/天) | | 调用次数趋势 | provider_usage_logs.request_count | 柱状图 | | 费用走势 | provider_usage_logs.cost | 折线图 + 累计 | | 成功率趋势 | provider_health.success_rate_5m | 折线图 | | 各 Provider 占比 | provider_usage_logs | 饼图 | ### 9.3 历史记录 | 面板 | 数据源 | 展示方式 | |------|--------|----------| | 冷却事件历史 | cooldown_events | 时间线列表 | | 请求日志 | provider_usage_logs | 分页表格 | ### 9.4 告警机制(V3 扩展) 当前 V2 依赖 Dashboard 被动查看,V3 将增加主动告警: - Provider 全部冷却 → Webhook / 飞书通知 - 单 Provider RPM 利用率 > 80% → 预警 - 日费用突增 > 50% → 预警 - Provider 连续不健康 > 10 分钟 → 通知 V2 架构已在 Prometheus metrics 和 `system_config` 中预留告警配置接口。 --- ## 10. 安全设计 ### 10.1 API Key 加密存储 | 参数 | 配置 | |------|------| | 加密算法 | AES-256-GCM | | 密钥来源 | 环境变量 `SIDECAR_ENCRYPTION_KEY`(必填,32 字节 hex) | | 密钥生成 | 部署时:`openssl rand -hex 32` | | 密钥备份 | 备份到安全位置(Bitwarden / 密码管理器),标注于部署 SOP | | 密钥丢失 | 所有已存储 API Key 不可恢复,需重新添加 | ### 10.2 安全策略 | 策略 | 实现 | |------|------| | API Key 加密 | AES-256-GCM,密钥从环境变量注入,不存储在 DB 中 | | Admin API 鉴权 | Bearer Token (`SIDECAR_ADMIN_TOKEN`) | | 内网绑定 | 默认绑定 127.0.0.1,对外端口需显式配置 | | 日志脱敏 | API Key 只显示前 6 位 + 后 4 位(如 `nvapi-*****-xxxx`) | | systemd 加固 | ProtectSystem=strict, NoNewPrivileges=true | | Docker 非 root | USER 1000:1000 | | WebUI 远程访问 | 通过 nginx 反向代理 + Basic Auth / OAuth2 Proxy | ### 10.3 API Key 批量泄露远期防护 当前单机部署下,加密密钥和密文在同一台机器上。远期方案: - 使用 HashiCorp Vault 或云 KMS 管理加密密钥 - 密钥定期轮转(re-encrypt 已有 API Key) --- ## 11. 运维设计 ### 11.1 端口规划 | 端口 | 用途 | 绑定 | |------|------|------| | 9190 | API 代理 + WebUI + Admin API | 127.0.0.1(默认) | | 9191 | Prometheus Metrics | 127.0.0.1(默认) | 防火墙规则: ```bash sudo ufw allow from 192.168.1.0/24 to any port 9190 sudo ufw allow from 192.168.1.0/24 to any port 9191 sudo ufw deny 9190 # 禁止外网 sudo ufw deny 9191 ``` ### 11.2 日志管理 | 配置 | 值 | |------|-----| | 日志格式 | JSON 结构化(structlog) | | 输出目标 | systemd 部署→文件 `/var/log/sidecar-v2/`; Docker→stdout | | 日志轮转 | logrotate: 10MB/文件, 保留 30 天 | | 日志级别 | INFO(默认),可配置 LOG_LEVEL=DEBUG | ### 11.3 SQLite 备份策略 ```bash # /opt/sidecar-v2/scripts/backup.sh # 每日 cron: 0 3 * * * /opt/sidecar-v2/scripts/backup.sh #!/bin/bash DB="/opt/sidecar-v2/data/sidecar_v2.db" BACKUP_DIR="/opt/sidecar-v2/backups" DATE=$(date +%Y%m%d_%H%M%S) BACKUP_FILE="$BACKUP_DIR/sidecar_v2_$DATE.db" # WAL checkpoint 确保一致性 sqlite3 "$DB" "PRAGMA wal_checkpoint(TRUNCATE)" # 备份 sqlite3 "$DB" ".backup $BACKUP_FILE" # 保留 7 天 find "$BACKUP_DIR" -name "sidecar_v2_*.db" -mtime +7 -delete ``` ### 11.4 SQLite 数据库监控 新增 Prometheus 指标: | 指标 | 类型 | 说明 | |------|------|------| | `sidecar_db_size_bytes` | Gauge | SQLite 文件大小 | | `sidecar_db_wal_size_bytes` | Gauge | WAL 文件大小 | | `sidecar_db_integrity_ok` | Gauge | integrity_check 结果 (0/1) | 系统级监控: - 磁盘使用率:> 80% 告警,> 90% 严重告警 - IO 延迟:> 100ms 告警 ### 11.5 配置热加载 - Provider CRUD 操作自动更新内存缓存(无需重启) - 系统配置项(`system_config` 表)支持热加载 - 环境变量变更需重启服务 --- ## 12. V1 → V2 迁移 ### 12.1 V1 兼容性 Checklist | 兼容项 | V1 行为 | V2 行为 | 状态 | |--------|---------|---------|------| | `/v1/chat/completions` | 代理到 NVIDIA | 代理到 Provider 池 | ✅ 兼容 | | `/v1/completions` | 代理到 NVIDIA | 代理到 Provider 池 | ✅ 兼容 | | `/v1/embeddings` | 代理到 NVIDIA | 代理到 Provider 池 | ✅ 兼容 | | `/health` | 存活检查 | 存活检查 | ✅ 兼容 | | `/health/ready` | 上游连通性 | DB + Provider 池就绪 | ⚠️ 增强 | | `/status` | 调试状态 | 增强:池+冷却状态 | ⚠️ 增强 | | `/metrics` | :9191 Prometheus | :9191 增强指标 | ⚠️ 增强 | | `SIDECAR_API_KEY` | NVIDIA Key | 自动创建单 Provider | ✅ 兼容 | | `SIDECAR_RATE_RPM` | 全局 RPM | 单 Provider RPM | ✅ 兼容 | | `SIDECAR_UPSTREAM` | 上游 URL | 单 Provider endpoint | ✅ 兼容 | | `SIDECAR_HOST/PORT` | 监听配置 | 监听配置 | ✅ 兼容 | | `SIDECAR_QUEUE_MAX` | 队列容量 | 队列容量 | ✅ 兼容 | | `X-Priority` Header | 优先级 | 优先级 | ✅ 兼容 | | 日志格式 | JSON structlog | JSON structlog | ✅ 兼容 | | Prometheus metric 命名 | sidecar_* | sidecar_* | ✅ 兼容 | ### 12.2 迁移步骤 ``` 1. 停止 V1 服务 systemctl stop nvidia-sidecar 2. 备份 V1 配置 cp /opt/nvidia-sidecar/.env /tmp/nvidia-sidecar.env.bak 3. 安装 V2 pip install sidecar-v2 4. 配置加密密钥 export SIDECAR_ENCRYPTION_KEY=$(openssl rand -hex 32) # 备份此密钥到密码管理器! 5. 首次启动(自动检测无 DB → 从环境变量创建单 Provider) systemctl start sidecar-v2 6. 验证 curl http://127.0.0.1:9190/health curl http://127.0.0.1:9190/api/v2/providers # 应看到从 SIDECAR_API_KEY 创建的 source=env Provider 7. 通过 WebUI 添加更多 Provider 8. 监控观察 24h,确认无异常 回滚方案: systemctl stop sidecar-v2 systemctl start nvidia-sidecar ``` ### 12.3 回滚方案 如果 V2 出现问题: ```bash # 1. 停止 V2 systemctl stop sidecar-v2 # 2. 恢复 V1 systemctl start nvidia-sidecar # 3. 清理 V2 数据(可选,保留用于排查) # rm /opt/sidecar-v2/data/sidecar_v2.db # V1 纯环境变量驱动,不受 V2 SQLite DB 影响,回滚无数据损失 ``` --- ## 13. Prometheus Metrics 增强 ### 13.1 继承 V1 指标 | 指标 | 说明 | |------|------| | `sidecar_requests_total` | 总请求数 | | `sidecar_errors_total` | 总错误数 | | `sidecar_latency_seconds` | 延迟分布 | | `sidecar_queue_depth` | 当前队列深度 | | `sidecar_token_bucket_available` | 令牌桶可用令牌 | ### 13.2 V2 新增指标 | 指标 | 类型 | Label | 说明 | |------|------|-------|------| | `sidecar_provider_requests_total` | Counter | provider_id, pool, status_code | 每 Provider 请求数 | | `sidecar_provider_latency_seconds` | Histogram | provider_id | 每 Provider 延迟 | | `sidecar_provider_rpm_current` | Gauge | provider_id | 当前 RPM | | `sidecar_provider_cooldown_active` | Gauge | provider_id | 冷却中=1 | | `sidecar_cooldown_events_total` | Counter | provider_id | 冷却触发次数 | | `sidecar_pool_degradation_total` | Counter | from_pool, to_pool | 降级事件 | | `sidecar_emergency_channel_total` | Counter | provider_id | 紧急通道使用次数 | | `sidecar_db_size_bytes` | Gauge | — | SQLite 文件大小 | | `sidecar_db_wal_size_bytes` | Gauge | — | WAL 文件大小 | | `sidecar_db_integrity_ok` | Gauge | — | integrity_check 结果 | --- ## 14. 核心模块设计 ### 14.1 模块结构 ``` sidecar_v2/ ├── server.py # FastAPI 入口 + 生命周期 ├── config.py # 系统配置管理 ├── crypto.py # AES-256-GCM 加解密 │ ├── core/ │ ├── provider.py # Provider 数据模型 + CRUD 服务 │ ├── pool.py # 池管理器(main/fallback/cooldown) │ ├── router.py # 负载均衡路由器 │ ├── rate_limiter.py # Per-Provider Token Bucket │ ├── cooldown.py # 429 冷却管理(含紧急通道) │ └── queue.py # FIFO 优先级队列 │ ├── proxy/ │ ├── handler.py # 请求代理处理器 │ └── retry.py # 429 重试 + 降级逻辑 │ ├── storage/ │ ├── db.py # SQLite/SQLAlchemy 连接管理 │ ├── models.py # ORM 模型 │ └── backup.py # 备份逻辑 │ ├── stats/ │ ├── collector.py # 实时统计采集 │ ├── aggregator.py # 按时段聚合(小时桶 + 日汇总 cron) │ └── cost.py # 费用计算 │ ├── health.py # 健康检查(主动探测 + 被动统计) ├── metrics.py # Prometheus 指标 │ ├── webui/ │ ├── dashboard.py # Dashboard 页面 + SSE │ └── admin.py # Admin API 路由 │ └── templates/ ├── dashboard.html └── static/ ├── app.js └── style.css ``` ### 14.2 并发控制关键代码模式 Provider 选择 + RPM 预占必须原子化: ```python # router.py — Provider 选择(伪代码) def select_provider(pool: str, model: str) -> Provider: with db_session() as session: # BEGIN IMMEDIATE 事务确保原子性 session.execute(text("BEGIN IMMEDIATE")) providers = get_active_providers(session, pool) for p in weighted_round_robin(providers): if rate_limiter.acquire(p.id, estimated_tokens): # 更新当前 RPM 计数 increment_current_rpm(session, p.id) session.commit() return p session.rollback() raise AllProvidersExhausted() ``` 用量日志并发写入(UPSERT 避免竞态): ```python # collector.py — 用量记录 def record_usage(provider_id, tokens, cost, latency, status_code): with db_session() as session: session.execute(text(""" INSERT INTO provider_usage_logs (id, provider_id, model, total_tokens, cost, request_count, error_count, avg_latency_ms, hour_bucket) VALUES (:id, :pid, :model, :tokens, :cost, 1, :err, :lat, :bucket) ON CONFLICT(provider_id, hour_bucket) DO UPDATE SET total_tokens = total_tokens + excluded.total_tokens, cost = cost + excluded.cost, request_count = request_count + excluded.request_count, error_count = error_count + excluded.error_count, avg_latency_ms = (avg_latency_ms * request_count + excluded.avg_latency_ms) / (request_count + excluded.request_count) """), {...}) ``` --- ## 15. 非功能需求 ### 15.1 性能目标 | 指标 | 目标值 | 测量方式 | |------|--------|---------| | 代理延迟增加 | < 5ms (P95) | 对比直连 vs Sidecar 代理 | | 并发处理能力 | 100 req/s | P1 完成后并发压测验证 | | Provider 池查询延迟 | < 1ms | 内存缓存 + SQLite | | Dashboard SSE 延迟 | < 1s | 实时推送间隔 | | 启动时间 | < 3s | systemd 启动计时 | ### 15.2 可用性 | 指标 | 目标 | |------|------| | 主池可用率 | > 99.5%(含冷却恢复) | | Fallback 降级触发率 | < 5% | | 完全不可用(503)概率 | < 0.1% | | 配置热加载 | 支持(Provider CRUD 无需重启) | | 服务重启恢复 | 冷却状态从 DB 恢复,不丢失上下文 | ### 15.3 可观测性 | 维度 | 工具 | |------|------| | 结构化日志 | structlog(JSON 格式) | | 实时指标 | Prometheus(独立 :9191 端口,13 项指标) | | Dashboard | SSE 实时推送 + Chart.js(5 个面板) | | 健康检查 | `/health` + `/health/ready` + `/status` | --- ## 16. 开发排期(修订版) ### 新增 P0:文档推送 + 环境准备(3h) - [ ] 推送架构文档 + 图表到 Git - [ ] 确认开发环境(Python 3.12 + 依赖) ### Phase 1: 数据层 + Provider CRUD(12h) - [ ] SQLite 数据库 + SQLAlchemy ORM + 6 张表 DDL - [ ] AES-256-GCM 加密模块(密钥从环境变量注入) - [ ] Provider CRUD API(添加含 test call 验证) - [ ] 并发压测(100 QPS+ 验证 SQLite WAL) ### Phase 2: 池管理 + 负载均衡(14h) - [ ] PoolManager(main / fallback / cooldown 三池) - [ ] Weighted RR(权重 5-10s 刷新) - [ ] 429 冷却 + 指数退避 + 紧急通道 - [ ] 降级链路(main → fallback 预检 → emergency → 503) - [ ] Prometheus 基础指标接入 - [ ] Provider 健康检查(主动探测 + 被动统计) ### Phase 3: 流控 + 统计(12h) - [ ] Per-Provider Token Bucket + FIFO 优先级队列 - [ ] 请求代理处理器(池选 + 流控 + 重试) - [ ] 用量统计(UPSERT 写入 + 小时桶 + cron 日聚合) - [ ] Prometheus V2 新增指标 ### Phase 4: WebUI + Dashboard(10h) - [ ] Admin 管理页面(Provider 列表/添加/编辑/测试) - [ ] Dashboard(5 个面板:状态卡片、趋势图表、历史记录) - [ ] SSE 实时推送 - [ ] 系统配置页面 ### Phase 5: 测试 + 集成(12h) - [ ] 单元测试(各核心模块) - [ ] 集成测试(多 Provider 429 场景模拟) - [ ] V1 兼容性测试(10 项兼容 checklist) - [ ] 并发压测(100 QPS 持续 5 分钟) ### Phase 6: 部署 + 文档(8h) - [ ] systemd service + Dockerfile - [ ] 部署 SOP(初始化、升级、回滚) - [ ] SQLite 备份脚本 + cron - [ ] V1→V2 迁移文档 - [ ] API 文档(OpenAPI Schema) - [ ] 防火墙 + 日志轮转配置 | Phase | 内容 | 工时 | |-------|------|------| | P0 | 文档推送 + 环境准备 | 3h | | P1 | 数据层 + Provider CRUD | 12h | | P2 | 池管理 + 负载均衡 | 14h | | P3 | 流控 + 统计 | 12h | | P4 | WebUI + Dashboard | 10h | | P5 | 测试 + 集成 | 12h | | P6 | 部署 + 文档 | 8h | | **合计** | | **71h (~12 天)** | --- ## 17. 协作节点 | 阶段 | 协作 Agent | 事项 | |------|-----------|------| | P0 完成 | 严维序 | 审查部署拓扑,确认运维可行性 | | P2 完成 | 严维序 | Prometheus metrics 接入 + 告警规则 | | P5 阶段 | 沈路明 | 按 PRD 验收标准做功能验收 | | P6 完成 | 严维序 | 灰度部署 + 监控确认 | --- ## 18. 技术决策摘要 | 决策 | 选择 | 关键理由 | |------|------|---------| | 数据库 | SQLite WAL | 零运维,满足当前 50 Provider 规模,预留 MySQL 迁移 | | 负载均衡 | Weighted RR (5-10s刷新) | 按 RPM 余量动态加权,平衡性能与响应 | | 冷却策略 | 指数退避 30s→600s + 紧急通道 | 平衡恢复速度与保护上游,避免完全断流 | | 冷却持久化 | SQLite cooldown_events 表 | 服务重启后冷却状态可恢复 | | API Key 存储 | AES-256-GCM,密钥 env var | 密钥不落盘,满足安全基线 | | RPM 粒度 | Provider 级别 | V2 采用,Model 级别 V3 扩展 | | 队列模型 | FIFO + 优先级,容量 500 | 共享队列,溢出 REJECT (503) | | 健康检查 | 主动探测 + 被动统计 | 双重保障,覆盖请求低谷和高分期 | | WebUI | Alpine.js + Chart.js | 与 V1 一致,零构建步骤 | | 统计写入 | UPSERT (ON CONFLICT) | 并发安全,避免竞态 | | Provider 选择 | BEGIN IMMEDIATE 事务 | 原子化选 Provider + 预占 RPM | | 加密密钥备份 | 密码管理器 | 密钥丢失 = 所有 API Key 不可恢复 | | SQLite 备份 | cron 每日 `.backup`,保留 7 天 | 灾备基础,防数据丢失 | | 部署方式 | systemd(主)+ Docker(可选) | 内网环境优先 systemd | --- ## 19. 风险评估 | 风险 | 概率 | 影响 | 应对措施 | |------|------|------|---------| | SQLite 并发瓶颈 | 低 | 中 | WAL 模式 + P1 完成后压测;监控 DB 锁等待 | | API Key 泄露 | 低 | 高 | AES-256 + 日志脱敏 + Admin Token + systemd 加固 | | 429 风暴导致全冷却 | 中 | 高 | 紧急通道 10% RPM + 冷却时间上限 600s | | 权重计算滞后 | 低 | 中 | 滑动窗口计数 + 5-10s 刷新周期 | | 加密密钥丢失 | 低 | 高 | 备份到密码管理器,标注于部署 SOP | | 服务重启丢失冷却状态 | 中 | 中 | cooldown_events 表持久化 + 启动时恢复 | | V1→V2 迁移中断 | 低 | 中 | 回滚方案:重启 V1 即可,V1 纯环境变量驱动 | | SSE 连接数过多 | 低 | 低 | 单 Dashboard 连接,1s 推送间隔 | | 磁盘占满 | 低 | 中 | SQLite 备份保留 7 天,日志轮转 10MB/30 天 | --- ## 20. 下游交付说明 ### 20.1 给 costcodev(徐聪) - Provider 模型和数据库 Schema 严格按本文档 DDL 实现(v2.0 修订版) - 429 冷却使用指数退避 + 紧急通道 + 冷却预检 - Provider 选择 + RPM 预占使用 `BEGIN IMMEDIATE` 事务 - 用量日志使用 `INSERT ... ON CONFLICT DO UPDATE` - V1 兼容性按 13 项 checklist 逐项验证 - API Key 加解密通过 `crypto.py` 统一处理 - 健康检查实现主动探测 + 被动统计混合模式 ### 20.2 给 opengineer(严维序) - 端口:9190 (API+WebUI+Admin), 9191 (metrics) - 数据库:`/opt/sidecar-v2/data/sidecar_v2.db` (WAL 模式) - 环境变量:`SIDECAR_ENCRYPTION_KEY`(加密密钥)、`SIDECAR_ADMIN_TOKEN`(Admin Token) - SQLite 备份:cron 每日 03:00,保留 7 天 - 日志轮转:10MB/文件,保留 30 天 - 防火墙:仅 192.168.1.0/24 可访问 - 部署方式:systemd 主方案,Docker 备选 --- > **架构格言**:好的架构就像城市的下水道——平时看不见,但一旦出问题就是灾难。 > — 梁思筑,2026-06-25 (v2.0 评审修订版)