Files
sub2api-cn-relay-manager/docs/PLUGIN_ROUTE_STICKY_DESIGN.md
2026-05-28 15:37:13 +08:00

12 KiB
Raw Blame History

插件层逻辑分组与粘性路由设计

日期2026-05-28

目标

不修改宿主源码的前提下,由 relay-manager 插件层提供一层“逻辑分组 + 多 route 调度”能力,实现:

  • 前端只看到一个逻辑分组
  • 插件自动把请求路由到背后的多个真实线路
  • 同一会话尽量保持 route 粘性,提高宿主与上游缓存命中
  • 宿主继续只承载单线路 group不承担多 route 聚合

这个设计对应的真实部署形态是:

User
  -> relay-manager plugin router
  -> sub2api host (shadow group A / B / C)
  -> upstream route A / B / C

核心概念

1. 逻辑分组 logical_group

逻辑分组是插件层对用户暴露的“产品分组”,例如:

  • gpt-shared
  • deepseek-shared

它不是宿主里的真实 group,而是插件自己的聚合对象。

职责:

  • 面向前端展示
  • 绑定一组公开模型
  • 绑定一组 route
  • 承载 route policy、sticky policy、fallback policy

2. 路由线路 route

route 是逻辑分组下的一条具体出站线路,例如:

  • asxs
  • codex2api
  • official

职责:

  • 指向一个真实宿主 shadow group
  • 声明支持哪些公开模型
  • 提供优先级、权重、健康状态、熔断状态

3. 宿主影子分组 shadow_group

shadow group 是宿主里的真实 group例如

  • gpt-shared__asxs
  • gpt-shared__codex2api

职责:

  • 承载单条 route 对应的账号池
  • 继续使用宿主既有的 group 内 sticky/account scheduling
  • 不再对用户直接暴露

数据结构

A. 逻辑分组定义

建议插件层持久化表:logical_groups

{
  "logical_group_id": "gpt-shared",
  "display_name": "GPT Shared",
  "status": "active",
  "description": "GPT 多线路逻辑分组",
  "public_models": [
    "gpt-5.4",
    "gpt-5.4-mini"
  ],
  "default_route_policy": "priority",
  "sticky_policy": {
    "mode": "conversation_preferred",
    "conversation_ttl_seconds": 7200,
    "user_model_ttl_seconds": 1800
  },
  "failover_policy": {
    "consecutive_retryable_failures": 2,
    "cooldown_seconds": 600
  },
  "created_at": "2026-05-28T00:00:00Z",
  "updated_at": "2026-05-28T00:00:00Z"
}

字段说明:

  • public_models: 对用户暴露的公开模型
  • default_route_policy: 第一版建议只支持 priority
  • sticky_policy: route 级粘性配置
  • failover_policy: 熔断与切换阈值

B. route 定义

建议插件层持久化表:logical_group_routes

{
  "route_id": "gpt-shared.asxs",
  "logical_group_id": "gpt-shared",
  "name": "asxs",
  "status": "active",
  "priority": 10,
  "weight": 100,
  "shadow_group_id": "gpt-shared__asxs",
  "shadow_group_host_id": "remote43-route-lab-18169",
  "supported_public_models": [
    "gpt-5.4",
    "gpt-5.4-mini"
  ],
  "upstream_base_url_hint": "https://api.asxs.top/v1",
  "retryable_error_classes": [
    "upstream_5xx",
    "upstream_429",
    "gateway_timeout"
  ],
  "non_retryable_error_classes": [
    "model_unsupported",
    "invalid_api_key",
    "group_disabled"
  ],
  "cooldown_until": null,
  "metadata": {}
}

字段说明:

  • priority: 数值越小优先级越高
  • shadow_group_id: 该 route 对应宿主真实 group
  • shadow_group_host_id: 对应宿主实例
  • cooldown_until: 熔断冷却截止时间

C. 模型覆盖定义

第一版可以不单独拆表,直接使用 route.supported_public_models
如果后续要支持 route 级别模型差异,可以扩展成:

  • logical_group_route_models

例如:

{
  "route_id": "gpt-shared.codex2api",
  "public_model": "gpt-5.4",
  "shadow_model": "gpt-5.4",
  "enabled": true
}

第一版建议不预先复杂化,先假设:

  • public_model == shadow upstream model
  • 或 shadow model 已由宿主 provider/account mapping 解决

运行态上下文

插件层处理一次请求时,建议构造统一上下文:

{
  "logical_group_id": "gpt-shared",
  "public_model": "gpt-5.4",
  "user_id": "u_123",
  "api_key_id": "k_456",
  "conversation_id": "conv_789",
  "session_id": "sess_789",
  "request_id": "req_abc",
  "sticky_key": "computed-later",
  "selected_route_id": "gpt-shared.asxs",
  "selected_shadow_group_id": "gpt-shared__asxs"
}

路由流程

Phase 1优先支持“同逻辑分组多 route不同公开模型名或相同模型名但明确 priority”

插件层一次请求的最小流程应为:

1. 解析请求
2. 确定 logical_group_id
3. 确定 public_model
4. 生成 sticky key
5. 先查 sticky route
6. sticky route 可用则继续使用
7. 否则按 route policy 选择候选 route
8. 写入 sticky
9. 转发到对应 shadow group
10. 记录结果,必要时更新失败计数 / 冷却状态

详细步骤

1. 解析用户请求

从请求中提取:

  • 逻辑分组
  • 公开模型名
  • 用户标识
  • conversation/session 标识

推荐优先级:

  • conversation_id
  • session_id
  • 请求体 metadata 里的稳定用户标识
  • API key / user id

2. 加载逻辑分组配置

读取:

  • logical_groups
  • logical_group_routes

过滤:

  • logical_group.status == active
  • route.status == active
  • 当前时间未落入 cooldown_until
  • public_modelsupported_public_models

3. 生成 sticky key

按“强粘性优先”原则:

  1. conversation 级
  2. session 级
  3. user+model 级
  4. 稳定哈希兜底

推荐 key 生成规则

3.1 conversation 级 sticky

conversation_id 存在时:

lg:{logical_group_id}:m:{public_model}:conv:{conversation_id}

3.2 session 级 sticky

session_id 存在时:

lg:{logical_group_id}:m:{public_model}:sess:{session_id}

3.3 user-model 级 sticky

当只有用户标识时:

lg:{logical_group_id}:m:{public_model}:user:{user_or_api_key_id}

3.4 hash bucket 兜底

完全无会话标识时:

lg:{logical_group_id}:m:{public_model}:bucket:{stable_hash(user_or_ip_or_api_key)%128}

这不是强 session 粘性,只是为了避免每次随机抖动。

Redis key 设计

A. sticky route key

value 建议:

{
  "route_id": "gpt-shared.asxs",
  "shadow_group_id": "gpt-shared__asxs",
  "public_model": "gpt-5.4",
  "bound_at": "2026-05-28T12:00:00Z",
  "expires_at": "2026-05-28T14:00:00Z",
  "last_ok_at": "2026-05-28T12:34:00Z",
  "fail_count": 0
}

TTL

  • conversation/session 级:默认 7200s
  • user-model 级:默认 1800s
  • bucket 级:默认 600s

B. route failure counter

key

routefail:{route_id}

value

{
  "consecutive_retryable_failures": 1,
  "last_failure_at": "2026-05-28T12:35:00Z",
  "last_error_class": "upstream_5xx"
}

作用:

  • 辅助 route 熔断
  • 与 sticky key 解耦

C. route cooldown key

key

routecool:{route_id}

value

{
  "cooldown_until": "2026-05-28T12:45:00Z",
  "reason": "consecutive_retryable_failures"
}

作用:

  • route 进入冷却期后,不参与新 sticky 选择
  • 已命中该 route 的旧 sticky 也要在下次请求时重新评估

D. 可选:逻辑分组模型首选 route cache

key

routepref:{logical_group_id}:{public_model}

value

{
  "route_id": "gpt-shared.asxs",
  "updated_at": "2026-05-28T12:00:00Z"
}

第一版可直接读 DB不一定要上 Redis。
只有当路由配置频繁热更新、且请求量明显上来后,才值得加这层缓存。

选路算法

第一版推荐:priority + sticky + failover

不要一开始做复杂权重、实时负载、成本优化。
第一版最稳的算法:

  1. 先查 sticky route
  2. 如果 sticky route 当前健康且支持该模型,则继续使用
  3. 否则按 priority 从小到大选第一个健康 route
  4. 成功后写新的 sticky
  5. 失败时按错误类别决定是否切换

伪代码

resolveRoute(ctx):
  candidates = active routes for logical_group + public_model
  sticky = redis.get(sticky_key)

  if sticky exists:
    route = find route by sticky.route_id
    if route is healthy and not cooling down:
      return route

  sort candidates by priority asc
  for route in candidates:
    if route healthy and not cooling down:
      redis.set(sticky_key, route)
      return route

  return no_route_available

