Files
Edu/services/api-gateway/docs/02-architecture-design.md
SpecialX 4307f6b73c feat(api-gateway): 实现 W1-W8 网关硬化与 P2-P5 路由扩展
依据 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 均通过。
2026-07-10 18:15:48 +08:00

23 KiB
Raw Permalink Blame History

模块架构设计文档 — api-gateway

AIai01Go 网关层) 阶段:阶段 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 令牌桶
CircuitBreakergobreaker 内部状态机 per-服务熔断状态

3. 数据模型

无数据库。所有状态在内存:

  • rateLimiter.buckets sync.Map[string]*bucketIP → 令牌桶
  • CircuitBreaker 实例(每个下游服务一个,当前共享一个 "downstream"

P6 演进

  • 限流迁 Redisredis_rate 包),支持多副本一致
  • 熔断状态保持本地(每个副本独立判断下游健康)

3.1 配置项Config 结构)

字段 环境变量 默认值 说明
Port API_GATEWAY_PORT 8080 监听端口
JWTSecret JWT_SECRET (必填) HS256 密钥P1P2 弃用)
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 classesP3 起合并入 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.Map per-IP
  • P6Redis 令牌桶(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 headerkid
        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 是业务层 IDtraceparent 是 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/slogW3 裁决),未接入 shared-go/loggerzap待后续评估。当前 slog.SetDefaultInitTracer 侧初始化时隐式使用默认 logger。

// 全局使用 slogmain.go + 各 middleware 直接调用 slog.Info/slog.Error
import "log/slog"

// 日志级别由 LOG_LEVEL 控制(当前默认 info未单独封装 InitLogger

日志字段规范

  • timestamp ISO8601
  • level INFO/WARN/ERROR
  • service "api-gateway"
  • request_id 从 context 取
  • trace_id 从 OTel span 取
  • user_id 从注入头取
  • method path status latency_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 /metricsprom-client 默认 handler

6.5 Tracer 初始化

已实现:internal/observability/tracer.goOTLP HTTP exporter + W3C TraceContext 传播。

资源属性W6 完整):

  • service.name = "api-gateway"
  • service.version = 编译时注入的 version 变量
  • deployment.environment = cfg.Env
  • host.name = os.Hostname()

签名

func InitTracer(serviceName, endpoint, env, version, hostName string) (shutdown func(context.Context) error, err error)

业务 span 命名规范HTTP GET /api/v1/classesotelgin 自动注入)。

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 检查逻辑

已实现W4Readyz(cfg *config.Config),并行 ping 9 个下游 /healthz2s 超时,软失败规则。

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}
    // 并行 pingsync.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=production panic
  • 未决:是否在 Gateway 层做 IP 黑名单WAF建议 P6 在 Istio 层做,本服务不介入

9. 实施优先级

更新日期2026-07-10P2-P5 实施后复核)

优先级 任务 状态 阶段
P0 删除 auth.go 死代码 已完成 P2.0
P0 修复 go.mod 与 Dockerfile Go 版本不一致 已完成 P2.0
P0 新增 /metrics 端点 + 7 个业务指标 已完成 P2.4W5
P0 引入 log/slog 替换标准 log 已完成 P2.4W3
P1 /readyz 真实健康检查 已完成 P2.5W4
P1 JWT RS256 升级 + JWKS 缓存 已完成 P2.2
P1 错误码 GW_ 前缀 + ActionState 信封 已完成 P2.3W1/W2
P1 DevMode 生产防护 已完成 P2.6W7
P1 tracer 资源属性补全 已完成 P2.4W6
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