☰
lakeFS 未提交数据垃圾回收:Rust SDK 中 PrepareGcUncommittedResponse 模型深度解析
2026/10/13 2:52:40 网站建设 项目流程
  • 数据工程
  • 数据湖
  • 大数据
  • 对象存储
  • 后端

【免费下载链接】lakeFS

lakeFS - Data version control for your data lake | Git for data

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

导读

在 lakeFS 的数据湖版本控制体系中,未提交(uncommitted)数据会因对象删除、覆盖或分支重置而变成"孤儿数据",长期占用存储并带来合规风险。PrepareGcUncommittedResponse是 lakeFS 内部 GC 接口POST /repositories/{repository}/gc/prepare_uncommited的响应模型,它向 GC 客户端(如 Spark 作业)返回本次 GC 运行的run_id、未提交数据文件的存储位置以及分页续传令牌。读完本文,你将完整掌握该模型的三个字段语义、Rust SDK 中的序列化实现、后端数据生成链路的真实工作原理,以及如何在 Rust 程序中正确消费这个响应以驱动未提交数据垃圾回收。

一、背景:为什么未提交数据需要 GC

lakeFS 的 GC(垃圾回收)设计遵循一个核心思想:对象存储中"存在但不被引用"的数据应当被清除。根据 uncommitted-gc.md 的说明,未提交数据的来源非常典型:

  • 分阶段写入(staging)后被删除或覆盖的对象;
  • reset branch等操作遗留下来的孤儿对象;
  • 拷贝(copy)操作产生的临时数据。

GC 过程由三部分构成:列出现名空间对象(namespace listing)、列出已提交对象(committed objects)、列出未提交对象(uncommitted objects)。对象存在于存储中、但既不在已提交列表也不在未提交列表中,即可被安全删除。PrepareGcUncommittedResponse所对应的接口,正是"列出未提交对象"这一步的服务端实现入口——它把仓库的未提交对象信息(物理地址 + 创建时间)写入结构化数据文件,供 GC 客户端读取比对。

下图展示了为支撑大规模命名空间扫描而设计的仓库分片(slice)结构,这也是 uncommitted GC 在存储布局层面上的基础:

二、接口概览:内部 API 的职责与位置

该响应模型对应的接口在 swagger.yml 中定义如下:

/repositories/{repository}/gc/prepare_uncommited: parameters: - in: path name: repository required: true schema: type: string post: tags: - internal operationId: prepareGarbageCollectionUncommitted summary: save repository uncommitted metadata for garbage collection responses: 201: description: paths to commit dataset content: application/json: schema: $ref: "#/components/schemas/PrepareGCUncommittedResponse"

几个关键事实:

  • 这是一个POST请求,路径为/repositories/{repository}/gc/prepare_uncommited(注意 lakeFS 官方拼写即为uncommited,与英文单词uncommitted相差一个字母,使用时请勿"纠正");
  • 它属于internal标签,面向 GC 客户端这类系统级调用方;
  • 成功时返回201 Created,而非 200;
  • 请求体可选,schema 为PrepareGCUncommittedRequest(仅含可选字段continuation_token)。

三、PrepareGcUncommittedResponse 字段详解

按照 PrepareGcUncommittedResponse.md 的定义,响应模型包含以下字段:

名称类型描述备注
run_idString本次 GC 运行的唯一标识必填
gc_uncommitted_locationString未提交信息数据的存储位置(location of uncommitted information data)必填
continuation_tokenOption<String>分页续传令牌可选

在 swagger.yml 的 schema 定义中,run_id与gc_uncommitted_location被标记为required,而continuation_token不是——这一点与 Rust 结构体中的Option<String>一一对应。下面逐一展开三个字段的深层语义。

3.1 run_id:一次 GC 运行的身份证

run_id标识一次"准备未提交 GC 数据"的运行。它的重要性体现在:

  1. 关联未提交数据文件:本次运行产出的 Parquet 数据文件会被存放到以run_id命名的目录下(详见 3.2);
  2. 保证分页一致性:当一次准备过程因数据量过大被拆分为多次请求时,后续请求携带的continuation_token中会包含同一个run_id,服务端据此判定这些请求属于同一次运行。

从源码看,run_id由Catalog.PrepareGCUncommitted在首次调用(mark == nil)时通过c.Store.GCNewRunID()生成(catalog.go),底层委托给 graveler 层的GCNewRunID()(graveler.go)。分页续传时则直接沿用既有run_id,确保同一批次的未提交数据文件归属同一个目录。

3.2 gc_uncommitted_location:未提交数据文件的存放位置

gc_uncommitted_location表示本次运行生成的未提交数据文件在底层对象存储中的目录位置。

根据设计文档 uncommitted-gc.md 的约定,未提交数据被保存到:

_lakefs/retention/gc/uncommitted/{run_id}/uncommitted/

即:在仓库的存储命名空间(storage namespace)下,固定前缀_lakefs/retention/gc/uncommitted/+ 运行 ID +uncommitted/子目录。GC 客户端读取这些文件中的物理地址列表(Uncommitted DF),与命名空间全量列表、已提交对象列表做集合差运算,从而定位可删除的孤儿对象。

