Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

20 KiB

重试机制技术文档

本文档详细描述 new-api 的请求重试机制,包括重试流程、优先级降级、权重选择、状态码判断、跨组重试、渠道亲和性、计费退款、自动禁用等完整链路。


目录

  1. 整体流程概述
  2. 重试次数与配置
  3. 优先级降级机制
  4. 权重选择机制
  5. 重试判断规则
  6. 可重试状态码范围
  7. Auto-Group 跨组重试
  8. 渠道亲和性与重试
  9. 计费与退款
  10. 自动禁用渠道
  11. Task Relay 重试
  12. RetryParam 状态管理

1. 整体流程概述

请求完整链路

HTTP Request
  → router (SetApiRouter)
    → middleware/auth.go (认证)
    → middleware/distributor.go (初始渠道选择,retry=0)
    → middleware/rate_limit.go (限流)
    → controller/relay.go Relay()
      → PreConsumeBilling() (预扣费)
      → retry loop (retry=0 ~ RetryTimes):
          → getChannel() (retry=0 用中间件选好的,retry≥1 重新选择)
          → restore request body
          → relayHandler() (发送到上游)
          → 成功则 return
          → 失败则 processChannelError() + shouldRetry() 判断
      → 全部失败 → BillingSession.Refund() (退款)

关键文件与行号

文件 函数/逻辑 行号
controller/relay.go Relay() 重试循环 181-234
controller/relay.go getChannel() 渠道获取 283-313
controller/relay.go shouldRetry() 重试判断 315-339
controller/relay.go processChannelError() 错误处理 341-392
controller/relay.go shouldRetryTaskRelay() 任务重试判断 604-641
model/channel_cache.go GetRandomSatisfiedChannel() 优先级+权重选择 96-191
service/channel_select.go CacheGetRandomSatisfiedChannel() 跨组重试编排 83-162
service/channel_select.go RetryParam 结构体 14-46
middleware/distributor.go 初始渠道选择 60-137
setting/operation_setting/status_code_ranges.go 状态码范围配置 1-197
service/channel_affinity.go 渠道亲和性重试判断 493-532
service/billing_session.go 计费会话 1-115
service/channel.go ShouldDisableChannel() 自动禁用 47-102

重试循环核心代码

controller/relay.go:181-234

retryParam := &service.RetryParam{
    Ctx:        c,
    TokenGroup: relayInfo.TokenGroup,
    ModelName:  relayInfo.OriginModelName,
    Retry:      common.GetPointer(0),
}

for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() {
    channel, channelErr := getChannel(c, relayInfo, retryParam)
    // ... 渠道选择、body 恢复、请求转发 ...
    if newAPIError == nil {
        return  // 成功,直接返回
    }
    processChannelError(c, ...)      // 记录错误 + 可能自动禁用
    if !shouldRetry(c, newAPIError, common.RetryTimes-retryParam.GetRetry()) {
        break  // 不可重试,退出循环
    }
}
// 循环结束,defer 执行退款

2. 重试次数与配置

RetryTimes 配置

属性
变量 common.RetryTimes
定义 common/constants.go:122
默认值 0(不重试,只做 1 次初始尝试)
存储位置 数据库 options 表,key=RetryTimes
加载方式 model/option.go:496-497strconv.Atoi(value)
配置入口 管理后台 → 运营设置

循环次数计算

总尝试次数 = RetryTimes + 1

循环条件 retryParam.GetRetry() <= common.RetryTimes

RetryTimes 值 循环范围 总尝试次数
0 retry=0 1 次(仅初始尝试)
1 retry=0, 1 2 次
3 retry=0, 1, 2, 3 4 次
5 retry=0, 1, 2, 3, 4, 5 6 次

与优先级的关系

shouldRetry()retryTimes 参数 = common.RetryTimes - retryParam.GetRetry(),表示剩余重试次数。当 retryTimes <= 0 时不会重试。


3. 优先级降级机制

核心算法

