# 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/` | `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)。