---
url: /zh/migrate/cutover.md
description: 在保留完整历史的前提下，把 9.x 服务的流量迁到 V10：默认离线，大型系统可在线进行；演练必须留下哪些证据，以及两种回退方式。
---

# 切换

切换把流量从 9.x 服务迁到它的 V10 服务。9.x 和 V10 从不同时写入同一份数据：它们之间没有混合版本的协议，所以切换就是一次写入停止，你要选择的是这次停止的时长。

* **离线，默认方式。** 停止 9.x 的写入，导入全部数据，比对，写出快照和标签，启动 V10。它的步骤最少，可能出错的地方也最少。
* **在线，大型系统的选项**，用于导入时间超出可停机窗口的系统。数据导入和跟随期间 9.x 照常服务，V10 处于待机，写入停止缩短为一次最后遍历。

[迁移数据](./data)描述 `migrate-data` 的每个命令；本页讲围绕它们的这次切换。

## 离线还是在线 {#offline-or-online}

用演练实测的导入时间与业务能接受的停机时间对比来选择，并记录这个选择。对 9.x 示例及其视图存储（四个租户之上的 50,000 个订单和 12,500 个视图）的演练，在同一个 runner 上走了两条路径：

| 路径 | 写入停止，从 9.x 停止到 V10 提供服务 | 时间花在哪里 |
| --- | --- | --- |
| 离线 | 243.4 s | 完整导入 98.2 s，比对 23.5 s，快照 43.8 s，标签 35.4 s，启动两个服务 36.3 s |
| 在线 | 159.5 s | 最后遍历 15.2 s，比对 24.2 s，快照 46.5 s，标签 38.0 s，恢复两个服务 29.3 s |

离线的停止随整个历史增长，在线的停止随 tail 上次读取以来 9.x 写入的数据增长，再加上两者共有的步骤。能在可停机窗口内导入完的历史，就走离线。

## 写入之前先盘点 {#inventory-before-any-write}

为每个存储和每个流记录：

* 环境、负责人、数据库、限界上下文，以及 9.x 和 V10 的版本；
* 9.x 的存储（MongoDB 事件存储、`PrepareKey` 集合、补偿数据库）和 V10 的目标（日志、快照、预留、失败台账）；
* 每种聚合类型的流和聚合数量，以及其中有 9.x 快照的比例；
* 9.x 数据的每个写入者和读取者，以及各自怎样停止；9.x 事件总线的每个外部消费者，以及它要迁往的 V10 Kafka 事件流；
* 每个 9.x 存储的备份，以及一次经过测试的恢复；
* 迁移映射，以及检查它时所对照的 V10 Manifest。

存在未知的写入者、未知的负责人或未经测试的备份时，不要开始任何写入。

## 演练必须留下什么 {#what-the-rehearsal-must-leave}

在隔离的目标上、用生产规模的数据进行端到端演练，留下：

1. 在生产映射上通过的 `check`；
2. 一次被中断后无重复地续跑的 `import`，以及一次什么都不写的第二次运行；
3. 每种差异都已解释并签字确认的 `compare`；
4. 在目标上启动的 V10 服务：对迁移来的聚合发出的命令在下一个版本追加，读取和查询回答迁移来的状态，投影已重建，停放条目已列出；
5. 耗时：离线时是完整导入加上 `compare`、`snapshot` 和 `backfill-tags`，在线时是最后遍历；
6. 两种回退都已演练，并附上无法回退的内容清单。

## 不再轻易回头的那一刻 {#the-point-of-no-easy-return}

在 V10 写入之后回退，需要 V10 写入的每个流都有 9.x 的形式。根据映射和 V10 的 Manifest，列出每个没有 9.x 形式的 V10 事件类型，把清单交给决定切换的人：

* **超出映射 `toRevision` 的事件。** V10 的类升级到了 9.x 所存储形状之外，这往往就发生在迁移本身：以修订号 1 导入的 9.x `OrderCreated`，V10 以修订号 2 写入。V10 创建的每个新订单都会阻断回退。
* **V10 新增的事件或聚合**，没有任何 9.x 名字映射到它。

生产中第一次这样的写入就结束了回退窗口。要么接受从那次写入起切换是单向的，要么在窗口关闭之前，不让这些形状和类型进入 V10 的第一个版本。演练自己的拒绝说明了这一点：V10 以修订号 2 创建了订单和视图之后，`export --dry-run` 拒绝了这两种类型。

## 离线切换 {#the-offline-switch}

