Files
Edu/services/push-gateway/docs/01-understanding.md
SpecialX a0f6c228af docs(push-gateway): ai01 阶段1+2 模块理解确认书与架构设计文档
阶段1交付:services/push-gateway/docs/01-understanding.md
- 8节:架构位置/限界上下文/契约/技术栈/阶段归属P5骨架/审计
- 审计16项差距(6高/5中/5低)

阶段2交付:services/push-gateway/docs/02-architecture-design.md
- 13节:分层图/Connection+Hub模型/Redis key/WS端点
- 双通道协议/生命周期/心跳RFC6455/多实例PubSub
- 容量目标10w+连接

AI Agent: ai01 (api-gateway/push-gateway)
Branch: main
Coordinator: coord
2026-07-09 17:22:23 +08:00

14 KiB
Raw Blame History

模块理解确认书 — push-gateway

AIai01Go 网关层) 阶段:阶段 1 交付物 日期2026-07-09 关联:004 架构影响地图AI 分配方案


1. 我在架构中的位置

  • 层级L3 网关层004 §3.1 六层架构,与 api-gateway 并列)
  • 上游
    • 4 个微前端(浏览器/移动端通过 WebSocket 长连接接入)
    • msg 服务(通过 HTTP /internal/push /internal/broadcast 调用)
  • 下游
    • msg 服务P5 设计重点之一是与 msg 的 gRPC 推送通道协议;当前骨架走 HTTP
    • iam 服务(拉取 JWKS 公钥校验 JWTP2 起)
    • Redis跨实例 Pub/Sub 广播P5 待实现)
  • 通信方式
    • 入口WebSocketgorilla/websocket+ HTTP内部 API
    • 出口HTTP调 msg/ Redis Pub/Sub跨实例广播
  • 不持有业务状态:仅持有 WebSocket 连接池in-memory map[userID]map[*Connection]bool),无 DB

2. 我的限界上下文

  • 我负责
    • WebSocket 长连接生命周期(认证 → 心跳 → 断线清理)
    • 在线用户 session 映射userID → 多个 WebSocket 连接)
    • 接收 msg 服务的推送请求,投递到目标用户的在线连接
    • 跨实例广播P5 待实现Redis Pub/Sub
  • 聚合/实体:无业务聚合,仅 Connection 值对象UserID + conn + send chan
  • 业务领域:不属于 D1-D6属于基础设施层
  • 我不负责
    • 通知业务逻辑msg 服务负责:通知模板、已读/未读、通知偏好)
    • 推送内容持久化msg 服务落 MySQL
    • 离线推送msg 服务通过 SMS/邮件渠道补推)
    • JWT 签发iam 服务签发,本服务只校验)

3. 我与外部的契约

3.1 我消费的 proto message

proto message 用途
msg.proto Notification 接收 msg 服务的推送请求时,结构对齐(虽走 HTTP JSON但字段与 Notification 一致)
iam.proto UserInfo P2 起 RS256 校验 JWT通过 JWKS 端点,非 gRPC