服务端获取该位置的调用链是:Catalog.PrepareGCUncommitted→Store.GCGetUncommittedLocation(repository, runID)(catalog.go)→Graveler.GCGetUncommittedLocation(graveler.go)。注意一个细节:只有当确实扫描到未提交数据(hasData == true)时,响应中的gc_uncommitted_location才会有实际值;如果仓库中没有任何未提交对象,该字段可能为空字符串。这一点在 catalog_test.go 的测试场景("no objects")中得到了明确验证。

3.3 continuation_token:分页游标

continuation_token是可选字段,但它恰恰是支撑海量未提交数据处理的关键设计。由于单次请求可能因为文件大小上限或时间预算而被截断,服务端通过该令牌告知客户端"从哪里继续"。

从实现看,令牌是GCUncommittedMark结构体的Base64 编码 JSON:

// pkg/catalog/catalog.go type GCUncommittedMark struct { BranchID graveler.BranchID `json:"branch"` Path Path `json:"path"` RunID string `json:"run_id"` Key string `json:"key"` }
  • 响应中的令牌由encodeGCUncommittedMark(uncommittedInfo.Mark)生成(controller.go),即对 mark 做json.Marshal后再base64.StdEncoding.EncodeToString(controller.go);
  • 客户端在下一轮请求中把该令牌放入PrepareGcUncommittedRequest.continuation_token,服务端用decodeGCUncommittedMark解码(controller.go)并恢复扫描位置;
  • 当令牌为空(nil)时,表示本次准备过程已全部完成,客户端应停止轮询。

四、Rust SDK 中的模型与 API 绑定

4.1 模型结构体

在 Rust SDK 中,该模型定义于 prepare_gc_uncommitted_response.rs,由 OpenAPI Generator 生成:

#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct PrepareGcUncommittedResponse { #[serde(rename = "run_id")] pub run_id: String, /// location of uncommitted information data #[serde(rename = "gc_uncommitted_location")] pub gc_uncommitted_location: String, #[serde(rename = "continuation_token", skip_serializing_if = "Option::is_none")] pub continuation_token: Option<String>, } impl PrepareGcUncommittedResponse { pub fn new(run_id: String, gc_uncommitted_location: String) -> PrepareGcUncommittedResponse { PrepareGcUncommittedResponse { run_id, gc_uncommitted_location, continuation_token: None, } } }

实现要点:

  • #[serde(rename = "run_id")]等属性把 Rust 的 snake_case 字段名与 API 的 snake_case JSON 键精确对齐;
  • continuation_token声明为Option<String>,并配合skip_serializing_if = "Option::is_none",表示该字段在反序列化时允许缺失(swagger.yml 中该字段无 required 标记);
  • 构造函数new()只接收两个必填参数,continuation_token默认置为None,与 API schema 的 required 列表(run_id、gc_uncommitted_location)完全吻合。

对应的请求模型 prepare_gc_uncommitted_request.rs 则只有可选的continuation_token字段:

pub struct PrepareGcUncommittedRequest { #[serde(rename = "continuation_token", skip_serializing_if = "Option::is_none")] pub continuation_token: Option<String>, }

4.2 调用函数

Rust SDK 暴露的调用入口位于 internal_api.rs:

pub async fn prepare_garbage_collection_uncommitted( configuration: &configuration::Configuration, repository: &str, prepare_gc_uncommitted_request: Option<models::PrepareGcUncommittedRequest>, ) -> Result<models::PrepareGcUncommittedResponse, Error<PrepareGarbageCollectionUncommittedError>>

从生成代码可以确认该函数的关键行为:

  • 请求方法为POST,路径为{base_path}/repositories/{repository}/gc/prepare_uncommited,repository会经过 URL 编码;
  • 请求体通过local_var_req_builder.json(&prepare_gc_uncommitted_request)以application/json序列化;
  • 认证支持basic_auth与bearer_access_token(与 InternalApi.md 列出的basic_auth、cookie_auth、jwt_token三种授权方式一致);
  • 成功时返回反序列化后的PrepareGcUncommittedResponse;HTTP 状态码为 4xx/5xx 时返回Error::ResponseError。

五、一次典型调用:从客户端请求到 Parquet 文件落盘

结合后端源码,一次完整的"准备未提交 GC 数据"流程如下:

  1. 客户端发起请求(Rust 程序调用prepare_garbage_collection_uncommitted,首次调用时请求体传None或空 token);
  2. 授权检查:服务端 handlerPrepareGarbageCollectionUncommitted(controller.go)首先校验调用方是否具备PrepareGarbageCollectionUncommittedAction权限,资源为RepoArn(repository);
  3. 令牌解码:将请求体中的continuation_token解码为GCUncommittedMark;若为空则mark = nil,表示从头开始;
  4. 数据扫描与生成:Catalog.PrepareGCUncommitted(catalog.go)调用gcWriteUncommitted(gc_write_uncommitted.go),逐个遍历分支的 staging 区域:
    • 使用 parquet-go 写出临时文件,压缩方式为GZIP(parquet.CompressionCodec_GZIP),行结构为UncommittedParquetObject { physical_address, creation_date };
    • 受两个上限约束:单文件大小上限maxFileSize与准备时间预算prepareDuration,达到任一上限即截断并生成下一个 mark;
  5. 上传与响应:若count > 0(存在未提交数据),把临时 Parquet 文件通过uploadFile(catalog.go)上传到gc_uncommitted_location目录下,文件名是xid.New().String()生成的唯一 ID;随后构造PrepareGCUncommittedInfo,服务端将其映射为 HTTP 201 响应(run_id、gc_uncommitted_location、continuation_token)返回客户端;
  6. 客户端循环:若响应中continuation_token非空,携带该令牌再次调用,直到令牌为空为止。测试 TestCatalog_PrepareGCUncommitted 完整验证了这一循环:包括 0 分支、0 对象、常规规模以及 500 分支 × 500 记录的"tokenized"分页场景,并校验每次返回的run_id保持一致。

六、影响分页行为的两项配置

分页的粒度由服务端配置决定,默认值定义在 defaults.go,对应配置结构在 config.go:

ugc: prepare_interval: 1m # 单次准备运行的时间预算(默认 1 分钟) prepare_max_file_size: 20971520 # 单份未提交数据文件的大小上限(默认 20 MiB)
  • ugc.prepare_interval:在 gc_write_uncommitted.go 中作为"时间预算"检查,超时则截断并把下一分支写进 mark——这正是continuation_token需要承载BranchID的原因;
  • ugc.prepare_max_file_size:控制单文件写入量,由UncommittedWriter在写入时累计字节数(catalog.go),达到上限即滚动出下一个 mark。

这两个参数直接决定了一次prepareGarbageCollectionUncommitted调用返回的continuation_token是否为空:数据量小、耗时短时一次调用即完成(token 为空);海量分支/对象时则需多轮携带 token 轮询。设计文档给出的性能参考是:5M 未提交对象、1K 分支时,文件按约 20MB 目标大小被切分为 3 份。

七、GC 客户端如何使用该响应

按照设计文档中的 Clean Run(干净运行)流程(uncommitted-gc.md),PrepareGcUncommittedResponse的消费者——GC 客户端(典型实现为 Spark 作业)——应按以下步骤处理:

  1. 轮询调用prepare_garbage_collection_uncommitted直到continuation_token为空,期间校验各轮返回的run_id一致;
  2. 从gc_uncommitted_location读取所有 Parquet 文件,得到Uncommitted DF(物理地址 + 创建日期);
  3. 对命名空间全量列表(Store DF)减去已提交对象(Committed DF)再减去Uncommitted DF,得到可安全删除的候选列表;
  4. 特殊路径文件(如_lakefs/内部数据)需额外过滤。

在 esti/unified_gc_test.go 的端到端集成测试中,可以观察到完整的闭环验证:测试预先通过创建/删除事件制造未提交对象,然后以uncommitted_min_age_seconds等参数驱动 GC 运行,最终断言过期未提交对象被清理、仍在引用的数据得以保留。

八、常见问题与注意事项

  • 接口路径拼写:/gc/prepare_uncommited是官方拼写(少一个t),由 swagger.yml 与生成的 Rust 代码共同固定,调用时不要"修正";
  • HTTP 状态码:成功是201而非 200,Rust SDK 的Error分支会按 4xx/5xx 返回ResponseError,编写重试逻辑时应注意;
  • 空位置:仓库无未提交数据时,gc_uncommitted_location为空,GC 客户端应容忍该场景,不读取任何文件;
  • 只读仓库:若目标仓库处于只读状态,服务端返回graveler.ErrReadOnlyRepository错误(catalog.go);
  • 权限要求:调用方需具备仓库级PrepareGarbageCollectionUncommittedAction权限(controller.go);
  • 令牌失效:continuation_token是 Base64 编码的状态标记,包含分支、路径、键与run_id,不应长期缓存复用;新一轮 GC 运行应使用新的空令牌从头开始。

九、小结

PrepareGcUncommittedResponse虽然只有三个字段,却是 lakeFS 未提交数据 GC 流程中"准备阶段"的服务端出口:run_id锁定运行批次,gc_uncommitted_location指向存放未提交物理地址列表的 Parquet 文件位置,continuation_token驱动跨请求分页续传。结合 Rust SDK 的模型定义(prepare_gc_uncommitted_response.rs)与调用函数(internal_api.rs),以及后端从 controller.go 到 catalog.go 再到 gc_write_uncommitted.go 的完整链路,开发者可以准确无误地在自定义 GC 客户端中驱动这一内部接口,实现针对未提交孤儿数据的安全回收。

  • 数据工程
  • 数据湖
  • 大数据
  • 对象存储
  • 后端

【免费下载链接】lakeFS

lakeFS - Data version control for your data lake | Git for data

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

相关推荐

上一篇:PL-2303芯片Windows 10驱动完整解决方案:让旧设备重获新生
下一篇:在Windows上直接安装Android应用:APK Installer完整指南

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

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

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

立即咨询