1. **公布窗口，停止 9.x 的写入。** 停止 9.x 服务的入口流量，排空它们的命令队列，并同时停止补偿服务。检查没有遗留的 9.x 写入者。
2. **备份 9.x 存储并核实备份。**
3. **导入：** 先 `check`，再 `import`。
4. **关卡：** `compare` 只显示已签字确认的差异种类，没有无法解释的未解决的键或执行。否则就在这里停下并重启 9.x：V10 什么都没有写。
5. **准备读取侧：** `snapshot`，以及在 ABAC 读取事件模型的地方运行 `backfill-tags`。
6. **启动 V10 服务**并探测关键路径：对迁移来的聚合发一条命令、一次读取、一次查询、一个订阅、停放条目。
7. **迁移 9.x 总线的外部消费者**到 V10 发布的事件流。
8. **把流量切到 V10**，然后扩容。让 9.x 保持停止，而不是删除。
9. **观察**约定的信号：错误和 problem 错误码、延迟、等待、订阅延迟、停放的聚合以及消费者。回退的决定依据它们做出。

## 在线切换 {#the-online-switch}

从第一次导入到切换之前，9.x 提供服务，V10 等待：`tail` 跟随 9.x 的写入，每个 V10 节点都以 `wow.standby=true` 启动。待机的节点提供对导入所写数据的读取和查询，以 503 `wow.node.standby` 拒绝每条命令，不运行任何订阅、停放重试、作业或快照维护，所以 V10 所做的任何事都不会写入。它报告就绪，所以切换之前就能对照生产数据检查读取侧。

1. **公布窗口，停止 9.x 的写入**，与离线时相同。
2. **做最后一次备份并核实。**
3. **最后遍历。** tail 一直跟随 9.x 直到它停止时，运行 `import --final`：从检查点开始的一次 tail 遍历，并向回看，以捕捉较晚才可见的提交，然后再处理一次预留和未解决的执行。当导入不完整或 tail 已经落后时，它会拒绝，什么都不写；这时最后遍历就是 `import --restart`，时长与完整历史相当。然后运行 `compare`、`snapshot` 和 `backfill-tags`。
4. **关卡**，与离线时相同。否则回到 9.x：V10 节点仍在待机，什么都没有写。
5. **对着生产目标探测待机节点。**
6. **迁移外部消费者。**
7. **恢复每个 V10 节点：** 在每个节点的内部端口上发送 `POST /wow/node/resume`，带上集群令牌和 `{"by": "<operator>", "reason": "<why>"}`。节点加入它的分组，开始它的工作，然后接受命令。恢复结束了写入停止；`GET /wow/node/standby` 显示每个节点对此的记录。
8. **把流量切到 V10**，然后扩容。让 9.x 保持停止。
9. **观察**，与离线时相同。

## 回退 {#roll-back}

两条路径，绝不混淆：

* **V10 还没有写入：** 停在关卡处、仍在待机，或者在 V10 第一次提交之前切了回来。停止 V10，把流量切回，在未被改动的存储上重启 9.x。没有需要对账的东西。
* **V10 已经写入。** 9.x 没有 V10 的流，所以只重启 9.x 会丢失它们。停止 V10 的写入，让 9.x 保持停止，运行 `migrate-data export --dry-run`。它为每个上下文输出一个结论：切回、导出 N 个流、窗口已在某个时间关闭，或者 9.x 在切换之后被写入过，这需要先由人来解决。然后 `export` 按反向读取的映射，把 V10 新增的流写回 9.x 存储，可续跑且幂等，并检查每个 9.x 聚合保存的流数量与 V10 中该类型的流数量相同，然后 9.x 再次启动。在演练中，V10 写入的 16,914 条订单命令和 16,815 条视图命令在 52.9 s 内导出并完成检查。

只有事件流能回退：V10 做出的预留、它停放的条目和它的投影视图都没有 9.x 的形式，要手工对账。9.x 的快照显示的是导出的流之前的状态，直到 9.x 再次写入该实例或重新生成快照。一旦 V10 写入过，重新部署 9.x 就不是回退。

## 报告 {#report}

记录离线还是在线以及原因；盘点结果；演练的证据，即每个命令及其退出码和计数；已签字确认的差异种类；无法回退的类型以及对它们做出的决定；窗口的各项耗时；以及切换中实际执行的步骤。数据已导入、V10 已提供服务、消费者已迁移、回退已演练，是四个独立的事实：任何一个都不意味着下一个。
