---
url: /zh/migrate/data.md
description: 用 migrate-data 把 9.x 服务的事件历史、预留键和未解决的补偿执行搬进 V10，检查 V10 重建出相同的状态，并为切换做好准备。
---

# 迁移数据

本页一步步讲怎样用 `migrate-data` 工具把 9.x 服务的数据搬进 V10 并完成切换。走完之后，V10 日志里有完整的历史，对每个能比对的聚合，V10 都重建出与 9.x 相同的状态，并由 V10 承接流量。所有命令和设置都列在 [migrate-data 参考](../reference/migrate-data)里。

切换有两种方式：

| | 离线，默认 | 在线，用于大型系统 |
| --- | --- | --- |
| 9.x 停止写入 | 在导入之前，整个迁移期间都停止 | 只在最后一次遍历期间，历史在 9.x 照常服务时导入 |
| 步骤 | 导入、比对、快照与标签、启动 V10 | 导入、追上 9.x、V10 待机，然后是一次短的最后遍历和恢复 |
| 适用于 | 服务能停止写入的时间，足够完成一次全量导入 | 一次全量导入的时间，比服务能停止写入的时间更长 |

离线方式更短、环节更少，所以除非写入停止的时间太长，都用它。在演练中测出这个时间：在 50,000 个订单上，一次离线全量导入用了 97 秒；在线方式停止写入 116 秒，其中最后一次遍历用了 13 秒。只有当历史远大于最后的窗口时，在线方式才划算。

## 搬什么，不搬什么 {#what-moves-and-what-does-not}

| 9.x 数据 | V10 中的结果 |
| --- | --- |
| MongoDB 事件存储里的事件流 | V10 日志的条目，存在 PostgreSQL 或 MongoDB 上 |
| 快照 | 不复制：与 V10 重建的状态比对，然后 V10 写自己的快照 |
| 已确认的 `PrepareKey` 键 | 已确认的预留 |
| 补偿服务里未解决的执行 | V10 服务失败台账里停放的聚合 |
| 自定义投影及其视图 | 不迁移：它们的 V10 订阅从日志重建它们 |

每条导入的流保留它的版本、命令 ID、请求 ID 和提交时间，所以幂等和历史都能延续。它的租户和聚合 ID 组成 V10 的聚合引用。所有者和空间按 V10 聚合的归属确定，操作人来自 9.x 的操作人请求头，9.x 的 trace id 成为链的根。映射文件没有列出的请求头、框架请求头和等待请求头都被丢弃。

## 开始之前 {#before-you-start}

