NewAPI 渠道亲和性实践:为什么旧渠道会被持续命中,以及如何让失败渠道自动切换
在 AI 中转站或模型网关中,我们通常会同时接入多个上游渠道,并通过优先级、权重、分组、模型映射等策略做调度。但在开启 NewAPI 的“渠道亲和性”之后,你可能会遇到一个看似反直觉的问题:
某个渠道已经失败,甚至已经被关闭了,但请求仍然不断命中旧渠道;新启用的高优先级、高权重渠道没有马上生效。
这并不一定是调度系统失效,而是“渠道亲和性”本身的设计结果:它会优先复用上一次成功的渠道,从而提升同一会话、同一缓存键、同一请求来源的稳定性。本文会解释这个机制的工作方式,并给出一套脱敏后的推荐配置,帮助你在稳定性和故障切换之间取得平衡。
1. 什么是渠道亲和性
渠道亲和性可以理解为一种“会话级渠道记忆”。
当某个请求第一次成功命中某个渠道后,NewAPI 会根据请求中的某个标识生成缓存键,例如:
- OpenAI Responses 的
prompt_cache_key - Anthropic Messages 的
metadata.user_id - OpenAI Chat Completions 的
user - 请求头里的
X-Request-Id - 平台内部的
token_id
之后,只要请求再次携带相同的标识,并且模型、分组、规则等条件匹配,NewAPI 就会优先使用上一次成功的渠道,而不是重新按照优先级和权重选择渠道。
这可以让同一个用户、同一个会话、同一个 Agent 任务更稳定地落到同一个上游,减少多渠道之间响应差异带来的问题。
2. 为什么高优先级渠道没有马上生效
开启渠道亲和性后,请求选择渠道的大致顺序是:
- 判断当前请求是否命中渠道亲和性规则。
- 如果命中规则,生成亲和缓存键。
- 如果缓存键已有对应渠道,则优先尝试该渠道。
- 如果该渠道仍然可用,就直接使用它。
- 如果没有可用亲和渠道,才进入普通的优先级、权重调度。
- 请求成功后,再把成功渠道写回亲和缓存。
所以,当你新增一个高权重、高优先级渠道时,如果某些请求已经有旧的亲和缓存,它们不会马上切换到新渠道。
这是预期行为。
如果希望新调度策略立刻生效,需要清理渠道亲和性缓存,或者降低 TTL,让旧缓存更快过期。
3. 失败渠道为什么还会“粘住”
常见原因有三个。
3.1 TTL 过长
如果默认 TTL 是 3600 秒,那么旧渠道最多可能被保留 1 小时。
如果你的渠道经常调整,建议把 TTL 改成:
300 秒
或:
600 秒
这样既保留一定亲和性,又不会让旧渠道长期影响调度。
3.2 skip_retry_on_failure 配置不合理
渠道亲和性规则里有一个关键字段:
"skip_retry_on_failure": false
如果设置成 true,命中的亲和渠道失败后,系统可能不会按你预期继续尝试其他渠道并更新亲和缓存。
如果你的目标是:
亲和渠道失败后,自动切换到其他可用渠道,并用新的成功渠道覆盖旧缓存
那么建议设置为:
"skip_retry_on_failure": false
3.3 禁用渠道不会主动全局删除所有相关缓存
当你关闭某个渠道时,NewAPI 通常不会立即扫描并删除所有指向该渠道的亲和缓存。
更常见的处理方式是:
- 下次请求命中该缓存;
- 系统发现缓存中的渠道不可用;
- 当前请求重新走普通调度;
- 成功后写入新的亲和渠道。
如果你希望关闭渠道后立即生效,最直接的方法是:
后台手动清空渠道亲和性缓存
4. 推荐后台配置
如果你希望渠道亲和性更适合生产环境中的故障切换,可以参考以下策略:
启用:开启
成功后切换亲和:开启
默认 TTL:300 或 600
最大条目数:按平台规模设置,例如 100000
渠道关闭后保留亲和:关闭
推荐理解如下:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 启用 | 开启 | 允许同一会话复用成功渠道 |
| 成功后切换亲和 | 开启 | 失败后如果新渠道成功,可更新缓存 |
| 默认 TTL | 300 / 600 | 避免旧渠道粘太久 |
| 最大条目数 | 100000 | 根据业务规模调整 |
| 渠道关闭后保留亲和 | 关闭 | 禁用渠道后尽快回到普通调度 |
5. 推荐规则 JSON 示例
以下配置已脱敏,模型名、请求头和规则名称均为示例。可以根据自己的平台模型命名进行调整。
[
{
"name": "openai-responses-affinity",
"model_regex": [
"^gpt-.*$",
"^o[0-9].*$"
],
"path_regex": [
"^/v1/responses$",
"^/responses$"
],
"key_sources": [
{
"type": "gjson",
"path": "prompt_cache_key"
},
{
"type": "request_header",
"key": "X-Session-Id"
},
{
"type": "context_int",
"key": "token_id"
}
],
"value_regex": "",
"ttl_seconds": 300,
"include_using_group": true,
"include_model_name": true,
"include_rule_name": true,
"skip_retry_on_failure": false,
"param_override_template": {
"operations": [
{
"keep_origin": true,
"mode": "pass_headers",
"value": [
"User-Agent",
"X-Request-Id",
"X-Session-Id"
]
}
]
}
},
{
"name": "openai-chat-completions-affinity",
"model_regex": [
"^gpt-.*$",
"^o[0-9].*$",
"^example-openai-.*$"
],
"path_regex": [
"^/v1/chat/completions$",
"^/chat/completions$"
],
"key_sources": [
{
"type": "gjson",
"path": "user"
},
{
"type": "request_header",
"key": "X-Request-Id"
},
{
"type": "context_int",
"key": "token_id"
}
],
"value_regex": "",
"ttl_seconds": 300,
"include_using_group": true,
"include_model_name": true,
"include_rule_name": true,
"skip_retry_on_failure": false,
"param_override_template": {
"operations": [
{
"keep_origin": true,
"mode": "pass_headers",
"value": [
"User-Agent",
"X-Request-Id"
]
}
]
}
},
{
"name": "anthropic-messages-affinity",
"model_regex": [
"^claude-.*$",
"^example-anthropic-.*$"
],
"path_regex": [
"^/v1/messages$",
"^/messages$"
],
"key_sources": [
{
"type": "gjson",
"path": "metadata.user_id"
},
{
"type": "gjson",
"path": "container"
},
{
"type": "request_header",
"key": "X-App"
},
{
"type": "context_int",
"key": "token_id"
}
],
"value_regex": "",
"ttl_seconds": 300,
"include_using_group": true,
"include_model_name": true,
"include_rule_name": true,
"skip_retry_on_failure": false,
"param_override_template": {
"operations": [
{
"keep_origin": true,
"mode": "pass_headers",
"value": [
"User-Agent",
"X-App",
"Anthropic-Version",
"Anthropic-Beta"
]
}
]
}
},
{
"name": "deepseek-anthropic-affinity",
"model_regex": [
"^deepseek-.*$",
"^example-deepseek-.*$"
],
"path_regex": [
"^/v1/messages$",
"^/messages$"
],
"key_sources": [
{
"type": "gjson",
"path": "metadata.user_id"
},
{
"type": "gjson",
"path": "container"
},
{
"type": "request_header",
"key": "X-App"
},
{
"type": "context_int",
"key": "token_id"
}
],
"value_regex": "",
"ttl_seconds": 300,
"include_using_group": true,
"include_model_name": true,
"include_rule_name": true,
"skip_retry_on_failure": false,
"param_override_template": {
"operations": [
{
"keep_origin": true,
"mode": "pass_headers",
"value": [
"User-Agent",
"X-App",
"Anthropic-Version",
"Anthropic-Beta"
]
}
]
}
},
{
"name": "deepseek-openai-chat-affinity",
"model_regex": [
"^deepseek-.*$",
"^example-deepseek-.*$"
],
"path_regex": [
"^/v1/chat/completions$",
"^/chat/completions$"
],
"key_sources": [
{
"type": "gjson",
"path": "user"
},
{
"type": "request_header",
"key": "X-Request-Id"
},
{
"type": "context_int",
"key": "token_id"
}
],
"value_regex": "",
"ttl_seconds": 300,
"include_using_group": true,
"include_model_name": true,
"include_rule_name": true,
"skip_retry_on_failure": false,
"param_override_template": {
"operations": [
{
"keep_origin": true,
"mode": "pass_headers",
"value": [
"User-Agent",
"X-Request-Id"
]
}
]
}
},
{
"name": "gemini-generate-content-affinity",
"model_regex": [
"^gemini-.*$",
"^models/gemini-.*$"
],
"path_regex": [
"^/v1beta/models/.+:generateContent$",
"^/v1beta/models/.+:streamGenerateContent$",
"^/v1/models/.+:generateContent$",
"^/v1/models/.+:streamGenerateContent$"
],
"key_sources": [
{
"type": "gjson",
"path": "user"
},
{
"type": "request_header",
"key": "X-Request-Id"
},
{
"type": "context_int",
"key": "token_id"
}
],
"value_regex": "",
"ttl_seconds": 300,
"include_using_group": true,
"include_model_name": true,
"include_rule_name": true,
"skip_retry_on_failure": false,
"param_override_template": {
"operations": [
{
"keep_origin": true,
"mode": "pass_headers",
"value": [
"User-Agent",
"X-Request-Id"
]
}
]
}
}
]
6. 调整渠道后的操作建议
当你执行以下操作时:
- 关闭某个渠道;
- 修改渠道优先级;
- 修改渠道权重;
- 新增高优先级渠道;
- 删除故障渠道;
- 修改模型映射;
- 修改分组可用渠道;
建议同步执行:
清空渠道亲和性缓存
否则旧请求可能仍会继续命中旧的亲和渠道,直到 TTL 到期或请求触发重新选择。
7. 推荐排查方式
如果你怀疑某个请求仍然命中旧渠道,可以查看请求日志里的 channel_affinity 信息。
重点关注这些字段:
{
"rule_name": "openai-responses-affinity",
"channel_id": 1001,
"model": "example-model",
"request_path": "/v1/responses",
"key_source": "gjson",
"key_path": "prompt_cache_key",
"using_group": "default"
}
如果日志里持续出现同一个 key_fp 和同一个 channel_id,说明它确实被亲和缓存固定到了该渠道。
如果你已经调整过优先级和权重,但请求仍然命中旧渠道,优先检查:
- 该规则的 TTL 是否太长。
- 是否清空过亲和缓存。
skip_retry_on_failure是否为false。- 旧渠道是否仍然处于可用状态。
- 该请求是否一直携带相同的亲和 key。
8. 总结
渠道亲和性不是普通的负载均衡规则,而是一层“成功渠道记忆”。
它的优势是稳定:
- 同一会话更容易落到同一渠道;
- Agent、长上下文、缓存类请求更稳定;
- 多渠道响应差异更小。
它的代价是调度变化不会立刻生效:
- 高优先级渠道不一定马上接管;
- 老渠道可能在 TTL 内继续被命中;
- 禁用渠道后最好主动清空缓存;
- 失败切换依赖
skip_retry_on_failure和重试策略。
生产环境推荐采用:
短 TTL + 成功后切换亲和 + 失败允许重试 + 调整渠道后清空缓存
这样既能保留渠道亲和性的稳定性,又能在渠道失败、关闭或调度策略变化时尽快切换到新的可用渠道。