系统架构
Sub2API 采用模块化单体架构:前端、HTTP API、网关协议适配、调度、计费和后台任务由同一个 Go 服务交付,PostgreSQL 和 Redis 是外部依赖。
┌────────────────────────────┐
用户 / Claude Code ─────▶│ │
Codex / Gemini CLI ─────▶│ Sub2API 服务 │──────▶ AI 上游平台
Web 管理后台 ────────────▶│ Gin + Handler + Service │
│ │
└──────┬──────────────┬──────┘
│ │
┌──────▼─────┐ ┌─────▼──────┐
│ PostgreSQL │ │ Redis │
│ 持久化数据 │ │ 协调与缓存 │
└────────────┘ └────────────┘应用服务
HTTP 与路由层
Gin 同时注册三类路由:
/api/v1/*:登录、用户、管理、支付和设置 API。/v1/*:Anthropic/OpenAI 风格的统一网关入口。/v1beta/*:Gemini 原生兼容入口。
路由会先应用请求体大小、Request ID、错误采样、API Key 鉴权和分组检查,再交给协议 Handler。
Handler 层
Handler 负责处理与 HTTP 协议紧密相关的工作:
- 读取和校验请求体。
- 判断客户端、模型、流式模式和目标平台。
- 获取用户和账号并发槽位。
- 组织账号故障切换循环。
- 输出 JSON、SSE 或 WebSocket 响应。
Service 层
Service 承担主要业务规则:
- 账号候选过滤与调度评分。
- 模型映射和多协议转换。
- OAuth token 刷新和上游请求构建。
- 计价、扣费、订阅和配额判断。
- 支付、内容审核、渠道监控和运维聚合。
Repository 层
Repository 将业务对象映射到 PostgreSQL 与 Redis:
- Ent 负责核心关系数据。
- 手写 SQL 处理复杂统计、批处理和性能敏感路径。
- Redis Lua 脚本保证并发槽位、限流和部分计数操作的原子性。
PostgreSQL 保存什么
- 用户、身份绑定和 TOTP 状态。
- API Key、账号、分组和渠道配置。
- 订阅、订单、支付审计、兑换码和推广记录。
- 请求用量、计费去重、平台配额和统计聚合。
- 监控历史、错误日志、告警和后台任务状态。
数据库迁移在服务启动过程中按文件名顺序执行,并记录文件名与校验和。迁移是向前执行的,正式升级前必须备份数据库。
Redis 保存什么
- 用户、API Key 和账号并发槽位。
- RPM、Token、窗口用量和临时限流状态。
- API Key 鉴权、订阅和余额缓存。
- 粘性会话与账号绑定。
- OAuth 刷新锁和后台任务 leader lock。
- 批量图片任务队列与临时状态。
Redis 不是“可有可无的页面缓存”。即使 PostgreSQL 数据完整,Redis 故障也会影响调度、限流、并发和部分后台任务。
多实例运行
Sub2API 的请求处理可以运行多个副本,Redis 负责跨实例并发和锁协调。周期任务通常通过 Redis leader lock 选出单个执行者,部分任务在 Redis 异常时使用 PostgreSQL advisory lock 兜底。
多实例部署时要特别注意:
- 所有实例必须连接同一个 PostgreSQL 和 Redis。
JWT_SECRET、TOTP_ENCRYPTION_KEY、时区和关键运行配置必须一致。- 数据库总连接数约等于“实例数 × 每实例连接池上限”。
- 反向代理需要支持长连接、SSE、WebSocket 和真实客户端 IP。
- 不要让某个实例使用本地独有的动态配置文件。
单二进制交付
正式发布时,Vite 构建产物会通过 Go embed 打进服务二进制。浏览器页面与 API 使用相同域名,避免额外维护独立前端服务和跨域配置。
一次请求跨越哪些层
server/routes
│ 注册 URL 与中间件
▼
handler
│ HTTP、协议、并发等待、failover 循环
▼
service
│ 账号调度、协议转换、上游访问、计费规则
▼
repository
│ PostgreSQL、Redis、Lua、复杂 SQL
▼
external systems
AI 上游 / 支付平台 / SMTP / 对象存储这个分层不是绝对隔离。网关 Handler 为了处理流式输出和故障切换,会持有多个 Service;部分 Service 为性能直接使用缓存接口或批量 SQL。因此理解真实调用链比只看目录名更重要。
启动过程
应用启动大致经历:
- 解析
--version、--setup等命令行参数。 - 判断是否首次安装。
- 首次安装时启动设置服务,或通过
AUTO_SETUP自动初始化。 - 加载 YAML、环境变量和默认配置。
- 初始化日志、PostgreSQL、Ent 与 Redis。
- 通过 Wire 构造 Repository、Service、Handler 和 HTTP Server。
- 启动 token 刷新、过期检查、聚合、清理、监控等后台任务。
- 注册健康检查、管理 API、用户 API、支付回调和网关路由。
- 开始监听端口,并等待 SIGINT/SIGTERM 优雅退出。
优雅退出时,应用会先停止后台任务和 worker pool,再关闭 Redis 与 Ent。容器编排的终止宽限时间不应短于应用自身的清理时间。
依赖注入
后端使用 Google Wire 在编译期生成依赖装配代码。主要 ProviderSet 包括:
config.ProviderSetrepository.ProviderSetservice.ProviderSetpayment.ProviderSetmiddleware.ProviderSethandler.ProviderSetserver.ProviderSet
它的优点是依赖关系在编译期检查,没有运行时容器反射;代价是新增 Service 构造参数后需要重新生成 Wire,并同步更新大量测试 stub。
数据路径与控制路径
数据路径
模型请求属于高频数据路径:
鉴权缓存 → 配额检查 → Redis 并发 → 调度快照 → 上游连接池
→ SSE/WS 转发 → 异步用量记录 → 余额/订阅更新它追求低延迟、少读数据库和可控的内存占用,因此大量使用 L1/L2 缓存、Redis Lua、调度快照和有界 worker pool。
控制路径
后台配置属于低频控制路径:
管理员修改账号/分组/设置 → PostgreSQL 事务
→ 缓存失效/版本递增 → 调度快照或运行时配置刷新控制路径写入成功但缓存未及时刷新,会造成短时间旧配置;因此项目包含缓存版本、outbox、快照重建和受控数据库回源机制。
一致性策略
Sub2API 不是所有操作都使用同一种一致性模型:
| 数据 | 典型策略 | 原因 |
|---|---|---|
| 用户、订单、订阅 | PostgreSQL 事务 | 需要持久化和约束 |
| 并发槽位 | Redis Lua 原子操作 | 高频、跨实例、带 TTL |
| API Key 鉴权 | 进程 L1 + Redis L2 + DB | 降低热路径查询 |
| 调度候选 | Redis 快照 + 受控 DB 回源 | 兼顾性能与恢复能力 |
| 粘性会话 | Redis TTL | 临时关系,无需永久保存 |
| 用量记录 | 有界异步队列,默认溢出同步执行 | 避免请求路径无界 goroutine,同时尽量不丢计费 |
| 仪表盘 | 预聚合表 + 缓存 | 避免扫描完整 usage_logs |
部署者需要接受:部分管理修改和统计展示是最终一致,但余额、支付和订阅等财务状态必须以 PostgreSQL 事务结果为准。
水平扩展时的共享与本地状态
必须共享
- PostgreSQL 数据库。
- Redis 数据库。
- JWT 与 TOTP 密钥。
- 平台 OAuth 客户端配置。
- 对外 URL、时区和计费配置。
每实例本地状态
- 进程内 L1 缓存。
- HTTP 与 WebSocket 连接池。
- 日志文件。
- 部分 worker pool 和运行时指标。
- 本地挂载的自定义模板或静态覆盖文件。
多实例时,本地文件要么在所有副本保持一致,要么改用共享存储。不要让请求随机落到配置不同的副本。
主要故障域
| 故障 | 直接影响 | 应对重点 |
|---|---|---|
| PostgreSQL 不可用 | 登录、配置、用量持久化、订单 | 数据库 HA、连接池、备份恢复 |
| Redis 不可用 | 鉴权缓存、并发、调度、锁 | Redis HA、超时、连接池、降级观察 |
| 上游账号失效 | 部分模型请求失败 | token 刷新、账号测试、failover |
| 代理故障 | 绑定账号全部不可用 | 代理监控、禁止静默直连 |
| 应用实例退出 | 该实例连接中断 | 多副本、优雅退出、负载均衡 |
| 用量队列拥塞 | 结算延迟或同步回退 | 监控队列、worker、数据库写延迟 |
| 配置错误 | 大范围无可用账号或错误计费 | 变更审计、灰度、快速回滚 |
容量瓶颈通常在哪里
- 上游账号并发与额度:先于应用 CPU 成为瓶颈。
- Redis 延迟:每个请求会执行多次并发、限流和缓存操作。
- PostgreSQL 用量写入:高 QPS 下 usage_logs、聚合和索引维护压力上升。
- 长连接数量:SSE/WS 会长期占用文件描述符、内存和连接池。
- 管理查询:大时间范围统计可能扫描大量历史数据。
因此扩容不能只增加应用副本。还要同步检查 Redis、数据库连接数、上游账号池和反向代理文件描述符。