☰
Lore 0.8.7+ AWS 不可变存储迁移:用 lore-aws-migrate 将片段元数据从 DynamoDB 迁移到 S3 对象头
2026/9/25 2:44:24 网站建设 项目流程
  • 版本控制
  • 后端

【免费下载链接】lore

Lore is a next-generation, open source version control system

项目地址:https://gitcode.com/gh_mirrors/lore6/lore
点击查看免费下载

Lore 从 v0.8.7 起,AWS 不可变存储(immutable store)中的片段(fragment)不再由 DynamoDB 单独维护一行元数据,而是由承载载荷的 S3 对象通过对象头(object metadata)自描述,DynamoDB 仅保留记录哈希存在的状态行(state row)。contrib/aws-migrate-0.9.0是仓库提供的独立迁移工具(独立于 Lore workspace 之外的独立 crate),专用于把 v0.8.7 之前写入的存量存储改写为新布局。读完本文,你将掌握:旧/新两种布局的差异与迁移必要性、如何构建(含 Oodle 特性开关)、如何用--dry-run排演并计算维护窗口、生产迁移的完整步骤与验证/清理流程、关键统计量与命令行参数的含义,以及中断续跑、多机分片、本地栈测试等实战细节。

背景:片段描述信息为什么从 DynamoDB 行搬到了 S3 对象头

在旧布局中,一个片段(fragment)由两部分组成:其元数据(flags、payload 大小、content 大小等描述信息)存放在 DynamoDB 的元数据表(fragment metadata table)中,而载荷(payload)本身存放在 S3。自 v0.8.7 起,Lore 改为把片段描述信息直接写到 S3 对象的元数据头上——即lore-fragment这个 metadata key——DynamoDB 中只剩一张状态表(fragment state table),其中的状态行只负责记录"该哈希存在",并携带毁坏(obliteration)状态。

这一改动在仓库的 ADR-00020:Store fragment metadata as S3 object metadata 中有完整论证:把描述信息与载荷放到同一个对象版本上,读写都遵循 S3 全对象 PUT 的原子性,从结构上消灭了"对象字节与记录描述互不一致"这类永久性损坏;同时"哈希是否存在"仍然只需一次GetItem即可回答,无需 S3 请求。

因此,凡是在 v0.8.7 之前写入、对象头还不带自描述元数据的存储,都需要逐个对象重写一遍:把元数据挂到对象头上,并为每个哈希发布一条状态行。这正是lore-aws-migrate的职责。迁移器的核心实现在 lore-aws/src/store/immutable_store/metadata_migrator.rs,本工具是它的运维前端(operator front-end):负责把命令行参数组装成 store、覆盖元数据表的全部分段(segment)、报告进度,并在所有分段都完成时以 0 退出。

工具本身位于contrib/aws-migrate-0.9.0/,是一个独立 crate:它通过路径依赖../../lore-aws与../../vendor/quinn-proto,并在自身Cargo.toml中提交了锁定的Cargo.lock,不与 Lore 主 workspace 一起构建与测试(见 Cargo.toml 的[workspace]空表声明与注释)。

迁移前的铁律:存储必须停止对外服务

迁移期间必须让所有能触达该存储的节点进入维护模式,直到运行结束。注意时机:是在排演(rehearsal)之后、正式运行之前进入维护模式——排演的作用正是用来估算维护窗口大小的。

这不是单纯的负载考虑,而是正确性问题。一次转换的流程是:读取片段的 state → 一次 GET → 一次解压 → 一次重压 → 一次 PUT → 写状态行。如果在这个窗口内恰好有某哈希的毁坏(obliteration)操作完成,那么载荷仍会被上传,墓碑(tombstone)会被"复活"回Stored状态:毁坏被撤销、载荷回到桶里、哈希重新可读且可被去重。这类事件不会被任何统计量计数,存储层只在 info 级别记录一条Payload revives a tombstoned hash——这正是后面 Verify 阶段要 grep 的日志。

维护模式的进入方式

设置环境变量LORE_SERVER_MAINTENANCE=1即可让节点进入维护模式。进入后节点仍然:

  • 通过 HTTP 提供/health_check,返回200 OK,让负载均衡器继续把节点视为已注册,而不是反复重启任务;
  • 提供 gRPC environment 服务,在节点正常暴露的所有端口上返回UNAVAILABLE,让探针看到的是一个"降级节点"而不是"连接被拒";
  • 不注册任何存储(storage)、复制(replication)或管理(admin)服务。

