# 腾讯云 TAT 滚动部署方案

本方案只部署本仓库 Go 微服务，不接管同机已有 Java 项目。远端连接使用腾讯云 TAT，发布时只操作 `hyapp-*` systemd unit、`hyapp-*` Docker 容器和 `/etc/hyapp/<service>/docker.env`。

MySQL、Redis、MQ 都按腾讯云托管资源处理。本方案不会在这些 CVM 上部署、重启或迁移本地 MySQL/Redis/MQ；服务配置只需要指向腾讯云托管实例的内网地址。

## 机器拓扑

`deploy` 机器是发布控制面，不承载业务流量：

| 机器 | 实例 ID | 公网 IP | 私网 IP | 角色 |
| --- | --- | --- | --- | --- |
| `deploy` | `ns-ntmpcd3a` | `43.164.75.199` | `10.2.1.3` | 构建镜像、推送镜像、调用 TAT/CLB API 调度发布 |

当前机器按截图落到以下服务：

| 机器 | 实例 ID | 私网 IP | 本仓库服务 |
| --- | --- | --- | --- |
| `gateway-1` | `ins-aeyaoj40` | `10.2.1.8` | `gateway-service` |
| `gateway-2` | `ins-irvi5b7u` | `10.2.2.17` | `gateway-service` |
| `new-app-1` | `ins-fi5zufpk` | `10.2.1.4` | `room-service`, `user-service` |
| `new-app-2` | `ins-hwhaoe8c` | `10.2.1.13` | `room-service`, `user-service` |
| `new-game-1` | `ins-ir229jtw` | `10.2.1.10` | `game-service` |
| `new-game-2` | `ins-460rw7gu` | `10.2.1.6` | `game-service` |
| `new-app-activity-1` | `ins-n8wut6n8` | `10.2.1.11` | `activity-service`, `notice-service` |
| `new-app-activity-2` | `ins-d1n303sa` | `10.2.1.7` | `activity-service`, `notice-service` |
| `pay-1` | `ins-ceqfcxd2` | `10.2.11.14` | `wallet-service` |
| `pay-2` | `ins-awhb74q6` | `10.2.12.5` | `wallet-service` |

如果 `new-app-*` 只部署 `room-service` 或只部署 `user-service`，直接删掉 `inventory` 中对应服务的 host 绑定，不要让脚本猜。

## 部署模式

采用“deploy 服务器调度 + 业务机只运行镜像”的模式：

1. `deploy` 服务器拉取本仓库代码，构建 Docker 镜像，推送到 CCR/Harbor。
2. `deploy` 服务器调用腾讯云 CLB API 对目标实例摘流。
3. `deploy` 服务器通过 TAT 在目标业务机执行 `docker pull` 和 `systemctl restart hyapp-<service>`。
4. 目标业务机只保存 `/etc/hyapp/<service>/config.yaml` 和 `docker.env`，不拉代码、不编译、不保存 Git 凭据。

不要让每台业务服务器自己 `git pull && build`。那会把代码权限、构建环境、版本一致性和回滚都扩散到业务机，且多服务发布时无法统一做 CLB 摘流。

## 前置条件

1. 每台目标机已安装腾讯云 TAT agent，控制台能执行命令。
2. 每个 Go 服务已按 `deploy/standalone/systemd` 安装对应 systemd unit。
3. 每个 Go 服务已有 `/etc/hyapp/<service>/config.yaml` 和 `/etc/hyapp/<service>/docker.env`。
4. 每个服务至少有两台实例，CLB 后端都挂实例端口。
5. `room-service` 每台机器必须有唯一 `node_id`，`advertise_addr` 必须是当前机器私网地址，例如 `10.2.1.4:13001`，不能写内网 CLB。
6. 每个 `config.yaml` 的 `mysql_dsn`、`redis_addr`、MQ endpoint/topic/group 指向腾讯云托管资源的私网地址。
7. 发布机安装腾讯云 Python SDK 和 YAML 解析依赖：

```bash
python3 -m pip install tencentcloud-sdk-python PyYAML
```

凭据通过环境变量提供：

```bash
export TENCENTCLOUD_SECRET_ID="..."
export TENCENTCLOUD_SECRET_KEY="..."
export TENCENTCLOUD_REGION="me-saudi-arabia"
```

