迁移数据
本页一步步讲怎样用 migrate-data 工具把 9.x 服务的数据搬进 V10 并完成切换。走完之后,V10 日志里有完整的历史,对每个能比对的聚合,V10 都重建出与 9.x 相同的状态,并由 V10 承接流量。所有命令和设置都列在 migrate-data 参考里。
切换有两种方式:
| 离线,默认 | 在线,用于大型系统 | |
|---|---|---|
| 9.x 停止写入 | 在导入之前,整个迁移期间都停止 | 只在最后一次遍历期间,历史在 9.x 照常服务时导入 |
| 步骤 | 导入、比对、快照、启动 V10 | 导入、追上 9.x、V10 待机,然后是一次短的最后遍历和激活 |
| 适用于 | 服务能停止写入的时间,足够完成一次全量导入 | 一次全量导入的时间,比服务能停止写入的时间更长 |
离线方式更短、环节更少,所以除非写入停止的时间太长,都用它。在演练中测出这个时间:在 50,000 个订单上,离线方式停止写入 243.4 秒,其中全量导入用了 98.2 秒;在线方式停止写入 159.5 秒,其中最后一次遍历用了 15.2 秒(切换)。只有当历史远大于最后的窗口时,在线方式才划算。
搬什么,不搬什么
| 9.x 数据 | V10 中的结果 |
|---|---|
| MongoDB 事件存储里的事件流 | V10 日志的条目,存在 PostgreSQL 或 MongoDB 上 |
| 快照 | 不复制:与 V10 重建的状态比对,然后 V10 写自己的快照 |
已确认的 PrepareKey 键 | 已确认的预留 |
| 补偿服务里未解决的执行 | V10 服务失败台账里停放的聚合 |
| 自定义投影及其视图 | 不迁移:它们的 V10 订阅从日志重建它们 |
| 标签,打上的和推出的 | V10 状态的访问属性:打上的写进 9.x 标签事件所映射到的领域事件,推出的由 V10 推出 |
每条导入的流保留它的版本、命令 ID、请求 ID 和提交时间,所以幂等和历史都能延续。它的租户和聚合 ID 组成 V10 的聚合引用。所有者和空间按 V10 聚合的归属确定,操作人来自 9.x 的操作人请求头,9.x 的 trace id 成为链的根。映射文件没有列出的请求头、框架请求头和等待请求头都被丢弃。
开始之前
- 改写后的源码。 V10 服务能编译,领域测试通过。它的构建已经为你要迁移其聚合的每个服务生成了合并后的 Manifest。
- 映射文件。
wow-migrateSkill 在改写源码时写出了migration-map.json。它为每个 9.x 聚合写明对应的 V10 类型,以及每一条命令和事件的映射。见映射文件。对于大型服务,draft-map会根据 9.x 存储和 Manifest 起草这些改名,并列出它匹配不上、需要你解决的内容(起草映射文件)。 - 对 9.x 存储的读权限。 工具只读 9.x 的 MongoDB 存储,除非你允许它创建追尾所需的索引;回滚用的
export例外,它会写回 9.x 存储。 - 一个空的 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"或者运行该版本的镜像 ahoowang/wow-migrate-data:<version>,它的参数就是命令行的参数(运行):
docker run --rm -v /path/to/migration:/config:ro -e WOW_PG_PASSWORD \
ahoowang/wow-migrate-data:<version> check --config /config/migrate-data.json在存储旁边运行
长时间的导入逐个流地等待存储。存储在 Kubernetes 集群里时,就在集群里运行工具,而不是连续几个小时通过 port-forward 或 VPN 访问它们。deploy/migrate-data 把每个命令作为镜像的一个 Job 运行,把 tail 作为单副本的 Deployment 运行:
- 服务的类。 一个 init 容器把服务的
lib从其节点所运行的镜像复制到工具读取的卷里("classpath": ["/service/lib"])。配置不写manifests:工具从这个lib中各模块的片段读取每个服务的 Manifest,与节点启动时相同。 - 配置和密钥。 配置文件和映射文件来自 ConfigMap,密码来自 Secret,由
env:NAME读取。 - 检查点。 它们保存在目标里(
"checkpoint": "target:<name>"),所以在另一个节点上重启的 Pod 会从上一个停下的地方继续。每类运行同时只有一个写它们;第二个会等前一个的租约,或者失败并给出前一个(检查点)。 - 网络。 NetworkPolicy 不放行任何入站流量,只允许 Pod 访问 DNS 和它的存储:9.x 的 MongoDB 和 V10 目标。
Job 以工具的退出码结束。检查失败、用法错误和发现差异会让它立即失败,因为再运行一次也不会改变什么。因节点或内存上限而丢失的 Pod 会重新运行,并从检查点继续。
在演练的 kind 集群里,50,000 个订单的完整导入在集群内用了 99 秒,在集群所在主机上经存储的 NodePort 用了 114 秒,经 kubectl port-forward 用了 227 秒。跨真实网络时,差距随往返时间增大,因为导入是逐个流地受往返时间限制(限制)。
2. 检查映射文件
check 扫描 9.x 存储,把找到的东西与映射文件和 V10 的 Manifest 比较。它不写任何东西。以下情况它会失败:
- 存储里有某个事件类型、修订号或命令名,没有对应的映射;
- 某条映射指向 Manifest 里没有的 V10 命令、事件或订阅,或者修订号高于 Manifest 里的修订号;
- 某个退役事件在 Manifest 里没有退役;
- 丢弃事件会让一条流一个事件都没有:一个版本不能为空,所以要把这样的事件映射为退役;
- 存储里有某个
PrepareKey,没有对应的预留映射; - 某个有未解决补偿执行的 9.x 处理器,没有对应的订阅映射;
- 某个事件流集合没有
(aggregateId, version)索引:9.x 启动时会在每个事件流集合上创建这个唯一索引,导入按它遍历实例;问题会打印创建它的命令; - 某个实例的聚合 ID 被 V10 拒绝(空、空白或
(none)),属于 9.x 的数据缺陷:问题会按映射文件excluded的命名方式列出每一个({ "aggregate": …, "stream": "(empty)", "reason": … }),以便由人确定这些事件流是什么并排除它(映射文件)。
修正映射文件或源码,再检查,直到通过。import --dry-run 更进一步:它读取并转换每条流,但不写入。
V10 读不出的事件。 V10 用 Jackson 3 的 Kotlin 模块读取事件,它会拒绝非空类型集合里的 null 元素,例如含有 null 的 List<String>:有些 9.x 事件存储的正是这种形状。Wow 对新数据保持这种严格,所以这种形状的 9.x 事件在 V10 中无法解码,包含它的状态也无法重建,无论在 compare、snapshot 还是服务中。check --decode 用 V10 模型读取每个 9.x 事件,一次列出所有读不出的事件,给出类型、修订版本、所在事件流和原因。不要为旧数据放宽类:为列出的每个类型写一个清理旧形状的 @Upgrade(例如去掉 null 元素、为旧形状缺少的字段补默认值,如一个数字或外部服务新版类要求的字段,或者把存储的 Wow 查询类型转换成服务自己的类型,见事件与状态),从映射文件给 9.x 事件的修订版本(toRevision,9.x 形状为 1)开始,如果类的 @Revision 还不高于它就提高,然后再运行,直到通过。
代码已删除的历史。 9.x 的流里保留着 9.x 代码早已删除的事件、命令和聚合,9.x 快照里也保留着那些事件的效果。按它当年做了什么来映射,而不是看它的类还在不在;draft-map 分辨不了这些:
- 代码里已经没有的聚合,9.x 从不加载它的流:带着原因排除它,连同它的
PrepareKey; - 删除聚合的事件,即实现了
AggregateDeleted的 9.x 领域事件或某个移除事件:给它加上lifecycle: { "to": "wow.lifecycle.AggregateDeleted" },类已经不在时直接映射到wow.lifecycle.AggregateDeleted。永远不要retire它:退役的删除会让聚合继续存在; - 没有改变状态的事件,比如一次失败或一个通知:
retire; - 改变过状态、而 9.x 已删除其类的事件:在 V10 聚合里加一个只用于历史的事件,带上溯源函数,不由任何命令产生,把 9.x 的名字映射到它。退役它会丢掉它的效果,而 9.x 快照里还保留着这个效果;
- 9.x 已删除的命令:映射到与它最接近的 V10 命令,流只把命令记为元数据。
选错了会在 compare(第 5 步)里显出来:$deleted 不同,或者 9.x 改过而 V10 没改的字段。
标签。 9.x 的标签变成 V10 状态的访问属性(标签与基于属性的访问)。对存储里的每个标签键,映射文件写明它变成哪个属性,以及它是 9.x 的标签事件打上的还是 9.x 的状态推出的,并以 "tags": true 把标签事件映射到设置这些属性的领域事件(映射文件)。检查读取存储里的标签事件和快照,遇到映射文件没有列出的键,或者导入将要写入、而属性容纳不了的值时失败:单值属性有两个或更多的值,或者某个值读不成其属性所要求的整数或布尔值。它给出这些值的计数,并列出至多三条持有它们的流。导入之前,让这样的属性改为多值,把这个键映射到另一个属性,或者修正 9.x 数据:导入会让持有这种值的流失败。推出的键在快照里出现同样的值只是警告:9.x 把它的状态推出的值并入它保存的标签,所以快照里可能在一个键的当前值旁边还留着它以前的值,compare 会报告它们。
3. 停止 9.x 的写入
停止每个会写入的 9.x 节点,以及 9.x 的补偿服务。9.x 可以继续从它自己的存储提供读取,但在 V10 接手之前不能再提交任何东西:9.x 在导入之后提交的流不会进入 V10。
4. 导入
import 先再检查一次,然后导入每个已映射的聚合、预留键和未解决的补偿执行。它把进度记在检查点里,可以是文件,也可以是 V10 目标(检查点),所以中途停下的运行会从停下的地方继续,最多重复它当时在处理的几页。它在追加一页的同时读取下一页,并同时遍历 readParallelism 个 id 区间,每个区间是对 9.x 存储的一次独立扫描。追加是幂等的:已经在日志里的流不会写两次。它按 9.x 自己的 (aggregateId, version) 索引读取每个聚合,一次扫描读出一页实例连同它们的流,所以无论这一页在存储中的哪个位置,代价都一样。
映射改动需要全新的目标。 import(包括 --restart)会跳过日志里已有的流,保持它当时的转换结果:映射的改动,比如加上生命周期或者不再退役某个事件,到不了改动之前导入的流。把改动涉及的聚合导入一个全新的目标,或者先把它们从目标里删掉,然后重新比对。
- 预留键。 9.x 永久持有的键,变成已确认的预留:属于其值里所含 ID 对应的那个聚合,在那个聚合的 9.x 流所记录的租户里,使用映射文件给出的命名空间和租户范围。有存活时间的键和已过期的键被跳过。找不到所有者、或出现在多个租户里的键,打印为
UNRESOLVED,不导入。 - 未解决的执行。 9.x 补偿服务中每个
FAILED或PREPARED的执行,变成所映射的 V10 订阅里的一个停放的聚合,从失败的版本开始,带着它的重试状态和可恢复标记。切换之后,V10 服务会重试它,ops 控制台会列出它。无法安置的执行打印为UNRESOLVED。
5. 比对
compare 用 V10 模型,把每个有 9.x 快照的聚合重建到快照的版本,然后比较:
- 状态,按规范化的 JSON:字段顺序不限,值为 null 的字段与缺失的字段视为相同,数字按值比较,数组按顺序与长度比较(null 元素也算一个元素);
- 删除标记、所有者和空间;
- 映射文件的
tags列出的访问属性,记作$access.<attribute>:9.x 快照的标签按导入的方式转换,与 V10 从重建的状态推出的属性比较,两侧以同样的方式规范化,所以一个键的值的顺序从不构成差异; - V10 日志里究竟有没有快照的那个版本。
它还双向检查预留和停放的条目。每处差异都连同聚合、版本和字段一起打印出来:
DIFFERS example-service.order (0)/0VY3GwaK4OmV00E v4: paidAmount: 9.x 25.00, V10 20.00每个聚合的汇总给出有 9.x 快照的实例所占的比例、比对了多少个,以及按种类分的差异字段。有任何差异时,命令以 3 退出。
导入的流本身。 状态是在 9.x 快照的版本上比较的,所以实例最新快照之后的流看不到:9.x 使用 version_offset 快照策略时、9.x 尚未写入快照时,或者实例没有快照时。因此 compare 还会按追尾的提交时间索引,读取每个全量导入开始以来(减去追尾的 lookbackMillis)9.x 提交的流,按版本和命令检查日志持有每个实例最新的那一条,每个实例读一次日志。它读到追尾检查点之前 final.lookbackMillis 为止:双跑期间,追尾可能还在读之后的部分;import --final 之后,最后这个窗口正是 9.x 停止后最后一次遍历读过的。每条缺失的流都会打印出来,命令以 3 退出:
MISSING view-store.view t0/0VZwlu3X00016xF v4: 9.x stream 0VZwlu3X00016xG (command 0VZwlu3X00016xH), committed at 2026-11-03T22:33:01.200Z, is not in the log, which holds v3在切换之前导入它们:运行一次 lookbackMillis 回看到最早那一条的 tail --once,或者 import --restart。没有发现缺失的检查会记录读到了哪里,所以下一次只读 9.x 此后提交的流(减去回看窗口);--restart 从每个导入的开始重新读。追尾从未跟随过的聚合不做这项检查:它的全量导入在 9.x 停止之后遍历了每一条流。
不要带着无法解释的差异切换。 零差异可能做不到:9.x 快照可能带有 V10 不会重现的缺陷,有些聚合也没有可以比对的快照。所以要按种类逐一审阅差异。每一类都必须得到解释(已知的 9.x 缺陷,或有意的语义变化),并有人签字确认。某个字段按设计只存在于一侧时,把它列进映射文件的 state.ignore;Skill 重命名了某个状态字段时,列进 state.fields。在演练中审阅这些种类,这样正式切换时只需确认它们。
compare --sample 10 只比对 10% 的实例,每次都是同一批,便于更快地迭代。
在服务的时区下比对。 用系统时区把日期换成时刻的状态,例如 LocalDate.atStartOfDay(ZoneId.systemDefault()),换一个时区重建就会得到另一个时刻:在 9.x 运行的时区下运行 compare(-Duser.timezone 或 TZ),V10 服务也必须这样运行。
中途停止的比对只在相同的输入上继续。 compare 每比完一页就记录进度,所以中途停止的运行会从停下的地方继续,并保留之前发现的差异。否则已经比对过的结果就不再成立,它会从第一个实例重新开始,并说明原因:<aggregate> is compared in full again from the first instance: <reason>。这些情况是:
- 抽样比例与保存的比对不同,无论那次比对是中途停止还是已经完成;
- 那次比对由更早的版本停止,它没有记录输入;
- 映射文件、某个 Manifest、类路径(V10 模型)、时区或工具在它停止之后变了。
再次比对时,只比对变化的部分。 某个聚合的完整比对完成之后,compare 只再次比对那些在上次比对开始之后结果可能变化的实例:
- 那之后 9.x 拍下的快照,按其
snapshotTime,并往前回看最后一次遍历的回看窗口(final.lookbackMillis),以覆盖 9.x 节点的时钟比工具的时钟慢的情况; - 那之后 9.x 提交过流的实例,在追尾的提交时间索引上读取;
- V10 日志在上次比对开始时的末尾之后有条目的实例;
- 上次比对发现有差异的实例。
它的汇总给出比对了多少个实例,以及总共有多少个实例存在差异;每处差异都会再次打印。它会记录本次的起点,下一次 compare 从那里继续。预留和停放的条目每次都完整检查。
结果与完整比对相同。一个实例的比对只读取它的 9.x 快照、它在 V10 日志里直到快照版本的部分,以及映射文件、Manifest、V10 的类、抽样比例、时区和工具本身,别的都不读。V10 日志只增不减。所以两侧都没有变化的实例,比对结果与上次相同。这在以下条件下成立:
- 9.x 用拍下快照时的时间给每个快照盖时间戳,而且所用时钟比工具的时钟慢得不超过回看窗口。时间戳早于这个范围的快照不会再次读取;
- 映射文件、Manifest、类路径中的文件、抽样比例、时区和工具,与上次比对时相同。工具用它们的指纹检查这一点。
否则它会完整比对,并说明原因:<aggregate> is compared in full again: <reason>。这些情况是:
- 上次比对由更早的版本运行,没有记录可以继续比对的位置;
- 自上次比对以来,映射文件、某个 Manifest、类路径(V10 模型)、抽样比例、时区或工具变了;
- 超过 1000 个实例存在差异,多到无法记录;
- 9.x 存储没有提交时间索引,无法读取 9.x 之后提交的内容。追尾会创建它。
compare --restart 无论记录了什么都完整比对。切换流量之前仍然必须运行 compare:再次比对只是让它变快,它的结果才是需要签字确认的那一份。
解释之前,先给每个差异归类:
- 形状:一侧按设计才有的字段,比如 V10 带默认值的字段、只有较旧 9.x 快照才有的字段、外部服务的新版类、9.x 存成字符串的数字、9.x 以较低精度存储的小数。解释之后列入
state.ignore。 - 环境:在正确条件下重跑就会消失,比如导入之后 9.x 写入而 tail 没有跟上的内容、按当前时间计算的值、时区。
- 真实:丢掉了事件效果或删除的映射,或者 9.x 快照写入之后改变了的逻辑。修正映射或代码,或者请领域负责人签字确认这个差异。
6. 写入快照
snapshot(或 import --snapshots)为每个导入的聚合写入它最后一个版本的 V10 快照,这样流量一到,V10 的查询就有数据。V10 服务自己的快照订阅随后会发现这些快照已经就位。它同时为每个实例的历史盖上访问属性(访问属性)。
一次遍历完成之后,snapshot 改为追赶:日志在那次遍历开始时(或上次追赶时)的末尾之后,凡是有条目的实例,都重写其快照,然后记录日志现在的末尾。所以双轨运行期间做过一次遍历,切换时就只剩之后变化的实例。snapshot --restart 重新遍历每个实例。
这样做是安全的,因为 V10 快照只是日志的缓存:V10 从快照和快照之后的日志加载聚合。日志没有变化的实例,其历史与写快照时相同。在 MongoDB 上,遍历读取日志末尾时仍在途的追加,可能落在那个位置之下,追赶看不到它:这个实例保留较旧的快照,V10 加载它时会重放之后的日志。两次运行之间 V10 模型的变化不在覆盖范围内,追赶和 --restart 都一样:两者都不会在存储已有的版本上重写快照。
7. 访问属性
导入把映射文件列出的标签写进 9.x 标签事件所映射到的领域事件,所以状态在每个版本上都持有它的访问属性,snapshot 写入的快照也带着它们。导入追加的流没有访问属性,由 snapshot 为它们盖章:它在写每个实例的快照时,用它重建出的状态的属性,按这段历史的最后版本,为这个实例的整段历史盖章,与 V10 节点的访问遍历一样(重建与重放)。它的追赶为此后日志变化过的实例重新盖章,所以最后一遍的 snapshot 之后,导入的流都带着访问属性,没有一条要等到激活。按更晚版本盖过章的流保留它的属性。
- 遍历记为完成。 映射到同一个 V10 类型的每个 9.x 聚合都无失败地写完快照之后,
snapshot把该类型的访问遍历按它的访问模式记为完成,并打印<type>: its access walk is recorded as completed for the schema [<schema>]: <n> instances stamped.V10 节点启动或被激活时就不再遍历这个类型,它的wow.access.walk.pending为 0。此后它们读到的不带属性的流(snapshot没有盖到的),由内置订阅wow.access盖章。V10 模型的访问属性与snapshot运行时的不同时,节点会重新遍历这个类型。 - 什么要等它。 还没盖章的导入流,不出现在访问策略下的事件查询里;没有访问策略时,以所有者身份或在某个空间里行事的调用方,其事件查询也不包含它。整个租户的调用方仍能读到它。状态读取、快照查询,以及按版本、按时间和 trace 的读取不受影响:它们按状态判定。
- 代价。 每个实例在它的快照之外再为整段历史盖一次章,并发度仍是
concurrency。最后一遍里,snapshot落在联机切换的停写之内:在演练中测出这个时间。 - 没有快照时。
snapshot没有运行,或者某个类型有实例失败时,V10 节点启动之后,或者从待机被激活之后,为该类型的历史盖章:同一时刻只由一个节点遍历,至多占日志存储wow.access.walk-share的时间(默认 25%),所以用时是导入的实例数除以它的节奏,命令负载下会更慢(节奏以及如何在维护窗口里调高,见重建与重放)。不限份额时,演练中 50,000 个订单(每个三到四条流)在一个 PostgreSQL 上用了三到四分钟,每秒约 250 个,比联机切换的停写还长;请运行snapshot,让节点无需遍历。每个类型的遍历完成之前,wow.access.walk.pending为 1,执行遍历的节点记录日志The access walk of <type> is completed: <n> aggregates stamped.遍历开始之后追加的流,在wow.access读到它们时盖章:它的滞后是订阅wow.access的wow.subscription.lag,通常是几秒(指标)。在那之前切换流量,只会让上面这些查询在盖章之前看不到这些流,别的都不受影响。
8. 启动 V10 并切换流量
- 启动 V10 服务的节点。服务自己上下文的 Saga 或反应器从它的日志末尾开始,所以不会对导入的 9.x 历史采取行动;投影从开头开始,并据此构建它的读模型。它的传输 Sink 也发布导入的历史,导入记下的切换点及之前的每条记录都标为导入的:对端的 Saga 或反应器跳过它,对端的投影用它构建(发布与消费事件流)。
- 把流量切到 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 没有可以续读的位置,所以追尾按提交时间扫描,每一轮往回多看一个时间窗口(默认五分钟),以捕捉晚可见的流;重叠部分由日志的版本唯一键吸收。每一轮读到它开始时最新提交的那条流为止,所以 9.x 持续写入时一轮也会结束、保存追尾的检查点并转到下一个聚合;在提交时间之后一个窗口内变为可见的流,会被之后的一轮读到。
追尾按 (createTime, _id) 索引读取每个事件流集合,9.x 不会创建这个索引。没有这个索引,追尾拒绝启动,并打印创建索引的命令。创建索引会写入 9.x 存储,所以它是 9.x 侧的切换前提:在双跑之前、对 9.x 数据库合适的时候建好它,或者让工具来创建(source.createCommitTimeIndex)。工具以只读凭据读取的存储,需要由存储的所有者来建。check 会对每个缺少该索引的已映射集合发出警告。
tail 一直运行到被停止为止。tail --once 只跑一轮。让 tail 一直运行到 9.x 停止写入:这样切换时的最后一次遍历就是 import --final,它从追尾停下的地方读起,而不是再遍历一遍所有的流。最后一次遍历之前先停止追尾,因为它们持有同一个角色:在 Kubernetes 上,删除追尾的 Deployment,它会结束当前这一轮并释放租约。
V10 待机
在这段双轨运行期间,让 V10 服务的节点以待机方式启动,即把 wow.node.standby 设为 true。待机的节点提供状态读取和对追尾导入数据的查询,并以 wow.node.standby(503)拒绝每条命令,所以切换之前没有任何东西写入 V10。它不加入任何租约组:没有订阅处理任何东西,没有停放条目被重试,也没有任务或定时运行。从流末尾开始的 Saga 或反应器,会在节点被激活时从流的末尾开始,所以它们永远不会对导入的 9.x 历史采取行动。
在双轨运行期间比对并写快照
导入完成、追尾跟上 9.x 之后,运行 compare 和 snapshot。两者各自遍历每个实例一次,并记录起点。到了切换时,它们都只处理之后变化的部分(第 5、6 步),写入停止因此很短。现在就审阅完整比对发现的差异,这样切换时只需确认它们。
切换
- 让 9.x 停止写入,并停止 9.x 的补偿服务。
- 运行最后一次遍历。如果
tail一直追到 9.x 停止,运行import --final:从追尾的检查点跑一轮追尾,然后重新遍历预留键和补偿执行,因为追尾不追踪这两样。如果某个聚合的追尾没有连续运行,它会拒绝执行,什么都不写。否则,或者在它拒绝之后,运行import --restart:从第一个实例开始完整走一遍,耗时与整个历史一样长。 - 运行
compare并审阅结果(第 5 步)。双轨运行期间做过完整比对之后,它只比对之后变化的部分,除非它说明了为什么要完整比对。 - 运行
snapshot(第 6 步)。双轨运行期间做过一次遍历之后,它只为日志之后变化的实例写快照。 - 激活每个 V10 节点:在它的内部端口上
POST /wow/node/activate,带上Wow-Service: <节点的服务>和一个 JSON 请求体,写明由谁激活、为什么激活(by、reason)。节点加入它的租约组,启动订阅、重试和任务,然后开始接收命令;激活也就结束了写入停止。订阅要等节点启动了它接管的每个分区之后才开始读取,所以导入的历史不会拖慢激活:投影在激活之后追上这段历史,节点同时最多追赶wow.subscription.max-catch-ups(2)个分区,所以这期间它接收的命令仍有存储可用;追上之前subscriptions健康状态会显示它们的滞后。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 日志存储由各自的一次运行迁移,使用一份只含该存储所存聚合的映射文件。
- 导入的速度主要受流数限制,而不是事件数:实例各自只有一条流的聚合,以存储往返的速度导入。按演练中每个聚合的实测来规划窗口,而不是按事件数。