# push-gateway 推送网关服务 > 版本:1.0(仲裁回写) > 日期:2026-07-10 > 状态:P5 已交付 > 关联文档: > > - [架构影响地图](../../architecture/004_architecture_impact_map.md) > - [02 架构设计](../../../services/push-gateway/docs/02-architecture-design.md) > - [president-final-rulings](../../architecture/president-final-rulings.md) --- ## 1. 模块职责 push-gateway 是实时推送基础设施服务(Go 实现),管理 WebSocket 长连接(**仅 WebSocket,不支持 SSE** — ISSUE-001)。 接收 msg 服务的推送请求(HTTP `/internal/*` + Kafka 事件),维护用户在线连接池,将消息实时投递到浏览器/移动端。 支持连接鉴权(JWT RS256)、RFC 6455 控制帧心跳、断线重连、多设备同步、连接数监控、跨实例 Pub/Sub 广播。 **无 DB**(ISSUE-005):消息审计、离线落库、重试队列均由 msg 服务负责。 --- ## 2. 技术栈 | 类别 | 技术 | 版本 | 用途 | | --------- | ------------------------ | ------ | -------------------------------------------------- | | 语言 | Go | 1.25+ | 高并发长连接 | | Web 框架 | Gin | 1.12+ | HTTP 升级 WebSocket + 内部 API | | WebSocket | gorilla/websocket | 1.5+ | WebSocket 协议 | | 缓存 | Redis | 7.x | 在线连接表、pub/sub 跨实例 fanout | | Kafka | segmentio/kafka-go | 0.4+ | 消费 `edu.notification.requested`(纯 Go,无 cgo) | | JWT | golang-jwt/jwt/v5 | 5.2+ | RS256 token 校验(via shared-go/jwks) | | 配置 | shared-go/env | — | 环境变量加载(替代 viper) | | 日志 | log/slog | stdlib | 结构化日志(JSON prod / text dev) | | 追踪 | OpenTelemetry | 1.44+ | 分布式追踪(via shared-go/tracer) | | 指标 | prometheus/client_golang | 1.23+ | Prometheus 指标 | | 共享包 | shared-go | — | tracer/logger/env/jwks 复用(ARB-015 §17.5) | | 编排 | Kubernetes | 1.28+ | 生产部署 | --- ## 3. 架构图 ```mermaid graph TB MSG[msg 服务] -->|HTTP /internal/push| PG[push-gateway] MSG -->|Kafka edu.notification.requested| PG PG --> HUB[Hub
连接池管理
map userID -> connID -> Connection] HUB --> C1[用户 1 连接] HUB --> C2[用户 2 连接] HUB --> CN[用户 N 连接] PG --> REDIS[(Redis
在线 SET + pub/sub)] MFE[微前端] -.WebSocket /ws?token=JWT.-> PG PG --> HEART[心跳保活
RFC 6455 Ping/Pong 30s] PG --> JWKS[iam JWKS
RS256 公钥 5min 缓存] PG --> METRICS[/metrics
9 个 Prometheus 指标] ``` > 完整分层图、跨实例同步图、失败模式矩阵见 [02 架构设计](../../../services/push-gateway/docs/02-architecture-design.md) §1 / §8 / §16。 --- ## 4. 核心流程图 ```mermaid sequenceDiagram participant U as 用户 participant PG as push-gateway participant R as Redis participant MSG as msg 服务 U->>PG: WebSocket /ws?token=JWT PG->>PG: JWT RS256 校验(via JWKS) PG->>HUB: Register(userID, conn) HUB->>R: SADD edu:push:online: + EXPIRE 60s PG-->>U: 连接建立 Note over PG: RFC 6455 Ping/Pong 30s;60s 无帧断开 MSG->>PG: POST /internal/push {user_id, event, data} PG->>HUB: SendToUser(userID, msg) alt 本实例有此用户 PG->>U: WebSocket 推送 PG-->>MSG: {delivered:true, online:true} else 本实例无此用户 PG->>R: SMEMBERS edu:push:online: alt 用户在线(其他实例) PG->>R: PUBLISH edu:push:channel:user. R-->>PG: 持有实例订阅后投递 PG-->>MSG: {delivered:true, online:true} else 用户离线 PG-->>MSG: {delivered:false, online:false} Note over MSG: msg 走离线推送(SMS/邮件) end end ``` --- ## 5. 对外契约 ### 5.1 HTTP 端点 | 方法 | 路径 | 鉴权 | 用途 | | ---- | --------------------------- | ------------------------------------------------------- | ----------------------------------- | | GET | `/ws` | JWT RS256(query `?token=` 或 `Authorization: Bearer`) | WebSocket 升级 | | POST | `/internal/push` | `X-Internal-Token` 头(ISSUE-002) | 定向推送 | | POST | `/internal/broadcast` | `X-Internal-Token` 头 | 广播 | | GET | `/internal/online/` | `X-Internal-Token` 头 | 查在线状态 | | GET | `/healthz` | 无 | liveness 探针 | | GET | `/readyz` | 无 | readiness 探针(软失败,ISSUE-058) | | GET | `/metrics` | 无 | Prometheus 指标 | ### 5.2 Kafka 消费 | Topic | Consumer Group | Message | 说明 | | ---------------------------- | -------------- | ----------------------- | ------------------------------------------------ | | `edu.notification.requested` | `push-gateway` | `NotificationRequested` | ISSUE-053 重命名(原 `edu.notification.events`) | - 至少一次 + Redis SETNX 幂等去重(event_id,TTL 24h) - 失败重试 3 次后入 DLQ(`edu.notification.requested.dlq`) ### 5.3 Redis 数据结构 | Key 模式 | 类型 | TTL | 用途 | | -------------------------------- | ----------------- | --- | -------------------- | | `edu:push:online:` | SET(instanceID) | 60s | 在线用户所在实例集合 | | `edu:push:channel:user:` | Pub/Sub | — | 跨实例定向推送 | | `edu:push:channel:broadcast` | Pub/Sub | — | 跨实例广播 | | `edu:push:idempotent:` | SETNX | 24h | Kafka 幂等去重 | > **无 gRPC 服务**(ISSUE-007):push-gateway 仅暴露 HTTP + WebSocket,不实现 gRPC 服务,`packages/shared-proto/proto/` 无 push-gateway.proto。 --- ## 6. 依赖关系 - **上游**:msg 服务(HTTP `/internal/*` + Kafka 事件) - **下游**:Redis(在线 SET + pub/sub 跨实例同步)、iam(JWKS 公钥) - **客户端**:teacher-portal、student-portal、parent-portal、admin-portal(WebSocket 连接) - **共享包**:shared-go(tracer/logger/env/jwks) --- ## 7. 架构约束 1. 无业务逻辑,仅做连接管理与消息转发 2. **无 DB**(ISSUE-005):审计/落库/重试由 msg 负责 3. **仅 WebSocket**(ISSUE-001):不支持 SSE 4. **无 gRPC**(ISSUE-007):仅 HTTP + WebSocket 5. 连接鉴权必须校验 JWT RS256(DevMode 接受 `dev-token`) 6. RFC 6455 控制帧心跳(Ping/Pong 30s,60s 无帧断开) 7. 单用户连接数上限 5(默认,可配 `MAX_CONNS_PER_USER`) 8. **单实例容量 10w+**(ISSUE-003,原 50k 已提升) 9. Redis 故障软失败(ISSUE-058/006):`/readyz` 返 200 + `degraded:true`,不断本地 WebSocket 10. Redis online SET 启动重建(ISSUE-058):先 SREM 旧 instanceID,再基于内存重建 --- ## 8. 关键 ADR(完整见 02 §14) | ADR | 决策 | 来源 | | ------- | ---------------------------------------- | ---------------- | | ADR-001 | 仅 WebSocket,不支持 SSE | ISSUE-001 | | ADR-002 | `X-Internal-Token` 命名 | ISSUE-002 | | ADR-003 | 单实例 10w+ 连接 | ISSUE-003 | | ADR-004 | 无 DB,审计由 msg 负责 | ISSUE-005 | | ADR-005 | Kafka topic `edu.notification.requested` | ISSUE-053 | | ADR-006 | `/readyz` 软失败 | ISSUE-058/006 | | ADR-007 | Redis SET 启动重建 | ISSUE-058 | | ADR-008 | Redis 必需但软失败 | ISSUE-055 vs 058 | | ADR-009 | 纯 HTTP + WebSocket,无 gRPC | ISSUE-007 | | ADR-010 | shared-go 复用 | ARB-015 §17.5 | --- ## 9. 环境变量 | 变量 | 默认值 | 说明 | | ----------------------------- | ---------------------------------------------- | ---------------------------------------- | | `PUSH_GATEWAY_PORT` | 8081 | HTTP 监听端口 | | `DEV_MODE` | false | 开发模式(跳过内部鉴权,接受 dev-token) | | `JWT_SECRET` | (DevMode 有默认) | JWT 密钥(生产必填) | | `JWKS_URL` | `http://localhost:50052/.well-known/jwks.json` | iam JWKS 端点 | | `INTERNAL_API_TOKEN` | (生产必填) | `/internal/*` 鉴权 token(ISSUE-002) | | `REDIS_URL` | `redis://localhost:6379/0` | Redis 连接 URL | | `KAFKA_BROKERS` | `localhost:9092` | Kafka broker 列表(逗号分隔) | | `KAFKA_NOTIFICATION_TOPIC` | `edu.notification.requested` | 消费 topic(ISSUE-053) | | `KAFKA_CONSUMER_GROUP` | `push-gateway` | consumer group | | `WS_ALLOWED_ORIGINS` | (空) | WebSocket Origin 白名单(逗号分隔) | | `MAX_CONNS_PER_USER` | 5 | 单用户最大连接数 | | `HEARTBEAT_INTERVAL_SECONDS` | 30 | 心跳间隔 | | `INSTANCE_ID` | (hostname) | 实例标识(Redis SET 成员) | | `OTEL_EXPORTER_OTLP_ENDPOINT` | `localhost:4318` | OTLP 端点 |