如果继续沿用 `hy-app-monitor`，可以把这些变量放在 deploy 机器的 `/opt/hy-app-monitor/.env`，运行脚本时传 `--env-file /opt/hy-app-monitor/.env`。本地开发机路径 `/Users/hy/Documents/hy/hy-app-monitor/.env` 只用于本机调试。

## Deploy 服务器初始化

在 `deploy / 10.2.1.3` 上安装基础工具：

```bash
sudo mkdir -p /opt/deploy/sources /opt/deploy/inventory
sudo yum install -y git docker python3 python3-pip || sudo apt-get update && sudo apt-get install -y git docker.io python3 python3-pip
sudo systemctl enable --now docker
python3 -m pip install --user tencentcloud-sdk-python PyYAML
```

拉取本仓库：

```bash
cd /opt/deploy/sources
git clone <YOUR_GIT_REPO_URL> hyapp-server
cd hyapp-server
```

准备 inventory：

```bash
cp deploy/tencent-tat/inventory.prod.example.json /opt/deploy/inventory/hyapp.prod.json
vi /opt/deploy/inventory/hyapp.prod.json
```

`inventory` 里的 `registry` 必须和实际镜像仓库一致。如果使用 `hy-app-monitor` 里的 Harbor 配置，通常是：

```text
registry = ${HARBOR_REGISTRY}/${HARBOR_PROJECT}
```

业务配置放到各目标机 `/etc/hyapp/<service>/config.yaml`。腾讯云 MySQL、Redis、MQ、JWT、IM/RTC/COS 等密钥不要提交到 Git。

配置字段契约以 `services/<service>/configs/config.tencent.example.yaml` 为准。开发时如果在本地 `config.yaml` 或 `config.docker.yaml` 新增字段，必须同时补到 `config.tencent.example.yaml`；生产渲染前运行校验，避免线上漏字段：

```bash
python3 deploy/tencent-tat/validate_rendered_configs.py \
  --inventory deploy/tencent-tat/inventory.prod.example.json
```

如果已经渲染了生产最终配置，还要校验每台机器的最终 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 key，不打印配置值，避免把生产密钥带到日志里。

## 构建并推送镜像

在 `deploy` 服务器上执行。2 核 2GiB 机器可以作为调度器；如果全量构建 OOM 或太慢，把构建放到 CI，deploy 服务器只负责调度发布。

```bash
cd /opt/deploy/sources/hyapp-server
TAG="$(date -u +%Y%m%d%H%M%S)-$(git rev-parse --short HEAD)"
REGISTRY="10.2.1.3:18082/hyapp"

docker build -t "$REGISTRY/gateway-service:$TAG" -f services/gateway-service/Dockerfile .
docker build -t "$REGISTRY/room-service:$TAG" -f services/room-service/Dockerfile .
docker build -t "$REGISTRY/user-service:$TAG" -f services/user-service/Dockerfile .
docker build -t "$REGISTRY/wallet-service:$TAG" -f services/wallet-service/Dockerfile .
docker build -t "$REGISTRY/activity-service:$TAG" -f services/activity-service/Dockerfile .
docker build -t "$REGISTRY/game-service:$TAG" -f services/game-service/Dockerfile .
docker build -t "$REGISTRY/notice-service:$TAG" -f services/notice-service/Dockerfile .

docker push "$REGISTRY/gateway-service:$TAG"
docker push "$REGISTRY/room-service:$TAG"
docker push "$REGISTRY/user-service:$TAG"
docker push "$REGISTRY/wallet-service:$TAG"
docker push "$REGISTRY/activity-service:$TAG"
docker push "$REGISTRY/game-service:$TAG"
docker push "$REGISTRY/notice-service:$TAG"
```

如果实际用 Harbor，把 `REGISTRY` 换成 `${HARBOR_REGISTRY}/${HARBOR_PROJECT}`，并先 `docker login`。

## 配置 inventory

复制模板后填真实 CLB：

```bash
cp deploy/tencent-tat/inventory.prod.example.json deploy/tencent-tat/inventory.prod.json
vi deploy/tencent-tat/inventory.prod.json
```

