Files
Edu/services/push-gateway/README.md

105 lines
6.0 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.0P5 仲裁回写)
> 端口8081
> 关联:[02 架构设计](./docs/02-architecture-design.md)、[modules/push-gateway/README](../../docs/modules/push-gateway/README.md)
## 职责
实时推送基础设施服务Go 实现),管理 WebSocket 长连接(仅 WebSocket不支持 SSE — ISSUE-001
接收 msg 服务的推送请求HTTP `/internal/*` + Kafka `edu.notification.requested`),维护用户在线连接池,
将消息实时投递到浏览器/移动端。无 DBISSUE-005审计/落库由 msg 负责。
## 技术栈
- Go 1.25+ / Gin 1.12+
- gorilla/websocket 1.5RFC 6455 控制帧心跳)
- segmentio/kafka-go 0.4(纯 Go无 cgo
- redis/go-redis/v9在线 SET + Pub/Sub 跨实例 fanout
- golang-jwt/jwt/v5 + shared-go/jwksRS256 校验5min 缓存)
- log/slog结构化日志JSON prod / text dev
- prometheus/client_golang9 个指标)
- shared-gotracer/logger/env/jwks 复用 — ARB-015 §17.5
## 开发
```bash
go mod tidy
go run main.go # :8081
# DevMode跳过内部鉴权接受 dev-token
DEV_MODE=true go run main.go
# 生产模式(需设置 JWT_SECRET + INTERNAL_API_TOKEN
JWT_SECRET=xxx INTERNAL_API_TOKEN=yyy go run main.go
```
## API
| 方法 | 路径 | 鉴权 | 说明 |
| ---- | --------------------------- | ------------------ | ------------------------------------------------ |
| GET | `/ws?token=JWT` | JWT RS256 | WebSocket 升级(亦支持 `Authorization: Bearer` |
| POST | `/internal/push` | `X-Internal-Token` | 定向推送msg 服务调用) |
| POST | `/internal/broadcast` | `X-Internal-Token` | 广播 |
| GET | `/internal/online/<userID>` | `X-Internal-Token` | 查在线状态 |
| GET | `/healthz` | 无 | liveness 探针 |
| GET | `/readyz` | 无 | readiness 探针(软失败 — ISSUE-058 |
| GET | `/metrics` | 无 | Prometheus 指标 |
## 环境变量
| 变量 | 默认值 | 说明 |
| ----------------------------- | ---------------------------------------------- | ---------------------------------------- |
| `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 端点 |
## WebSocket 心跳RFC 6455 控制帧)
- 客户端每 30s 发送 WebSocket **Ping 控制帧**(非文本消息)
- 服务端自动回 Ponggorilla/websocket 默认行为)
- 服务端 `SetReadDeadline(60s)`60s 未收到任何帧则断开
- 每次 Pong 刷新 Redis 在线 SET 的 TTL60s
## 内部包结构
```
internal/
├─ config/ # 环境变量配置shared-go/env
├─ hub/ # 连接池userID -> connID -> Connection
├─ ws/ # WebSocket 升级 + /internal/* HTTP API
├─ redisclient/ # Redis Pub/Sub + 在线 SETISSUE-058 启动重建)
├─ kafkaconsumer/ # Kafka 消费edu.notification.requested
├─ observability/ # slog logger + OTel tracer + Prometheus metrics
└─ health/ # /healthz + /readyz软失败
```
## 优雅关闭顺序
1. 收到 SIGTERM → 标记 Hub `closing`(拒绝新连接,`/readyz` 返 503
2. `Hub.CloseAll()`:向所有在线连接发送 close 帧code=1001 going away
3. `srv.Shutdown(10s)`:停止 HTTP 服务drain 在途请求
4. Kafka consumer cancel + reader.Close
5. Redis Pub/Sub cancel + client.Close
6. Tracer shutdownflush span
## 关键设计决策
- **软失败**ISSUE-058/006Redis/Kafka 故障时 `/readyz` 返 200 + `degraded:true`,不断本地 WebSocket
- **Redis SET 启动重建**ISSUE-058启动时 SREM 旧 instanceID基于内存重建
- **无 gRPC**ISSUE-007仅 HTTP + WebSocket
- **shared-go 复用**ARB-015 §17.5tracer/logger/env/jwks 不重复实现
完整 ADR 见 [02 架构设计 §14](./docs/02-architecture-design.md#14-架构决策记录adr)。