错误分类与 failover

A. 可重试失败 retryable

建议包括:

  • 宿主 502/503/504
  • upstream 5xx
  • upstream timeout
  • upstream 429
  • route host temporarily unavailable

处理策略:

  • 单次失败先累加 fail_count
  • 达到阈值后触发 route fallback
  • 对旧 route 进入 cooldown

B. 不可重试失败 non-retryable

建议包括:

  • invalid api key
  • model unsupported
  • group disabled
  • account disabled
  • provider misconfigured

处理策略:

  • 当前 route 立即标记不可用于该模型
  • 直接尝试下一个 route
  • 如果没有其他 route则返回明确错误

C. fallback 规则

默认建议:

  • consecutive_retryable_failures >= 2 时切 route
  • cooldown = 600s

这组值保守、简单,足够第一版使用。

粘性策略

1. 为什么插件层必须自己做 sticky

因为宿主当前 sticky 是按真实 group_id 做的,而不是按逻辑分组。
如果插件层不先把会话稳定送到同一个 shadow group

  • 宿主会在不同 shadow group 之间重新选账号
  • 上游缓存命中会下降
  • 会话体验会抖动

所以正确顺序是:

  1. 插件层先做 route sticky
  2. 宿主层再做 shadow group 内 account sticky

2. TTL 建议

默认建议:

  • conversation sticky2h
  • session sticky2h
  • user-model sticky30m
  • bucket sticky10m

原则:

  • 长会话优先稳定
  • 无状态请求尽量降低长期粘死风险

3. 何时刷新 sticky TTL

只在这些情况下刷新:

  • 本次请求成功
  • 本次 route 未进入 cooldown

不要在失败请求上无脑刷新 TTL否则坏 route 会被粘住。

转发行为

插件在选定 route 后,应把请求转发到:

  • 指定宿主实例
  • 指定 shadow group 对应的用户入口/API key

第一版建议保持简单:

  • 一个 route 对应一个宿主 shadow group
  • 一个 shadow group 对应一组独立宿主 API key / group token / user key

插件需要做的是:

  • 保持原始请求体
  • 保留 conversation/session 标识
  • 可附加少量内部调试头,例如:
    • X-Relay-Logical-Group
    • X-Relay-Route-ID
    • X-Relay-Shadow-Group

这些头只用于内部审计,不暴露给终端用户。

最小观测字段

插件层至少要记录:

  • request_id
  • logical_group_id
  • public_model
  • selected_route_id
  • selected_shadow_group_id
  • sticky_key_type
  • sticky_hit
  • fallback_used
  • error_class
  • upstream_status
  • latency_ms

这样以后排查“为什么这次走了 codex2api 而不是 asxs”时才有证据。

第一版推荐实现边界

第一版只做这些,避免过度设计:

  • 逻辑分组
  • route 列表
  • priority 选路
  • Redis sticky
  • Redis cooldown
  • retryable / non-retryable 分类
  • 转发到 shadow group

第一版不要做:

  • 动态权重学习
  • 实时成本最优
  • 自动 A/B
  • 跨 route token 级缓存共享
  • 复杂多臂老虎机

以 asxs + codex2api 为例

逻辑分组

{
  "logical_group_id": "gpt-shared",
  "display_name": "GPT Shared",
  "public_models": ["gpt-5.4", "gpt-5.4-mini"]
}

routes

[
  {
    "route_id": "gpt-shared.asxs",
    "priority": 10,
    "shadow_group_id": "gpt-shared__asxs",
    "supported_public_models": ["gpt-5.4", "gpt-5.4-mini"]
  },
  {
    "route_id": "gpt-shared.codex2api",
    "priority": 20,
    "shadow_group_id": "gpt-shared__codex2api",
    "supported_public_models": ["gpt-5.4", "gpt-5.4-mini"]
  }
]

效果:

  • 默认优先 asxs
  • asxs 故障时切 codex2api
  • 同一会话尽量持续命中第一次成功选中的 route

一句话结论

这套设计的本质是:

  • 对外暴露一个 logical_group
  • 对内维护多个 route -> shadow_group
  • 由插件层先做 route sticky
  • 再由宿主在 shadow group 内做 account sticky

这样才能在不修改宿主源码的前提下,尽量接近“一个分组、多 URL、强粘性、高缓存命中”的目标效果。