在contrib/aws拓扑中,需要在两个任务定义上都设置该环境变量。因为边缘节点(edge)只能通过主节点(primary)触达存储——durable store 走 QUIC 的replicated、mutable store 走remote——所以让主节点静默(quiesce)就能挡住写往 S3 和 DynamoDB 的流量;同时也要让边缘节点静默,否则它还会继续接收客户端流量、写入自己的本地存储。

进入维护模式前,可以检查是否还有任务停留在旧版本(使用默认name = "lore"前缀产生的名称):

aws ecs describe-services --cluster lore-cluster --services lore lore-edge \ --query 'services[].deployments[].{taskDefinition:taskDefinition,running:runningCount}' \ --region us-west-2

前置条件

  • Rust 1.85 或更高(需要 edition 2024)。仓库中没有任何代码使用 nightly 特性,也没有固定工具链(toolchain);
  • 本仓库的检出:crate 通过路径依赖../../lore-aws和../../vendor/quinn-proto;
  • 首次构建需要网络访问,或者有一个温暖的 cargo 缓存并加--offline;
  • 标准 AWS 凭证链(standard chain)。

构建

cd contrib/aws-migrate-0.9.0 cargo build --release

产物位于target/release/lore-aws-migrate。必须使用 release 构建:一次迁移要读取、解压并重写存储中的每一个载荷。

Oodle 载荷需要 oodle 特性

如果存储中保存了 Oodle 载荷,需要 Oodle 库(不在本仓库中)。没有这些库时,每个 Oodle 片段都会被报告为unreadable并留在原地:

OODLE_LIB_DIR=/path/to/oodle cargo build --release --features oodle

该特性通过lore-storage/oodle传递开启(见 Cargo.toml 的[features]段);构建时需要OODLE_LIB_DIR指向 Oodle 库所在目录。

运行

一次完整迁移覆盖元数据表的全部分段,这也是默认行为:

./target/release/lore-aws-migrate \ --s3-bucket lore-fragments-abc123 \ --fragments-table lore-fragments \ --fragment-state-table lore-fragment-state \ --fragment-metadata-table lore-metadata \ --region us-west-2

如果某个部署从未有过单独的状态表——状态行与遗留元数据行共用一张表,靠行的形状(shape)区分——那么把同一个表名同时传给--fragment-state-table和--fragment-metadata-table即可。

几个要点:

  • 迁移会读取 S3 对象、其背后的元数据表以及状态表;写入的是对象和一条状态行。关联(associations)既不被读也不被写,因此--fragments-table只是为了让存储定义完整,运行时会对其做存在性检查;
  • 四个存储参数也可以用环境变量提供:LORE_MIGRATE_S3_BUCKET、LORE_MIGRATE_FRAGMENTS_TABLE、LORE_MIGRATE_FRAGMENT_STATE_TABLE、LORE_MIGRATE_FRAGMENT_METADATA_TABLE,--region对应AWS_REGION(这些在 src/main.rs 的Args结构体中通过 clap 的env属性声明)。

先排演:--dry-run

排演是必须的步骤。--dry-run会分析每一个片段并报告每个片段将得到的结果,不写任何东西,因此与真实运行不同,它对在线存储是安全的:

./target/release/lore-aws-migrate ... --dry-run

把结果当作下限来读:在未迁移的存储上,除了上传和状态写入之外,它做了真实运行会做的所有事。排演时必须使用与真实运行相同的--total-segments和--consumers,否则这个数字对真实运行没有任何参考意义。

并行扫描与多机分片

--total-segments决定表被划分为多少个并行扫描(scan)段;--consumers决定每个段一次并行转换多少个片段。要把一次迁移分摊到多台机器,就给每台机器相同的--total-segments和各自的--segment:

# machine 0 of 4 ./target/release/lore-aws-migrate ... --total-segments 4 --segment 0
  • 不指定--segment时运行全部分段(源码segments()返回0..total_segments,见 src/main.rs);
  • 指定的段号必须在0..total_segments范围内,重复与乱序会被排序去重;
  • 退出码为 0 仅当本次调用覆盖的每个分段都完成了。每 30 秒记录一次进度汇总(--progress-interval-secs 0可静默),RUST_LOG控制日志级别。

