架构总览与请求生命周期
专栏:new-api 源码拆解 · 第 2 / 12 篇:::info 学习目标 完成本篇后你能够:画出 new-api 的七层架构图;完整追踪一个中继请求经过的全部中间件与模块;说清模块之间的协作接口(上下文键、服务调用、共享表)。 前置:了解基本的 HTTP 服务概念。预计时长:60 分钟。 :::
:::note 本章术语速查(新手建议先读)
- 网关(Gateway):站在你和几十个 AI 服务商之间的”中间商”——你只对它说话,它负责转发、记账、换协议。
- gin:Go 语言最流行的 Web 框架,处理 HTTP 请求。
- 中间件(Middleware):请求进入业务代码前依次经过的”检查站”(鉴权、限流、选路都在这里)。
- gorm:Go 语言最流行的 ORM——用 Go 对象操作数据库表,不用手写 SQL。
- go:Go 的编译特性,把前端文件直接”打包进”二进制。
- SSE(Server-Sent Events):服务器向浏览器持续推送消息的技术,AI 回复的”打字机效果”靠它。 :::
new-api 是一个 Go 单体应用:gin 做 HTTP、gorm 做 ORM、React 前端经 go:embed 内嵌进同一个二进制。全仓库约 930 个 Go 文件,按目录划分成职责清晰的模块——读它的关键是先抓住「一次请求的流动路径」,再往路径两侧挂模块。
七层架构
启动流程:一张依赖关系图
main.go 的启动序列告诉你所有模块的初始化顺序与依赖:
InitResources():加载 .env → 初始化日志 → 倍率设置 → HTTP 客户端/token 编码器 →model.InitDB()→ casbin 授权初始化 →model.InitOptionMap()→ Redis → i18n → 自定义 OAuth provider;- 若开启内存缓存:
model.InitChannelCache()+ 后台SyncChannelCache(第 10 篇); - 启动后台协程:
SyncOptions(配置热更新)、SyncTaskPlugins(任务插件)、authz.StartPolicySync、UpdateQuotaData(看板落库)、订阅额度重置、系统任务 runner(DB 租约去重的定时任务); - HTTP 服务:
gin.New()+ 全局中间件 +router.SetRouter(...),退出时优雅关机(SSE 最长等 120 秒)并SaveQuotaDataCache()落库看板数据。
// main.go:236-262(节选)
server := gin.New()
server.Use(gin.CustomRecovery(func(c *gin.Context, err any) { ... }))
// This will cause SSE not to work!!!
//server.Use(gzip.Gzip(gzip.DefaultCompression))
server.Use(middleware.RequestId())
server.Use(middleware.Version())
server.Use(middleware.I18n())
middleware.SetUpLogger(server)
router.SetRouter(server, router.WebAssets{...})
那行注释是全仓库最实用的教训之一:全局 gzip 会破坏 SSE 流式——对网关来说流式是命根子。
两类流量,两套路由
router/main.go 的 SetRouter 依次挂 API、Dashboard、Relay、TaskPluginProtocol、Video、Task、Plugin、Web 八组路由。关键是控制台流量与模型流量泾渭分明:
| 控制台流量 | 中继流量 | |
|---|---|---|
| 前缀 | /api | /v1、/v1beta、/mj、/pg |
| 凭据 | session JWT / access token | sk-xxx 令牌 |
| 鉴权中间件 | UserAuth / AdminAuth / RootAuth | TokenAuth |
| 限流 | GlobalAPIRateLimit / CriticalRateLimit | ModelRequestRateLimit(按模型/用户) |
| 选路 | 无(业务路由固定) | Distribute(渠道选择) |
// router/relay-router.go:82-105(节选)
relayV1Router := router.Group("/v1")
relayV1Router.Use(middleware.RouteTag("relay"))
relayV1Router.Use(middleware.SystemPerformanceCheck()) // 过载保护
relayV1Router.Use(middleware.TokenAuth()) // sk-xxx 鉴权
relayV1Router.Use(middleware.ModelRequestRateLimit()) // 按模型限流
httpRouter.POST("/chat/completions", func(c *gin.Context) {
controller.Relay(c, types.RelayFormatOpenAI)
})
httpRouter.POST("/messages", func(c *gin.Context) {
controller.Relay(c, types.RelayFormatClaude)
})
未匹配的路由回落到内嵌的 React SPA——单二进制就是完整产品。
中间件链全景
中继请求按顺序穿过(这是全仓库的「主干道」):
全局:Recovery → RequestId → Version → I18n → Logger
relay 组:CORS → DecompressRequest → BodyStorageCleanup → Stats
→ SystemPerformanceCheck(过载→503)
→ TokenAuth(sk-xxx 鉴权 + 分组判定)
→ ModelRequestRateLimit(按模型限流)
→ Distribute(解析 model → 选渠道 → 写上下文)
handler:controller.Relay(c, relayFormat)
API 侧另有 TurnstileCheck(人机验证)、SessionCookieOriginGuard(防 CSRF)、secure_verification(敏感操作二次验证)等。
一次请求的完整生命周期
把中间件链展开成时序图(这是全仓库的主干道,后续每一篇都在放大图中的某一段):
模块之间靠什么协作
这是理解代码库的钥匙。模块几乎不互相 import 对方的内部状态,协作靠三种机制:
① gin 上下文键(context keys)——同一请求内的数据总线。 前面的中间件把计算结果写进请求上下文,后面的模块按常量键读取。例如 TokenAuth 写入 ContextKeyUsingGroup(生效分组)、token_id、token_quota;Distribute 写入 channel_id/type/key/setting/param_override/model_mapping、ContextKeyRequestStartTime;controller.Relay 与计费服务全部从这里取。新增一个中间件字段,下游自动可用——这就是模块解耦的方式。
② 服务层调用——跨模块的业务接口。 控制器不直接写库做复杂逻辑,而是调 service:service.PreConsumeBilling(计费)、service.CacheGetRandomSatisfiedChannel(选路)、service.ConvertRequest(协议转换)、service.GetUserUsableGroups(分组)。
③ 共享表与缓存——跨请求的状态。 Option 表(动态配置,SyncOptions 轮询热更新)、Ability 表(group×model×channel 的路由索引)、logs 表(消费/错误日志)、Redis hash(token/user 缓存)。
常见踩坑
- 在全局中间件挂 gzip——SSE 全挂(源码注释原话警告);
- 把控制台 token(PAT)当 sk-xxx 用——两套凭据体系(第 3 篇)互不相通;
- 忽略
SystemPerformanceCheck——过载保护返回 503 时,问题可能不在你的代码而在系统负载; - 新增中间件后顺序放错——
Distribute必须在TokenAuth之后(要用它写入的分组与令牌信息)。
随堂练习(带验收标准)
- clone 仓库后
docker compose up -d起一个实例,初始化后创建 token 并用 curl 调/v1/models。验收:返回模型列表 JSON; - 在
middleware/distributor.go里列出它写入上下文的全部 key,与controller/relay.go读取的 key 做配对。验收:配对完整,无”写了没人读/读了没人写”的孤儿键; - 追踪一次请求的日志输出,把每行日志对应到本篇时序图的某一步。