尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Lore 存储层的外键寻址优化:`get_resolved` / `put_resolved` 设计解析

Lore 存储层的外键寻址优化:`get_resolved` / `put_resolved` 设计解析 版本控制后端【免费下载链接】loreLore is a next-generation, open source version control system项目地址https://gitcode.com/gh_mirrors/lore6/lore点击查看免费下载导读Lore 的存储系统 API 将内容保存在以哈希寻址的不可变存储中把调用方选择的名称保存在可变存储中因此凡是使用外部系统标识资产 ID、构建目标、数据库主键寻址内容的调用方每次读写都必须串行地访问两个存储。本文基于 docs/proposals/2026-08-02-resolved-storage-operations.md 这份已接受的 LEPLore Enhancement Proposal完整讲解 Lore 如何通过KeyType::Resolve键类型与get_resolved/put_resolved及其文件变体四个操作把外键寻址的两次往返合并为一次同时由服务端保证键绝不指向存储中不存在的内容。读完本文你将掌握这套操作的完整语义、C API 用法、底层实现原理含FusedPublish融合发布机制与兼容性边界。背景为什么外键寻址要付两次往返Lore 的存储系统 APIlore::storageC 侧为lore_storage_*见 lore-capi/lore.h暴露两个键空间不可变存储以内容自身的哈希为地址天然支持去重、校验与安全缓存可变存储保存调用方自选的键每个键映射到单个哈希见 lore-storage/src/mutable_store.rs。这个拆分完美服务了 Lore 自身的版本控制修订指向树、树指向子节点遍历路径是哈希到哈希。但它并不适合另一种越来越常见的形态——用另一个系统的标识符asset id、build target、source record id来寻址 Lore 中的内容。这类调用方手里从来不是哈希而是外键它对外部系统有意义、对 Lore 无意义并且在内容变化时保持稳定——这恰恰是内容寻址无法表达的性质。服务这些调用方每个逻辑操作都要付出两次网络往返读先mutable_load(key)取哈希再get(hash)取字节写先put(bytes)得哈希再mutable_store(key, hash)发布映射。这笔开销不是带宽问题也无法摊销第二个请求依赖第一个请求的应答两个请求不能并行、批处理或重叠——无论调用方在队列里堆积了多少工作延迟都是两个完整的往返。一个解析一万个 asset id 的客户端可以并发发起全部请求但每个请求的串行深度仍然翻倍。在往返耗时数十毫秒的场景下这个翻倍直接决定了缓存方案是否可行。写方向还有第二笔与延迟无关的代价发布一个键只在一种顺序下是安全的——内容必须被持久存储之后键才能指向它否则读者解析键之后会读不到内容。API 中没有任何机制表达这一约束于是每个集成方都要自己实现一遍而且各自都可能实现错。这两笔代价都落在用外键寻址内容的集成方身上。已经持有哈希的调用方不受影响——这也是为什么该问题从未在 Lore 自身对存储层的使用中暴露出来。目标与非目标目标外键读只需一个往返持有键的调用方发一个请求即可拿到内容串行深度与直接持有哈希的调用方相同。外键写只需一个往返持有键与待发布内容的调用方发一个请求请求完成时内容已存储、键已指向它。键绝不指向存储中不存在的内容原先需要集成方自行实现的顺序约束由操作本身强制覆盖所有路径——包括内容大到被拆成多个 fragment、且其中某个 fragment 上传失败的情形。调用方能分辨实际发生了什么写操作报告内容最终落在哪里使调用方可以区分内容只到达本地存储与内容已到达远端——现有写路径不暴露这一区别而它决定了其他客户端能否看到该键。可表达移除调用方可以收回已发布的键使内容已失效的外键停止解析。两种传输、同一种行为操作在 QUIC 与 gRPC 上语义一致符合 docs/explanation/system-design.md §18.1 声明的传输对等属性。C API 可达操作以与所组合的存储操作相同的形态暴露于lore-capi——需要这些操作的集成方并不用 Rust 编写。同形态包括在大规模场景中关键的读取选项解析后的读取与普通读取一样按需逐 fragment 流式传输内容大小不决定内存占用。非目标查询或二级索引设施这些操作只把一个键解析为一个内容地址。列出键、前缀扫描、多键查询均不在范围内。已有的mutable_list也无法填补这一缺口它只读本地存储而本地映射存储是缓存而非权威无法枚举一个仓库已发布的全部键。乐观并发控制发布是后写覆盖先写last-writer-wins这是契约而非等待评审的默认值。对本提案面向的缓存场景这是正确的语义两个发布者竞争时任一映射都有效落败方也不吃亏。需要检测丢失更新的调用方可以使用mutable_compare_and_swap——它本就接受这类键代价是多一个往返这正是有意做出的取舍。把 compare-and-swap 折叠进put_resolved曾经被考虑过并已否决目标 3 强制内容先存储、键后发布因此交换只能是第二步而每次失败的交换都会留下已存储的内容——浪费的增长恰好与这个特性要应对的竞争成正比颠倒顺序能消除浪费却会破坏让这些操作值得存在的保证。客户端间缓存一致性这里没有任何失效传播机制。客户端本地缓存映射后要等下一次询问权威时才得知变化。改变可变存储的一般语义现有键类型、行为以及mutable_load/mutable_store/mutable_compare_and_swap均保持原样。设计专用键类型KeyType::Resolve以此方式使用的外键占据自己的键类型KeyType::Resolve定义于 lore-base/src/types/store_types.rs判别值7。将其限定在单一类型中可避免与 Lore 修订层维护的类型化指针分支顶端、分支元数据、仓库身份冲突同时意味着下面的操作在线上永远不需要携带键类型——它们只作用于唯一一种类型。这在部分上实现了目标 1 与 2键类型是被隐含而非被传输的。从源码看本地可变存储在持久化时把键类型编码进存储键本身Key::make_typed将键类型的字节写入哈希数据的第 3 个字节hash.data_mut()[2] key as u8见 lore-storage/src/local/mutable_store.rs。因此新类型只是扩展了持久化的键空间不会扰动既有条目。两个核心操作get_resolved接收一个键与上下文把键解析为哈希读取该哈希下的内容并把哈希随内容一起返回。返回哈希使调用方能缓存键到哈希的映射、并用它校验负载——这让该操作与普通get一样可验证目标 1。put_resolved接收一个键与一个 fragment先存储 fragment再发布指向它的键目标 2。存储永远先发生键只在存储成功后发布。对于小到只占单个 fragment 的内容存储与发布是一条命令顺序由服务端保证而非调用方保证对于拆成 fragment 列表的内容叶子走普通写路径上传键在其后发布并以整棵树都到达远端为门槛目标 3。两个操作在 QUIC 与 gRPC 上均可用目标 6并通过 C API 的lore_storage_get_resolved与lore_storage_put_resolved暴露目标 7。解析式读取接受与普通读取相同的streaming选项不开启时内容重组为单个缓冲区开启时调用方按每个叶子 fragment 收到一个事件峰值内存跟随 fragment 大小而非内容大小。文件变体内容在磁盘上的调用方对大到需要分片的内容来说这占了大多数无法直接使用上面两个操作否则就失去了它们各自的用途发布意味着把整个文件读进缓冲区再跨边界传递解析意味着把内容作为事件流收齐再写回磁盘。无论哪条路内容都会完整经过应用内存而streaming选项只是把问题从回调的一侧挪到另一侧。get_file_resolved与put_file_resolved以路径代替缓冲区。调用方与 Lore 都不持有完整内容不超过 fragment 阈值的文件被读入它所变成的那一个 fragment更大的文件在进入时直接从磁盘按块切分、在写出时按每个叶子各自的偏移逐叶写入——峰值内存跟随 fragment 大小而非文件大小。它们保持所镜像的两个操作一样的往返数包括内容分片的情况读把解析与它所需的第一个fragment——树的根——的获取融合为一个请求正如get_resolved所做因为根正是解析所返回的内容写把发布融合进它发送的最后一个fragment——fragment 列表的根或不分片内容的那个唯一 fragment——的上传中。在根处融合之所以安全正是两个操作一节中顺序约束的要求根存储在它下面的每个 fragment 之后因此每一层的落位在上一层写入之前就已确定某层若发现子节点在远端缺失就收回键而不是把它继续向上传递。一棵没有整体到达远端的树因此保持键未发布与映射作为独立写入跟随其后时行为一致。put_resolved发布分片内容的方式相同两个写操作的区别只在于字节从哪来。范围range语义与lore_storage_get_file一致读可以请求内容的一部分文件从第一字节起精确保存该范围只取范围覆盖的 fragment。零长度文件收回键——无内容可发布就是下文描述的移除而非第三种状态。放置报告内容落在哪里Lore 现有的写路径把远端上传视为尽力而为put在本地存储成功但上传失败时仍报告成功fragment 被标记为非持久留待后续写操作重试。这个契约对put是合理的——但它恰恰是朴素的键发布不安全的根源调用方无法从一次成功的写得知其他客户端需要的内容是否真的在那里。因此写操作会报告其内容的放置位置是否到达本地存储是否到达远端。对于拆成多个 fragment 的内容报告是树中每个 fragment 与每个中间节点的交集——单个叶子上传失败会显式出现在结果中而不是藏在一次成功后面目标 4。put_resolved在远端发布键之前查阅这份报告目标 3调用方也可以查阅它来决定其他客户端能否看到自己刚写的内容。在 C API 中这些信息由lore_storage_put_item_complete_event_data_t的stored_local/stored_remote字段承载见 lore-capi/lore.h——文档与头文件均明确提示需要键在远端可见的调用方要检查该字段而不是错误码。移除不带内容的发布会收回键目标 5。将键设置为零哈希正是可变存储删除一个键的既有方式而解析不到映射时本就会报告内容未找到——所以移除是与无内容可存储同一条操作而不是独立的动词。在实现中data.len 0即触发此路径不存储内容键被置为零哈希后续get_resolved对它会报告ADDRESS_NOT_FOUND。值得注意的实现细节当remote_write 0时这只驱逐本地映射——本地是缓存而非权威远端已发布的键会在下次解析时重新出现因此删除一个键需要remote_write 1。本地解析与缓存语义读操作在访问远端之前先查本地存储遵循与所有其他存储读相同的标志——调用方可以要求仅本地、仅远端或接受任一。本地可变存储扮演映射缓存而非权威的角色本地缓存的映射若缺失或指向本存储未持有的内容则回退到远端——远端在原本只花在映射上的同一个往返里同时回答映射与内容。由于本地是缓存缓存下来的映射可能过期需要权威答案的调用方应显式要求。这与现有读路径对内容的取舍相同区别在于可变键会移动而内容哈希不会——所以这是一个由调用方做出的选择而非可被默认假设的行为。兼容性线格式——增量式。QUIC 在现有lore-storage/0.4ALPN 上新增两个操作码对应LoreStorageGetResolvedArgs等四个参数类型注册于 lore/src/remote/command.rsgRPC 在lore.storage.v1新增两个 rpc 与四个消息类型见 lore-proto/proto/lore/storage/v1/storage.proto 中的GetResolved/PutResolved双向流 rpc并在lore.model.v1增加共享的逐项状态消息。v(N-1) 对端无法解码新命令gRPC 下它回答UnimplementedQUIC 没有该状态会把操作码报告为非法命令。v(N) 对端解码 v(N-1) 对端发送的一切内容不变。现有消息的布局均不改变。客户端/服务端协议——新客户端对旧服务端两个操作都会失败gRPC 报告 unimplemented、QUIC 报告非法命令且没有回退到两步请求序列见风险一节。旧客户端对新服务端不受影响因为它从不发送新命令。授权不变两个操作都运行在现有存储命令所用的会话授权之下不会读写任何调用方通过get/put/mutable_load/mutable_store本就无法触及的东西。磁盘格式——增量式。KeyType::Resolve是新判别值本地可变存储把键类型编码进存储键lore-storage/src/local/mutable_store.rs新类型扩展持久化键空间而不扰动既有条目。升级后的 Lore 原样读取既有仓库降级的 Lore 读取除新类型键以外的一切——它不识别新键也不需要它们。CLI 与公共 API——增量式。新增 8 个extern C入口点lore_storage_get_resolved、lore_storage_put_resolved、lore_storage_get_file_resolved、lore_storage_put_file_resolved及其_async变体及对应的参数/条目结构体。文件变体对没有自己的线格式它组合的是同两个命令并且可通过服务 IPC 边界到达——这是带缓冲区的put_resolved做不到的因为路径有跨进程表示而指向调用方内存的LoreBytes视图没有。写操作共享的逐项完成事件新增两个字段追加在既有字段之后C 层上它们落在结构体既有的尾部填充区结构体大小与所有先前字段偏移均不变字段携带#[serde(default)]使同一事件跨服务 IPC 边界时仍能从一个早于它们的对端反序列化——这正是lore_complete_event_data_t当初追加错误详情时确立的模式。没有任何 CLI 子命令改变。非功能考量并发——同一键的发布是后写覆盖先写与今天的mutable_store相同。对相同内容的并发发布会在既有 in-flight 守卫上合并N 个发布者发布相同字节只产生一次上传。同一键的并发解析在键移动时可能观察到不同映射——这是读取可变值的固有属性并非本提案引入。内存——两个方向都不变。一次发布携带单个 fragment受既有 fragment 大小阈值约束更大的内容通过既有写路径分片其流式与背压机制原样保留。解析式读取提供与lore_storage_get相同的streaming模式开启时内容按叶子 fragment 逐个到达峰值内存跟随 fragment 大小而非内容大小不开启时重组为单个缓冲区——这适合本 API 瞄准的小值但不适合一个命名了大内容的键。文件变体还限定了调用方的内存这是 streaming 模式做不到的内容在边界两侧都从不以缓冲区形态存在。无状态性——不新增进程级或库级状态。操作复用既有的每会话传输状态与既有本地存储。确定性——内容寻址不变相同内容无论由哪个操作存储都会产生相同地址。一个键解析到的映射按定义不随时间确定——这正是可变存储存在的意义。延迟——这是本提案的动机所在。外键读写从两个串行依赖往返降为一个覆盖所有内容大小。多 fragment 写也不再支付单独的发布映射搭在 fragment 列表根的上传上——这个根在叶子之后本来就必须发送。迁移计划无需迁移。操作是增量式且可选的既有调用方继续使用mutable_loadget与putmutable_store行为不变。集成方自主决定何时采用新操作。不涉及数据迁移因为新键类型只承载通过新操作创建的映射。安全与隐私安全不引入新的权限主体。两个操作只在组合它们的存储命令相同的会话授权下可达局限于会话的仓库且不能读写任何调用方通过get/put/mutable_load/mutable_store本就无法触及的内容。能发布新类型键的调用方本也可以通过mutable_store发布相同的映射读者依据服务端返回的哈希校验内容因此被投毒的映射无法令读者接受一个不哈希到所声明值的负载。唯一值得显式声明的性质新键类型也可以通过既有mutable_store写入因此上面描述的顺序保证只对通过put_resolved发布的内容成立对直接写入的映射不成立。读者必须把解析未找到内容视为可能的结果——无论怎样对在解析与读取之间被收回的键它都必须这样做。隐私无影响。操作移动的内容、仓库作用域与授权与它们组合的操作完全相同。外键由调用系统选择对 Lore 不透明它们像其他任何可变键一样被存储与传输本提案不新增对它们的保留、日志或遥测。风险与假设假设外键访问是真实且不断增长的集成形态而非小众——失效条件若促使本提案的集成方终究持有内容哈希或能容忍第二个往返。限制这些调用方的是延迟而非带宽——失效条件若测量显示第二个往返占端到端时间的比例远小于负载传输。每个键一个映射足够——失效条件若调用方需要一个键命名多个内容版本那时这就变成了版本管理问题而非缓存问题。风险新客户端与早于这些操作的服务端对话会直接失败而非回退到它本可使用的两步序列——缓解本提案未处理。回退机制实现简单在混合版本部署依赖这些操作之前值得补上它在未决问题中明确指出而非被默认忽略。本地缓存的映射会过期没有任何机制告知客户端其缓存的键已移动——缓解接受并已记录。需要权威映射的调用方显式请求默认偏好本地答案与其他所有存储读一致。新键类型会在本地可变存储中累积条目而无法回收因为 Lore 的维护流程只覆盖不可变存储——缓解暂时接受实际上有界——映射只在调用方要求缓存时才被缓存。回收可变存储条目是比本提案更宽泛的缺口。权衡Drawbacks两个传输上各多两条命令一个一旦发布就成为兼容承诺的 C API 上多两个入口点。发布被融合进最后一个 fragment 的上传中使写路径的放置折叠从仅用于报告变成对正确性负责某个层级若把子节点误报为已到达远端就会发布一个键指向服务端只持有一部分的内容。这是一道门、一个位置覆盖它的测试是混合树mixed-tree测试但它比独立的映射写是一道更锋利的边缘。放置报告新增了一个调用方必须理解才能正确使用写的概念——此前一次成功的写就是成功。这是真实性的改进而非新危险但确实是新的表面区域。备选方案回顾保留两步请求序列并把顺序要求写进文档调用方继续mutable_loadget与putmutable_store文档声明发布必须跟随其内容的顺序约束。否决原因它没有解决动机中的任何一笔代价。串行往返依旧——而正是这笔代价决定了这些集成是否可行记录顺序要求仍让每个集成方各自正确实现它。客户端对既有操作做批处理或流水线调用方并发发起大量键查询让传输在批量中摊销两个请求。否决原因两个请求串行依赖——第二个需要第一个的应答——所以跨键并发不会降低任何单个键的深度。并发解析一万个键的调用方每个仍要等两个往返。批处理解决的是吞吐而问题是延迟。通用的服务端脚本或多命令设施让客户端提交一段由依赖存储命令组成的小程序解析后读是其中一例。否决原因为这一个出现得足够频繁、值得一等公民操作的特定依赖引入一个带自身求值、资源与安全问题的庞然大物代价过大。存储命令集中没有任何其他东西需要这种通用性。把内容直接存在外键之下让可变存储持有内容而非哈希一次查找返回字节。否决原因为省一次查找而丢弃该数据的内容寻址——去重、校验、多键共享同一 blob——并让 Lore 的内容分裂成两个耐久性、复制与修复行为不同的存储。先例内容寻址存储与可变命名层配对是常见安排本提案消除的往返是这一安排公认的代价Git 分离对象存储与 refs客户端取分支时先解析 ref 再读取其命名的对象smart transport 的协商机制部分就是为了避免为每个对象付出这一依赖。Nix 分离 derivations 与 store paths并做了大量工作以避免对二进制缓存按路径解析的延迟。以 ETag 为键的内容寻址 HTTP 缓存面对同样的形态用条件请求作答——把校验与获取融合进一次交换与本提案融合解析与读取出于相同原因。未决问题当服务端未实现这些操作时客户端是否应回退到两步请求序列该回退应自动进行还是由调用方选择回退简单且永远正确反对自动化的论据是它会隐藏一个部署可能希望看见的版本不匹配。一旦可变存储的回收机制存在以此方式发布的内容是否应纳入其中今天没有任何机制修剪可变条目而本提案新增了一类生命周期由外部系统的键空间而非 Lore 自身决定的条目。收回操作是否应区分这个键从未存在与这个键已被收回今天两者都解析为 not-found——对缓存足够对想检测故意移除的调用方则不足。进一步阅读提案全文docs/proposals/2026-08-02-resolved-storage-operations.md键类型定义lore-base/src/types/store_types.rsC API 声明与逐项事件结构lore-capi/lore.h实现入口lore/src/storage/get_resolved.rs、lore/src/storage/put_resolved.rs、lore/src/storage/get_file_resolved.rs、lore/src/storage/put_file_resolved.rs底层存储原语write_resolved/FusedPublish见 lore-storage/src/write.rsread_resolved系列见 lore-storage/src/read.rs本地可变存储的键类型编码lore-storage/src/local/mutable_store.rsgRPC 线协议lore-proto/proto/lore/storage/v1/storage.proto传输对等原则docs/explanation/system-design.md赞分享版本控制后端【免费下载链接】loreLore is a next-generation, open source version control system项目地址https://gitcode.com/gh_mirrors/lore6/lore点击查看免费下载相关推荐深入解析 computer 项目 VFS 文件系统 SchemaDO SQLite 中的内容寻址存储设计深入解析 computer 项目 VFS 文件系统 SchemaDO SQLite 中的内容寻址存储设计 本指南系统讲解 cloudflare/comput后端云原生存储pnpm11 的 CAFS 类型契约深入解读 pnpm/store.cafs-types 与内容寻址存储设计pnpm11 的 CAFS 类型契约深入解读 pnpm/store.cafs types 与内容寻址存储设计 导读 pnpm/store.cafs ty包管理器开发工具CLI【pnpm】 深入pnpm架构内容寻址存储机制解析深入pnpm架构内容寻址存储机制解析 pnpm的内容寻址文件系统CAFS通过文件内容的SHA 512哈希值唯一标识和存储文件实现了革命性的存储效率和去重包管理器开发工具CLI上一篇FinalBurn Neo终极指南如何在5分钟内快速上手经典街机模拟器下一篇ant-design-vue PageHeader 页头组件完全指南API 详解、源码实现与实战用法创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表