控制面快照协议
ScootGate 通过两个独立 HTTPS pull 协议消费控制面状态:
- 授权快照:客户端 API key 摘要;
- 路由快照:backends 与公共 models(仅 snapshot 模式)。
MQTT 只提示“可能有新版本”,不能携带或直接应用快照。
通用拉取契约
ScootGate 在配置 URL 后追加当前版本:
GET <snapshot_url>?since=<current-version>
Authorization: Bearer <service-token>
如果配置 URL 已有 query,则使用 &since=。
控制面响应:
| 状态 | 含义 |
|---|---|
200 OK | body 为完整 JSON 快照 |
304 Not Modified | 没有更新,body 为空 |
| 其他 | 同步失败,保留当前状态 |
两类 body 上限均为 8 MiB。网络、HTTP、JSON、schema 或语义校验失败时, 旧快照保持不变。失败轮询指数退避,最长 300 秒;MQTT 通知会在 500ms debounce 后提前触发一次相同的受认证拉取。
控制面应使用 HTTPS。Bearer token 只授予对应节点读取快照的最小权限。
授权快照
配置:
[auth]
snapshot_url = "https://control.example.com/v1/auth-snapshot"
token = "AUTH_CONTROL_TOKEN"
cache_path = "/var/lib/scootgate/auth-snapshot.json"
poll_interval_secs = 30
JSON schema
{
"version": 7,
"keys": [
{
"key_id": "client-123",
"tenant_id": "tenant-a",
"key_sha256": "299ccbc5cc6d53eb7860d96cb94e58213a550cd3aaa12d18f6ec2a3b305808a4",
"status": "active"
}
]
}
完整示例:auth-snapshot.json
字段要求
| 字段 | 类型 | 要求 |
|---|---|---|
version | u64 | 大于 0 且严格大于当前版本 |
keys | array | 完整 key 集合 |
key_id | string | 非空 |
tenant_id | string | 非空 |
key_sha256 | string | 原始客户端 key 的 64 位 SHA-256 hex |
status | string | active 或 revoked |
同一快照不得出现重复 digest,未知字段会使整份快照失败。hex 大小写均可, 节点内部规范化为小写。只有 active key 进入查找表;revoked 与未知 key 在数据面都表现为 401。
控制面计算 digest:
lowercase_hex(SHA-256(UTF-8 bytes of the exact client API key))
不要对 key 做 trim、Unicode 规范化或其他预处理。
路由快照
仅在以下模式启用:
[routing_source]
mode = "snapshot"
snapshot_url = "https://control.example.com/v1/routing-snapshot"
token_env = "SCOOTGATE_ROUTING_TOKEN"
cache_path = "/var/lib/scootgate/routing-snapshot.json"
poll_interval_secs = 30
max_stale_secs = 300
Bearer token 从 token_env 指向的环境变量读取。
JSON schema
{
"schema": 1,
"version": 41,
"backends": {
"provider-a": {
"protocol": "openai_compatible",
"upstream": "https://api.example.com",
"credential_ref": "provider-a",
"max_inflight_per_node": 64,
"enabled": true
}
},
"models": {
"chat-fast": {
"protocol": "openai_compatible",
"enabled": true,
"targets": [
{
"backend": "provider-a",
"model": "real-model",
"weight": 100,
"supports_stream": true,
"supports_tools": true,
"supports_json_schema": true
}
]
}
}
}
完整示例:
顶层要求
| 字段 | 类型 | 要求 |
|---|---|---|
schema | u32 | 固定为 1 |
version | u64 | 严格大于已应用版本 |
backends | object | 完整 backend map,至少一项 |
models | object | 完整公共 model map,至少一项 |
内部字段与配置参考一致。额外要求:
credential_ref必须存在于节点本地[credentials];- 快照绝不能包含 Provider credential 值;
- model target 引用的 backend 必须存在且协议一致;
- upstream 必须是合法 origin,生产使用 HTTPS;
- model target 池不能为空,权重与容量必须在范围内;
- 任意未知字段或非法引用都会整体拒绝。
原子应用与状态继承
快照完整编译成功后一次性替换。请求在开始时固定一份表,热更新不影响在途请求。
backend 的 id、protocol、origin、credential 内容和容量均不变时,节点继承:
- EWMA 首包延迟;
- 在途计数;
- 熔断/half-open;
- 429 冷却;
- 401/403 停用状态。
任一身份字段变化会创建新的 backend runtime。
Freshness
成功的 200 应用或 304 会刷新 routing control-plane freshness。
超过 max_stale_secs 没有成功同步时:
/readyz返回 503;- 已有最后有效表继续服务;
- 流量入口应摘除该节点;
- 后续成功同步会自动恢复 ready。
发布与回滚
控制面必须先使完整快照可被 HTTPS 拉取,再发送 MQTT 版本通知。
版本号不能回退。若要恢复版本 41 的内容,应发布相同内容但使用新版本 43, 而不是重新发送 41。这样重复、乱序和延迟消息不会倒退节点状态。
缓存
节点将最后有效快照原子写入本地缓存(Unix 文件权限 0600),重启时先加载缓存:
- 授权缓存只含 digest 与身份;
- 路由缓存只含 credential reference;
- 缓存损坏或过大时忽略并等待控制面;
- 缓存不是控制面事实源,后续仍持续轮询。