当前骨架用 HTTP JSON 推送({userId, event, data}ai-allocation §5 设计重点要求与 msg 的 gRPC 推送通道协议。阶段 2 设计文档须明确 gRPC 契约(PushService.PushToUser / PushService.Broadcast)。

3.2 我暴露的 API 端点

方法 路径 鉴权 说明
GET /healthz liveness
GET /readyz 无(待实现) readiness应检查 Redis 连接)
GET /ws?token=JWT JWTquery 参数或 Authorization: Bearer WebSocket 升级端点
POST /internal/push 待补(内部 API应校验调用方身份 msg 服务定向推送
POST /internal/broadcast 待补 msg 服务广播
GET /metrics 无(待实现) Prometheus 指标

3.3 我发布/消费的 Kafka 事件

当前:无(骨架用 HTTP /internal/*)。

P5 设计目标ai-allocation §5

  • 消费edu.notification.eventsmsg 服务发布的 NotificationRequested 事件),消费后投递到对应用户的在线连接
  • 不发布任何 Kafka 事件(推送结果通过 gRPC response 返 msg

阶段 2 设计文档须明确:是 HTTP /internal/push 还是 Kafka 消费?建议双通道

  • HTTP /internal/pushmsg 同步调用,需立即知道推送结果(用户在线/离线)
  • Kafka 消费:广播类通知(全校广播),解耦 + 削峰

3.4 错误码前缀

错误码 HTTP 触发条件
UNAUTHORIZED 401 缺失 token / token 无效 / claims 解析失败
INVALID_REQUEST 400 请求体 JSON 解析失败
INVALID_PAYLOAD 400 event/data 字段缺失或格式错误
PUSH_FAILED 500 投递失败(当前不会触发,SendToUser 静默返回 nil
INTERNAL_ERROR 500 panic 兜底

4. 我的技术栈

  • 语言Go 1.22+go.mod 声明 1.25.0,需与 Dockerfile 对齐)
  • 框架Gin v1.12.0
  • 核心依赖
    • github.com/gorilla/websocket v1.5.3WebSocket
    • github.com/golang-jwt/jwt/v5 v5.2.1JWT 校验)
    • go.opentelemetry.io/otel v1.44.0 + otelgin v0.69.0
  • 存储:无 DBRedisP5 待引入,用于跨实例 Pub/Sub
  • 构建Dockerfile 单阶段builder→runner 但未做非 root 用户、healthcheck、ldflags 优化)

5. 我的阶段归属

  • 阶段P5沟通与 AI 阶段)
  • 当前阶段目标pending-features §P5
    • WebSocket 长连接管理 + 消费 Kafka 广播 + Redis PubSub 跨实例同步
    • 单节点支撑 10w+ 连接
    • 全校广播推送实时到达
  • 依赖上游阶段产出
    • P2 iam 服务JWT RS256 签发 + JWKS 端点)
    • P5 msg 服务(推送请求发起方)
    • P1 Redis 基础设施(已就绪)

6. 我需要对齐的黄金模板项(对照 classes 服务)

classes push-gateway 现状 差距
权限装饰器 @RequirePermission() N/A基础设施JWT 校验 + 内部 API 鉴权) ⚠️ 内部 API /internal/* 当前无鉴权
错误码前缀 CLASSES_* 无前缀 ⚠️ 待统一为 PUSH_*
logger pino 标准 log ⚠️ 待补 log/slog
metrics /metrics ⚠️ 待补:连接数/推送 QPS/心跳
tracer OTel SDK 对齐
/healthz 对齐
/readyz 检查 DB ⚠️ 待补:检查 Redis 连接
优雅关闭 SIGTERM → app.close() 5s 超时 ⚠️ 但未关闭所有 WebSocket 连接
测试覆盖率 ≥ 80% 0% ⚠️ 待补 hub/ws/config 测试
Dockerfile 多阶段 + 非 root + healthcheck 单阶段 + root + 无 healthcheck ⚠️ 待重构
Zod 输入验证 safeParse ShouldBindJSON 等价 对齐
GlobalErrorFilter GlobalErrorFilter ⚠️ gin.Recovery() 默认 ⚠️ 待自定义 JSON 信封错误

7. 服务审计表

服务 权限装饰器 错误码前缀 logger metrics tracer /healthz /readyz 优雅关闭 测试覆盖率 Dockerfile
push-gateway ⚠️ 内部 API 无鉴权 无前缀 标准 log OTel ⚠️ 未关 WS 0% 单阶段

7.1 详细问题清单(按严重度排序)

# 严重度 文件 问题 修复建议
1 全局 无 Redis Pub/Sub多实例部署时用户连接分散在各自实例跨实例推送失败 引入 Redis Pub/Sub本实例订阅 edu.push.user.<userID> channel跨实例推送通过 PUBLISH 投递
2 internal/ws/handler.go L22-26 CheckOrigin 永远返回 trueCSRF/Origin 校验缺失 从环境变量 WS_ALLOWED_ORIGINS 读取白名单
3 internal/ws/handler.go L67-75 心跳用文本消息 ping/pong,不符合 RFC 6455 控制帧规范;无空闲超时,僵尸连接永久占用 改用 conn.SetReadDeadline + WebSocket Ping/Pong 控制帧60s 无心跳关闭连接
4 internal/ws/handler.go L19 Send 通道满时静默丢弃,无指标无日志 增加 messages_dropped_total 指标 + slog.Warn
5 internal/hub/hub.go L44-57 无单用户最大连接数限制DoS 风险 限制每用户 ≤ 5 连接,超限拒绝新连接或踢掉最旧
6 main.go L44-46 /internal/* 无鉴权,内网任何服务可调用 引入 X-Internal-Token 头校验(与 msg 服务共享密钥)
7 internal/ws/handler.go L75 无 reconnect 协议,客户端断线后无法恢复未送达消息 协议层支持 session_id + last_seq,重连后 msg 服务补推(与 msg 协调)
8 全局 HS256 而非 RS256 P2 起改 RS256与 api-gateway 共用 JWKS 拉取逻辑
9 internal/config/config.go getEnvBool=="true" 字符串比较,不健壮 strconv.ParseBool
10 Dockerfile 单阶段构建、root 用户、无 healthcheck、无 ldflags 重构为多阶段 + 非 root + wget /healthz healthcheck
11 main.go L66-71 优雅关闭未关闭所有 WebSocket 连接 Hub 新增 CloseAll() 方法Shutdown 前调用
12 全局 /metrics 端点 新增 metrics.go:连接数 Gauge / 推送 QPS Counter / 心跳 Counter / 丢弃 Counter
13 go.mod L3 vs Dockerfile L1 go.mod 1.25.0 vs Dockerfile golang:1.22-alpine 统一
14 internal/hub/hub.go L17-23 Send 非阻塞丢弃,但 Outgoing 暴露 chan 给外部读,封装不彻底 改为 Connection 内部启动写协程,外部只调 Send
15 README.md L17 写 "Gin 1.10",实际 go.mod 是 v1.12.0 修正
16 测试 0% 覆盖率 补 hub_test.go注册/注销/推送/广播、ws_test.go鉴权/心跳)

8. 风险与假设

  • 假设P5 msg 服务会通过 gRPC 或 HTTP 调用本服务推送(双通道待设计文档定)
  • 假设Redis 在 P5 部署时可用P1 已就绪)
  • 风险:单实例 in-memory 连接池在水平扩展时失效,必须 Redis Pub/Sub
  • 风险:僵尸连接耗尽资源(无空闲超时)
  • 风险:内部 API 无鉴权,内网穿透后可被滥用
  • 未决:是否在 push-gateway 消费 Kafka 广播?建议是,避免 msg 服务同步调用造成推送延迟
  • 未决reconnect 协议是否在本期实现建议本期仅做基础协议reconnect 协议作为 P6 硬化项