Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Snapshot 多节点动态路由

本场景在多个 ScootGate 节点上集中管理 backends/models。节点本地只保存 Provider 凭据引用的实际值,控制面通过版本化快照下发路由数据。

1. 拓扑与职责

                    ┌────────────────────────────────┐
                    │ 控制面                          │
                    │ auth snapshot / routing snapshot│
                    └──────────────┬─────────────────┘
                                   │ HTTPS(Bearer)
               ┌───────────────────┴───────────────────┐
               ▼                                       ▼
        ScootGate prod-a                        ScootGate prod-b
        local credentials                      local credentials
               │                                       │
               └──────────────► egress proxy ◄─────────┘
                                   │
                                   ▼
                                Providers

MQTT routing/notify:只提示版本,不携带路由内容

控制面负责:

  • 生成完整、严格递增的路由快照;
  • 只下发 credential_ref,不持有或下发 Provider key;
  • since 查询返回最新版本或 304;
  • 快照发布后可发送 MQTT 通知加速拉取。

节点负责:

  • 从本地环境/文件解析 credential;
  • 校验并原子替换完整路由表;
  • 拒绝旧版本、未知引用与非法协议组合;
  • 控制面超期不可达时转 unready,但继续服务最后有效表。

2. 节点完整配置

下载:snapshot-node.toml

instance_id = "scootgate-prod-b"

[server]
listen = "0.0.0.0:8443"
max_concurrent_requests = 2048
request_head_timeout_ms = 5000

[server.tls]
cert = "/etc/scootgate/tls/fullchain.pem"
key = "/etc/scootgate/tls/privkey.pem"

[health]
listen = "127.0.0.1:9090"
max_concurrent_requests = 64
request_head_timeout_ms = 2000

[auth]
snapshot_url = "https://control.example.com/v1/scootgate/auth-snapshot"
token = "REPLACE_WITH_AUTH_CONTROL_PLANE_TOKEN"
cache_path = "/var/lib/scootgate/auth-snapshot.json"
poll_interval_secs = 30

[egress]
kind = "http_connect"
addr = "egress-proxy.internal.example.com:8443"
username = "scootgate-prod-b"
password = "REPLACE_WITH_EGRESS_SERVICE_PASSWORD"
tls = true
connect_timeout_ms = 5000

[api]
max_request_body_bytes = 1048576
response_head_timeout_ms = 30000
stream_idle_timeout_ms = 300000

[routing]
max_pre_send_attempts = 2

[routing.load_balancing]
algorithm = "weighted_p2c"
ewma_alpha = 0.2
initial_first_byte_ms = 1000
slow_start_secs = 30

[routing.circuit_breaker]
failure_threshold = 3
failure_window_secs = 30
cooldown_secs = 30
half_open_max_requests = 1
throttle_cooldown_secs = 10

[routing_source]
mode = "snapshot"
snapshot_url = "https://control.example.com/v1/scootgate/routing-snapshot"
token_env = "SCOOTGATE_ROUTING_TOKEN"
cache_path = "/var/lib/scootgate/routing-snapshot.json"
poll_interval_secs = 30
max_stale_secs = 300

[credentials.openai-primary]
file = "/run/secrets/openai-primary-api-key"

[credentials.compatible-backup]
file = "/run/secrets/compatible-backup-api-key"

[credentials.anthropic-primary]
file = "/run/secrets/anthropic-primary-api-key"

[usage]
schema = 2
delivery = "mqtt_outbox"
authoritative = false
outbox_path = "/var/lib/scootgate/usage-outbox"
max_outbox_bytes = 1073741824
flush_interval_ms = 500
on_full = "drop_newest"

[mqtt]
broker = "mqtts://mqtt.internal.example.com:8883"
username = "scootgate-prod-b"
password = "REPLACE_WITH_MQTT_SERVICE_PASSWORD"
topic_prefix = "scootgate/prod"
client_id = "scootgate-prod-b"
queue_capacity = 8192

[shutdown]
grace_period_secs = 60