model/channel_cache.go:125-142

// 1. 收集所有唯一优先级值
uniquePriorities := make(map[int]bool)
for _, channelId := range channels {
    uniquePriorities[int(channel.GetPriority())] = true
}

// 2. 降序排序(最高优先级在前)
sort.Sort(sort.Reverse(sort.IntSlice(sortedUniquePriorities)))

// 3. retry index 映射到优先级层级
if retry >= len(uniquePriorities) {
    retry = len(uniquePriorities) - 1  // 限制为最低优先级
}
targetPriority := int64(sortedUniquePriorities[retry])

映射规则

retry=0 → 最高优先级(sortedUniquePriorities[0])
retry=1 → 次高优先级(sortedUniquePriorities[1])
...
retry ≥ 层级数 → 降为最低优先级(不再降级)

示例场景

假设某模型有 5 个渠道:

渠道 优先级 权重
A 100 50
B 100 30
C 50 80
D 10 20
E 10 10

唯一优先级(降序):[100, 50, 10]

尝试 retry 值 选择优先级 候选渠道
第 1 次 0 100 A, B
第 2 次 1 50 C
第 3 次 2 10 D, E
第 4 次+ 超出范围,降为 2(最低优先级) 10 D, E

4. 权重选择机制

加权随机算法

model/channel_cache.go:145-188

在同一优先级的候选渠道中,使用加权随机选择:

totalWeight := sumWeight * smoothingFactor
randomWeight := rand.Intn(totalWeight)

for _, channel := range targetChannels {
    randomWeight -= channel.GetWeight()*smoothingFactor + smoothingAdjustment
    if randomWeight < 0 {
        return channel, nil
    }
}

权重特殊处理

model/channel_cache.go:162-174

场景 处理
所有渠道 weight=0 每个 channel 的 effective weight = 100(均等选择)
平均 weight < 10 乘以 smoothingFactor=100,提高区分度
正常 weight 直接使用配置的 weight 值

示例

渠道 A(weight=70)和 渠道 B(weight=30)在同一优先级:

  • totalWeight = 100
  • randomWeight ∈ [0, 100)
  • A 被选中的概率 = 70%
  • B 被选中的概率 = 30%

E2E 测试验证:30 次请求中,21 次到 A(70.0%),9 次到 B(30.0%),与配置完全一致。


5. 重试判断规则

shouldRetry() 完整判断链

controller/relay.go:315-339

shouldRetry(c, openaiErr, retryTimes)
│
├─ openaiErr == nil?               → 不重试 (false)
│
├─ 渠道亲和性跳过?                  → 不重试 (false)
│  ShouldSkipRetryAfterChannelAffinityFailure(c)
│
├─ 是 channel error?               → 必须重试 (true) ← 优先级最高
│  IsChannelError(openaiErr)        错误码以 "channel:" 开头
│
├─ 是 skip-retry error?            → 不重试 (false)
│  IsSkipRetryError(openaiErr)      skipRetry=true 标记
│
├─ retryTimes <= 0?                → 不重试 (false)
│  剩余重试次数用完
│
├─ 2xx 状态码?                      → 不重试 (false)
│  code >= 200 && code < 300
│
├─ 非 HTTP 状态码?                  → 重试 (true)
│  code < 100 || code > 599         连接/传输层错误
│
└─ 按状态码范围判断                  → ShouldRetryByStatusCode(code)

Channel Error(必重试)

以下错误码始终触发重试,无论其他条件:

错误码 说明
channel:no_available_key 渠道无可用 API Key
channel:param_override_invalid 参数覆写无效
channel:header_override_invalid Header 覆写无效
channel:model_mapped_error 模型映射错误
channel:aws_client_error AWS 客户端错误
channel:invalid_key 无效的 API Key
channel:response_time_exceeded 响应超时

Skip-Retry Error(不重试)

以下场景标记为 skipRetry=true,不触发重试:

  • 请求体读取失败
  • 渠道未找到
  • 计费/预扣费失败
  • 敏感词检测
  • 各种内部错误

