依据 coord-final-decisions §3.8 W1-W8 裁决与 president-final-rulings §2.15/§2.16/§2.19 完整实现网关硬化: - W1/W2: 错误码 GW_ 前缀 + ActionState 信封响应体 - W3: 全量替换为 log/slog 结构化日志 - W4: /readyz 并行 ping 9 下游 + 软失败规则 - W5: 7 个业务 Prometheus 指标 + /metrics 端点 - W6: tracer 资源属性补全(name/version/env/host) - W7: DevMode=true && ENV=production panic 防护 - W8: 保持共享 downstream 熔断 P2 RS256 升级:接入 shared-go/jwks.Fetcher(TTL 5min)。 P2.7+P3-P5 路由扩展:student/parent/messages/dashboard。 文档同步:README/01/02/known-issues,arch.db 已更新。 质量校验:go vet + build + test 均通过。
23 KiB
模块架构设计文档 — api-gateway
AI:ai01(Go 网关层) 阶段:阶段 2 交付物 日期:2026-07-09 关联:01 理解确认书、004 架构影响地图
本文档覆盖 ai-allocation §5 设计重点:路由表矩阵、限流策略表、熔断阈值、JWT RS256 流程、CORS 白名单、请求 ID 注入。
1. 模块内部分层图
graph TB
subgraph Client["客户端"]
B[浏览器/移动端]
end
subgraph MW["中间件链(按注册顺序)"]
M1[1. Recovery<br/>panic 兜底]
M2[2. OTelgin<br/>HTTP span]
M3[3. RequestID<br/>X-Request-Id]
M4[4. CORS<br/>Origin 白名单]
M5[5. SecurityHeaders<br/>安全响应头]
M6[6. RequestBodyLimit<br/>10MB]
M7[7. RateLimit<br/>令牌桶]
M8[8. CircuitBreaker<br/>gobreaker v2]
M9[9. Auth<br/>JWT RS256]
M10[10. Metrics<br/>prom-client]
end
subgraph Routes["路由"]
H[/healthz /readyz /metrics/]
V1[/api/v1/* 代理路由/]
end
subgraph Proxy["反向代理"]
P[httputil.ReverseProxy<br/>去 /api/v1 前缀]
end
subgraph Downstream["下游服务"]
D1[iam :3002]
D2[classes/core-edu :3001/:3004]
D3[teacher-bff :3003]
D4[content :3005]
D5[msg :3007]
D6[ai :3008]
D7[data-ana :3006]
end
B --> M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> M7
M7 --> H
M7 --> M8 --> M9 --> M10 --> V1 --> P
P --> D1
P --> D2
P --> D3
P --> D4
P --> D5
P --> D6
P --> D7
拦截点说明:
- Recovery 必须最外层(捕获后续所有 panic)
- OTelgin 第二(覆盖全链路 span)
- RequestID 第三(后续中间件日志可引用)
- CircuitBreaker 在 Auth 之前(未鉴权请求先经熔断器,避免对已熔断下游发起鉴权开销)
- Auth 在 Metrics 之前(仅统计通过鉴权的请求)
- Health/Metrics 端点旁路 Auth(在 Auth 之前注册)
2. 领域模型
无领域模型。api-gateway 是基础设施层,不持有业务聚合/实体/值对象。
仅有的值对象:
| 值对象 | 字段 | 用途 |
|---|---|---|
Config |
见 §3 配置项 | 启动时加载,不可变 |
bucket(限流) |
tokens float64, lastTime time.Time |
per-IP 令牌桶 |
CircuitBreaker(gobreaker) |
内部状态机 | per-服务熔断状态 |
3. 数据模型
无数据库。所有状态在内存:
rateLimiter.buckets sync.Map[string]*bucket:IP → 令牌桶CircuitBreaker实例(每个下游服务一个,当前共享一个 "downstream")
P6 演进:
- 限流迁 Redis(
redis_rate包),支持多副本一致 - 熔断状态保持本地(每个副本独立判断下游健康)
3.1 配置项(Config 结构)
| 字段 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
| Port | API_GATEWAY_PORT |
8080 | 监听端口 |
| JWTSecret | JWT_SECRET |
(必填) | HS256 密钥(P1,P2 弃用) |
| JWKSURL | IAM_JWKS_URL |
http://iam:3002/.well-known/jwks.json |
RS256 公钥端点(P2) |
| JWTIssuer | JWT_ISSUER |
next-edu-cloud | JWT iss 校验 |
| JWTAudience | JWT_AUDIENCE |
next-edu-cloud | JWT aud 校验 |
| CORSOrigins | CORS_ORIGINS |
* | 逗号分隔白名单 |
| DevMode | DEV_MODE |
false | 开发旁路(生产禁用) |
| RateLimitRPS | RATE_LIMIT_RPS |
100 | 全局默认 RPS |
| RateLimitBurst | RATE_LIMIT_BURST |
20 | 突发容量 |
| BodyLimitBytes | BODY_LIMIT_BYTES |
10485760 | 请求体上限(10MB) |
| OTLPEndpoint | OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4318 | OTLP trace 端点 |
| LogLevel | LOG_LEVEL |
info | slog 级别 |
| ServicesURL | *_SERVICE_URL |
见路由表 | 9 个下游服务地址 |
| ShutdownTimeout | SHUTDOWN_TIMEOUT |
5s | 优雅关闭超时 |
4. API 设计
4.1 路由表矩阵(核心交付物)
| 路径前缀 | 目标服务 | 默认端口 | 鉴权 | 公开子路径 | 阶段 |
|---|---|---|---|---|---|
/api/v1/iam + /*path |
iam | 3002 | JWT | /iam/register /iam/login /iam/refresh |
P2 |
/api/v1/classes + /*path |
classes(P3 起合并入 core-edu) | 3001 → 3004 | JWT | — | P1 |
/api/v1/teacher + /*path |
teacher-bff | 3003 | JWT | — | P2 |
/api/v1/student + /*path |
student-bff | 3009 | JWT | — | P3 |
/api/v1/parent + /*path |
parent-bff | 3010 | JWT | — | P4 |
/api/v1/exams /homework /grades + /*path |
core-edu | 3004 | JWT | — | P3 |
/api/v1/textbooks /chapters /knowledge-points /questions + /*path |
content | 3005 | JWT | — | P4 |
/api/v1/notifications /messages + /*path |
msg | 3007 | JWT | — | P5 |
/api/v1/ai + /*path |
ai | 3008 | JWT | — | P5 |
/api/v1/analytics /dashboard + /*path |
data-ana | 3006 | JWT | — | P4 |
/healthz |
本服务 | 8080 | 无 | — | P1 |
/readyz |
本服务 | 8080 | 无 | — | P1 |
/metrics |
本服务 | 8080 | 无 | — | P6 |
路由注册规则(强制):
- 每个前缀同时注册无尾斜杠与通配符两条路由(
/classes+/classes/*path) r.RedirectTrailingSlash = false(避免 Next.js rewrites 代理循环,见 known-issues)- 新增服务时按本表追加,禁止改其他服务路由
4.2 限流策略表
| 路由类别 | RPS | Burst | 备注 |
|---|---|---|---|
/api/v1/iam/login |
5 | 5 | 登录接口额外加用户级限流(防爆破) |
/api/v1/iam/register |
10 | 10 | 注册接口 |
/api/v1/iam/refresh |
20 | 20 | 刷新 token |
/api/v1/ai/* |
20 | 10 | AI 接口成本高,单独限流 |
/api/v1/analytics/* |
30 | 20 | 分析查询较重 |
其他 /api/v1/* |
100 | 20 | 默认全局限流 |
/healthz /readyz /metrics |
∞ | ∞ | 不限流(探针高频访问) |
实现方式:
- P1:内存令牌桶(
sync.Mapper-IP) - P6:Redis 令牌桶(
redis_rate),支持多副本一致 + 用户级限流
4.3 熔断阈值配置
| 服务 | Interval | Timeout | MaxRequests | ReadyToTrip | 备注 |
|---|---|---|---|---|---|
| iam | 5s | 30s | 1 | 错误率 > 50% | 鉴权失败影响全链路 |
| classes/core-edu | 5s | 30s | 1 | 错误率 > 50% | 默认 |
| teacher-bff | 5s | 30s | 1 | 错误率 > 50% | 默认 |
| content | 10s | 60s | 1 | 错误率 > 30% | ES 慢查询容忍度低 |
| msg | 5s | 30s | 1 | 错误率 > 50% | 默认 |
| ai | 10s | 60s | 1 | 错误率 > 30% | LLM 延迟高,容忍度低 |
| data-ana | 10s | 60s | 1 | 错误率 > 30% | ClickHouse 查询较重 |
熔断失败定义:下游返回 5xx 视为失败;4xx/2xx 不计入。
熔断打开响应:HTTP 503 { success: false, error: { code: "CIRCUIT_OPEN", message: "downstream unhealthy", retry_after: 30 } }
4.4 JWT RS256 校验流程(P2 升级)
sequenceDiagram
participant U as 用户
participant GW as api-gateway
participant IAM as iam 服务
participant JWKS as JWKS 缓存
rect rgb(255, 250, 240)
Note over GW,JWKS: 启动时与定期刷新
GW->>IAM: GET /.well-known/jwks.json
IAM-->>GW: { keys: [...] }
GW->>JWKS: 缓存公钥集(TTL 5min)
end
rect rgb(240, 248, 255)
Note over U,GW: 请求鉴权
U->>GW: GET /api/v1/classes + Authorization: Bearer <RS256 token>
GW->>GW: 解析 token header(kid)
GW->>JWKS: 查 kid 对应公钥
alt 缓存命中
JWKS-->>GW: 公钥
else 缓存未命中
GW->>IAM: GET /.well-known/jwks.json(强制刷新)
IAM-->>GW: 新公钥集
GW->>JWKS: 更新缓存
end
GW->>GW: jwt.Verify(token,公钥,iss,aud,exp)
alt 校验通过
GW->>GW: 提取 sub/roles/dataScope
GW->>GW: 注入 x-user-id / x-user-roles / x-data-scope 头
GW-->>U: 代理转发到 classes 服务
else 校验失败
GW-->>U: 401 INVALID_TOKEN
end
end
JWKS 缓存策略(由 shared-go/jwks.Fetcher 实现,TTL 5min):
- TTL 5min,到期后台异步刷新(不阻塞请求)
- kid 未命中时强制同步刷新一次
- 刷新失败保留旧公钥集继续服务(fail-open 1 次后 fail-close)
- 启动时同步拉取一次,失败则 panic 拒绝启动
4.5 CORS 白名单
// 默认白名单(生产环境通过 CORS_ORIGINS 覆盖)
allowedOrigins = []string{
"https://teacher.edu.example.com",
"https://student.edu.example.com",
"https://parent.edu.example.com",
"https://admin.edu.example.com",
"http://localhost:3000", // 开发
}
规则:
- 未配置
CORS_ORIGINS时默认*(仅开发环境) - 生产环境必须显式配置白名单,禁止
* - 允许方法:GET POST PUT DELETE OPTIONS PATCH
- 允许头:Authorization Content-Type X-Request-Id X-Trace-Id
- 暴露头:X-Request-Id X-Trace-Id
- 预检缓存:12 小时(
Access-Control-Max-Age: 43200)
4.6 请求 ID 注入
// 规则:
// 1. 优先透传客户端 X-Request-Id 头
// 2. 缺失则生成 req-<uuid-v4>
// 3. 写入 context + 响应头 X-Request-Id
// 4. 注入到下游请求头(ReverseProxy.Director 中保留)
// 5. 日志/metrics/trace 全部引用此 request_id
W3C Trace Context 透传:
traceparent/tracestate头透传(OTel SDK 自动处理)X-Request-Id是业务层 ID,traceparent是 OTel 层 ID,两者并存- 日志同时记录两者,便于 Loki → Tempo 关联查询
5. 事件设计
无。api-gateway 是纯同步代理,不发布/消费任何 Kafka 事件。
6. 横切关注点对齐清单
6.1 权限装饰器(等价实现)
| 端点 | 鉴权方式 | 备注 |
|---|---|---|
/healthz /readyz /metrics |
无 | 探针端点 |
/api/v1/iam/register /login /refresh |
白名单 | 公开接口 |
其他 /api/v1/* |
AuthMiddleware |
JWT RS256 校验 + 头注入 |
Go 无装饰器,用中间件链等价实现。Gateway 不做权限点校验(不持有 Permissions 常量),仅透传
x-user-id/x-user-roles/x-data-scope头,由下游业务服务 Controller 自校验。
6.2 错误码清单
依据 W1/W2 裁决:错误码统一
GW_前缀,响应体统一 ActionState 信封{success,error:{code,message}}。
| 错误码 | HTTP | 触发条件 | 响应体 |
|---|---|---|---|
GW_UNAUTHORIZED |
401 | 缺失 Authorization 头 | {success:false,error:{code:"GW_UNAUTHORIZED",message:"..."}} |
GW_INVALID_TOKEN |
401 | JWT 签名/格式错误 | 同上 |
GW_INVALID_CLAIMS |
401 | JWT claims 解析失败 | 同上 |
GW_RATE_LIMITED |
429 | 超出令牌桶限流 | 同上 + retry_after: 60 |
GW_CIRCUIT_OPEN |
503 | 下游熔断打开 | 同上 + retry_after: 30 |
GW_REQUEST_TOO_LARGE |
413 | 请求体超 10MB | 同上 |
GW_INTERNAL_ERROR |
500 | panic 兜底 | 同上 + request_id |
6.3 Logger 初始化
已实现:直接使用标准库
log/slog(W3 裁决),未接入shared-go/logger(zap,待后续评估)。当前slog.SetDefault由InitTracer侧初始化时隐式使用默认 logger。
// 全局使用 slog(main.go + 各 middleware 直接调用 slog.Info/slog.Error)
import "log/slog"
// 日志级别由 LOG_LEVEL 控制(当前默认 info,未单独封装 InitLogger)
日志字段规范:
timestampISO8601levelINFO/WARN/ERRORservice"api-gateway"request_id从 context 取trace_id从 OTel span 取user_id从注入头取methodpathstatuslatency_ms
6.4 Metrics 指标清单
| 指标名 | 类型 | 标签 | 描述 |
|---|---|---|---|
api_gateway_http_requests_total |
Counter | method,endpoint,status | 请求总数 |
api_gateway_http_request_duration_seconds |
Histogram | method,endpoint | 请求延迟 |
api_gateway_circuit_breaker_state |
Gauge | service,state | 熔断器状态(0=CLOSED 1=OPEN 2=HALF_OPEN) |
api_gateway_rate_limited_total |
Counter | ip | 被限流请求数 |
api_gateway_proxy_upstream_duration_seconds |
Histogram | upstream | 下游响应延迟 |
api_gateway_jwks_refresh_total |
Counter | result | JWKS 刷新次数 |
go_* |
— | — | prom-client 默认 Go runtime 指标 |
暴露端点:GET /metrics(prom-client 默认 handler)
6.5 Tracer 初始化
已实现:internal/observability/tracer.go,OTLP HTTP exporter + W3C TraceContext 传播。
资源属性(W6 完整):
service.name= "api-gateway"service.version= 编译时注入的 version 变量deployment.environment= cfg.Envhost.name= os.Hostname()
签名:
func InitTracer(serviceName, endpoint, env, version, hostName string) (shutdown func(context.Context) error, err error)
业务 span 命名规范:HTTP GET /api/v1/classes(otelgin 自动注入)。
6.6 /healthz 检查逻辑
func Healthz(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
"service": "api-gateway",
"version": Version, // 编译时注入
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}
6.7 /readyz 检查逻辑
已实现(W4):
Readyz(cfg *config.Config),并行 ping 9 个下游/healthz,2s 超时,软失败规则。
func Readyz(cfg *config.Config) gin.HandlerFunc {
checks := []downstreamCheck{
{name: "iam", url: cfg.IamServiceURL + "/healthz", required: true},
{name: "teacher-bff", url: cfg.TeacherBffURL + "/healthz", required: true},
{name: "core-edu", url: cfg.CoreEduServiceURL + "/healthz", required: false},
{name: "content", url: cfg.ContentServiceURL + "/healthz", required: false},
{name: "data-ana", url: cfg.DataAnaServiceURL + "/healthz", required: false},
{name: "msg", url: cfg.MsgServiceURL + "/healthz", required: false},
{name: "ai", url: cfg.AiServiceURL + "/healthz", required: false},
{name: "student-bff", url: cfg.StudentBffURL + "/healthz", required: false},
{name: "parent-bff", url: cfg.ParentBffURL + "/healthz", required: false},
}
client := &http.Client{Timeout: 2 * time.Second}
// 并行 ping(sync.WaitGroup + sync.Mutex)
// 软失败规则:
// - required 下游不可达 → 503
// - optional 下游不可达 → 200 + degraded 列表
}
下游清单与必需性(W4 软失败规则):
| 下游 | required | 不可达时 |
|---|---|---|
| iam | ✅ | 503 |
| teacher-bff | ✅ | 503 |
| core-edu | ❌ | 200 + degraded |
| content | ❌ | 200 + degraded |
| data-ana | ❌ | 200 + degraded |
| msg | ❌ | 200 + degraded |
| ai | ❌ | 200 + degraded |
| student-bff | ❌ | 200 + degraded |
| parent-bff | ❌ | 200 + degraded |
6.8 优雅关闭顺序
// 1. 收到 SIGTERM
// 2. srv.Shutdown(ctx) 停止接受新请求,等待在途请求完成(5s 超时)
// 3. tracerShutdown() flush 待发送 span
// 4. log.Println("exited")
// 5. os.Exit(0)
7. 与其他模块的交互点(契约清单)
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 |
|---|---|---|---|---|
| 调用 | iam | HTTP | GET /.well-known/jwks.json |
拉 RS256 公钥(P2) |
| 透传 | 所有下游 | HTTP | x-user-id x-user-roles x-data-scope 头 |
用户身份传递 |
| 透传 | 所有下游 | HTTP | X-Request-Id traceparent 头 |
链路追踪 |
| 调用 | 所有下游 | HTTP | GET /healthz |
/readyz 健康检查 |
| 被调用 | 微前端 | HTTP | /api/v1/* |
业务请求 |
| 被调用 | Prometheus | HTTP | GET /metrics |
指标采集 |
| 被调用 | K8s/Docker | HTTP | GET /healthz GET /readyz |
探针 |
8. 风险与假设
- 假设:iam P2 暴露 JWKS 端点;若延期,RS256 升级阻塞,临时保留 HS256
- 假设:所有下游服务实现
/healthz;若某服务未实现,/readyz误报 - 风险:单实例限流在多副本部署后失效(P6 迁 Redis 解决)
- 风险:JWKS 缓存过期时若 iam 不可达,fail-open 1 次后 fail-close,可能导致全量 401
- 风险:DevMode 旁路误开到生产 → 启动时强制校验
DevMode=true && ENV=productionpanic - 未决:是否在 Gateway 层做 IP 黑名单(WAF)?建议 P6 在 Istio 层做,本服务不介入
9. 实施优先级
更新日期:2026-07-10(P2-P5 实施后复核)
| 优先级 | 任务 | 状态 | 阶段 |
|---|---|---|---|
| P0 | 删除 auth.go 死代码 | ✅ 已完成 | P2.0 |
| P0 | 修复 go.mod 与 Dockerfile Go 版本不一致 | ✅ 已完成 | P2.0 |
| P0 | 新增 /metrics 端点 + 7 个业务指标 |
✅ 已完成 | P2.4(W5) |
| P0 | 引入 log/slog 替换标准 log |
✅ 已完成 | P2.4(W3) |
| P1 | /readyz 真实健康检查 |
✅ 已完成 | P2.5(W4) |
| P1 | JWT RS256 升级 + JWKS 缓存 | ✅ 已完成 | P2.2 |
| P1 | 错误码 GW_ 前缀 + ActionState 信封 | ✅ 已完成 | P2.3(W1/W2) |
| P1 | DevMode 生产防护 | ✅ 已完成 | P2.6(W7) |
| P1 | tracer 资源属性补全 | ✅ 已完成 | P2.4(W6) |
| P1 | CORS 纳入 Config | ✅ 已完成 | P2.4 |
| P1 | 路由扩展(student/parent/messages/dashboard) | ✅ 已完成 | P2.7 + P3-P5 |
| P2 | 限流策略表细化(per-路由) | ⏳ P6 | P6 |
| P2 | 熔断 per-服务实例 | ⏳ P6 | P6 |
| P3 | 补全测试覆盖率到 80% | ⏳ P6 | P6 |