[log]
level = "info"

注意此配置没有 [backends][models]。snapshot 模式声明任何本地 backend/model 都会启动失败,不存在本地与远端合并。

在进程管理器中注入路由控制面 token:

export SCOOTGATE_ROUTING_TOKEN='REPLACE_WITH_ROUTING_CONTROL_PLANE_TOKEN'
scootgate --config /etc/scootgate/scootgate.toml

生产环境不要在交互式 shell 中导出真实 token;应由 secret manager 注入进程环境。

3. 初始路由快照

版本 41 提供两个 OpenAI-compatible target 和一个 Anthropic target:

下载:routing-snapshot-v41.json

{
  "schema": 1,
  "version": 41,
  "backends": {
    "openai-primary": {
      "protocol": "openai_compatible",
      "upstream": "https://api.openai.com",
      "credential_ref": "openai-primary",
      "max_inflight_per_node": 64,
      "enabled": true
    },
    "compatible-backup": {
      "protocol": "openai_compatible",
      "upstream": "https://compatible.example.net",
      "credential_ref": "compatible-backup",
      "max_inflight_per_node": 32,
      "enabled": true
    },
    "anthropic-primary": {
      "protocol": "anthropic_messages",
      "upstream": "https://api.anthropic.com",
      "credential_ref": "anthropic-primary",
      "max_inflight_per_node": 64,
      "enabled": true
    }
  },
  "models": {
    "chat-fast": {
      "protocol": "openai_compatible",
      "enabled": true,
      "targets": [
        {
          "backend": "openai-primary",
          "model": "gpt-4o-mini",
          "weight": 100,
          "supports_stream": true,
          "supports_tools": true,
          "supports_json_schema": true
        },
        {
          "backend": "compatible-backup",
          "model": "vendor-chat-fast",
          "weight": 40,
          "supports_stream": true,
          "supports_tools": false,
          "supports_json_schema": false
        }
      ]
    },
    "claude-fast": {
      "protocol": "anthropic_messages",
      "enabled": true,
      "targets": [
        {
          "backend": "anthropic-primary",
          "model": "claude-3-5-haiku-latest",
          "weight": 100,
          "supports_stream": true,
          "supports_tools": true,
          "supports_json_schema": true
        }
      ]
    }
  }
}

控制面接口:

GET /v1/scootgate/routing-snapshot?since=0
Authorization: Bearer REPLACE_WITH_ROUTING_CONTROL_PLANE_TOKEN

响应:

HTTP/1.1 200 OK
Content-Type: application/json

{完整版本 41 快照}

若节点请求 since=41 且没有更新,返回:

HTTP/1.1 304 Not Modified
Content-Length: 0

成功的 200 或 304 都刷新 control-plane freshness。网络错误、401、非法 JSON 或编译失败都不会刷新版本,也不会覆盖旧表。

4. 冷启动顺序

  1. 节点加载授权缓存;没有则保持 unready;
  2. 节点加载路由缓存;没有则 API 返回 503 not_ready
  3. auth 与 routing poller 分别拉取控制面;
  4. 两份有效快照都加载后 /readyz 返回 200;
  5. 流量入口才把节点加入服务集合。

示例健康响应:

{
  "service": "scootgate",
  "instance_id": "scootgate-prod-b",
  "version": "0.1.0",
  "status": "ready",
  "ready": true,
  "auth_snapshot_version": 7,
  "routing_snapshot_version": 41,
  "usage_outbox": {
    "pending_events": 0,
    "pending_bytes": 0,
    "dropped_events": 0,
    "oldest_event_age_secs": null
  }
}

5. 热更新权重

版本 42 把备用 OpenAI-compatible target 从权重 40 提高到 100:

下载:routing-snapshot-v42.json

