数据工程数据湖大数据对象存储后端【免费下载链接】lakeFSlakeFS - Data version control for your data lake | Git for data项目地址https://gitcode.com/gh_mirrors/la/lakeFS点击查看免费下载本文围绕 lakeFS 已接受的设计文档 kv3-execution-plan.md 展开讲清 gravelerlakeFS 的元数据存储层从 SQLPostgreSQL迁移到通用 KV 存储的完整执行计划5 张元数据表如何映射为三类 KV 分区、仓库/分支/提交/暂存对象各实体的键布局、仓库删除在分布式失败场景下如何保持一致性以及基于SetIf的乐观并发提交算法。读完本文你可以掌握 lakeFS 在 KV 上设计键空间、处理乐观锁冲突与分区隔离的工程方法并能对照当前源码验证这套设计已经落地为pkg/graveler的生产实现。背景为什么要给 graveler 换存储lakeFS 的元数据分两类见总体设计文档 lakeFS on KV不可变元数据已提交的 entries以 range / metarange 形式存放在对象存储中可变元数据IAM 实体、分支指针、未提交stagingentries 等当时存放在 PostgreSQL 中并依赖其事务语义。KV3 执行计划的目标就是让第二类元数据改由一个语义窄得多的 Key/Value 接口承载从而让 lakeFS 能在多种后端数据库上运行、便于多租户托管甚至无外部依赖地本地运行。该接口在 index.md 中定义只有Get、Scan、Set、Delete和带谓词的SetIf等操作并提供按分区partition组织的键空间——这直接决定了后文三类分区的布局。在动手之前执行计划首先盘点了 graveler 当时使用的5 张 SQL 表并逐一评估其迁移难度SQL 表使用方迁移要点graveler_repositories、graveler_branches、graveler_commits、graveler_tagspkg/graveler/ref前三张表基本不可变graveler_commits仅在refs-restore场景下更新 generation且只发生于 bare 仓库可不关心可较直接地在 KV 上实现graveler_branches原先由锁保护需要新的无锁提交方案第一步可先用朴素表锁或基于 branch-key 的锁过渡graveler_staging_kvpkg/graveler/staging条目以当前分支的staging_token为基准创建且需防止提交期间继续写入同样先以朴素加锁起步文档同时注意到这些表连同auth_users还被pkg/diagnostics以只读方式用于诊断采集属于后续可选支持项。实体设计五种元数据在 KV 中的形态执行计划为每种实体给出了类型定义并明确每个数据访问都隐含一次GetRepository这一前提——既要验证仓库存在又要取回构建分区键所需的唯一 ID。Repository仓库仓库由唯一名称RepositoryIDstring标识内容包含存储命名空间StorageNamespace、默认分支 IDBranchID和创建时间time.Time。计划要求所有仓库条目放在同一个通用分区graveler中以支持仓库列表迭代同时为区分已删除的同名仓库与新建的同名仓库仓库条目必须包含一个随创建生成的随机UniqueID与仓库名一起构成该仓库所有实体的分区键——这样aRepo/UniqueID1与aRepo/UniqueID2两个分区天然可辨。计划中的结构定义如下RepositoryToken为新增字段type RepositoryToken string type Repository struct { StorageNamespace StorageNamespace db:storage_namespace json:storage_namespace CreationDate time.Time db:creation_date json:creation_date DefaultBranchID BranchID db:default_branch json:default_branch RepositoryToken RepositoryToken json:repository_token // new field }对照当前源码这一设计已被实现且有所演进pkg/graveler/graveler.go 中的Repository结构体用InstanceUID字段承载了计划中的UniqueID由NewRepoInstanceID()基于 xid 生成此外还增加了State仓库状态仅ACTIVE视为有效与ReadOnly字段删除流程即依赖IN_DELETION状态下文详述。Branch分支分支由仓库内唯一名称标识因此REPO_NAMEBRANCH_NAME全局唯一由于除仓库自身外的所有实体都归属于仓库分区分支在分区内以名称为键即可。除当前CommitID与StagingToken外为支持 lakeFS on KV 设计文档中提出的 commit flow分支新增sealed_tokens字段——一个StagingToken数组type Branch struct { CommitID CommitID json:commit_id StagingToken StagingToken json:staging_token SealedTokens []StagingToken json:sealed_tokens // new field }当前源码中该字段已落地为Branch.SealedTokens见 pkg/graveler/graveler.go注释说明staging token 从数组前部追加prepend以便方便地构建 diff 迭代器后续又扩展了CompactedBaseMetaRangeID与Hidden字段。Tag标签与 Commit提交标签在仓库分区内以TagID唯一标识内容为一个提交 ID提交在仓库分区内以 Commit IDsha256 的十六进制串唯一标识包含 version、committer、message、metarange ID、创建时间、父提交列表、metadata 映射与 generationtype TagRecord struct { TagID TagID json:tag_id CommitID CommitID json:commit_id } type Commit struct { Version CommitVersion db:version json:version Committer string db:committer json:committer Message string db:message json:message MetaRangeID MetaRangeID db:meta_range_id json:meta_range_id CreationDate time.Time db:creation_date json:creation_date Parents CommitParents db:parents json:parents Metadata Metadata db:metadata json:metadata Generation int db:generation json:generation }提交 ID 由其内容哈希派生addressProvider.ContentAddress(commit)这使得同一提交重复写入必然等价天然幂等——当前 pkg/graveler/ref/manager.go 的addCommit正是利用这一点对key 已存在的SetMsgIf谓词失败不报错。Staged Object暂存对象暂存对象由staging_token 对象路径唯一确定内容是序列化为 protobuf 的对象元数据DBEntry及其校验和。为最小化实现瓶颈每个 staging token 独占一个分区token 作为分区键对象路径作为分区内的 key。计划指出staging_token在分支内唯一且本身以仓库名、分支名为前缀因此是恰当的分区名type Value struct { Identity []byte db:identity json:identity Data []byte db:data json:data }当前源码中 staging token 的生成与计划一致GenerateStagingToken返回repoID-branchID:uuid见 pkg/graveler/graveler.go 中的GenerateStagingToken既保证唯一又便于按分支识别 token。迭代器从 SQL 游标到 KV Get/Scan执行计划将pkg/graveler用到的迭代器逐一映射到 KV 原语branch_iterator按仓库迭代分支按 ID 排序用标准Scan即可trivial按 CommitID 排序则实现为取回该仓库全部分支后在内存中排序。commit_iterator按祖先关系 BFS 迭代提交它不是 KV 意义上的迭代器——每个提交按 IDGet一次、父提交入队待扫KV 层无需特殊支持。commit_ordered_iterator按 commit ID 排序迭代仓库提交Scan的自然行为即可支持只迭代非 first-parent 提交的选项当时待定TBD计划认为可以为其定制一个专门的Next()。repository_iterator、tag_iterator按 ID 排序的标准Scan足够。staging/iterator以特定staging_token分区为范围、按 key 顺序扫描同样由Scan支持。这些迭代器在仓库中均有对应实现与测试branch_iterator.go、commit_iterator.go、commit_ordered_iterator.go、repository_iterator.go、tag_iterator.go 以及 staging/iterator.go。其中按提交排序的迭代器NewOrderedCommitIterator还被GetCommitByPrefix复用前缀定位SeekGE(prefix)后顺序扫描用于支持短提交 ID 前缀解析。建议的 Key Schema三类分区计划提出的键布局使用3 类分区这是整个 KV3 设计的核心* 分区 graveler通用分区 * Repository - repos/REPO_NAME * 分区 REPO_NAME-UNIQUE_ID每个仓库一个专属分区 * Branch - branches/BRANCH_NAME * Commit - commits/COMMIT_ID * Tag - tags/TAG_ID * 分区 STAGING_TOKEN每个暂存 token 一个专属分区token 内含仓库名与分支名 * Staged Obj - key/OBJ_KEY设计动机Key Schema 对功能的支持一节通用分区graveler不掌握任何分区信息即可访问仓库、支持仓库列表迭代仓库条目在该分区中的存在/缺失单独决定仓库是否有效。仓库专属分区仓库相关条目全部聚合在一个以REPO_NAME-UNIQUE_ID命名的分区下删除仓库即可按整分区删除UNIQUE_ID用于区分同名仓库的不同生命周期实例。由于取分区键需要先执行GetRepository拿到UniqueID这一强制前置访问同时起到了仓库未被删除的校验作用。staging token 专属分区在不同 token 之间天然负载均衡缓解 staging 与 commit 的热点。当前源码中该布局已完整实现见 pkg/graveler/model.goRepositoriesPartition()返回常量gravelerRepoPartition(repo)返回repoID-InstanceUIDRepoPath/BranchPath/CommitPath/TagPath分别生成repos/…、branches/…、commits/…、tags/…前缀StagingTokenPartition(token)直接以 token 字符串为分区名。此外该文件的init()通过kv.MustRegisterType为每个前缀注册了 protobuf 类型RepositoryData、BranchData、CommitData、TagData、StagedEntryData即所有实体在 KV 中的值均以 protobuf 编解码——这正是计划中复用现有DBEntry类型、序列化为 protobuf的落地方式。细节差异在于当前实现的 staging 分区内以对象路径本身为 key无key/前缀见 staging/manager.go 中直接以key调用kv.GetMsg/kv.SetMsg的代码。删除仓库没有事务还能保持一致吗其他实体的删除各自独立而删除仓库会级联删除其部分支、提交与标签。SQL 下这由一个事务保护KV 下则必须显式设计失败行为。计划给出的方案是先通过GetRepository构建分区键同时完成仓库存在性校验删除graveler通用分区中的仓库条目——仓库从此不可访问等于逻辑上不存在再用该分区键删除仓库的所有相关条目整个仓库分区及其 staging 分区。由此分析出两种失败边界若在第 2 步之前失败仓库原样保留、仍然可用若在之后失败仓库分区与相关 staging 分区都只是变得不可达。即使之后用同名重建仓库也会生成新的UniqueID旧分区的残留依旧不可达。任何情况下 DB 都保持一致只是可能遗留孤儿分区。计划据此提出实现周期性后台 GC 清理这些分区通过维护一份to-delete分区列表启动删除流程但明确该工作超出 KV3 范围对应 issue #3590。对照当前实现pkg/graveler/ref/manager.go 中的DeleteRepository在顺序上做了等价但更稳健的变形先将仓库State置为IN_DELETION此后GetRepository对该仓库返回ErrRepositoryInDeletion实现先不可访问、再清理的语义随后并发删除所有分支、标签、提交与仓库级 metadata最后才删除通用分区中的仓库主记录deleteRepository。这与计划先摘除入口、再清理子实体的一致性论证完全同构任一步骤失败要么仓库仍可完整访问要么仅留下孤儿数据graveler 本身不会读到半删除的活跃仓库。更新提交、staging 与无锁lock-free这是执行计划中最关键的并发设计。SQL 方案靠分支行锁串行化提交KV 下计划采用乐观并发利用 KV 的SetIf条件写更新分支条目保证没有另一个并发提交抢先完成而被本提交覆盖若SetIf失败谓词不成立说明有并发提交抢先更新了分支则本次提交失败并重试该算法要求分支支持多个 staging token即sealed_tokens提交开始时把当前staging_token封存进sealed_tokens并换发新 token同样经SetIf原子完成从而提交过程中新的 staging 写入落到新 token不会被本次提交误读最终迭代所有sealed_tokens构建提交。上图来自同目录的 committer flow 设计图完整展示了这条链路读分支 → 封存旧 token、换新 tokenSetIf→ 迭代全部sealed_tokens应用到新提交 → 重读分支 → 以SetIf写入分支替换 commit ID、清除已共享前缀的sealed_tokens→ 条件失败则重试或上报调用方。计划同时划定了范围边界完全无锁lock-free的提交对应设计 PR Change KV storage proposal to guarantee progress不在 KV3 里程碑内——KV3 只做防覆盖 重试的乐观方案无锁化留待后续。从当前源码可以验证这条链路Graveler.Commitpkg/graveler/graveler.go先在BranchUpdate中把branch.StagingToken前插进SealedTokens并调用GenerateStagingToken换发新 token随后在retryBranchUpdate内计算 parents、执行 hook、落 metarange 并更新CommitID而RefManager.BranchUpdatepkg/graveler/ref/manager.go正是读分支 谓词 → 回调更新 →kv.SetMsgIf条件写的乐观协议。在其之上当前实现又叠加了两层后续演进一是batch.Batcher对同键读请求做批合并getRepositoryBatch、getBranchWithPredicate等带maxBatchDelay摊平读放大二是可选的近似分支属主机制distributed.MostlyCorrectOwner见 pkg/distributed/mc_owner.go——通过周期性刷新属主记录让并发写者大多自动退让降低SetIf冲突重试率注释明确这只影响性能、不影响正确性任何时刻只有一个更新能赢得竞争。staging 侧同样体现了乐观协议与分区化思想pkg/graveler/staging/manager.go 中Set(requireExiststrue)用PrecondConditionalExists谓词、Update用读时谓词做条件写删除整个 staging token 时DropAsync只在cleanup-tokens分区登记一个条目由后台asyncLoop周期性取出 token 列表还会 shuffle 以降低多服务器重复处理概率再逐前缀清理——这是分区删除异步化、限流执行的实际形态。执行计划分步落地路线文档的 Execution Plan 一节把迁移拆成可独立验收的步骤issue 编号为原文引用此处仅作事项标识敲定 Key Schema即上文的三类分区布局#3567。KV 与 SQL 双轨运行与之前模块迁移的做法相同Step 1构建初始KV graveler与DB gravelerKV graveler将所有调用委托给DB graveler此步骤完全不用 KV以便按块逐块实现 KV 版本#3568——这是典型的 Facade/委托式渐进替换Step 2在KV graveler中实现基础 KV 使用Repository不含删除#3591、Branch#3592、Tag#3593、Commit不含乐观锁提交流程#3594、Staging#3595Step 3乐观锁提交——用SetIf设置分支的 commitSetIf失败即失败并重试#3569Step 4确定并实现删除仓库的工作方案#3570。Step 3 与 Step 4 相互独立。常用操作基准测试#3571手工在 SQL 与 KV 两套实现上执行验证性能令人满意。既有 SQL 数据向 KV 的迁移#3572大部分表迁移平凡graveler_branches预计也无需对sealed_tokens做特殊处理需验证KV 迁移测试自然包含在内。将 graveler 迁移纳入 Esti 迁移测试#3573lakeFS 的端到端测试框架 Esti位于 esti/含大量.golden期望输出文件承担迁移前后行为一致的回归验证。可选项Dump/Restore Refs 的 SQL 与 KV 双实现测试#3574pkg/diagnostics的 KV 支持#3575预计平凡。其中 Dump/Restore Refs 本身值得单列它使用 graveler 的 ref-store 对整个仓库分支、提交、标签做导出与向 bare 仓库的恢复。由于不直接访问数据库理论上可无缝工作但计划强调必须验证又因其覆盖面广还可兼作性能验证手段。测试与基准先想清楚测什么计划对测试的判断很坦率pkg/graveler/ref与pkg/graveler/staging当时测试覆盖度极高当前仓库中这两个目录下几乎每个实现文件都配有同名_test.go如 manager_test.go、staging/manager_test.go这让迁移 KV 更令人放心但缺少 benchmark。而 graveler 为支持无锁提交将引入新算法存在性能退化风险必须尽早识别。计划进而讨论了哪些场景值得测并给出取舍反面示例——多个并行提交互相失败重试直到全部成功这是一个穷举式极端场景能把非无锁算法的性能逼到边缘可以预见它必然表现糟糕但结论没有信息量不感兴趣正面示例——一次长提交进行的同时多个 staging 操作并发发生这是真实使用中更常见的形态如果此场景下 KV 相对 SQL 出现性能回归越早发现越好。因此基准工作的起点被定为Long-Commit-Multiple-Staging场景且可能仅此一个场景即已足够。此外KV 与 SQL 双轨期本身就提供了 A/B 对比的载体同一套功能测试、同一套 Esti 黄金文件两种存储实现各跑一遍差异即风险。小结从计划到现状回到仓库当前状态可以确认 KV3 计划已完整落地pkg/graveler/ref与pkg/graveler/staging的全部管理器直接以kv.Store为依赖不再持有 SQL 句柄三类分区布局、protobuf 值编码、SetIf乐观协议、staging token 分区化与异步清理都见诸 pkg/graveler/model.go、pkg/graveler/ref/manager.go、pkg/graveler/staging/manager.goKV 后端的表结构迁移由 pkg/kv/migration.go 的DatabaseMigrator负责。计划中留给未来的两块——失败删除分区的后台 GC 与完全无锁提交——前者以cleanup-tokens异步机制覆盖了 staging 侧的清理需求后者则以近似分支属主 批合并 重试的组合在实现中逐步逼近。这份执行计划的价值不仅在于完成了 graveler 的存储解耦更在于它示范了一套可复用的工程方法先盘点表与实体、再定键空间、用委托层双轨并行、以乐观协议替代行锁、以端到端黄金文件守护迁移正确性最后用贴近真实负载的基准锁定性能底线。赞分享数据工程数据湖大数据对象存储后端【免费下载链接】lakeFSlakeFS - Data version control for your data lake | Git for data项目地址https://gitcode.com/gh_mirrors/la/lakeFS点击查看免费下载相关推荐lakeFS 元数据存储从 Postgres 到 KV 的迁移执行计划接口设计、包级改造与无锁提交落地lakeFS 元数据存储从 Postgres 到 KV 的迁移执行计划接口设计、包级改造与无锁提交落地 导读 lakeFS 将「仓库/分支/提交/暂存区/认证数据工程数据湖大数据对象存储后端lakeFS 元数据存储的 Key/Value 化重构从 PostgreSQL 到可插拔 KV 抽象与无锁提交设计lakeFS 元数据存储的 Key/Value 化重构从 PostgreSQL 到可插拔 KV 抽象与无锁提交设计 导读 本文深入解析 lakeFS 官方设数据工程数据湖大数据对象存储后端lakeFS 元数据迁移Table DB → KV Store测试计划全解析从迁移验证到逐包测试与执行里程碑lakeFS 元数据迁移Table DB → KV Store测试计划全解析从迁移验证到逐包测试与执行里程碑 导读 本文以 lakeFS 仓库中的 lak数据工程数据湖大数据对象存储后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考