# 多机部署与无感发布 本文定义不使用 Kubernetes 时的生产部署边界。目标是让 gateway 和内部微服务可以多机滚动发布,并让 `room-service` 在普通内网负载均衡命中非 owner 节点时自动转发到当前 Room Cell owner。 ## 网络拓扑 推荐拓扑: ```text 公网 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 维护两个短租约: ```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/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//configs/config.tencent.example.yaml` 为准。开发时新增配置字段的动作必须同时更新三处: 1. `services//configs/config.yaml`:本地直跑默认配置。 2. `services//configs/config.docker.yaml`:本地 Docker/Compose 配置。 3. `services//configs/config.tencent.example.yaml`:线上渲染契约。 部署前运行: ```bash 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 和权限变更走云产品流程,不混进服务滚动发布。 当前 RocketMQ 资源约定: | Topic | Producer Group | Consumer Group | 说明 | | --- | --- | --- | --- | | `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-activity-room-outbox` | activity-service 消费房间事实,处理贵重礼物播报、宝箱播报、成长等活动逻辑 | | `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-notice-room-outbox` | notice-service 消费私有通知相关房间事实 | | `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-room-im-bridge` | room-service IM bridge 消费房间群系统消息和群成员控制事实 | | `hyapp_room_treasure_open` | `hyapp-room-treasure-open-producer` | `hyapp-room-treasure-open` | room-service 消费宝箱延迟开箱唤醒消息 | 线上推荐配置: - `services/room-service/configs/config.tencent.example.yaml` 保持 `outbox_worker.publish_mode: "mq"`。 - `rocketmq.room_outbox.enabled: true` 后,`room_outbox.status=delivered` 只表示事件已进入 RocketMQ;activity、notice、IM bridge 的成功状态由各自消费位点负责。 - `rocketmq.treasure_open.enabled: true` 后,宝箱倒计时使用毫秒级 `open_at_ms` 投递延迟消息;消息到达后 room-service 仍要重新校验 Room Cell 状态,不能把 MQ 当成宝箱状态来源。 - 本地 Docker 默认关闭 MQ,使用 direct publisher 维持无云依赖的可运行形态。 ## 真实数据验证 本地真实 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。