6. 可重试状态码范围

默认配置

setting/operation_setting/status_code_ranges.go:19-32

AutomaticRetryStatusCodeRanges = []StatusCodeRange{
    {Start: 100, End: 199},   // 1xx 信息响应
    {Start: 300, End: 399},   // 3xx 重定向
    {Start: 401, End: 407},   // 认证错误
    {Start: 409, End: 499},   // 409-499 客户端错误
    {Start: 500, End: 503},   // 500-503 服务端错误
    {Start: 505, End: 523},   // 505-523 服务端错误
    {Start: 525, End: 599},   // 525-599 服务端错误
}

alwaysSkipRetryStatusCodes = {504, 524}  // 超时不重试

完整状态码速查表

状态码范围 是否重试 说明
100-199 重试 信息响应
200-299 不重试 成功响应
300-399 重试 重定向
400 不重试 不在范围内
401-407 重试 认证错误
408 不重试 Request Timeout(不在范围内)
409-499 重试 各种客户端错误
500-503 重试 服务端错误
504 不重试 Gateway Timeout(always-skip)
505-523 重试 服务端错误
524 不重试 Cloudflare Timeout(always-skip)
525-599 重试 服务端错误
<100 或 >599 重试 非 HTTP 状态(传输层错误)

自定义配置

管理员可通过运营设置修改 AutomaticRetryStatusCodeRanges,格式为逗号分隔的范围:

"500-503,525-599"     # 范围表示
"429"                 # 单个状态码
"500-503,429,525-599" # 混合使用

解析函数 ParseHTTPStatusCodeRanges() 会自动排序、合并重叠范围。


7. Auto-Group 跨组重试

触发条件

当 Token 的 group 字段为 "auto" 时启用。系统会在多个渠道组之间依次尝试,每个组内耗尽所有优先级后才切换到下一个组。

跨组重试开关

需要在 Token 上启用 cross_group_retry,存储在 context key ContextKeyTokenCrossGroupRetry 中。未启用时只在第一个匹配的组内重试。

状态跟踪

通过 Gin Context 中的三个 key 跟踪状态:

Context Key 说明
ContextKeyAutoGroupIndex 当前正在尝试的组索引
ContextKeyAutoGroupRetryIndex 切换到当前组时的全局 retry 计数
ContextKeyAutoGroup 当前选中的组名

示例流程

2 个组,每组 2 个优先级,RetryTimes=3

Retry=0: GroupA, priority0  (startRetryIndex=0, priorityRetry=0)
         → GroupA 最高优先级渠道

Retry=1: GroupA, priority1  (startRetryIndex=0, priorityRetry=1)
         → GroupA 次高优先级渠道

Retry=2: GroupA 优先级用完 → GroupB, priority0  (startRetryIndex=2, priorityRetry=0)
         → GroupB 最高优先级渠道

Retry=3: GroupB, priority1  (startRetryIndex=2, priorityRetry=1)
         → GroupB 次高优先级渠道

组切换逻辑

service/channel_select.go:106-154

for i := startGroupIndex; i < len(autoGroups); i++ {
    priorityRetry := param.GetRetry()
    if i > startGroupIndex {
        priorityRetry = 0  // 新组从最高优先级开始
    }
    channel, _ = model.GetRandomSatisfiedChannel(autoGroup, param.ModelName, priorityRetry)

    if channel == nil {
        // 当前组没有该模型/优先级的可用渠道,切换到下一组
        common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1)
        param.SetRetry(0)
        continue
    }

    // 跨组重试:当前组重试次数耗尽,准备切换
    if crossGroupRetry && priorityRetry >= common.RetryTimes {
        common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1)
        param.SetRetry(0)
        param.ResetRetryNextTry()  // 防止外层循环再 +1
    }
    break
}

8. 渠道亲和性与重试

亲和性原理

