迁移数据
本页一步步讲怎样用 migrate-data 工具把 9.x 服务的数据搬进 V10 并完成切换。走完之后,V10 日志里有完整的历史,对每个能比对的聚合,V10 都重建出与 9.x 相同的状态,并由 V10 承接流量。所有命令和设置都列在 migrate-data 参考里。
切换有两种方式:
| 离线,默认 | 在线,用于大型系统 | |
|---|---|---|
| 9.x 停止写入 | 在导入之前,整个迁移期间都停止 | 只在最后一次遍历期间,历史在 9.x 照常服务时导入 |
| 步骤 | 导入、比对、快照与标签、启动 V10 | 导入、追上 9.x、V10 待机,然后是一次短的最后遍历和恢复 |
| 适用于 | 服务能停止写入的时间,足够完成一次全量导入 | 一次全量导入的时间,比服务能停止写入的时间更长 |
离线方式更短、环节更少,所以除非写入停止的时间太长,都用它。在演练中测出这个时间:在 50,000 个订单上,一次离线全量导入用了 97 秒;在线方式停止写入 116 秒,其中最后一次遍历用了 13 秒。只有当历史远大于最后的窗口时,在线方式才划算。
搬什么,不搬什么
| 9.x 数据 | V10 中的结果 |
|---|---|
| MongoDB 事件存储里的事件流 | V10 日志的条目,存在 PostgreSQL 或 MongoDB 上 |
| 快照 | 不复制:与 V10 重建的状态比对,然后 V10 写自己的快照 |
已确认的 PrepareKey 键 | 已确认的预留 |
| 补偿服务里未解决的执行 | V10 服务失败台账里停放的聚合 |
| 自定义投影及其视图 | 不迁移:它们的 V10 订阅从日志重建它们 |
每条导入的流保留它的版本、命令 ID、请求 ID 和提交时间,所以幂等和历史都能延续。它的租户和聚合 ID 组成 V10 的聚合引用。所有者和空间按 V10 聚合的归属确定,操作人来自 9.x 的操作人请求头,9.x 的 trace id 成为链的根。映射文件没有列出的请求头、框架请求头和等待请求头都被丢弃。
开始之前
- 改写后的源码。 V10 服务能编译,领域测试通过。它的构建已经为你要迁移其聚合的每个服务生成了合并后的 Manifest。
- 映射文件。
wow-migrateSkill 在改写源码时写出了migration-map.json。它为每个 9.x 聚合写明对应的 V10 类型,以及每一条命令和事件的映射。见映射文件。 - 对 9.x 存储的读权限。 工具只读 9.x 的 MongoDB 存储,除非你允许它创建追尾所需的索引。
- 一个空的 V10 目标,或者只被这个工具写过的目标。
- 类路径上的 V10 领域模块。
compare和snapshot用服务自己的 V10 模型重建状态,即服务运行时用的那些演化函数和升级函数。在classpath里列出这些模块。 - 每个 V10 上下文的分区数,即服务将来运行时用的分区数。日志会拒绝别的分区数。
正式切换之前,用一份生产数据的副本把整个流程演练一遍。
1. 配置
在映射文件旁边写一个配置文件。相对路径相对于这个文件解析,env:NAME 从环境变量读取密钥:
{
"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 版本的检出目录里运行工具:
./gradlew :migrate-data:run --args="check --config /path/to/migrate-data.json"2. 检查映射文件
check 扫描 9.x 存储,把找到的东西与映射文件和 V10 的 Manifest 比较。它不写任何东西。以下情况它会失败:
- 存储里有某个事件类型、修订号或命令名,没有对应的映射;
- 某条映射指向 Manifest 里没有的 V10 命令、事件或订阅,或者修订号高于 Manifest 里的修订号;
- 某个退役事件在 Manifest 里没有退役;
- 丢弃事件会让一条流一个事件都没有:一个版本不能为空,所以要把这样的事件映射为退役;
- 存储里有某个
PrepareKey,没有对应的预留映射; - 某个有未解决补偿执行的 9.x 处理器,没有对应的订阅映射。
修正映射文件或源码,再检查,直到通过。import --dry-run 更进一步:它读取并转换每条流,但不写入。
3. 停止 9.x 的写入
停止每个会写入的 9.x 节点,以及 9.x 的补偿服务。9.x 可以继续从它自己的存储提供读取,但在 V10 接手之前不能再提交任何东西:9.x 在导入之后提交的流不会进入 V10。
4. 导入
import 先再检查一次,然后导入每个已映射的聚合、预留键和未解决的补偿执行。它把进度记在检查点文件里,所以中途停下的运行会从停下的地方继续,最多重复一页。追加是幂等的:已经在日志里的流不会写两次。
- 预留键。 9.x 永久持有的键,变成已确认的预留:属于其值里所含 ID 对应的那个聚合,在那个聚合的 9.x 流所记录的租户里,使用映射文件给出的命名空间和租户范围。有存活时间的键和已过期的键被跳过。找不到所有者、或出现在多个租户里的键,打印为
UNRESOLVED,不导入。 - 未解决的执行。 9.x 补偿服务中每个
FAILED或PREPARED的执行,变成所映射的 V10 订阅里的一个停放的聚合,从失败的版本开始,带着它的重试状态和可恢复标记。切换之后,V10 服务会重试它,ops 控制台会列出它。无法安置的执行打印为UNRESOLVED。
5. 比对
compare 用 V10 模型,把每个有 9.x 快照的聚合重建到快照的版本,然后比较:
- 状态,按规范化的 JSON:字段顺序不限,null 与缺失视为相同,数字按值比较,数组按顺序比较;
- 删除标记、所有者、空间和标签;
- V10 日志里究竟有没有快照的那个版本。
它还双向检查预留和停放的条目。每处差异都连同聚合、版本和字段一起打印出来:
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. 写入快照
snapshot(或 import --snapshots)为每个导入的聚合写入它最后一个版本的 V10 快照,这样流量一到,V10 的查询就有数据。V10 服务自己的快照订阅随后会发现这些快照已经就位。
7. 记录标签
导入追加的流还没有标签,而 events 模型上基于属性的访问控制匹配不到没有标签的流。导入完成后运行 backfill-tags:它重放每个聚合,记录每条流之后聚合所带的标签。如果服务把事件流复制到 Elasticsearch,它接着会改写那些没有标签就被索引的检索文档。
8. 启动 V10 并切换流量
- 启动 V10 服务的节点。Saga 或反应器从它的流的末尾开始,所以不会对导入的 9.x 历史采取行动;投影从开头开始,并据此构建它的读模型。
- 把流量切到 V10,并把 9.x 主题的每个外部消费者改到 V10 发布的事件流。没有 9.x 格式的 Sink。
V10 停放的是聚合,不是事件:停放的聚合后续的事件在停放期间等待,恢复时从失败的版本开始重放它的流,包括 9.x 在失败之后已经处理过的流。处理函数必须容忍这一点,就像容忍任何重复投递一样。
大型系统的在线切换
一次全量导入的时间比服务能停止写入的时间更长时,在 9.x 仍在服务时导入历史,追上它后续的写入,只在一次短的最后遍历期间停止它的写入。
在 9.x 服务时导入
运行第 1、2 步,然后在不停止 9.x 的情况下运行 import(第 4 步)。迁移期间 9.x 还在不断改变键和执行:每次从头开始的导入,都会与之前运行导入的内容对账,释放 9.x 已放弃的键,并把 9.x 之后已解决的执行所对应的条目标为已解决。
追上 9.x
tail 追踪 9.x 在导入之后提交的流。9.x 没有可以续读的位置,所以追尾按提交时间扫描,每一轮往回多看一个时间窗口(默认五分钟),以捕捉晚可见的流;重叠部分由日志的版本唯一键吸收。
追尾按 (createTime, _id) 索引读取每个事件流集合。没有这个索引,追尾拒绝启动,并打印创建索引的命令;在对 9.x 数据库合适的时候建好它,或者让工具来创建。
tail 一直运行到被停止为止。tail --once 只跑一轮。让 tail 一直运行到 9.x 停止写入:这样切换时的最后一次遍历就是 import --final,它从追尾停下的地方读起,而不是再遍历一遍所有的流。
V10 待机
在这段双轨运行期间,让 V10 服务的节点以待机方式启动,即把 wow.standby 设为 true。待机的节点提供状态读取和对追尾导入数据的查询,并以 wow.node.standby(503)拒绝每条命令,所以切换之前没有任何东西写入 V10。它不加入任何租约组:没有订阅处理任何东西,没有停放条目被重试,也没有任务或定时运行。从流末尾开始的 Saga 或反应器,会在节点恢复时从流的末尾开始,所以它们永远不会对导入的 9.x 历史采取行动。
切换
- 让 9.x 停止写入,并停止 9.x 的补偿服务。
- 运行最后一次遍历。如果
tail一直追到 9.x 停止,运行import --final:从追尾的检查点跑一轮追尾,然后重新遍历预留键和补偿执行,因为追尾不追踪这两样。如果某个聚合的追尾没有连续运行,它会拒绝执行,什么都不写。否则,或者在它拒绝之后,运行import --restart:从第一个实例开始完整走一遍,耗时与整个历史一样长。 - 运行
compare并审阅结果(第 5 步)。 - 运行
snapshot和backfill-tags(第 6、7 步)。 - 恢复每个 V10 节点:在它的内部端口上
POST /wow/node/resume,带上集群令牌和一个 JSON 请求体,写明由谁恢复、为什么恢复(by、reason)。节点加入它的租约组,启动订阅、重试和任务,然后开始接收命令;恢复也就结束了写入停止。GET /wow/node/standby返回节点的状态。仍处于待机的节点会继续拒绝命令,也不接管任何分区。 - 把流量切到 V10,并把 9.x 主题的每个外部消费者改到 V10 发布的事件流。
回滚
在 V10 写入任何东西之前,回滚就是把流量切回 9.x。一旦 V10 处理过写入,9.x 就缺少 V10 在切换之后提交的内容,export 会把它们写回 9.x 存储,但只限于 9.x 原本就有的事件类型。
存在一个不可回头的时刻。 V10 以超出映射文件导入修订号的修订号写入的事件(V10 模型用 @Upgrade 演进过的事件),或者 9.x 从来没有的类型的事件,都没有 9.x 形式。V10 提交的第一个这样的事件,就关上了回滚的窗口。把这个时刻写进切换计划,并在那之前决定是否回滚。
- 停止 V10 的写入,并让 9.x 保持停止。
export开始之后 V10 提交的内容不会导出。 - 运行
export --dry-run。它扫描每个 V10 上下文的日志,按聚合类型打印 9.x 缺少多少条流。带有 V10 独有类型的流打印为REFUSED,9.x 在同一版本上持有、但内容不同的流打印为CONFLICT;任何一种都会在写入任何东西之前让导出停下。每个上下文以一行VERDICT结束:通过切回来回滚、通过导出回滚(以及有多少条流要写回),或者无法通过导出回滚,并给出窗口关闭的时间。 - 运行
export。它再扫描一次,写入 9.x 缺少的流,从检查点续做,并跳过 9.x 已经持有的流。然后它按聚合比较 9.x 和 V10 各持有多少条流,不一致时以3退出。 - 启动 9.x。在 9.x 再次写入这些聚合或重新生成快照之前,它的快照仍显示导出那些流之前的状态。
只有事件流会写回去。V10 建立的预留键、它停放的聚合以及它的投影的视图都不会写回,9.x 也不会在它的事件总线上发布导出的流。
限制
- 只读取 9.x 的 MongoDB 事件存储。Redis 或 Elasticsearch 事件存储、或者 Elasticsearch 上的补偿服务,要先在 9.x 中迁到 MongoDB。
- Saga 发出的命令只把链的根记作原因,因为 9.x 只保留了根。
- 没有 9.x 快照的聚合无法比对;报告会给出能比对的比例。
- 跨租户冲突的聚合 ID 目前还不会报告。
- 每个 V10 日志存储由各自的一次运行迁移,使用一份只含该存储所存聚合的映射文件。