- 数据工程
- 数据湖
- 大数据
- 对象存储
- 后端
【免费下载链接】lakeFS
lakeFS - Data version control for your data lake | Git for data
本文基于 lakeFS 设计文档《Uncommitted Garbage Collection - Execution Plan》撰写,围绕 lakeFS "GC+"(未提交对象垃圾回收)特性的实施路线展开:先梳理计划中 lakeFS 侧与 GC 侧的全部改动项,再结合当前仓库中已落地的源码(PrepareUncommittedForGCAPI、Get/LinkPhysicalAddress校验签名、CopyObject浅/全量复制、data/分片存储结构等)逐项印证其实现方式,帮助读者理解未提交数据 GC 的完整技术链路。
背景:为什么未提交数据需要独立的 GC 执行计划
lakeFS 中,staged(未提交)数据在被删除、覆盖、reset 分支等操作后,其底层的物理对象可能不再被任何引用,但 lakeFS 并不会自动将其从对象存储中删除。这会带来持续的存储开销,甚至潜在的合规问题。针对这一问题的在线(online)清理方案曾多次尝试但均被否决(相关否决方案见 hard-delete 设计),最终 lakeFS 采用了离线 GC的思路:由一个外部的 GC Client(如 Spark 作业)同时消费 lakeFS 元数据与对象存储中的对象信息,判定哪些对象可以被安全硬删除。
离线 GC 流程由三部分构成(见 uncommitted-gc 提案):
- 列举 namespace 中的全部对象(直接来自对象存储);
- 列举 lakeFS 仓库的已提交对象(通过 meta-range / range 读取);
- 列举 lakeFS 仓库的未提交对象(通过 lakeFS 新暴露的 API 导出)。
凡是出现在第 1 步、但不属于第 2、3 步的对象,即可被 GC 安全删除。
执行计划文档将整体实施拆分为3 个主要改动块:
- lakeFS 服务端的增删改(API 与存储结构调整);
- 客户端侧实现未提交 GC 逻辑(GC Client 作业);
- 改造现有已提交数据 GC 流程,使其与未提交 GC 融合。
其中 lakeFS 侧的所有改动彼此独立,可以立即并行开工;部分 GC 侧改动依赖 lakeFS 侧改动,因此计划按依赖约束排定优先级(Milestone 2 的后续路线可参考 Milestone 2 执行计划)。
lakeFS 侧的 9 项改动及其优先级
执行计划按依赖顺序列出了 lakeFS 侧需要完成的 9 项改动。下面逐项说明,并给出当前仓库中已落地实现的源码位置。
1. 实现 PrepareUncommittedForGC API
计划要求:新增 API,生成"未提交对象清单文件"并保存在对象存储的指定 GC 路径下。这一步完成后,GC 侧即可开始实现新的未提交逻辑。
当前仓库中该 API 已实现:
- OpenAPI 定义:
POST /repositories/{repository}/gc/prepare_uncommited(operationId 为prepareGarbageCollectionUncommitted,internaltag),请求体PrepareGCUncommittedRequest支持continuation_token以分页遍历分支;响应PrepareGCUncommittedResponse返回run_id、gc_uncommitted_location(未提交信息数据的位置)与continuation_token,详见 api/swagger.yml。 - HTTP 层:Controller.PrepareGarbageCollectionUncommitted 负责鉴权(
PrepareGarbageCollectionUncommittedAction+ 仓库级 ARN)、解码 continuation token,然后调用Catalog.PrepareGCUncommitted,并把返回的 mark 重新编码为下一次请求的 token。 - 目录层:Catalog.PrepareGCUncommitted 驱动整个导出流程;核心的数据写出逻辑在 pkg/catalog/gc_write_uncommitted.go 中:
gcWriteUncommitted遍历仓库的全部分支(ListBranches带WithShowHidden(true),即隐藏分支也纳入,保证 GC 视野完整),对每个分支调用DiffUncommitted读取该分支的全部未提交差异;- 未提交条目被写成Parquet 文件(GZIP 压缩),字段为
physical_address与creation_date,正好对应提案中"address + creation date"的数据格式; - 通过
GCUncommittedMark{RunID, BranchID, Path}实现断点续传:当单文件大小超过maxFileSize,或超过prepareDuration(防止单次 API 调用超时)时,记录当前分支与路径位置的 mark 并返回,供下一次调用从该位置继续(branchIterator.SeekGE/diffIterator.SeekGE); - 差异中类型为
DiffTypeRemoved的 tombstone 会被跳过,因为它们不对应需要保留的物理对象。
- 存放路径:未提交清单写入
_lakefs/retention/gc/uncommitted/<run_id>/uncommitted/前缀下,路径模板定义在 pkg/graveler/retention/garbage_collection_manager.go(uncommittedFilePrefixTemplate = "%s/retention/gc/uncommitted/%s/uncommitted/"),与提案中_lakefs/retention/gc/uncommitted/run_id/uncommitted/的约定一致。
这一 API 的落地是 GC+ 的"第一块多米诺骨牌"——GC Client 从此可以拿到结构化的未提交对象数据(提案中称为UncommittedData/Uncommitted DF),而不需要直接扫描分支的 staging 元数据。
2. 改造 Get/Link PhysicalAddress:一次性校验令牌
计划要求:GetPhysicalAddress在签发物理地址时附带一个有有效期的一次性令牌并登记到有效令牌表;LinkPhysicalAddress在创建条目前校验令牌并移除之。其核心目的有两个:
- 利用"地址已签发但尚未 link"的时间窗口,把这段时间内上传的对象排除在 GC 删除范围之外,避免 GC 删掉一个正在上传流程中、马上要 link 的对象;
- 反之,已上传但令牌过期仍未 link 的对象,最终会被 GC 清理,消除永久泄漏(该设计同时修复了提案引用的 issue #4438 中"staging 对象悬挂"的问题)。
从当前源码看,这一校验已以HMAC 签名 + 时间戳的形式实现:
- 上传入口返回的物理地址内嵌了签名(分隔符
LinkAddressSigningDelimiter),签名由signingKey对repository/branch/path/address的 JSON 计算得到; - Catalog.VerifyLinkAddress 在
LinkPhysicalAddress路径上被调用:它拆分地址与签名、重算 HMAC(hmac.Equal常量时间比较)、再通过PathProvider.ResolvePathTime(address)从地址本身解析出签发时间,若time.Since(creationTime) > LinkAddressTime则返回ErrLinkAddressExpired; - HTTP 层在 LinkPhysicalAddress 处理逻辑 中,仅当地址被识别为位于存储 namespace 内的"相对地址"时才执行该校验,namespace 外的外部地址不受影响。
这种"签名 + 地址自携带时间戳"的实现使 lakeFS 无需维护集中式令牌表即可完成一次性、有时效的校验,属于对原计划"维护 valid token list"思路的实现级简化。
3. 在 ref-store 中跟踪被复制对象
计划要求:每次 copy 操作向 ref-store 的 copied object 表写入一条记录,并周期性清理,或在 commit / delete / reset branch 等定义好的操作时清理。GC 读取这份信息以避免 GC 作业与 rename(copy+delete)操作之间的竞态:如果 GC 在 copy 已产生新条目、delete 尚未发生之间扫描,可能误删旧对象。lakeFS 会按时间戳定期清除这些 copy 记录。
提案文档(uncommitted-gc.md)中还明确了时序约束:copy 信息必须在全部分支的未提交数据收集完成后读取,才能正确并入UncommittedData。
4. 实现 CopyObject API
计划要求的流程:
- 从分支读取 staging token;
- 取出源 entry;
- 若 entry 处于 staged 状态:更新 copy 跟踪表 + 执行浅拷贝(shallow copy,仅复制元数据条目,物理对象共享);
- 若 entry 已提交或位于其他分支:使用底层存储适配器的 copy 方法执行全量拷贝;
- 用旧 entry 数据 + 新路径 stage 一个新条目。
当前仓库中,HTTP 入口为 Controller.CopyObject:鉴权要求对源路径具备读权限、对目标路径具备写权限(两者以 AND 组合),要求目标分支存在,未指定srcRef时默认使用目标分支作为源引用,随后委托给目录层。目录层的核心实现在 Catalog.CopyEntry:
- 若设置选项
graveler.WithShallow,走cloneEntry——即浅拷贝,新条目直接复用源的物理地址; - 否则为全量拷贝:为新条目分配
c.PathProvider.NewPath()生成的新物理地址(落在data/前缀下),并通过block.ObjectPointer描述的源/目标指针调用存储适配器完成真实数据拷贝; - 跨仓库拷贝时会校验两个仓库
StorageID相同,否则返回ErrInvalidStorageID,防止跨底层存储的无效复制。
前置条件参数(如If-Match/If-None-Match)通过 BuildOptsFromParams 转为 graveler 的 set options,保证拷贝操作的并发语义可控。
5. 改造 S3 Gateway 的 CopyObject
计划要求 Gateway 的 CopyObject 遵循同一策略:同一分支内对 staged 对象的 copy 执行浅拷贝并登记到 ref-store;其余所有 copy 走底层适配器 copy。Gateway 侧 copy 处理位于 pkg/gateway/operations/putobject.go(S3 CopyObject 请求的识别与serde.CopyObjectResult响应编码),它最终复用同一套目录层 copy 逻辑,保证 OpenAPI 客户端与 S3 客户端看到一致的语义。
6. 改造 lakeFSFS 的 renameObject 方法
计划要求 Hadoop 客户端(lakeFSFS)的renameObject统一改为经由 Gateway 或 OpenAPI 的CopyObject + DeleteObject实现。这一点保证了 Hadoop 生态的 rename 语义与 S3/OpenAPI 客户端一致,从而落入上述 ref-store 跟踪与浅拷贝机制,不会绕过竞态防护。对应客户端实现位于 clients/hadoopfs 模块。
7. lakeFS 客户端适配
计划在此项中提出了三个待决问题(原文以问句形式列出):
- 客户端侧是否需要动作?
- 是否需要让客户端版本感知(version aware)?
- 是否需要发布通知要求用户升级客户端?
这些问题的背景是:GC+ 引入的行为变化(如Get/LinkPhysicalAddress令牌校验)对旧客户端可能不透明,需要评估兼容性与升级策略。
8. 改造 StageObject API:拒绝仓库 namespace 内的地址
计划要求:当StageObject的物理地址位于仓库自身存储 namespace 之内时返回错误。这样,通过 StageObject 直接挂接的对象永远不会被 GC 当作可回收对象处理——namespace 内的对象应当只经由 lakeFS 的上传管线(data/前缀)产生。
从源码结构看,该约束与地址归类机制已经落地:Controller.normalizePhysicalAddress 会把位于<namespace>/<dataPrefix>/下的地址归一化为AddressTypeRelative(相对地址,含data前缀),其余一律视为AddressTypeFull(完整外部地址)。staging 相关入口(如 pkg/api/helpers/upload.go 中封装的 presign 上传流程)围绕"相对地址必须来自 lakeFS 自身签发"的原则校验,与提案的意图一致。
9. 实现新的仓库存储结构:data/前缀与时间/大小分片
计划要求新仓库结构满足:
- lakeFS 数据统一存放在根前缀
data/下; - 按提案定义的分片命名规则生成对象路径;
- 跟踪对象上传数量;
- 按时间或数量创建新 slice(分片)。
这正是提案"Objects Path Conventions"六条约定的落地,也是 Clean Run 中"跳过比<TOKEN_EXPIRY_TIME>更新的 slice"这一优化(避免 GC 误删正在上传的对象)得以成立的基础。当前实现位于 pkg/upload/path_provider.go:
DefaultDataPrefix = "data":数据统一位于data/前缀;DefaultDataPartitionSize = 50000:单个 slice 最多承载 5 万个对象,超过即换新 slice(对应"track object upload count");DefaultDataInterval = time.Hour:每小时滚动一个新 slice(对应"timely basis, for example hourly");- slice 名由
newDescendingID生成——用year 4000 的 unix 时间戳减去当前时间再编码为xid,使 slice 名随时间单调递减。这一"反向排序"技巧让按字典序正序 listing 时,最新写入的 slice 恰好排在最前面,GC 因此可以在读到"上一轮已处理到的 slice"后直接停止扫描,实现 Optimized Run 的局部扫描; PathPartitionProvider.NewPath()生成data/<partition>/<xid>形式的对象路径,每个 slice 由单个 lakeFS 实例写入以正确跟踪大小。
新的分片结构示意(namespace 内按时间线排列的 slice,listing 方向沿字典序向下,右侧为 slice 内部对象):
GC 侧改动:未提交 GC 逻辑与既有流程的融合
执行计划在 "Changes on GC" 一节列出两条主线:
1. GC 新增未提交垃圾回收逻辑
- Prepare uncommitted for GC:对全部分支调用
PrepareUncommittedForGC(对应计划中的Mark uncommitted data步骤); - 从 lakeFS 读取未提交数据(即上文的
UncommittedData文件); - 实现优化的 run deltas(基于上一轮运行信息的增量扫描)。
2. 将未提交数据并入现有 GC 流程
- 已提交数据处理所需的适配改动;
- 支持 uncommitted 的 mark and sweep(标记-清除);
- 统一未提交与已提交两条 GC 流水线。
配套的测试策略覆盖四个维度:
- 功能性:基础 GC 功能正常,确认已提交 GC 逻辑未被破坏,未提交 GC 行为符合预期;
- 数据完整性:确保只有"确已可删除"的对象被硬删除;
- 性能:验证未提交 GC 满足既定性能需求;
- 存量仓库兼容:在旧结构仓库上测试首次运行(含/不含新结构对象),并验证 Optimized Run 下连续多次运行的正确性。
提案中给出的最低性能底线是:在 2000 万对象、1K 分支、30K commit、500 万未提交对象、100 万待删除对象且网络延迟严重的条件下,整个流程应在约 3 小时内完成(其中 Clean Run 的 namespace 全量列举约 1 分钟、未提交清单写出与上传合计约 1.5 分钟、100 万对象批量删除最坏约 2.5 小时)。
Milestone(里程碑)定义为:
- 实现Clean Run(含全部 lakeFS 侧改动),满足最低性能要求;
- 实现 GC 侧改动——Optimized Run + mark and sweep;
- 部署到 lakeFS Cloud。
Milestone 1 完成后(首个 beta 版本),Milestone 2 的目标聚焦于"解除 GC+ 作业运行期间 lakeFS 只读的限制",其依赖项正是本文第 2~6 节的 lakeFS 改动(Get/LinkPhysicalAddress、StageObject、CopyObject全家桶与 ref-store 跟踪),详见 Milestone 2 执行计划。
关键设计要点小结
- 两个流程(Flow)的分工:Clean Run 做全量(namespace 全列举 − 已提交 − 未提交);Optimized Run 用上一轮的
Uncommitted DF与"最后读到的 slice"做增量——先清理"上轮未提交、现已消失"的对象(Step 1),再只扫描到上一轮边界为止的 namespace 对象(Step 2)。一个重要的顺序约束是:未提交数据标记必须在已提交数据列举之前完成,以避免误删。 - 每轮 GC 的落盘信息:
Uncommitted DF(由PrepareUncommittedForGC写入)+ GC 报告(运行开始时间、最后读到的 slice),均保存在_lakefs/retention/gc/uncommitted/<run_id>/下,供下一轮 Optimized Run 使用。 - 已知限制(继承自提案):该方案依赖新的
data/分片结构,不具备向后兼容性,存量(旧结构)仓库需要另一套方案(Milestone 1 中对旧结构的 Clean Run 即为过渡支持);即便有分片与增量优化,GC 耗时仍高度依赖自上次运行以来的仓库变更量。
参考资料
- GC+ 执行计划(本文主体)
- 未提交 GC 设计提案
- Milestone 2 执行计划
- PrepareUncommittedForGC API 定义
- 未提交清单 Parquet 写出逻辑
- Link 地址签名校验
- CopyEntry 浅拷贝/全量拷贝
data/分片路径生成器- GC 数据存放路径模板
- 数据工程
- 数据湖
- 大数据
- 对象存储
- 后端
【免费下载链接】lakeFS
lakeFS - Data version control for your data lake | Git for data
相关推荐
lakeFS 未提交数据垃圾回收(Uncommitted GC)设计解析:基于分片扫描与时间令牌的离线回收方案
lakeFS 未提交数据垃圾回收(Uncommitted GC)设计解析:基于分片扫描与时间令牌的离线回收方案 导读 lakeFS 的常规 GC(Garbage
数据工程数据湖大数据对象存储后端lakeFS Python SDK 模型解读:PrepareGCUncommittedResponse 与未提交数据垃圾回收实战
lakeFS Python SDK 模型解读:PrepareGCUncommittedResponse 与未提交数据垃圾回收实战 导读 本文围绕 lakeFS
数据工程数据湖大数据对象存储后端lakeFS 未提交数据垃圾回收:Rust SDK 中 PrepareGcUncommittedResponse 模型深度解析
lakeFS 未提交数据垃圾回收:Rust SDK 中 PrepareGcUncommittedResponse 模型深度解析 导读 在 lakeFS 的数据湖
数据工程数据湖大数据对象存储后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考