多机部署与无感发布
本文定义不使用 Kubernetes 时的生产部署边界。目标是让 gateway 和内部微服务可以多机滚动发布,并让 room-service 在普通内网负载均衡命中非 owner 节点时自动转发到当前 Room Cell owner。
网络拓扑
推荐拓扑:
公网 CLB
-> gateway-service 多实例
-> room-service 内网入口
-> user-service 内网入口
-> wallet-service 内网入口
-> activity-service 内网入口
-> game-service 内网入口
-> notice-service 内网入口
room-service 实例之间通过 Redis owner route 和 node registry 直连转发。
规则:
- 公网只暴露
gateway-service。 - 内部 gRPC 服务只监听私网,安全组只允许服务网段访问
13xxx端口。 user-service、wallet-service、activity-service、game-service、notice-service可以直接挂内网 CLB 或 DNS。room-service可以挂内网 CLB/DNS 给 gateway 使用,但 owner 转发必须使用实例级advertise_addr,不能使用 CLB 地址。- MySQL、Redis、MQ 使用腾讯云托管资源,不在业务 CVM 上部署;服务只通过内网 endpoint 访问这些托管依赖。
当前生产入口:
| 层级 | 地址 | 说明 |
|---|---|---|
| 公网 API | https://api.global-interaction.com |
HTTPS 443 由公网 CLB 转发到两台 gateway-service:13000 |
| 后台平台 | https://hyapp-platform.global-interaction.com |
香港 Lighthouse 上 nginx 终止 HTTPS,反代本机 admin-server:13100 |
| room/user/activity/game/notice 内网入口 | 10.2.1.16:13001/13005/13006/13008/13009 |
lb-epvnr4o0 的 Go 服务监听器 |
| wallet 内网入口 | 10.2.1.15:13004 |
lb-4f5xi6p0 的 Go 服务监听器 |
当前生产镜像仓库前缀是 10.2.1.3:18082/hyapp。部署脚本和 systemd env 只允许使用 hyapp 项目路径。
Room Owner 路由
room-service 使用 Redis 维护两个短租约:
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 和完整房间快照查询的处理顺序:
- 请求先到任意
room-service节点。 - 节点按
app_code + "\0" + room_id查询 Redis owner route。 - 如果当前节点就是 owner,直接进入本地 Room Cell。
- 如果 owner 是其他节点,按
room:node:{owner_node_id}找到实例私网地址并转发 gRPC。 - 如果 route 不存在或已过期,当前节点按原有
EnsureOwner逻辑接管并从 MySQL snapshot + command log 恢复。
转发只做一跳。advertise_addr 写错成 CLB 地址时,请求会在一跳后停止递归,并暴露真实 owner 冲突或 unavailable。
room-service 配置
每个实例必须有唯一配置:
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 的窗口。
无感发布步骤
单个服务滚动发布顺序:
- 在新机器或新端口启动新版本实例。
- 等
health_http_addr的/readyz或 gRPC health 通过。 - 把新实例加入对应 CLB 后端或内部 DNS。
- 观察错误率、延迟和日志。
- 从 CLB 摘除一个旧实例,等待 CLB 健康检查和连接排空周期。
- 对旧实例发送
SIGTERM。 - 旧实例进入 draining,gRPC 完成 in-flight 请求后释放已加载房间 lease,并删除
room:node:{node_id}。 - 新请求命中新节点后,如果 Redis route 已释放,会立即接管并从 MySQL 恢复;如果旧实例异常退出,则等待
lease_ttl到期后接管。
发布约束:
- 不要先杀进程再摘 CLB。
- 不要把
room-service的advertise_addr写成内网 CLB。 - 不要多个实例复用同一个
node_id。 - 数据库变更要先执行兼容当前发布窗口的迁移,再滚动服务。
配置拆分
不用统一配置中心也可以上线。推荐发布系统渲染最终 config.yaml:
- 公共环境配置:腾讯云 MySQL/Redis/MQ 私网地址、日志等级、腾讯云 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 为边界。
线上字段契约以 services/<service>/configs/config.tencent.example.yaml 为准。开发时新增配置字段的动作必须同时更新三处:
services/<service>/configs/config.yaml:本地直跑默认配置。services/<service>/configs/config.docker.yaml:本地 Docker/Compose 配置。services/<service>/configs/config.tencent.example.yaml:线上渲染契约。
部署前运行:
python3 deploy/tencent-tat/validate_rendered_configs.py \
--inventory deploy/tencent-tat/inventory.prod.example.json \
--rendered-dir /tmp/hyapp-rendered-prod
这个检查只比较 YAML 字段路径,不比较也不输出配置值。只要本地新增字段没有进入线上契约,或者最终渲染 YAML 漏字段,部署前会直接失败。
托管依赖边界:
- 腾讯云 MySQL 是业务事实和恢复来源;DSN 必须保留
loc=UTC。 - 腾讯云 Redis 保存 owner route、node registry、登录风控和短 TTL 缓存;Redis 不能作为业务恢复事实来源。
- 腾讯云 MQ 只作为 outbox/event bus 的托管消息通道;创建 topic、consumer group 和权限变更走云产品流程,不混进服务滚动发布。
真实数据验证
本地真实 MySQL/Redis 验证:
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。