{
  "schema": 1,
  "version": 42,
  "backends": {
    "openai-primary": {
      "protocol": "openai_compatible",
      "upstream": "https://api.openai.com",
      "credential_ref": "openai-primary",
      "max_inflight_per_node": 64,
      "enabled": true
    },
    "compatible-backup": {
      "protocol": "openai_compatible",
      "upstream": "https://compatible.example.net",
      "credential_ref": "compatible-backup",
      "max_inflight_per_node": 32,
      "enabled": true
    },
    "anthropic-primary": {
      "protocol": "anthropic_messages",
      "upstream": "https://api.anthropic.com",
      "credential_ref": "anthropic-primary",
      "max_inflight_per_node": 64,
      "enabled": true
    }
  },
  "models": {
    "chat-fast": {
      "protocol": "openai_compatible",
      "enabled": true,
      "targets": [
        {
          "backend": "openai-primary",
          "model": "gpt-4o-mini",
          "weight": 100,
          "supports_stream": true,
          "supports_tools": true,
          "supports_json_schema": true
        },
        {
          "backend": "compatible-backup",
          "model": "vendor-chat-fast",
          "weight": 100,
          "supports_stream": true,
          "supports_tools": false,
          "supports_json_schema": false
        }
      ]
    },
    "claude-fast": {
      "protocol": "anthropic_messages",
      "enabled": true,
      "targets": [
        {
          "backend": "anthropic-primary",
          "model": "claude-3-5-haiku-latest",
          "weight": 100,
          "supports_stream": true,
          "supports_tools": true,
          "supports_json_schema": true
        }
      ]
    }
  }
}

推荐发布顺序:

  1. 控制面先把版本 42 设为当前完整快照;

  2. 再向 scootgate/prod/routing/notify 发布 QoS 1 消息:

    {"version": 42}
    
  3. 节点收到通知后等待 500ms 合并突发,再用 bearer token 拉取完整快照;

  4. /healthzrouting_snapshot_version 变为 42;

  5. 观察两个 target 的选择量、EWMA、429 与熔断状态。

通知只是一种加速。消息丢失时,30 秒轮询仍会获取新版本;通知不能直接改状态。

6. 状态继承与请求一致性

版本 41 → 42 只修改 target 权重,三个 backend 的 id、协议、origin、 credential_ref 与容量不变,因此节点保留其:

  • 在途计数;
  • EWMA 首包延迟;
  • 熔断与 half-open 状态;
  • 429 冷却;
  • 因 401/403 产生的 disabled 状态。

若上述 backend 身份字段任一变化,则创建新 runtime 状态。

每个请求在开始时固定一个 Arc<RoutingTable>。更新后:

  • 新请求使用版本 42;
  • 进行中的请求继续使用版本 41;
  • 不会在一次流式响应中跨版本换 backend。

7. 回滚

版本号必须严格递增,不能重新发布 41。回滚版本 42 的内容时:

  1. 复制版本 41 的 backends/models 内容;
  2. 把版本号设为 43;
  3. 先发布完整版本 43;
  4. 再发送 {"version":43} 通知;
  5. 确认所有节点健康面已切换。

这保证乱序、重复或延迟消息不会让节点状态倒退。

8. 故障与恢复

故障节点行为恢复
快照引用未知 credential整份拒绝,继续版本 41发布更高版本的有效完整快照
schema 不是 1整份拒绝修正 schema 并提高版本
收到版本 40/41 通知忽略,不拉取旧状态无需操作
MQTT 断线轮询继续,数据面不受阻重连后恢复通知
控制面短暂不可达旧表继续服务,仍 ready成功 200/304 后刷新 freshness
超过 300 秒不可达旧表继续服务,/readyz 非 200拉取成功后自动恢复 ready
新版本编译失败后发布 43版本 41 保留直到 43 成功原子切到 43

9. 多节点验收

对每个节点分别确认:

  • auth_snapshot_versionrouting_snapshot_version 达到目标;
  • /readyz 为 200 后才加入入口;
  • 版本更新期间持续请求无中断;
  • 坏版本从未出现在健康面;
  • 控制面超期时节点被入口摘除;
  • MQTT/outbox 不包含 Provider key、客户端 key 或请求正文;
  • 任一节点退出只损失容量,不影响其他节点路由。