面向 AI Agent 与大模型应用的 API 网关、Token 配额与可靠用量计费系统。
将模型调用统一为“鉴权 → 额度预占 → 普通 / 流式代理 → 用量归一化 → 异步结算”的完整链路,处理并发额度竞争、重复调用、流式中断和消息重投带来的账务问题。
Java 21 · Spring Boot 3.5.16 · WebFlux / WebClient · MyBatis-Plus · MySQL · Redis / Lua · Caffeine · Redisson · RabbitMQ
架构与计费 · 配置与缓存 · 验证报告 · 演示指南 · 阶梯压测与可靠性报告 · 故障矩阵
| 模块 | 实现 |
|---|---|
| 模型接入 | 模型别名路由;硅基流动 GLM 真实接入与独立模拟服务;JSON / SSE 两种响应模式 |
| 准入控制 | Redis Lua 原子校验 RPM、活跃并发与可用 Token;MySQL 账户行锁维护权威预占 |
| 请求幂等 | 账户级幂等键、请求指纹与数据库唯一约束,避免重复推理及重复占额 |
| 流式代理 | 增量转发、累计 usage 归一化;区分完整结束、超时、断流和客户端取消 |
| 可靠结算 | 用量与事务 Outbox 同事务落库;发布确认、失败重投、消费重试、唯一账单及死信保留 |
| 精确计费 | 不可覆盖的价格版本及请求快照;按输入、输出、缓存 Token 使用整数 nano-CNY 计费 |
| 配置缓存 | Caffeine / Redis 两级缓存;Redisson 互斥重建;版本校验与 TTL 收敛 |
| 异常恢复 | UNKNOWN 保留预占;按外部用量凭据核对并恢复结算;账户、请求与账本一致性核查 |
| 控制台 | 账户预算、普通与流式调用、请求防重、调用详情、模型费用统计及恢复演示 |
flowchart LR
A[Agent / 应用] --> B[API Key + 幂等校验]
B --> C[Redis Lua 准入 / MySQL 预占]
C --> D[WebClient 模型代理]
D --> E[JSON / SSE 增量转发]
D --> F[用量归一化]
F --> G[请求用量 + Outbox 同事务]
G --> H[RabbitMQ]
H --> I[幂等结算 / 唯一账单]
I --> J[额度同步 / 成本统计]
F --> K[UNKNOWN 保留预占]
K --> L[外部凭据核对]
L --> G
JDBC 与阻塞 Redis 操作通过 boundedElastic 隔离。网络代理不自动重试模型推理,避免上游已执行却因重试产生重复用量。MQ 允许重复投递,账务通过事务和唯一约束保证同一请求只结算一次。
将已有 Outbox 改为有界异步发布,使用最多 32 个在途投递、4 个发布线程及 100ms 扫描;保留原租约、事务、唯一账单和消费幂等。回调使用尝试号条件更新,超期记录改为逐主键处理,并对瞬时数据库错误有限重试。
| 对照项 | 保留的旧 JAR | 优化 JAR |
|---|---|---|
| 负载条件 | 每轮 120 秒 × 25 请求/秒,20 账户 | 相同 |
| ABBA 两轮累计测量请求 | 6,000 | 6,000 |
| HTTP 错误 / 成功响应未结算 | 0 / 0 | 0 / 0 |
| 两轮采样 PENDING 峰值均值 | 1405 | 3 |
| 流量结束后的排空观测等待均值 | 104.91s | 2.05s |
| 包含排空的端到端结算速度 | 13.36 笔/s | 24.58 笔/s |
固定输入流量限制了新版吞吐,上表不能解释为最大容量对比;PENDING 每 2 秒采样,排空等待含轮询误差。原始请求、账本、积压与资源记录均保留,见 优化与持续负载报告。
失败归因只将明确的连接池获取超时 / 等待队列超限判定为未发送,按零用量经原 Outbox 链路释放预占。优化版 200 并发长流中,64 次成功、136 次池拒绝,模拟服务实际收到 64 次调用;136 次失败各自只生成一条零费用账单并释放预占。断流、读超时、取消、缺失 usage 和 500 仍保留未知用量,不自动退款。
# 需要保留的旧构建;每轮创建新测试账户,不覆盖历史结果。
.\scripts\Run-OptimizationComparison.ps1 -BaselineJar <baseline-directory>/modelgate-0.1.0.jar -Seconds 120 -Rate 25
.\scripts\Run-Validation.ps1 -Mode Pool复用现有 Java / Python 标准库测试链路,完成 32 / 100 / 200 并发阶梯,没有新增组件或改动服务架构。
| 验证项 | 最终结果 |
|---|---|
| 自动化回归 | 61 项 Java(29 单元、29 MySQL / Redis 集成、3 上游连接池探针)+ 12 项前端 + 8 项指标计算,共 81 项通过 |
| 阶梯工作负载 | 多账户 JSON 两轮、单热点账户 JSON 一轮、长 SSE 一轮,共 12 阶段、6,332 次本地模拟调用 |
| 普通请求 | 6,000 次中 5,999 次成功并各自结算,1 次 503;成功响应未结算 0 次,普通调用没有新增遗留预占 |
| 长 SSE 超载 | 32 并发全部成功;100 / 200 并发各成功 64 次、失败 36 / 136 次;未知用量保留预占、无猜测账单 |
| 故障注入 | 15 场景通过,覆盖真实 RabbitMQ 停服恢复、重复投递、MySQL 事务冲突、SSE 中断、取消、强杀和陈旧恢复 |
| 幂等与独立结算 | 12 个不同请求 + 60 次额外投递仅生成 12 条对应账单;30 次同键并发只有 1 次合法调用 |
| 资源与瓶颈 | CPU / 内存、JVM 线程、MySQL 行锁、Redis 预算和结算排空记录;确认账户锁争用、数据库连接等待、64 个上游连接及批次派发限制 |
完整 p50 / p95 / p99、完成 / 成功 QPS、响应体吞吐、端到端结算速度、资源占用与失败归因见 性能与可靠性报告。原始记录索引 保留全部尝试,包括一轮 200 并发的 7 次 503、6 个成功响应用量落库失败,以及一次测试命令参数绑定失败。没有删除这些记录或回填为成功。
本轮仅增加瞬时用量落库失败的有限退避重试,最多尝试 3 次,不重试模型推理、不重放流。HTTP 短时吞吐远高于异步结算排空速度,不能将 HTTP QPS 当作可持续计费吞吐;所有性能数字均为本机模拟结果。
# 先按下文启动网关和中间件。每次运行使用新输出目录。
.\scripts\Run-Validation.ps1 -Mode Load -Rounds 2
.\scripts\Run-Validation.ps1 -Mode Faults
# 指定子场景时将逗号列表整体加引号
.\scripts\Run-Validation.ps1 -Mode Load -Suites 'balanced,streaming' -Rounds 2故障测试会重启本网关并暂停指定本机 RabbitMQ 应用,必须在没有其他服务使用者的本地测试环境执行;finally 恢复 Broker 与网关,不清空队列或账本。完整构建与复现命令见报告。
| 验证项 | 结果 |
|---|---|
| 自动化测试 | 26 项 Java 单元、25 项真实 MySQL / Redis 集成、12 项前端核心测试,共 63 项通过 |
| HTTP 与故障检查 | 23 项网关检查、14 项 MQ 故障及账本检查通过 |
| 配置回源对照 | 两种路径各读取 200 次,无缓存回源 200 次,两级缓存回源 1 次 |
| 并发缓存重建 | 两个独立 L1、32 个并发未命中,仅触发 1 次数据库回源 |
| 结算幂等 | 同一请求 20 次并发结算任务、5 次 MQ 重复投递,均仅记账一次 |
| 模拟调用 | 本机 32 并发、200 次调用,全部结算,无非预期错误及新增遗留预占 |
| 真实模型 | 硅基流动 zai-org/GLM-5.3-Flash SSE 调用、用量持久化与计费完成 |
数据为本地验证结果:模拟请求没有真实推理延迟,缓存对照针对配置读取;两个独立 L1 位于同一 JVM。完整条件、历史构建 SHA-256、测试名称和调用记录见 验证结果 及 机器可读报告。报告保留对应构建的快照,后续文档和启动脚本整理不改写历史数据。
安装 JDK 21、Maven 3.9+,配置 JAVA_HOME 与 PATH。可选安装 Python 3.10+ 和 Node.js,以执行 HTTP / 前端验证。
启动本地中间件,应用默认连接:
| 组件 | 默认配置 |
|---|---|
| MySQL 8+ | 127.0.0.1:3306,独立数据库 modelgate |
| Redis 7+ | 127.0.0.1:16379,DB 1,业务键使用 mg: 前缀 |
| RabbitMQ | AMQP 127.0.0.1:5672,本地 guest/guest;故障检查需启用管理插件,端口 15672 |
本仓库直接连接这些服务,不下载或安装中间件。Redis / RabbitMQ 连接可通过 Spring Boot 标准环境变量配置,例如 SPRING_DATA_REDIS_PORT、SPRING_RABBITMQ_PASSWORD;本地故障脚本按上表默认管理端配置执行。
在仓库根目录运行:
New-Item -ItemType Directory -Path .runtime -Force | Out-Null
Copy-Item config/mysql.example.json .runtime/modelgate-mysql.local.json
Copy-Item config/mysql-admin.example.json .runtime/mysql-root.local.json
# 编辑两个本地文件:分别填写应用账号和本机 MySQL root 密码。
# 应用账号固定为 modelgate_app,应用密码需使用自己设置的非占位值。
.\scripts\Build.ps1
.\scripts\Init-Database.ps1
.\scripts\Start-App.ps1首次初始化创建独立数据库和 modelgate_app@localhost 用户,并生成 5 个本地演示 / 测试账户。重复执行不会清空数据库、重置额度或轮换已有密钥,也不会覆盖已存在 MySQL 用户的密码。
打开 http://127.0.0.1:8088/,使用 .runtime/modelgate/clients.local.json 中 name=demo 的网关 API Key 连接。该 Key 与供应商 Key 独立;页面仅在内存中保存,刷新或断开后清除。默认 mock-chat 产生本地演示账单,无外部模型调用。
运行配置、密钥、日志和 PID 均保存在被 Git 忽略的 .runtime/。支持 MG_RUNTIME_DIR 指定外部绝对目录;现有工作区也兼容相邻目录下的运行配置。PowerShell 中可用 . .\scripts\Common.ps1 后的 $Runtime、$GateRuntime 获取实际位置。
Copy-Item config/provider.example.json .runtime/modelgate-provider.local.json
# 仅在本地文件 apiKey 中填写硅基流动 API Key,然后重启应用。
.\scripts\Stop-App.ps1
.\scripts\Start-App.ps1控制台选择 glm-flash 才调用真实供应商并产生用量。模型目录已持久化后,修改模型名和价格使用 配置发布命令,环境变量只负责首次种子配置。网关不接受客户端传入任意上游 URL。
. .\scripts\Common.ps1
$client = Get-Content "$GateRuntime\clients.local.json" -Raw -Encoding utf8 |
ConvertFrom-Json | Where-Object name -eq 'demo'
$headers = @{
Authorization = 'Bearer ' + $client.apiKey
'Idempotency-Key' = [Guid]::NewGuid().ToString('N')
}
$body = @{
model = 'mock-chat'
messages = @(@{role='user';content='hello'})
max_tokens = 32
stream = $false
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:8088/v1/chat/completions' `
-Headers $headers -ContentType 'application/json' -Body $body每次新调用使用新的 Idempotency-Key。同账户重复键返回 409 和已有请求 ID,不再次推理、不重放已输出内容。响应头 X-ModelGate-Request-Id 用于查询结算状态。
| 接口 | 用途 |
|---|---|
GET /health |
就绪状态 |
GET /v1/models |
模型别名及价格版本 |
POST /v1/chat/completions |
普通 / SSE 文本调用;需 Bearer 与 Idempotency-Key |
GET /v1/account |
当前账户预算、已用与预占 |
GET /v1/requests?limit=20 |
当前账户最近调用;limit 1–100 |
GET /v1/requests/{id} |
当前账户请求状态、用量、费用及失败原因 |
GET /v1/usage |
按模型和价格版本统计已结算用量及精确费用 |
.\scripts\Build.ps1 # 26 项 Java 单元测试及打包
.\scripts\Test-Database.ps1 # 25 项 MySQL / Redis 集成测试;会停止本网关
node --test .\scripts\console-core.test.mjs
.\scripts\Start-App.ps1
python .\scripts\verify_gateway.py # 23 项检查及 200 次本地模拟调用
.\scripts\Test-Faults.ps1 # 暂停恢复、路由故障、重复消费与账本核对
# 可选:一次真实供应商流式调用,会产生用量
python .\scripts\verify_real.pyPython 验证脚本仅依赖标准库。故障测试会重启本项目并短暂移除、恢复专用 MQ 路由;UNKNOWN 用例故意保留预占。RPM 测试账户每自然分钟仅允许一个请求,连续运行需等下一分钟。正常流式、请求幂等和结算恢复的操作步骤见 演示指南。
- 当前支持文本 Chat Completions,最多 64 条消息 / 32 KiB,
max_tokens范围 1–4096;未实现多模态、Agent 编排及模型训练。 - Token 预占为估计值,不是精确 tokenizer 或严格用量上界;实际超预占如实记账,后续预算不足时拒绝新请求。
- 超时或断流不代表零用量,UNKNOWN 请求保留预占,凭外部用量证据核对后恢复。
- SSE 开始后错误使用
event: error,不补[DONE];HTTP 200 不等于流已完整结束或账务已结算。 - 缓存采用 TTL 一致性,账务仍由数据库事务保证;未验证多节点灾备或持续生产容量。
- 当前费用为按价格快照计算的内部用量账本,不包含支付、购买流程或供应商发票对账。
设计背景及第三方文档见 技术参考。