大存储或异常存储上值得关注的四个旋钮

参数默认值用途
--timeout-millis30000单次 S3 或 DynamoDB 操作超时。一次载荷重写算一次操作,因此它限定了迁移能搬动的最大片段
--max-retries5首次尝试之后的额外重试次数,作用于扫描页、片段转换、以及转换内部的载荷写入。后两者嵌套,所以一个持续失败的写入会被尝试 (1 + max)² 次
--retry-base-delay-ms200按尝试次数相乘、封顶五秒。DynamoDB 节流时调大它
--scan-limit未设置每个扫描页的条目数,用于让"发现"不要跑得比"转换"还快

--help会列出其余全部参数;Args结构体的 doc 注释即为--help文本(见 src/main.rs)。

中断与续跑

重复执行同一条命令即可续跑:一个既有状态行、对象又能自描述自己的片段会被跳过(对应ConvertOutcome::SkippedMigrated)。

  • Ctrl-C会设置一个在每个片段顶部检查的 abort 标志,因此正在进行中的片段会执行完毕再停止(stop_on_interrupt在 src/main.rs 中把首个中断转成 abort 标志);
  • 续跑不是免费的:发现过程不保留检查点(checkpoint),所以续跑会重读每一行,并对每个已迁移片段额外花费一次状态查询和一次HeadObject,然后才到达新的工作。

失败模式

  • 一个读或写持续失败、耗尽重试的片段,会停掉整个运行、所有分段;
  • 一个持续失败的扫描页影响面更窄:它只结束自己那个段的发现,运行报告不完整(incomplete),其他段仍完成各自的扫描;
  • 没有任何编解码器能恢复的载荷不会停任何东西——它被计为unreadable并跳过;
  • 在--dry-run下,什么都不会停住运行,所以一次排演就能把存储的所有问题都报告出来。

统计量(totals)的含义

迁移器维护一套运行计数(RewriteStats,见 metadata_migrator.rs),每个统计量都有明确语义:

统计量含义
scanned/metadata_rows从元数据表读取的行数,以及其中解析出哈希的行数
maintained声明的编解码器正确且不是 Oodle:载荷原样重新上传以挂上其元数据
recompressed_oodleOodle 载荷重新压缩为 Zstd
recompressed_mismatch声明的编解码器与实际存储字节不符;重新压缩为 Zstd
stored_uncompressed重压缩不划算(压缩无效),因此载荷以未压缩形式存储
payloads_deduced编解码器必须通过探测确定,而非信任声明
already_migrated状态行存在且对象已自描述,无事可做
state_with_no_head找到了状态行但对象不自描述,因此片段仍会被转换。当状态表与元数据表是同一张表时,这会是每一个片段,因为状态读取找到的就是遗留行本身
obliterated行表明载荷已被毁坏,因此跳过、不写状态行。预期行为——见下文
oversized对象的长度或行声明的尺寸超过阈值,因此被拒。长度本身就能暴露问题的对象甚至不会被读取:检查发生在Content-Length早于 body 到达之时
unreadable没有编解码器能复现该哈希,载荷无法恢复,不写状态行
errored转换在重试后仍然失败。对象已消失的行会落在这里——这正是"毁坏连载荷一起删掉"之后留下的东西

obliterated是一种结果,不是失败

被毁坏的片段有意不写状态行:它的载荷已被销毁、关联已消失,没有任何东西能取回它,状态行只会把这个哈希重新放回存储当前读取的布局里。因此,在一个发生过毁坏的存储上,obliterated计数非零是预期的。

只有载荷仍在桶里的行才会进入该计数——即被中断、尚未删除对象的毁坏操作。真正删掉了对象的那种毁坏,会留下一个迁移器无法加载的行,计入errored。

成功是按分段计,不是按片段计

退出码 0 意味着每个分段都走到了扫描末尾,不代表每个片段都迁移了。有两种结果会留下未迁移的片段却仍以 0 退出:

  • unreadable——没有编解码器能复现哈希。未带oodle特性构建、而存储中又有 Oodle 载荷时,所有 Oodle 片段都会落在这里;
  • oversized——尺寸超过阈值,片段被拒绝而不是转换。