* **改写后的源码。** V10 服务能编译，领域测试通过。它的构建已经为你要迁移其聚合的每个服务生成了合并后的 Manifest。
* **映射文件。** `wow-migrate` Skill 在改写源码时写出了 `migration-map.json`。它为每个 9.x 聚合写明对应的 V10 类型，以及每一条命令和事件的映射。见[映射文件](../reference/migrate-data#the-map)。
* **对 9.x 存储的读权限。** 工具只读 9.x 的 MongoDB 存储，除非你允许它创建追尾所需的索引。
* **一个空的 V10 目标**，或者只被这个工具写过的目标。
* **类路径上的 V10 领域模块。** `compare` 和 `snapshot` 用服务自己的 V10 模型重建状态，即服务运行时用的那些演化函数和升级函数。在 `classpath` 里列出这些模块。
* **每个 V10 上下文的分区数**，即服务将来运行时用的分区数。日志会拒绝别的分区数。

正式切换之前，用一份生产数据的副本把整个流程演练一遍。

## 1. 配置 {#\_1-configure}

在映射文件旁边写一个配置文件。相对路径相对于这个文件解析，`env:NAME` 从环境变量读取密钥：

```json
{
  "map": "migration-map.json",
  "manifests": ["../order-service/build/wow/manifest.json"],
  "checkpoint": "migrate-data.checkpoint.json",
  "source": {
    "uri": "mongodb://mongo-9x:27017",
    "database": "order_db"
  },
  "target": {
    "type": "postgres",
    "jdbcUrl": "jdbc:postgresql://postgres:5432/wow",
    "username": "wow",
    "password": "env:WOW_PG_PASSWORD",
    "poolSize": 16
  },
  "partitions": { "example": 64 },
  "classpath": ["../order-service/build/install/order-service/lib"],
  "compensation": {
    "database": "compensation_db",
    "service": "order-service"
  }
}
```

在你要迁移到的那个 Wow 版本的检出目录里运行工具：

```bash
./gradlew :migrate-data:run --args="check --config /path/to/migrate-data.json"
```

## 2. 检查映射文件 {#\_2-check-the-map}

`check` 扫描 9.x 存储，把找到的东西与映射文件和 V10 的 Manifest 比较。它不写任何东西。以下情况它会失败：

* 存储里有某个事件类型、修订号或命令名，没有对应的映射；
* 某条映射指向 Manifest 里没有的 V10 命令、事件或订阅，或者修订号高于 Manifest 里的修订号；
* 某个退役事件在 Manifest 里没有退役；
* 丢弃事件会让一条流一个事件都没有：一个版本不能为空，所以要把这样的事件映射为退役；
* 存储里有某个 `PrepareKey`，没有对应的预留映射；
* 某个有未解决补偿执行的 9.x 处理器，没有对应的订阅映射。

修正映射文件或源码，再检查，直到通过。`import --dry-run` 更进一步：它读取并转换每条流，但不写入。

## 3. 停止 9.x 的写入 {#\_3-stop-9-x-writing}

停止每个会写入的 9.x 节点，以及 9.x 的补偿服务。9.x 可以继续从它自己的存储提供读取，但在 V10 接手之前不能再提交任何东西：9.x 在导入之后提交的流不会进入 V10。

## 4. 导入 {#\_4-import}

`import` 先再检查一次，然后导入每个已映射的聚合、预留键和未解决的补偿执行。它把进度记在检查点文件里，所以中途停下的运行会从停下的地方继续，最多重复一页。追加是幂等的：已经在日志里的流不会写两次。

* **预留键。** 9.x 永久持有的键，变成已确认的预留：属于其值里所含 ID 对应的那个聚合，在那个聚合的 9.x 流所记录的租户里，使用映射文件给出的命名空间和租户范围。有存活时间的键和已过期的键被跳过。找不到所有者、或出现在多个租户里的键，打印为 `UNRESOLVED`，不导入。
* **未解决的执行。** 9.x 补偿服务中每个 `FAILED` 或 `PREPARED` 的执行，变成所映射的 V10 订阅里的一个停放的聚合，从失败的版本开始，带着它的重试状态和可恢复标记。切换之后，V10 服务会重试它，ops 控制台会列出它。无法安置的执行打印为 `UNRESOLVED`。

## 5. 比对 {#\_5-compare}

`compare` 用 V10 模型，把每个有 9.x 快照的聚合重建到快照的版本，然后比较：

* 状态，按规范化的 JSON：字段顺序不限，null 与缺失视为相同，数字按值比较，数组按顺序比较；
* 删除标记、所有者、空间和标签；
* V10 日志里究竟有没有快照的那个版本。

它还双向检查预留和停放的条目。每处差异都连同聚合、版本和字段一起打印出来：

```text
DIFFERS example-service.order (0)/0VY3GwaK4OmV00E v4: paidAmount: 9.x 25.00, V10 20.00
```

每个聚合的汇总给出有 9.x 快照的实例所占的比例、比对了多少个，以及按种类分的差异字段。有任何差异时，命令以 `3` 退出。

**不要带着无法解释的差异切换。** 零差异可能做不到：9.x 快照可能带有 V10 不会重现的缺陷，有些聚合也没有可以比对的快照。所以要按种类逐一审阅差异。每一类都必须得到解释（已知的 9.x 缺陷，或有意的语义变化），并有人签字确认。某个字段按设计只存在于一侧时，把它列进映射文件的 `state.ignore`；Skill 重命名了某个状态字段时，列进 `state.fields`。在演练中审阅这些种类，这样正式切换时只需确认它们。

`compare --sample 10` 只比对 10% 的实例，每次都是同一批，便于更快地迭代。

## 6. 写入快照 {#\_6-write-the-snapshots}

`snapshot`（或 `import --snapshots`）为每个导入的聚合写入它最后一个版本的 V10 快照，这样流量一到，V10 的查询就有数据。V10 服务自己的快照订阅随后会发现这些快照已经就位。

## 7. 记录标签 {#\_7-record-the-tags}

导入追加的流还没有标签，而 `events` 模型上基于属性的访问控制匹配不到没有标签的流。导入完成后运行 `backfill-tags`：它重放每个聚合，记录每条流之后聚合所带的标签。如果服务把事件流复制到 Elasticsearch，它接着会改写那些没有标签就被索引的检索文档。

## 8. 启动 V10 并切换流量 {#\_8-start-v10-and-switch-traffic}

1. 启动 V10 服务的节点。Saga 或反应器从它的流的末尾开始，所以不会对导入的 9.x 历史采取行动；投影从开头开始，并据此构建它的读模型。
2. 把流量切到 V10，并把 9.x 主题的每个外部消费者改到 V10 发布的事件流。没有 9.x 格式的 Sink。

V10 停放的是聚合，不是事件：停放的聚合后续的事件在停放期间等待，恢复时从失败的版本开始重放它的流，包括 9.x 在失败之后已经处理过的流。处理函数必须容忍这一点，就像容忍任何重复投递一样。

## 大型系统的在线切换 {#online-cut-over-for-large-systems}

一次全量导入的时间比服务能停止写入的时间更长时，在 9.x 仍在服务时导入历史，追上它后续的写入，只在一次短的最后遍历期间停止它的写入。

### 在 9.x 服务时导入 {#import-while-9-x-serves}

运行第 1、2 步，然后在不停止 9.x 的情况下运行 `import`（第 4 步）。迁移期间 9.x 还在不断改变键和执行：每次从头开始的导入，都会与之前运行导入的内容对账，释放 9.x 已放弃的键，并把 9.x 之后已解决的执行所对应的条目标为已解决。

### 追上 9.x {#follow-9-x}

`tail` 追踪 9.x 在导入之后提交的流。9.x 没有可以续读的位置，所以追尾按提交时间扫描，每一轮往回多看一个时间窗口（默认五分钟），以捕捉晚可见的流；重叠部分由日志的版本唯一键吸收。

追尾按 `(createTime, _id)` 索引读取每个事件流集合。没有这个索引，追尾拒绝启动，并打印创建索引的命令；在对 9.x 数据库合适的时候建好它，或者让工具来创建。

`tail` 一直运行到被停止为止。`tail --once` 只跑一轮。让 `tail` 一直运行到 9.x 停止写入：这样切换时的最后一次遍历就是 `import --final`，它从追尾停下的地方读起，而不是再遍历一遍所有的流。

### V10 待机 {#v10-on-standby}

在这段双轨运行期间，让 V10 服务的节点以待机方式启动，即把 [`wow.standby`](../reference/configuration) 设为 `true`。待机的节点提供状态读取和对追尾导入数据的查询，并以 [`wow.node.standby`](../reference/error-codes)（503）拒绝每条命令，所以切换之前没有任何东西写入 V10。它不加入任何租约组：没有订阅处理任何东西，没有停放条目被重试，也没有任务或定时运行。从流末尾开始的 Saga 或反应器，会在节点恢复时从流的末尾开始，所以它们永远不会对导入的 9.x 历史采取行动。

### 切换 {#the-cut-over}

1. 让 9.x 停止写入，并停止 9.x 的补偿服务。
2. 运行最后一次遍历。如果 `tail` 一直追到 9.x 停止，运行 `import --final`：从追尾的检查点跑一轮追尾，然后重新遍历预留键和补偿执行，因为追尾不追踪这两样。如果某个聚合的追尾没有连续运行，它会拒绝执行，什么都不写。否则，或者在它拒绝之后，运行 `import --restart`：从第一个实例开始完整走一遍，耗时与整个历史一样长。
3. 运行 `compare` 并审阅结果（第 5 步）。
4. 运行 `snapshot` 和 `backfill-tags`（第 6、7 步）。
5. 恢复每个 V10 节点：在它的内部端口上 `POST /wow/node/resume`，带上集群令牌和一个 JSON 请求体，写明由谁恢复、为什么恢复（`by`、`reason`）。节点加入它的租约组，启动订阅、重试和任务，然后开始接收命令；恢复也就结束了写入停止。`GET /wow/node/standby` 返回节点的状态。仍处于待机的节点会继续拒绝命令，也不接管任何分区。
6. 把流量切到 V10，并把 9.x 主题的每个外部消费者改到 V10 发布的事件流。

## 回滚 {#roll-back}

在 V10 写入任何东西之前，回滚就是把流量切回 9.x。一旦 V10 处理过写入，9.x 就缺少 V10 在切换之后提交的内容，`export` 会把它们写回 9.x 存储，但只限于 9.x 原本就有的事件类型。

**存在一个不可回头的时刻。** V10 以超出映射文件导入修订号的修订号写入的事件（V10 模型用 `@Upgrade` 演进过的事件），或者 9.x 从来没有的类型的事件，都没有 9.x 形式。V10 提交的第一个这样的事件，就关上了回滚的窗口。把这个时刻写进切换计划，并在那之前决定是否回滚。

1. 停止 V10 的写入，并让 9.x 保持停止。`export` 开始之后 V10 提交的内容不会导出。
2. 运行 `export --dry-run`。它扫描每个 V10 上下文的日志，按聚合类型打印 9.x 缺少多少条流。带有 V10 独有类型的流打印为 `REFUSED`，9.x 在同一版本上持有、但内容不同的流打印为 `CONFLICT`；任何一种都会在写入任何东西之前让导出停下。每个上下文以一行 `VERDICT` 结束：通过切回来回滚、通过导出回滚（以及有多少条流要写回），或者无法通过导出回滚，并给出窗口关闭的时间。
3. 运行 `export`。它再扫描一次，写入 9.x 缺少的流，从检查点续做，并跳过 9.x 已经持有的流。然后它按聚合比较 9.x 和 V10 各持有多少条流，不一致时以 `3` 退出。
4. 启动 9.x。在 9.x 再次写入这些聚合或重新生成快照之前，它的快照仍显示导出那些流之前的状态。

只有事件流会写回去。V10 建立的预留键、它停放的聚合以及它的投影的视图都不会写回，9.x 也不会在它的事件总线上发布导出的流。

## 限制 {#limits}

* 只读取 9.x 的 MongoDB 事件存储。Redis 或 Elasticsearch 事件存储、或者 Elasticsearch 上的补偿服务，要先在 9.x 中迁到 MongoDB。
* Saga 发出的命令只把链的根记作原因，因为 9.x 只保留了根。
* 没有 9.x 快照的聚合无法比对；报告会给出能比对的比例。
* 跨租户冲突的聚合 ID 目前还不会报告。
* 每个 V10 日志存储由各自的一次运行迁移，使用一份只含该存储所存聚合的映射文件。
