113 lines
4.8 KiB
Markdown
113 lines
4.8 KiB
Markdown
# 多机部署与无感发布
|
||
|
||
本文定义不使用 Kubernetes 时的生产部署边界。目标是让 gateway 和内部微服务可以多机滚动发布,并让 `room-service` 在普通内网负载均衡命中非 owner 节点时自动转发到当前 Room Cell owner。
|
||
|
||
## 网络拓扑
|
||
|
||
推荐拓扑:
|
||
|
||
```text
|
||
公网 CLB
|
||
-> gateway-service 多实例
|
||
-> room-service 内网入口
|
||
-> user-service 内网入口
|
||
-> wallet-service 内网入口
|
||
-> activity-service 内网入口
|
||
-> game-service 内网入口
|
||
|
||
room-service 实例之间通过 Redis owner route 和 node registry 直连转发。
|
||
```
|
||
|
||
规则:
|
||
|
||
- 公网只暴露 `gateway-service`。
|
||
- 内部 gRPC 服务只监听私网,安全组只允许服务网段访问 `13xxx` 端口。
|
||
- `user-service`、`wallet-service`、`activity-service`、`game-service` 可以直接挂内网 CLB 或 DNS。
|
||
- `room-service` 可以挂内网 CLB/DNS 给 gateway 使用,但 owner 转发必须使用实例级 `advertise_addr`,不能使用 CLB 地址。
|
||
|
||
## Room Owner 路由
|
||
|
||
`room-service` 使用 Redis 维护两个短租约:
|
||
|
||
```text
|
||
room:route:{app_code + "\0" + room_id} -> owner node_id / lease_token / expires_at_ms
|
||
room:node:{node_id} -> 实例 advertise_addr / expires_at_ms
|
||
```
|
||
|
||
写命令、IM guard 和完整房间快照查询的处理顺序:
|
||
|
||
1. 请求先到任意 `room-service` 节点。
|
||
2. 节点按 `app_code + "\0" + room_id` 查询 Redis owner route。
|
||
3. 如果当前节点就是 owner,直接进入本地 Room Cell。
|
||
4. 如果 owner 是其他节点,按 `room:node:{owner_node_id}` 找到实例私网地址并转发 gRPC。
|
||
5. 如果 route 不存在或已过期,当前节点按原有 `EnsureOwner` 逻辑接管并从 MySQL snapshot + command log 恢复。
|
||
|
||
转发只做一跳。`advertise_addr` 写错成 CLB 地址时,请求会在一跳后停止递归,并暴露真实 owner 冲突或 unavailable。
|
||
|
||
## room-service 配置
|
||
|
||
每个实例必须有唯一配置:
|
||
|
||
```yaml
|
||
node_id: "room-node-prod-1"
|
||
grpc_addr: ":13001"
|
||
advertise_addr: "10.0.1.12:13001"
|
||
health_http_addr: ":13101"
|
||
node_registry_ttl: "30s"
|
||
node_registry_heartbeat_interval: "10s"
|
||
owner_forward_timeout: "2s"
|
||
lease_ttl: "10s"
|
||
```
|
||
|
||
配置边界:
|
||
|
||
- `grpc_addr` 是本进程监听地址,可以是 `:13001`。
|
||
- `advertise_addr` 是其他 room 节点能直连的实例私网地址,必须是单实例地址。
|
||
- `node_id` 参与 room owner lease、节点注册和日志排障,不能在多个实例间复用。
|
||
- `node_registry_ttl` 必须大于心跳周期;默认 `30s/10s`。
|
||
- `lease_ttl` 是节点故障后的自动接管窗口;正常滚动发布会在 gRPC drain 后主动释放本节点已加载房间 lease,减少等待 TTL 的窗口。
|
||
|
||
## 无感发布步骤
|
||
|
||
单个服务滚动发布顺序:
|
||
|
||
1. 在新机器或新端口启动新版本实例。
|
||
2. 等 `health_http_addr` 的 `/readyz` 或 gRPC health 通过。
|
||
3. 把新实例加入对应 CLB 后端或内部 DNS。
|
||
4. 观察错误率、延迟和日志。
|
||
5. 从 CLB 摘除一个旧实例,等待 CLB 健康检查和连接排空周期。
|
||
6. 对旧实例发送 `SIGTERM`。
|
||
7. 旧实例进入 draining,gRPC 完成 in-flight 请求后释放已加载房间 lease,并删除 `room:node:{node_id}`。
|
||
8. 新请求命中新节点后,如果 Redis route 已释放,会立即接管并从 MySQL 恢复;如果旧实例异常退出,则等待 `lease_ttl` 到期后接管。
|
||
|
||
发布约束:
|
||
|
||
- 不要先杀进程再摘 CLB。
|
||
- 不要把 `room-service` 的 `advertise_addr` 写成内网 CLB。
|
||
- 不要多个实例复用同一个 `node_id`。
|
||
- 数据库变更要先执行兼容当前发布窗口的迁移,再滚动服务。
|
||
|
||
## 配置拆分
|
||
|
||
不用统一配置中心也可以上线。推荐发布系统渲染最终 `config.yaml`:
|
||
|
||
- 公共环境配置:MySQL/Redis 私网地址、日志等级、腾讯云 endpoint。
|
||
- 服务配置:端口、worker 周期、批量大小、超时。
|
||
- 实例配置:`node_id`、`advertise_addr`、`user-service.id_generator.node_id`。
|
||
- Secret:MySQL/Redis 密码、JWT secret、腾讯 IM/RTC/COS secret、callback token。
|
||
|
||
只有当需要动态配置、灰度下发、审计回滚时,再引入 Consul/Nacos/Apollo。当前代码的启动配置仍以单服务 YAML 为边界。
|
||
|
||
## 真实数据验证
|
||
|
||
本地真实 MySQL/Redis 验证:
|
||
|
||
```bash
|
||
docker compose up -d mysql redis
|
||
ROOM_SERVICE_MYSQL_TEST_DSN='hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=UTC' \
|
||
ROOM_SERVICE_REDIS_TEST_ADDR='127.0.0.1:13379' \
|
||
go test -count=1 ./services/room-service/internal/transport/grpc -run TestOwnerForwardingUsesRedisRouteAndNodeRegistry
|
||
```
|
||
|
||
该测试会启动两个真实 gRPC `room-service` 实例,共用真实 MySQL schema 和 Redis route/node registry:在 node A 创建房间,通过 node B 调 `JoinRoom`、`CheckSpeakPermission` 和 `GetRoomSnapshot`,验证请求会转发到当前 owner。
|