105 lines
6.0 KiB
Markdown
105 lines
6.0 KiB
Markdown
# Push Gateway 推送网关服务
|
||
|
||
> 版本:1.0(P5 仲裁回写)
|
||
> 端口: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`),维护用户在线连接池,
|
||
将消息实时投递到浏览器/移动端。无 DB(ISSUE-005),审计/落库由 msg 负责。
|
||
|
||
## 技术栈
|
||
|
||
- Go 1.25+ / Gin 1.12+
|
||
- gorilla/websocket 1.5(RFC 6455 控制帧心跳)
|
||
- segmentio/kafka-go 0.4(纯 Go,无 cgo)
|
||
- redis/go-redis/v9(在线 SET + Pub/Sub 跨实例 fanout)
|
||
- golang-jwt/jwt/v5 + shared-go/jwks(RS256 校验,5min 缓存)
|
||
- log/slog(结构化日志,JSON prod / text dev)
|
||
- prometheus/client_golang(9 个指标)
|
||
- shared-go(tracer/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/*` 鉴权 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 端点 |
|
||
|
||
## WebSocket 心跳(RFC 6455 控制帧)
|
||
|
||
- 客户端每 30s 发送 WebSocket **Ping 控制帧**(非文本消息)
|
||
- 服务端自动回 Pong(gorilla/websocket 默认行为)
|
||
- 服务端 `SetReadDeadline(60s)`:60s 未收到任何帧则断开
|
||
- 每次 Pong 刷新 Redis 在线 SET 的 TTL(60s)
|
||
|
||
## 内部包结构
|
||
|
||
```
|
||
internal/
|
||
├─ config/ # 环境变量配置(shared-go/env)
|
||
├─ hub/ # 连接池(userID -> connID -> Connection)
|
||
├─ ws/ # WebSocket 升级 + /internal/* HTTP API
|
||
├─ redisclient/ # Redis Pub/Sub + 在线 SET(ISSUE-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 shutdown(flush span)
|
||
|
||
## 关键设计决策
|
||
|
||
- **软失败**(ISSUE-058/006):Redis/Kafka 故障时 `/readyz` 返 200 + `degraded:true`,不断本地 WebSocket
|
||
- **Redis SET 启动重建**(ISSUE-058):启动时 SREM 旧 instanceID,基于内存重建
|
||
- **无 gRPC**(ISSUE-007):仅 HTTP + WebSocket
|
||
- **shared-go 复用**(ARB-015 §17.5):tracer/logger/env/jwks 不重复实现
|
||
|
||
完整 ADR 见 [02 架构设计 §14](./docs/02-architecture-design.md#14-架构决策记录adr)。
|