# 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 评审修订版)