必须替换：

- `gateway-service.clb.load_balancer_id`：公网 gateway CLB。
- `gateway-service.clb.listener_ids`：公网 HTTP/HTTPS 监听器。
- 内部服务各自的 `load_balancer_id` 和 `listener_ids`：room/user/wallet/activity/game 内网 CLB。

脚本通过实例 ID、私网 IP、端口三者匹配 CLB target。发布前会把当前实例在相关 listener 下的权重改成 `0`，服务健康后恢复原权重。

`managed_dependencies` 只记录部署边界：腾讯云 MySQL/Redis/MQ 是外部托管依赖，不纳入 TAT 发布动作。生产变更这些资源时单独走云数据库、Redis 或 MQ 的变更流程。

当前生产 inventory 已写入真实绑定：

| 服务 | CLB | 监听器 | 对外/内网入口 |
| --- | --- | --- | --- |
| `gateway-service` | `lb-cbiabr1q` | `lbl-ktvz8m6k` / rule `loc-pwedmsmm` | `https://api.global-interaction.com` -> `13000` |
| `room-service` | `lb-epvnr4o0` | `lbl-d2urlzba` | `10.2.1.16:13001` |
| `user-service` | `lb-epvnr4o0` | `lbl-honi8z3a` | `10.2.1.16:13005` |
| `activity-service` | `lb-epvnr4o0` | `lbl-j7oqtfhq` | `10.2.1.16:13006` |
| `game-service` | `lb-epvnr4o0` | `lbl-ku138b4y` | `10.2.1.16:13008` |
| `notice-service` | `lb-epvnr4o0` | `lbl-aux9xsjq` | `10.2.1.16:13009` |
| `wallet-service` | `lb-4f5xi6p0` | `lbl-9wi5mvu4` | `10.2.1.15:13004` |

Harbor 项目使用 `hyapp`，当前生产镜像前缀是 `10.2.1.3:18082/hyapp`。不要使用其他业务项目名前缀。

## 线上服务配置边界

这些机器上只部署 Go 服务进程：

- `gateway-service`：`room_service_addr/user_service_addr/wallet_service_addr/activity_service_addr/game_service_addr` 指向各服务内网 CLB/DNS。
- `room-service`：`mysql_dsn` 指向腾讯云 MySQL，`redis_addr` 指向腾讯云 Redis；`advertise_addr` 指向本机私网 IP。
- `user-service`：`mysql_dsn`、worker 里引用的 wallet/room MySQL DSN、登录风控 Redis 都指向腾讯云托管实例。
- `wallet-service`、`activity-service`、`game-service`：MySQL 和下游 gRPC 地址都使用腾讯云托管资源或内网 CLB/DNS。
- `notice-service`：`mysql_dsn` 指向 `hyapp_notice`，`wallet_database` 指向 `hyapp_wallet`；它只读取 wallet outbox 并写自己的投递位点，不修改钱包事实表。
- MQ 当前只作为 outbox/event bus 方向的外部托管依赖；本脚本不会创建 topic、consumer group 或本地 broker。

腾讯云 MySQL DSN 必须保留 `loc=UTC`。Redis 使用同一托管实例时，用 `redis_db` 或 key prefix 区分业务域，不要把 Redis 当恢复事实来源。

## 常用命令

先看计划，不执行远端动作：

```bash
python3 deploy/tencent-tat/hyapp_tat_deploy.py plan \
  --inventory deploy/tencent-tat/inventory.prod.json \
  --services gateway-service,room-service
```

用 TAT 连接目标机并自动发现真实部署状态和 CLB 绑定：

```bash
python3 deploy/tencent-tat/hyapp_tat_deploy.py discover \
  --env-file /opt/hy-app-monitor/.env \
  --inventory /opt/deploy/inventory/hyapp.prod.json \
  --services gateway-service,room-service,user-service,wallet-service,activity-service,game-service,notice-service
```

`discover` 会读取每台目标机的 `hyapp-*` systemd unit、`/etc/hyapp/*/docker.env`、`hyapp-*` 容器和监听端口，并通过 CLB API 按后端私网 IP/端口反查负载均衡绑定。

单服务重启：

