v2.1 changes from 2nd-round review: + Emergency channel RPM: max(1, max_rpm * 0.1) + Queue 503: add Retry-After: 30 header + sidecar_backup_success Prometheus metric + Startup crypto.py key validation on boot + SQLite size limits: 100MB practical, 500MB WAL + RPM flow: per-request counting, not token-based + SSE streaming: TTFT for avg_latency_ms + Merge proxy/retry.py into core/cooldown.py Added sidecar-v2-nvidia-providers.yaml (11 keys) Co-authored-by: multica-agent <github@multica.ai>
42 KiB
ADR-006: Sidecar V2 — Provider 池管理与负载均衡架构设计
| 属性 | 值 |
|---|---|
| 编号 | ADR-006 |
| 日期 | 2026-06-25 |
| 版本 | v2.1(第二轮评审修订版) |
| 状态 | 提议中 |
| 作者 | 梁思筑 (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 池化网关",实现:
- Provider 池体系:主池 + Fallback 池 + 冷却池
- 多 Provider 负载均衡:每 Provider 独立 RPM 流控
- 429 自动冷却:检测 429 → 进入冷却池 → 指数退避恢复
- 完整 WebUI:配置管理、Dashboard 统计、健康监控
- 用量统计:Token 用量、调用次数、费用核算
- V1 兼容:升级不破坏现有部署
2. 架构设计
2.1 系统拓扑图
graph TB
subgraph "OpenClaw Gateway"
OC[OpenClaw 调度器]
OC_FB[OpenClaw Fallback<br/>传统配置链路]
end
subgraph "Sidecar V2 (systemd / Docker)"
direction TB
subgraph "入口层"
GW["API Gateway :9190<br/>FastAPI + 路由匹配"]
end
subgraph "核心调度层"
LB["负载均衡器<br/>Weighted RR 5-10s刷新"]
QM["队列管理器<br/>FIFO + 优先级<br/>容量500 + 溢出策略"]
end
subgraph "Provider 池层"
direction LR
MP["主池 Main Pool"]
FP["Fallback 池"]
CP["冷却池<br/>Cooldown Pool"]
end
subgraph "流控层"
RL["Rate Limiter<br/>Per-Provider Token Bucket"]
CD["Cooldown Detector<br/>429检测+指数退避<br/>+紧急通道10%RPM"]
end
subgraph "存储与统计层"
MT["Metrics :9191<br/>Prometheus"]
ST["统计引擎<br/>Token/费用/调用量"]
DB[("SQLite WAL<br/>sidecar_v2.db<br/>+ cron备份")]
end
subgraph "WebUI 层 :9190"
UI["Dashboard<br/>SSE 实时推送"]
AP["Admin API<br/>Provider CRUD<br/>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 数据流设计
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 部署架构
graph TB
subgraph "Ubuntu Server (192.168.1.99)"
subgraph "systemd Services"
SVC["sidecar-v2.service<br/>MemoryMax=512M<br/>LimitNOFILE=65536<br/>ProtectSystem=strict"]
end
subgraph "端口规划"
P9190[":9190 API + WebUI + Admin"]
P9191[":9191 Prometheus Metrics"]
end
subgraph "存储"
DBFILE["/opt/sidecar-v2/data/sidecar_v2.db<br/>WAL模式"]
BACKUP["/opt/sidecar-v2/backups/<br/>每日cron .backup + 7天保留"]
end
subgraph "日志"
LOG["/var/log/sidecar-v2/<br/>JSON结构化 + logrotate<br/>10MB/文件 保留30天"]
end
subgraph "防火墙"
FW["UFW: 仅192.168.1.0/24可访问 :9190/:9191"]
end
end
subgraph "可选 Docker"
DOCKER["docker run sidecar-v2<br/>-v /opt/sidecar-v2/data:/data<br/>非root用户运行<br/>HEALTHCHECK"]
end
subgraph "外部"
PROM["Prometheus :9090<br/>抓取 :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,以 max(1, max_rpm * 0.1) RPM 继续尝试(底线至少 1 RPM),避免完全断流。
冷却预检:降级到 Fallback 池前,检查主池是否有 Provider 剩余冷却时间 < 10s,可短暂等待其恢复。
3.6 队列控流机制
| 参数 | 配置 | 说明 |
|---|---|---|
| 队列模型 | FIFO + 优先级 | 继承 V1 的四级优先级(URGENT > HIGH > NORMAL > LOW) |
| 最大队列深度 | 500 | 超出后触发溢出策略 |
| 令牌等待超时 | 30s(默认) | 可配置 SIDECAR_QUEUE_TIMEOUT |
| 溢出策略 | REJECT (503) | 队列满时拒绝新请求,返回 503 + Retry-After: 30 header |
溢出策略选择:
- 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 图
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
-- 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: <id> # 指定 Provider(调试用)
X-Priority: normal | high | urgent # 优先级
8.3 Provider 管理 API
# 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)
请求:
{
"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
}
响应(成功):
{
"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 无效):
{
"error": "api_key_invalid",
"message": "API Key 验证失败:上游返回 401 Unauthorized"
}
GET /api/v2/pools
响应:
{
"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(默认) |
防火墙规则:
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 备份策略
# /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) |
| sidecar_backup_success | Gauge | 最近一次备份是否成功 (0/1),连续 3 天失败触发告警 |
系统级监控:
- 磁盘使用率:> 80% 告警,> 90% 严重告警
- IO 延迟:> 100ms 告警
- SQLite 文件大小经验上限:~100MB(实用),~500MB(WAL 上限),超出则触发 MySQL 迁移评估
11.5 启动时加密密钥校验
启动时 crypto.py 初始化必须执行以下校验:
- 检查
SIDECAR_ENCRYPTION_KEY环境变量是否存在 → 不存在则启动失败,输出FATAL: SIDECAR_ENCRYPTION_KEY not set - 验证密钥长度 = 64 字符(32 字节 hex)→ 不符合则启动失败,输出
FATAL: SIDECAR_ENCRYPTION_KEY must be 64 hex chars (32 bytes) - 尝试解密
providers表中已有的api_key字段 → 解密失败则输出 WARNING 日志(密钥可能已更换,已有 Key 不可恢复)
11.6 配置热加载
- 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 出现问题:
# 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.0 徐聪三项技术答疑
Q1: RPM 流控是请求数还是 Token 数?
A: 请求数流控。每个 HTTP 请求消耗 1 个 RPM 令牌。Token 用量仅用于事后统计(provider_usage_logs)和费用计算(cost 字段)。rate_limiter.acquire(estimated_tokens=0)——estimated_tokens 参数保留但在 V2 中固定为 0,为未来 TPM 流控预留接口。
Q2: SSE 流式响应的用量统计?
A: 流式响应在流结束后写入一条 provider_usage_logs 记录:
total_tokens:从最后一个usagechunk 或累加choices[].delta.content长度估算avg_latency_ms:记录 TTFT(Time To First Token);非流式记录端到端延迟request_count:流式也计为 1 次请求
Q3: proxy/retry.py 与 core/cooldown.py 的关系?
A: 建议采纳。将重试逻辑合并到 core/cooldown.py,对外暴露 should_retry() 和 select_fallback() 接口。简化后的模块结构如下。
15. 核心模块设计
15.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 # 请求代理处理器(重试/降级调用 cooldown.py)
│
├── 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
15.2 并发控制关键代码模式
Provider 选择 + RPM 预占必须原子化:
# 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 避免竞态):
# 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)
"""), {...})
16. 非功能需求
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 |
17. 开发排期(最终版 v2.1)
新增 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 天) |
18. 协作节点
| 阶段 | 协作 Agent | 事项 |
|---|---|---|
| P0 完成 | 严维序 | 审查部署拓扑,确认运维可行性 |
| P2 完成 | 严维序 | Prometheus metrics 接入 + 告警规则 |
| P5 阶段 | 沈路明 | 按 PRD 验收标准做功能验收 |
| P6 完成 | 严维序 | 灰度部署 + 监控确认 |
19. 技术决策摘要
| 决策 | 选择 | 关键理由 |
|---|---|---|
| 数据库 | 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 |
20. 风险评估
| 风险 | 概率 | 影响 | 应对措施 |
|---|---|---|---|
| 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 天 |
21. 下游交付说明
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.1 第二轮评审修订版)