一次迁移只有在这两项都为 0时才算是完整的。在此之前,部署仍需保留元数据表的配置——[plugins.aws.immutable_store]下的dynamodb_fragment_metadata_table,或contrib/aws中的fragment_metadata_table变量——因为遗留片段只能通过该表读取。

Verify:验证迁移

验证要在节点仍处于维护模式时进行。

1. 阅读统计量

从运行日志的final行看:

  • unreadable、oversized、errored必须为 0。每一项都代表一个没有状态行、只能通过元数据表读取的遗留片段;
  • scanned与metadata_rows应一致。有差距意味着存在解析不出哈希的行,运行会逐条记录它们;
  • obliterated可以是任意值——这些片段本来就该没有状态行。

然后用 grep 检查日志中的Payload revives a tombstoned hash。没有任何统计量会计数它,而且它比"迁移不完整"更糟:运行为一个标记为已毁坏的哈希上传了载荷,并把该哈希重新置回了Stored。这意味着在一次转换读取 state 与发布之间完成了一次毁坏——正是让存储静默所要防止的竞态——所以它根本不应该出现。如果出现了,在重新放行流量之前要查清是哪些哈希。

2. 以 dry run 再跑一遍

这是验证通道(verify pass)。它重读每一行并报告每个片段现在的状态,不写任何东西,因此即使存储已经重新开始对外服务也是安全的:

./target/release/lore-aws-migrate ... --dry-run

already_migrated与obliterated之和应等于metadata_rows,其余全部为 0。already_migrated意味着"状态行 + 携带自身元数据的对象"这一对新布局读取所需的组合——这正是为什么这一趟能确立迁移完成,而单纯去数状态表不行。任何落在maintained、recompressed_*或stored_uncompressed下的,都是运行没有转换的片段;unreadable或oversized则是它无法转换的。

注意:这一趟会重新扫描整张表、head 每个对象,并下载任何未迁移片段的载荷,所以要像真实运行一样为它预留窗口。

3. 抽查一个对象

一个已迁移对象通过单个元数据头携带其完整片段信息:

aws s3api head-object --bucket lore-fragments-abc123 \ --key <the 64 hex characters of the hash> --region us-west-2 --query Metadata
{ "lore-fragment": "8:4096:16384" }

格式为flags:size_payload:size_content:flags 是十六进制(因为它是位域,十六进制最便于读位),两个大小是十进制(因为它们是量级)。编码实现在 lore-aws/src/store/object_metadata.rs,刻意采用纯文本而不是紧凑编码,让对象形态无需 lore 工具也能从aws s3api head-object直接读懂;写入时 flags 会被约简到载荷相关位(PAYLOAD_FLAGS),让对象只描述载荷本身。没有lore-fragmentkey 的对象就是没被迁移,无论状态表对它的哈希怎么说。

Clean up:清理

仅在 Verify 报告存储已完全迁移之后进行。

1. 让节点退出维护模式,但先保留元数据表配置。已迁移的存储永远不会回退到该表,而保留它能让存储即使验证有所遗漏也仍然可读。这并非完全免费——只要表还被命名,批量查询(batch query)就还会顺带读状态表;一旦配置移除,它就跳过状态表。

2. 停止命名元数据表。从[plugins.aws.immutable_store]移除dynamodb_fragment_metadata_table,并作为一次普通部署滚动出去。在contrib/aws拓扑上对应fragment_metadata_table变量,它会在两个任务定义上设置LORE__PLUGINS__AWS__IMMUTABLE_STORE__DYNAMODB_FRAGMENT_METADATA_TABLE;早于该改名的部署可能写成dynamodb_metadata_table,仍作为别名被接受。取消它等于声明"不存在没有自身元数据的对象",这样的对象将报告为损坏(damaged)而不是从某行来描述。这一步是可以回滚的——如果读开始失败就回滚;此时还没有销毁任何东西。

3. 删除元数据表(如果它是独立的一张表)。在第 2 步运行足够长、可以信任之后:

aws dynamodb delete-table --table-name lore-metadata --region us-west-2

