Files
Edu/docs/modules/push-gateway/README.md

195 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<br/>连接池管理<br/>map userID -> connID -> Connection]
HUB --> C1[用户 1 连接]
HUB --> C2[用户 2 连接]
HUB --> CN[用户 N 连接]
PG --> REDIS[(Redis<br/>在线 SET + pub/sub)]
MFE[微前端] -.WebSocket /ws?token=JWT.-> PG
PG --> HEART[心跳保活<br/>RFC 6455 Ping/Pong 30s]
PG --> JWKS[iam JWKS<br/>RS256 公钥 5min 缓存]
PG --> METRICS[/metrics<br/>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:<userID> + EXPIRE 60s
PG-->>U: 连接建立
Note over PG: RFC 6455 Ping/Pong 30s60s 无帧断开
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:<userID>
alt 用户在线(其他实例)
PG->>R: PUBLISH edu:push:channel:user.<userID>
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 RS256query `?token=``Authorization: Bearer` | WebSocket 升级 |
| POST | `/internal/push` | `X-Internal-Token`ISSUE-002 | 定向推送 |
| POST | `/internal/broadcast` | `X-Internal-Token` 头 | 广播 |
| GET | `/internal/online/<userID>` | `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_idTTL 24h
- 失败重试 3 次后入 DLQ`edu.notification.requested.dlq`
### 5.3 Redis 数据结构
| Key 模式 | 类型 | TTL | 用途 |
| -------------------------------- | ----------------- | --- | -------------------- |
| `edu:push:online:<userID>` | SETinstanceID | 60s | 在线用户所在实例集合 |
| `edu:push:channel:user:<userID>` | Pub/Sub | — | 跨实例定向推送 |
| `edu:push:channel:broadcast` | Pub/Sub | — | 跨实例广播 |
| `edu:push:idempotent:<event_id>` | SETNX | 24h | Kafka 幂等去重 |
> **无 gRPC 服务**ISSUE-007push-gateway 仅暴露 HTTP + WebSocket不实现 gRPC 服务,`packages/shared-proto/proto/` 无 push-gateway.proto。
---
## 6. 依赖关系
- **上游**msg 服务HTTP `/internal/*` + Kafka 事件)
- **下游**Redis在线 SET + pub/sub 跨实例同步、iamJWKS 公钥)
- **客户端**teacher-portal、student-portal、parent-portal、admin-portalWebSocket 连接)
- **共享包**shared-gotracer/logger/env/jwks
---
## 7. 架构约束
1. 无业务逻辑,仅做连接管理与消息转发
2. **无 DB**ISSUE-005审计/落库/重试由 msg 负责
3. **仅 WebSocket**ISSUE-001不支持 SSE
4. **无 gRPC**ISSUE-007仅 HTTP + WebSocket
5. 连接鉴权必须校验 JWT RS256DevMode 接受 `dev-token`
6. RFC 6455 控制帧心跳Ping/Pong 30s60s 无帧断开)
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/*` 鉴权 tokenISSUE-002 |
| `REDIS_URL` | `redis://localhost:6379/0` | Redis 连接 URL |
| `KAFKA_BROKERS` | `localhost:9092` | Kafka broker 列表(逗号分隔) |
| `KAFKA_NOTIFICATION_TOPIC` | `edu.notification.requested` | 消费 topicISSUE-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 端点 |