```bash
python3 deploy/tencent-tat/hyapp_tat_deploy.py restart \
  --env-file /opt/hy-app-monitor/.env \
  --inventory /opt/deploy/inventory/hyapp.prod.json \
  --services wallet-service \
  --yes
```

多服务重启：

```bash
python3 deploy/tencent-tat/hyapp_tat_deploy.py restart \
  --env-file /opt/hy-app-monitor/.env \
  --inventory /opt/deploy/inventory/hyapp.prod.json \
  --services room-service,user-service,wallet-service,activity-service,game-service,notice-service \
  --yes
```

单服务更新部署：

```bash
python3 deploy/tencent-tat/hyapp_tat_deploy.py deploy \
  --env-file /opt/hy-app-monitor/.env \
  --inventory /opt/deploy/inventory/hyapp.prod.json \
  --services gateway-service \
  --tag 20260513-1349 \
  --yes
```

多服务更新部署：

```bash
python3 deploy/tencent-tat/hyapp_tat_deploy.py deploy \
  --env-file /opt/hy-app-monitor/.env \
  --inventory /opt/deploy/inventory/hyapp.prod.json \
  --services gateway-service,room-service,user-service,wallet-service,activity-service,game-service,notice-service \
  --tag 20260513-1349 \
  --yes
```

如果某个服务镜像 tag 不同，可以显式传镜像：

```bash
python3 deploy/tencent-tat/hyapp_tat_deploy.py deploy \
  --env-file /opt/hy-app-monitor/.env \
  --inventory /opt/deploy/inventory/hyapp.prod.json \
  --services gateway-service,wallet-service \
  --image gateway-service=10.2.1.3:18082/hyapp/gateway-service:gw-20260513 \
  --image wallet-service=10.2.1.3:18082/hyapp/wallet-service:pay-20260513 \
  --yes
```

只维护某一台机器时使用 `--hosts`，例如只重启 `gateway-1`：

```bash
python3 deploy/tencent-tat/hyapp_tat_deploy.py restart \
  --env-file /opt/hy-app-monitor/.env \
  --inventory /opt/deploy/inventory/hyapp.prod.json \
  --services gateway-service \
  --hosts gateway-1 \
  --yes
```

## 无感更新顺序

对每个服务实例，脚本固定执行：

1. 读取该实例当前 CLB 绑定和原权重。
2. 将该实例在所有匹配 listener/rule 下的权重改为 `0`。
3. 等待 `drain_seconds`，让 CLB 停止分配新请求。
4. 通过 TAT 在目标机更新 `docker.env` 的 `IMAGE`，执行 `docker pull`。
5. `systemctl restart hyapp-<service>`，Docker 发送 `SIGTERM`，服务进入 graceful shutdown。
6. 等待 systemd active 和 Docker health 恢复。
7. 恢复该实例原 CLB 权重。
8. 等待 `stabilize_seconds` 后进入下一台实例。

如果重启或健康检查失败，脚本会停止后续步骤；已经摘流的故障实例保持权重 `0`，避免坏实例重新接流，需要人工确认后再恢复。

## 用户无感边界

- 客户端长连接、房间公屏、单聊仍由腾讯云 IM 承接，gateway 重启不会断开 IM。
- gateway 是 HTTP 入口，摘流后只影响当前实例的新 HTTP 请求；另一台 gateway 继续接流。
- 内部服务通过各自内网 CLB 摘流滚动；gateway 的 gRPC client 已配置短重试，只覆盖瞬时 `UNAVAILABLE`。
- `room-service` 已有 owner route/node registry。旧 owner 停机时会释放 lease；异常退出时最多等 `lease_ttl` 到期后其他节点接管。
- 正在执行的单个 HTTP/gRPC 请求可能返回失败，客户端必须按现有接口幂等键重试；“无感”指发布期间整体入口不断流、房间 IM 不断连。

## 禁止项

- 不要使用 `--no-clb` 做正常发布；它只适合孤立排障。
- 不要多个 `room-service` 共用同一个 `node_id`。
- 不要把 `room-service.advertise_addr` 写成 CLB 地址。
- 不要先 `systemctl restart` 再摘 CLB。
- 不要在同机 Java compose 目录里执行本仓库部署命令。