渠道亲和性(Channel Affinity)是一种缓存机制:对于相同用户+模型的请求,优先使用上次成功的渠道,以保持会话连续性(如 Claude Code 的对话上下文)。

对重试的影响

service/channel_affinity.go:493-506

当亲和性选择的渠道请求失败时,可能跳过重试

ShouldSkipRetryAfterChannelAffinityFailure(c)
→ 检查 ginKeyChannelAffinitySkipRetry 标记
→ true = 不重试(直接返回错误)
→ false = 正常重试(默认行为)

亲和性标记设置

service/channel_affinity.go:508-532

func MarkChannelAffinityUsed(c *gin.Context, selectedGroup string, channelID int) {
    // ...
    c.Set(ginKeyChannelAffinitySkipRetry, meta.SkipRetry)
    // ...
}

标记值来自亲和性规则的 SkipRetryOnFailure 字段。

默认规则

规则名 匹配模型 端点 亲和性 Key SkipRetryOnFailure
codex trace ^gpt-.*$ /v1/responses prompt_cache_key false
claude code trace ^claude-.*$ /v1/messages metadata.user_id false

默认两条规则都不阻止重试。只有管理员创建了 SkipRetryOnFailure=true 的规则时,亲和性失败才会跳过重试。

亲和性记录时机

  • 记录:请求成功(status < 400)后,middleware/distributor.go:135 调用 RecordChannelAffinity()
  • 查询:初始渠道选择时,distributor.go:86-107 调用 GetPreferredChannelByAffinity()

9. 计费与退款

计费生命周期

PreConsumeBilling()          ← 预扣费(retry 循环之前)
  │
  ├─ Retry 0: 尝试请求 ─── 成功 → Settle() 结算 → 结束
  │                         └─ 失败 → 继续重试
  ├─ Retry 1: 尝试请求 ─── 成功 → Settle() 结算 → 结束
  │                         └─ 失败 → 继续重试
  ├─ ...
  │
  └─ 全部失败 → BillingSession.Refund()   ← defer 函数,只执行一次

关键特性

  1. 预扣费只一次:在 retry 循环开始前执行 PreConsumeBilling()controller/relay.go:164
  2. 多次重试复用同一 BillingSession:不重复扣费
  3. 退款只一次:通过 defer 函数(controller/relay.go:170-179),仅在 newAPIError != nil 时触发
  4. 退款幂等BillingSession.Refund() 检查 settledrefundedfundingSettled 标记
  5. 异步执行:退款操作通过 gopool.Go() 异步执行

退款流程代码

defer func() {
    if newAPIError != nil {
        newAPIError = service.NormalizeViolationFeeError(newAPIError)
        if relayInfo.Billing != nil {
            relayInfo.Billing.Refund(c)   // 退还预扣额度
        }
        service.ChargeViolationFeeIfNeeded(c, relayInfo, newAPIError)
    }
}()

信任用户优化

高余额用户(Trust Quota Bypass)跳过预扣费,preConsumedQuota 为 0,直接在 Settle 时扣费。


10. 自动禁用渠道

触发时机

每次 relay 失败后调用 processChannelError()controller/relay.go:229),其中包含自动禁用逻辑。

判断条件

service/channel.go:47-102 ShouldDisableChannel()

条件 说明
AutomaticDisableChannelEnabled = false 全局开关关闭则不触发
IsChannelError(err) channel:* 错误码,始终触发
ShouldDisableByStatusCode(code) 默认 401 触发
403 + Gemini 渠道 Gemini 特有
错误码匹配 invalid_api_keyaccount_deactivatedbilling_not_activeArrearage
错误类型匹配 insufficient_quotaauthentication_errorpermission_errorforbidden
关键词匹配 AutomaticDisableKeywords 中的关键词

执行方式

