- 数据工程
- 数据湖
- 大数据
- 对象存储
- 后端
【免费下载链接】lakeFS
lakeFS - Data version control for your data lake | Git for data
导读
在 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_id | String | 本次 GC 运行的唯一标识 | 必填 |
| gc_uncommitted_location | String | 未提交信息数据的存储位置(location of uncommitted information data) | 必填 |
| continuation_token | Option<String> | 分页续传令牌 | 可选 |
在 swagger.yml 的 schema 定义中,run_id与gc_uncommitted_location被标记为required,而continuation_token不是——这一点与 Rust 结构体中的Option<String>一一对应。下面逐一展开三个字段的深层语义。
3.1 run_id:一次 GC 运行的身份证
run_id标识一次"准备未提交 GC 数据"的运行。它的重要性体现在:
- 关联未提交数据文件:本次运行产出的 Parquet 数据文件会被存放到以
run_id命名的目录下(详见 3.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 数据"流程如下:
- 客户端发起请求(Rust 程序调用
prepare_garbage_collection_uncommitted,首次调用时请求体传None或空 token); - 授权检查:服务端 handler
PrepareGarbageCollectionUncommitted(controller.go)首先校验调用方是否具备PrepareGarbageCollectionUncommittedAction权限,资源为RepoArn(repository); - 令牌解码:将请求体中的
continuation_token解码为GCUncommittedMark;若为空则mark = nil,表示从头开始; - 数据扫描与生成:
Catalog.PrepareGCUncommitted(catalog.go)调用gcWriteUncommitted(gc_write_uncommitted.go),逐个遍历分支的 staging 区域:- 使用 parquet-go 写出临时文件,压缩方式为GZIP(
parquet.CompressionCodec_GZIP),行结构为UncommittedParquetObject { physical_address, creation_date }; - 受两个上限约束:单文件大小上限
maxFileSize与准备时间预算prepareDuration,达到任一上限即截断并生成下一个 mark;
- 使用 parquet-go 写出临时文件,压缩方式为GZIP(
- 上传与响应:若
count > 0(存在未提交数据),把临时 Parquet 文件通过uploadFile(catalog.go)上传到gc_uncommitted_location目录下,文件名是xid.New().String()生成的唯一 ID;随后构造PrepareGCUncommittedInfo,服务端将其映射为 HTTP 201 响应(run_id、gc_uncommitted_location、continuation_token)返回客户端; - 客户端循环:若响应中
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 作业)——应按以下步骤处理:
- 轮询调用
prepare_garbage_collection_uncommitted直到continuation_token为空,期间校验各轮返回的run_id一致; - 从
gc_uncommitted_location读取所有 Parquet 文件,得到Uncommitted DF(物理地址 + 创建日期); - 对命名空间全量列表(
Store DF)减去已提交对象(Committed DF)再减去Uncommitted DF,得到可安全删除的候选列表; - 特殊路径文件(如
_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
相关推荐
Path of Building中文版PoeCharm完整指南:流放之路角色构建深度解析
Path of Building中文版PoeCharm完整指南:流放之路角色构建深度解析 PoeCharm是《流放之路》社区最专业的角色构建工具Path of
数据工程数据湖大数据对象存储后端lakeFS Rust SDK 中的 PrepareGcUncommittedRequest 模型:解锁未提交对象垃圾回收的分页 API
lakeFS Rust SDK 中的 PrepareGcUncommittedRequest 模型:解锁未提交对象垃圾回收的分页 API 导读 PrepareG
数据工程数据湖大数据对象存储后端Flask 设计决策深度解析:显式应用对象、路由系统、上下文局部变量与 async 支持背后的权衡
Flask 设计决策深度解析:显式应用对象、路由系统、上下文局部变量与 async 支持背后的权衡 导读 本文基于 Flask 官方文档 docs/design
数据工程数据湖大数据对象存储后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考