当--fragment-state-table与--fragment-metadata-table指向同一张表时,没有可删的东西,删了反而会毁掉整个存储。两种行形态共用它,而迁移到那里的片段保留了自己已有的行——状态写入发现行已存在就放着不动——所以遗留形态的行就是状态表。在这种情况下,第 2 步就是清理的全部。

4. 归还权限。见下节;其中dynamodb:Scan尤其没有授予任何其他角色。

5. 移除构建产物。工具不安装任何东西、不留下任何状态;删除contrib/aws-migrate-0.9.0/target可回收数 GB 空间。

权限要求

工具运行所用的凭证需要比服务器自身任务角色更多的权限:

  • 元数据表上的dynamodb:Scan——服务器从不扫描,因此它的策略没有授予此项;
  • 元数据表与状态表上的dynamodb:GetItem,状态表上的dynamodb:PutItem;
  • 三张表上的dynamodb:DescribeTable与桶上的s3:ListBucket(DescribeTable与HeadBucket都据此授权)。两者在启动时发出,因此名字写错会在运行触碰任何数据之前就失败;
  • 片段桶上的s3:GetObject(覆盖跳过检查所用的HeadObject)与s3:PutObject(用于重写)。

可以把--s3-endpoint-url与--dynamodb-endpoint-url指向本地栈(local stack)来排演,需要时再加--s3-force-path-style(按路径寻址桶,某些 S3 兼容存储要求如此)。

附录:针对本地栈的端到端测试

这不是迁移存储的一部分,而是工具本身——或其背后的lore-aws中的迁移器——发生改动时的检查方式。

tests/migrate_local_stack.rs用旧布局的形态播种了 1000 多个片段,运行本二进制、中途用SIGINT打断、再次运行,然后检查状态行、对象头、验证通道的统计量,以及在元数据表被移除的情况下通过存储回读数据。每个片段轮流采用五种遗留形态之一,覆盖每一条恢复路径,其中包括:以 16 个分片保存的 1 MB 内容及其命名列表(测试随后将其重组),以及一个被毁坏的片段(必须无状态行地通过、并读取为不存在)。

从仓库根目录运行:

docker compose --file lore-integration-tests/compose.yaml up --detach minio dynamodb cd contrib/aws-migrate-0.9.0 cargo test --features integration_tests

integration_tests特性默认关闭,所以裸cargo test只跑参数测试、不需要任何服务。测试首次使用时创建桶、按运行命名建表,通过时删除自己创建的东西;失败运行会留下以lore-migrate-test-*命名的表。

工作流速查

把上述步骤收敛为一次生产迁移的完整顺序:

  1. 构建:release 构建,存储含 Oodle 载荷时加oodle特性;
  2. 排演:以--dry-run对在线存储运行(不写任何东西),用与真实运行相同的--total-segments/--consumers估算维护窗口;
  3. 停服:为整个窗口让存储退出服务——这是硬性要求。在contrib/aws拓扑上,LORE_SERVER_MAINTENANCE=1设置在主、边两个任务定义上,并用aws ecs describe-services确认没有任务停留在旧版本;
  4. 运行:执行迁移(可多机分片、可中断续跑),每 30 秒观察进度日志;
  5. 验证:退出码 0 不等于迁移完成——检查final行统计量(unreadable/oversized/errored必须为 0、scanned与metadata_rows一致)、grepPayload revives a tombstoned hash、以--dry-run复跑确认already_migrated+obliterated=metadata_rows,再aws s3api head-object抽查lore-fragment头;
  6. 清理:节点退出维护模式 → 移除dynamodb_fragment_metadata_table配置(可回滚步骤)→ 删除独立元数据表 → 收回权限 → 删除target构建产物。

迁移的核心是把 ADR-00020 确定的新布局落到存量数据上:让对象自我描述、让状态表只回答存在性,把"描述信息与载荷"的不一致从结构上消除。lore-aws-migrate只是把这一步做成可排演、可续跑、可分片、可验证的运维操作。

  • 版本控制
  • 后端

【免费下载链接】lore

Lore is a next-generation, open source version control system

项目地址:https://gitcode.com/gh_mirrors/lore6/lore
点击查看免费下载

相关推荐

上一篇:Horovod终极资源利用率优化指南:10个策略最大化GPU硬件性能
下一篇:揭秘wasm-service工作原理:ServiceWorker如何拦截HTTP请求并驱动WASM渲染

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询