Files
lijiaoqiao/docs/sub2api_scheduler_billing_flow_deep_dive_v2_2026-03-17.md

334 lines
12 KiB
Markdown
Raw Normal View History

# sub2api 调度-并发-Failover-用量计费链路深挖v2
- 版本v2.0
- 日期2026-03-17
- 代码基线:`/home/long/project/立交桥/llm-gateway-competitors/sub2api-tar/backend`
- 目标:沉淀 S2 阶段可直接迁移到自研 Router Core 的主路径能力(特别是国内供应商 100% 接管场景)
## 1. 迁移状态确认(回答你的问题)
已确认开源项目库整体已迁入统一项目根:
- `立交桥/llm-gateway-competitors/sub2api-tar`
- `立交桥/llm-gateway-competitors/sub2api-src`
- `立交桥/llm-gateway-competitors/sub2api-full`
- `立交桥/llm-gateway-competitors/sub2api-code`
- `立交桥/llm-gateway-competitors/litellm`
- `立交桥/llm-gateway-competitors/one-api`
- `立交桥/llm-gateway-competitors/new-api`
结论:`subapi/sub2api`及其他对比仓库都已在`立交桥`目录下,可作为后续持续深读与集成源。
## 2. 本次深挖覆盖范围
核心入口与主链路文件:
1. 调度
- `internal/service/openai_account_scheduler.go`
- `internal/service/openai_gateway_service.go`
2. Handler 主流程Responses / Chat Completions / Anthropic 兼容)
- `internal/handler/openai_gateway_handler.go`
- `internal/handler/openai_chat_completions.go`
3. 并发控制
- `internal/handler/gateway_helper.go`
- `internal/service/concurrency_service.go`
4. 异步用量任务池
- `internal/service/usage_record_worker_pool.go`
5. 计费幂等
- `internal/service/gateway_service.go`
- `internal/service/usage_billing.go`
- `internal/repository/usage_billing_repo.go`
6. 流式 failover 边界(对照)
- `internal/handler/gateway_handler.go`
- `internal/handler/failover_loop.go`
- `internal/handler/gateway_handler_stream_failover_test.go`
## 3. 端到端主链路OpenAI Responses / Chat Completions
```mermaid
sequenceDiagram
autonumber
participant C as Client
participant H as OpenAIGatewayHandler
participant S as OpenAIGatewayService
participant SCH as OpenAIAccountScheduler
participant CON as ConcurrencyService
participant U as Upstream(OpenAI)
participant P as UsageRecordWorkerPool
participant B as UsageBillingRepo
C->>H: 请求(/v1/responses 或 /v1/chat/completions)
H->>CON: TryAcquireUserSlot
alt 未拿到用户槽位
H->>CON: IncrementWaitCount + AcquireUserSlotWithWait
end
H->>SCH: SelectAccountWithScheduler(previous_response_id/session_hash/model)
SCH-->>H: AccountSelectionResult(已拿槽 or WaitPlan)
alt selection.Acquired = false
H->>CON: TryAcquireAccountSlot
alt 快速未拿到
H->>CON: IncrementAccountWaitCount + AcquireAccountSlotWithWaitTimeout
end
end
H->>S: Forward(...)
S->>U: 上游请求
alt 上游可 failover 错误(如 429/5xx)
U-->>S: error(status>=400)
S-->>H: UpstreamFailoverError
H->>H: 同账号重试/换号重试(受 max switches 限制)
else 正常返回
U-->>S: success(stream 或 non-stream)
S-->>H: OpenAIForwardResult(usage/request_id/ttft)
H->>P: submitUsageRecordTask
P->>S: RecordUsage
S->>B: applyUsageBilling(幂等)
end
H-->>C: 响应JSON 或 SSE
```
## 4. 调度机制细节SelectAccountWithScheduler
`OpenAIGatewayService.SelectAccountWithScheduler``openai_account_scheduler.go`)是 3 层选择逻辑:
1. `previous_response_id` 粘性层(最高优先级)
- 入口:`Select()` 内先尝试 `SelectAccountByPreviousResponseID`
- 命中后 `decision.Layer=previous_response_id`
- 若有 `sessionHash`,会同步绑定 sticky session
2. `session_hash` 粘性层
- 读取 sticky account id
- 校验账号是否仍可调度、模型是否匹配、传输协议是否兼容
- 优先尝试直接抢账号槽位;失败则返回 WaitPlansticky 专用 timeout/max waiting
3. 负载均衡层load_balance
- 过滤维度:排除集、可调度状态、模型支持、传输协议兼容
- 从并发服务批量拉取 `loadRate/waitingCount`
- 融合运行时统计error_rate EWMA + TTFT EWMA
- 评分项:`priority/load/queue/error_rate/ttft`
- 策略:先取 Top-K再按权重随机顺序尝试抢槽避免单账号长期垄断
- 若都抢不到,返回 fallback WaitPlan
评分来源(可调参数)在 `openAIWSSchedulerWeights()`,默认:
- Priority: 1.0
- Load: 1.0
- Queue: 0.7
- ErrorRate: 0.8
- TTFT: 0.5
## 5. 并发槽位模型(用户槽 + 账号槽)
### 5.1 获取顺序
`openai_gateway_handler.go` 中是明确的双层门控:
1. 先用户并发槽(防止单用户打爆)
2. 再账号并发槽(防止单账号过载)
### 5.2 快慢路径
`gateway_helper.go` 实现了统一并发辅助:
- 快路径:`TryAcquireUserSlot/TryAcquireAccountSlot`
- 慢路径:`Acquire*WithWait` + 指数退避 + 抖动
- 流式请求等待中会发 SSE ping避免客户端超时
### 5.3 等待队列上限控制
- 用户等待队列:`IncrementWaitCount`,上限=`CalculateMaxWait(userConcurrency)`
- 账号等待队列:`IncrementAccountWaitCount`,上限来自调度返回的 `WaitPlan.MaxWaiting`
- 队列满直接返回 429
### 5.4 槽位释放安全性
`wrapReleaseOnDone``context.AfterFunc + sync.Once` 保证:
- 正常完成会释放
- context 取消(客户端断开/超时)也会释放
- 多次释放只执行一次
这是并发槽不泄漏的关键机制。
## 6. Failover 机制与边界
## 6.1 触发条件
OpenAI 侧 failover 判定在 service 层:
- 显式状态码:`401/402/403/429/529``>=500`
- 以及 OpenAI 瞬态处理错误(内容解析)
满足时返回 `UpstreamFailoverError` 给 handler 进行重试/换号。
## 6.2 Handler 层重试策略
`openai_gateway_handler.go` / `openai_chat_completions.go`
1.`RetryableOnSameAccount=true` 且账号是 pool mode
- 同账号短延迟重试(受 `pool retry limit` 限制)
2. 同账号重试耗尽:
- 计入失败账号集合,切换账号
- `switchCount` 超过 `maxAccountSwitches` 后 failover 结束
## 6.3 流式 no-replay 边界
对“已向客户端写出流式内容后是否还能 failover”的处理代码体现为两种模式
1. 通用 Gateway 路径Anthropic/Gemini有显式保护
- `gateway_handler.go` 记录 `writerSizeBeforeForward`
-`UpstreamFailoverError` 返回时 `Writer.Size()` 已变化,判定“流已写出”,禁止继续 failover
- 测试 `gateway_handler_stream_failover_test.go` 明确验证“防止双 message_start 流拼接腐化”
2. OpenAI Responses/ChatCompat 路径
- failover 只在上游 `status>=400` 的响应阶段触发(此时尚未进入流转换写出)
- 进入 `handleStreamingResponse/handleChatStreamingResponse/handleAnthropicStreamingResponse` 后,发生的是流读写错误或超时,不再转成 `UpstreamFailoverError` 进行换号
推断基于代码行为OpenAI 这条链路等价于“流开始后不做 replay failover”。
## 7. 异步用量记录submitUsageRecordTask -> WorkerPool
`openai_gateway_handler.go` 在 Forward 成功后统一走:
- `requestPayloadHash := HashUsageRequestPayload(body)`
- `submitUsageRecordTask(func(ctx){ RecordUsage(...) })`
`usage_record_worker_pool.go` 关键点:
1. 有界队列 + worker 池
- 默认 `worker=128``queue=16384`
2. 队列满降级策略
- `drop`:直接丢
- `sync`:同步执行
- `sample`:按比例同步执行(默认 10%)其余丢弃
3. 自动扩缩容
- 扩容触发:队列占比超过上阈值
- 缩容触发:队列空且运行利用率低
4. 任务保护
- 每个任务有超时上下文
- panic recover
5. 兜底路径
- 如果 handler 未注入 worker 池,改为同步执行(避免无界 goroutine
## 8. 计费与幂等request_id + fingerprint
## 8.1 request_id 生成优先级
`resolveUsageBillingRequestID()` 的优先级:
1. `ctxkey.ClientRequestID` -> `client:<id>`
2. `ctxkey.RequestID` -> `local:<id>`
3. 上游 `result.RequestID`
4. `generated:<uuid>`
中间件来源:
- `ClientRequestID()`:注入 `ctx_client_request_id`
- `RequestLogger()`:读取/生成 `X-Request-ID` 并注入 `ctx_request_id`
## 8.2 payload fingerprint
`resolveUsageBillingPayloadFingerprint()`
- 优先使用 `requestPayloadHash`(即 `HashUsageRequestPayload(payload)`
- 其次回退到 client/local request id
目的:降低“同一个 request_id 被误复用”导致的静默误去重风险。
## 8.3 UsageBillingCommand 字段映射
| 来源 | 字段 | 说明 |
|---|---|---|
| `requestID` | `RequestID` | 幂等主键组成部分 |
| `APIKey.ID` | `APIKeyID` | 幂等主键组成部分 |
| `RequestPayloadHash` | `RequestPayloadHash` | 幂等冲突鉴别增强 |
| `UsageLog.Model` | `Model` | 计费维度 |
| `UsageLog.InputTokens` | `InputTokens` | 输入 token |
| `UsageLog.OutputTokens` | `OutputTokens` | 输出 token |
| `UsageLog.CacheCreationTokens` | `CacheCreationTokens` | cache create token |
| `UsageLog.CacheReadTokens` | `CacheReadTokens` | cache read token |
| `UsageLog.ImageCount` | `ImageCount` | 图像请求计费 |
| `Cost.ActualCost` | `BalanceCost` | 余额扣费 |
| `Cost.TotalCost` + 订阅 | `SubscriptionCost` | 订阅计费 |
| `Cost.ActualCost` + API Key quota | `APIKeyQuotaCost` | key 配额计费 |
| `Cost.ActualCost` + key ratelimit | `APIKeyRateLimitCost` | key 限速窗口用量 |
| `Cost.TotalCost*AccountRateMultiplier` | `AccountQuotaCost` | 账号配额计费 |
## 8.4 仓储层幂等执行(强一致)
`usage_billing_repo.go` 关键行为:
1. 事务内先 `claimUsageBillingKey`
- `INSERT usage_billing_dedup(request_id, api_key_id, request_fingerprint)`
- 冲突则读取已有 fingerprint 对比
2. 对比规则
- fingerprint 相同:视为重复请求,`Applied=false`(幂等成功,不重复扣费)
- fingerprint 不同:返回 `ErrUsageBillingRequestConflict`
3. claim 成功后执行扣费副作用
- 订阅累计
- 用户余额扣减
- API Key 配额/限速窗口
- 账号 quota含 daily/weekly
结论:这是“请求级 at-most-once 记账”语义,而非“至少一次”。
## 9. 失败模式与重试边界
| 阶段 | 错误类型 | 默认行为 | 是否自动重试 | 备注 |
|---|---|---|---|---|
| 用户槽获取 | 并发满/超时 | 429 | 否 | 可等待到超时 |
| 账号槽获取 | 并发满/超时 | 429 | 否 | 受 WaitPlan 限制 |
| 上游请求前 | 网络错误 | 502 | 否 | 非 failover 错误路径 |
| 上游 HTTP 错误 | failover 状态码 | 换号/同号重试 | 是 | 受 max switches/同号重试上限 |
| 流写出后错误(通用网关) | UpstreamFailoverError | 终止 failover | 否 | 防止流拼接腐化 |
| 流处理中断OpenAI | scan/read/timeout | 返回已采集 usage + 错误 | 否 | 不 replay failover |
| 用量任务池满 | 队列溢出 | drop/sample/sync | 视策略 | 可观测 dropped 指标 |
| 计费重复请求 | dedup 命中 | Applied=false | N/A | 不重复扣费 |
| 计费冲突 | 同 request_id 不同指纹 | 错误 | 否 | 需上层治理 request_id 生成 |
## 10. 对自研 Router Core 的直接启发S2 主路径)
结合你的目标S2 结束 >=60% 主路径接管,国内供应商 100%
1. 必须先自研的“不可外包”能力
- 并发双槽模型user/account+ wait queue 上限
- 幂等计费仓储request_id + fingerprint
- 流式 no-replay 边界控制(防流拼接)
2. 可先复用 subapi、后续替换的能力
- 账号调度具体打分权重
- 多协议转换细节Responses/Chat/Messages
3. 国内供应商 100% 接管建议
- 在 Router Core 里先实现国内供应商专属 scheduler + billing pipeline
- subapi connector 仅保留海外通道与过渡流量
4. 核心指标(建议纳入 S2 验收)
- 调度层命中率:`previous_response/session/load_balance`
- failover 成功率与平均切换次数
- 并发等待队列溢出率
- usage task dropped/sync fallback 比例
- billing conflict raterequest_id 冲突)
## 11. 后续 v3 建议
1. 深挖 `gateway_service.go` 的 Anthropic/Gemini 混合调度与模型路由规则优先级
2. 逐项抽取可复用测试用例为 Router Core 契约测试
3. 建立“流式写出后 failover 禁止”跨协议统一中间件(避免行为分叉)