if service.ShouldDisableChannel(channelType, err) && channelError.AutoBan {
    gopool.Go(func() {
        service.DisableChannel(channelError, err.ErrorWithStatusCode())
    })
}
  • 异步执行:通过 gopool.Go() 不阻塞请求处理
  • AutoBan 控制:每个渠道可配置是否允许自动禁用(auto_ban 字段)
  • 自动恢复:当 AutomaticEnableChannelEnabled=true 时,渠道后续请求成功会自动重新启用

自动禁用状态码范围

AutomaticDisableStatusCodeRanges = []StatusCodeRange{{Start: 401, End: 401}}

默认只有 401(未授权)触发自动禁用。可自定义。


11. Task Relay 重试

适用场景

异步任务提交(Midjourney、Suno 等非实时请求),使用 RelayTask() 函数(controller/relay.go:476)。

独立的重试判断

shouldRetryTaskRelay()controller/relay.go:604-641)与主 shouldRetry() 的区别:

条件 shouldRetry() shouldRetryTaskRelay()
channel error 必重试 不检查
skip-retry 标记 不重试 不检查
429 按状态码范围判断 必重试
307 按状态码范围判断 必重试
5xx 按状态码范围判断 重试(排除 504/524)
400 不在默认范围,不重试 明确不重试
408 不在默认范围,不重试 明确不重试(Azure 超时)
LocalError 不适用 不重试
渠道亲和性 检查 检查
2xx 不重试 不重试
其他 委托 ShouldRetryByStatusCode 重试(兜底)

Locked Channel

Task Relay 支持 LockedChannel:如果 relayInfo 中有锁定的渠道,所有重试都使用该渠道(不切换)。


12. RetryParam 状态管理

结构体定义

service/channel_select.go:14-20

type RetryParam struct {
    Ctx          *gin.Context
    TokenGroup   string
    ModelName    string
    Retry        *int      // 当前重试计数
    resetNextTry bool      // 标记下一次 IncreaseRetry 是否跳过
}

方法

方法 说明
GetRetry() int 返回当前 retry 值,nil 时返回 0
SetRetry(retry int) 直接设置 retry 值
IncreaseRetry() retry + 1;若 resetNextTry=true 则跳过并重置标记
ResetRetryNextTry() 设置 resetNextTry=true

resetNextTry 机制

用于 Auto-Group 跨组重试时的状态管理,防止 retry 计数器被双重递增:

场景:GroupA 重试用完,准备切换到 GroupB

1. CacheGetRandomSatisfiedChannel 检测到 priorityRetry >= RetryTimes
2. 调用 param.SetRetry(0)        ← 重置计数器
3. 调用 param.ResetRetryNextTry() ← 标记下次跳过
4. 外层 for 循环调用 IncreaseRetry()
5. 检测到 resetNextTry=true → 跳过递增,重置标记
6. 结果:retry 保持为 0,GroupB 从最高优先级开始

如果不跳过,retry 会从 0 变成 1,导致 GroupB 直接从次高优先级开始,跳过了最高优先级的渠道。

Context Key

constant/context_key.go

ContextKeyAutoGroup             // 当前 auto-group 名称
ContextKeyAutoGroupIndex        // 当前组索引
ContextKeyAutoGroupRetryIndex   // 当前组开始时的全局 retry 数
ContextKeyTokenCrossGroupRetry  // 是否启用跨组重试

附录:重试链路追踪

日志

重试过程中使用了多个渠道时,循环结束后会打印重试链路:

useChannel := c.GetStringSlice("use_channel")
if len(useChannel) > 1 {
    retryLogStr := fmt.Sprintf("重试:%s", strings.Trim(...))
    logger.LogInfo(c, retryLogStr)
}

输出示例:重试:5->12->8(渠道 ID 5 失败后重试到 12,再到 8)。

错误日志

每次失败的渠道信息都会记录到数据库 logs 表(需开启 ErrorLogEnabled),包含:

  • 渠道 ID、名称、类型
  • 状态码、错误类型、错误码
  • 上游请求 ID
  • use_channel 数组(重试链路)
  • 渠道亲和性信息