---
name: migration
owner: team-lead
version: 1
last-updated: 2026-04-23
---

# SOP: 换机迁移（维护窗口模式）

**场景**：epsilon / alpha 到期换新机 / 升级规格（如 epsilon 2026-07-13 换 U1 独占）。

**承诺**：1-5 分钟维护窗口停机。用户看到"几分钟 503"然后恢复。真·zero-downtime 对 SQLite 架构不可能，要等 v0.5+ Postgres 迁移后再谈。

**自动化演练**：`chaos-migration-full-drill.yml` 每月 15 号跑一次 + dispatch，证明这套流程不腐。到期真换机时手工跑一遍（或点 workflow_dispatch）。

## 前置

- [ ] 目标机型确定（vault 里 `metadata.migration_target_type` 可查，如 epsilon 标的 `ecs.u1-c1m2.large`）
- [ ] 最近一次备份已在 OSS（`vault who-uses aliyun/oss/agentaily-backup-xiangqin-prod` 能查谁在用）
- [ ] 维护窗口通知（对外用户 < 100 时可跳过）

## 流程（7 步，目标 5 分钟内完成）

```
[老机还在服务] ──────────────────────────────┐
                                             │
1. 开新机（aliyun console 或 chaos workflow） │ 并行
2. 新机 provision.sh + deploy.sh             │
3. 新机空 xiangqin 服务起来（db 空）            │
                                             │
[老机停服 开始维护窗口] ←──── T0                │
4. 老机 systemctl stop xiangqin               ← 维护开始
5. 老机跑 scripts/backup.sh → 最新快照到 OSS     (~30s)
6. 新机 scp 或 restore.sh 拉最新 OSS backup     (~30s)
   → 覆盖新机 /opt/xiangqin/data/xiangqin.db
   → chown xiangqin:xiangqin + chmod 600
   → systemctl restart xiangqin
7. DNS 切换 xq.agentaily.com → 新机 IP            (~1-3min DNS TTL)
[维护窗口结束 用户访问恢复] ←──── T1
8. 老机：等几天确认无回滚需要 → 释放 / 到期不续
```

## 具体命令

### Step 1-3: 开新机（推荐走 workflow_dispatch）

```bash
# GHA UI: Actions → chaos / migration full drill (M6) → Run workflow
# instance_type 填目标机型（比如 ecs.u1-c1m2.large）
# 或命令行触发：
gh workflow run chaos-migration-full-drill.yml \
    -f instance_type=ecs.u1-c1m2.large \
    -f keep_on_failure=true    # 真迁移要保留机器不销毁
```

但这 workflow 默认会销毁新机（drill 模式）。**真迁移不能用 drill workflow**，要**手动**开按量 / 包月新机：

```bash
# aliyun 控制台买新机（U1 独占包月），或 aliyun CLI RunInstances with InstanceChargeType PrePaid
# 拿到新机 IP 后：
bash scripts/provision.sh <new-ip>
bash scripts/deploy.sh <new-ip>
```

### Step 4-6: 维护窗口（老机停 + 数据迁移）

```bash
# 老机操作
ssh root@<old-ip>
systemctl stop xiangqin
bash /opt/xiangqin/scripts/backup.sh   # 最终 snapshot 到 OSS

# 新机操作
ssh root@<new-ip>
systemctl stop xiangqin
bash /opt/xiangqin/scripts/restore.sh --in-place   # 从 OSS 拉最新 + 覆盖
systemctl start xiangqin
curl http://127.0.0.1/health   # 确认起来
sqlite3 /opt/xiangqin/data/xiangqin.db "SELECT COUNT(*) FROM users"   # 确认数据在
```

### Step 7: DNS 切换

```bash
# aliyun DNS 控制台改 A 记录 xq.agentaily.com → <new-ip>
# 或 aliyun CLI：
aliyun alidns UpdateDomainRecord --RecordId <record-id> --RR xq --Type A --Value <new-ip>

# 等 DNS TTL 过（当前 TTL 可能 60s-600s，xq.agentaily.com 的 TTL 查 vault 或 dig xq.agentaily.com）
dig xq.agentaily.com +short   # 确认全球 resolver 都返回新 IP
```

### Step 8: 验收 + 老机处理

- [ ] 外网 `curl https://xq.agentaily.com/health` 返回新机 version
- [ ] `xq --endpoint https://xq.agentaily.com register <test-phone>` 走一遍端到端
- [ ] 观察 3 天无异常 → 老机释放（或到期自然释放）
- [ ] 更新 vault：老机 `renewal: no` 标到期释放；新机添加 resource record
- [ ] 更新 `xiangqin/vault.json` 的 `resources[].ref` 指向新机 ID
- [ ] CHANGELOG 加一条迁移 note

## 失败处理（回滚）

**情况 1：新机 restore 后起不来**
- 老机还在（没释放），重启老机服务 → `systemctl start xiangqin`（数据还在，业务恢复）
- DNS 如果切过了，切回老 IP

**情况 2：新机起来但数据不全**
- 回滚前停新机写入（`systemctl stop xiangqin`）
- 把老机最终 backup 重 restore 到新机
- 如果老机也挂了 → 从 OSS 倒数第 2 份 daily backup restore（`xq-YYYYMMDD.db.gz`）

**情况 3：DNS 切过了用户已经访问新机写入了数据**
- **不要回滚** —— 会丢新机写入的那几分钟数据
- 接受现状，回滚会破坏 linearity

## 真·zero-downtime 未来路径

v0.5+ 可选：
- SQLite → Postgres + 主从复制
- 业务层 connection pooling
- 新机作为 read replica 先 warm up → promote 为 primary + demote 老机 → DNS 切
- 整个过程用户看不到 503

**现在不做**（工作量 2-3 周，xiangqin 规模不值）。

## 相关

- `chaos-migration-full-drill.yml` M6 演练 workflow
- `scripts/provision.sh` / `scripts/deploy.sh` 参数化任意 IP
- `scripts/backup.sh` / `scripts/restore.sh` 数据迁移工具
- vault `aliyun/ecs/*.metadata.migration_target_type` 标迁移意图
