Feast 差异引擎深度解析:registry diff 与 infra diff 如何驱动 `feast plan` / `feast apply`
2026/9/18 6:47:24 网站建设 项目流程

Feast 差异引擎深度解析:registry diff 与 infra diff 如何驱动feast plan/feast apply

【免费下载链接】feastThe Open Source Feature Store for AI/ML项目地址: https://gitcode.com/GitHub_Trending/fe/feast

导读

在 Feast 中,每次执行feast apply之前,系统都要回答一个问题:当前注册表(registry)与基础设施(infrastructure)中已存在的内容,和 feature repo 中声明的期望状态相比,到底差在哪里?答案是feast.diff包。该包是 Feast 元数据与基础设施变更的核心引擎,它把"对象级增删改查"和"属性级字段对比"抽象为可序列化的差异结构,驱动feast plan的预演输出、feast apply的落地执行,以及 apply 过程中的进度展示。读完本文,你将掌握 Feast diff 机制的四层模块划分、五种变更状态(TransitionType)的语义、属性级对比的实现原理,以及它在 feature_store.py 与 repo_operations.py 中的完整调用链。

本文对应的 API 文档为 feast.diff.rst,它列出该包的 4 个子模块;本文将结合 sdk/python/feast/diff/ 下的全部源码与 sdk/python/tests/unit/diff/ 测试用例,对每个子模块逐一深入。

一、feast.diff 包概览:四个子模块的分工

从 feast.diff.rst 可以看到,feast.diff是一个标准 Python 包,包含 4 个子模块,它们共同构成一条"定义差异 → 分类差异 → 属性级对比 → 落地应用"的流水线:

子模块核心职责关键类型
feast.diff.property_diff定义最小差异单元与变更状态枚举PropertyDiffTransitionType
feast.diff.registry_diff注册表对象(Entity、FeatureView 等)的差异计算与落库FeastObjectDiffRegistryDiffdiff_betweenapply_diff_to_registry
feast.diff.infra_diff基础设施对象(在线存储表)的差异计算与更新InfraObjectDiffInfraDiffdiff_infra_protos
feast.diff.apply_progressapply 过程的双进度条追踪ApplyProgressContext

值得一提的是,文档中提到的feast.diff.apply_progress模块在运行时还会尝试导入feast.diff.progress_utils(提供create_positioned_tqdmget_color_for_phaseis_tty_available等辅助函数),并在导入失败时优雅降级为无进度条模式,这一细节保证了单元测试与无 TTY 环境(如 CI)下 apply 流程依然可用,见 apply_progress.py。

二、property_diff:差异的最小原子单元

一切 diff 最终都归结为属性的变化。property_diff.py 只定义了两个类型:

@dataclass class PropertyDiff: property_name: str val_existing: str val_declared: str class TransitionType(Enum): UNKNOWN = 0 CREATE = 1 DELETE = 2 UPDATE = 3 UNCHANGED = 4
  • PropertyDiff:一条属性级差异,记录"哪个属性(property_name)从旧值(val_existing)变成了新值(val_declared)"。注意命名语义:val_existing代表注册表中已存在的值,val_declared代表 feature repo 中声明(期望)的值。
  • TransitionType:五种状态枚举,是 diff 系统的通用语言。UNKNOWN为未知/未初始化状态,CREATE/DELETE/UPDATE/UNCHANGED则分别对应对象的创建、删除、更新和保持不变。该枚举同时被registry_diffinfra_diff复用,保证两层差异的语义完全一致。

在输出层面(见下文RegistryDiff.to_string),四种有效状态被映射为带颜色的动作词:Created(绿色)、Deleted(红色)、Updated(黄色)、Unchanged(浅蓝),让feast plan的控制台输出一眼可辨。

三、registry_diff:注册表对象的差异计算与落地

feast.diff.registry_diff是整套机制的核心,负责 Feast 中全部对象类型的差异处理,包括 Project、DataSource、Entity、FeatureService、FeatureView、OnDemandFeatureView、StreamFeatureView、LabelView、ValidationReference、SavedDataset、Permission 等。

3.1 对象级分类:tag_objects_for_keep_delete_update_add

首先,系统需要把"已有对象集合"与"期望对象集合"按**名称(name)**对齐,分成四类。核心函数是tag_objects_for_keep_delete_update_add(registry_diff.py):

existing_obj_names = {e.name for e in existing_objs if e.name} desired_obj_names = {e.name for e in desired_objs if e.name} objs_to_add = {e for e in desired_objs if e.name not in existing_obj_names} objs_to_update = {e for e in desired_objs if e.name in existing_obj_names} objs_to_keep = {e for e in existing_objs if e.name in desired_obj_names} objs_to_delete = {e for e in existing_objs if e.name not in desired_obj_names}

逻辑非常直观:以名称为唯一键,期望集合中有而现有集合没有的 →add;两边都有 →update(同时进入 keep,因为新旧对象需要成对出现才能做属性对比);现有集合中有而期望集合没有的 →deleteif e.name这一条件是为尚未强制命名的 DataSource 预留的兼容处理(源码中的 TODO 注释亦印证此点)。

extract_objects_for_keep_delete_update_add(registry_diff.py)则把这一分类逻辑按对象类型批量执行:通过FeastObjectType.get_objects_from_registry(registry, current_project)从注册表拉取当前状态,通过FeastObjectType.get_objects_from_repo_contents(desired_repo_contents)RepoContents拉取期望状态,然后遍历FEAST_OBJECT_TYPES中的每种类型分别分类,最终返回四个"类型 → 对象集合"的字典。

3.2 属性级对比:diff_registry_objects

分类只解决"哪些对象变了",具体"变了哪些字段"由diff_registry_objects(registry_diff.py)完成。它的实现要点:

  1. 统一转 proto 比较:把新旧 Feast 对象分别to_proto(),并断言两者的 protoDESCRIPTOR.full_name一致(类型必须匹配)。除 DataSource 与 ValidationReference 直接使用顶层 proto 外,其余对象取proto.spec作为比较基准。
  2. 逐字段比较:遍历current_spec.DESCRIPTOR.fields,跳过FIELDS_TO_IGNORE = {"project"}中的字段(project 属于命名空间隔离信息,不参与 diff);任一字段新旧值不同,即产生一条PropertyDiff,并将该对象的transition_type置为UPDATE
  3. UDF 特殊处理:当比较到feature_transformation字段时(OnDemandFeatureView 场景),不会直接比较整个字段,而是深入到user_defined_function的子字段逐项比较,并跳过body字段——因为 UDF 的函数体(Python 源码)变化会频繁触发不必要的 diff,Feast 只关心函数名、资源、环境等元信息的变化。代码中还兼容了旧版spec.user_defined_function字段到新版feature_transformation.user_defined_function的迁移场景。

最终返回一个FeastObjectDiff(registry_diff.py),它聚合了对象名、对象类型、新旧对象本身、属性差异列表与变更状态。

3.3 汇总与输出:RegistryDiff 与 to_string

RegistryDiff(registry_diff.py)持有feast_object_diffs列表,并提供两个关键行为:

  • add_feast_object_diff:追加一条对象差异;
  • to_string:生成人类可读的彩色摘要。输出时跳过三类对象:名为DUMMY_ENTITY_NAME的虚拟实体(Feast 内部用于无实体 FeatureView 的占位实体)、UNCHANGED状态对象,以及DATA_SOURCE类型(源码 TODO 注明自 Feast 0.24 起计划打印 DataSource 变更)。若无任何变更,则输出No changes to registryUPDATE状态的差异会进一步缩进打印每条PropertyDiff属性名: 旧值 -> 新值

3.4 主入口:diff_between 与 apply_diff_to_registry

diff_between(registry, current_project, desired_repo_contents)(registry_diff.py)是 registry 层 diff 的总入口:先调用extract_objects_for_keep_delete_update_add得到四类对象,然后——add对象直接生成CREATE差异、delete对象直接生成DELETE差异、update对象通过diff_registry_objects生成(可能为UPDATEUNCHANGED)差异。

apply_diff_to_registry(registry, registry_diff, project, commit=True, no_promote=False)(registry_diff.py)则把差异反向落地

  • DELETE:按对象类型分发到registry.delete_entity/delete_feature_service/delete_feature_view/delete_data_source/delete_permission,且统一commit=False延迟提交。注释解释了设计意图:UPDATE 无需先删除——直接 apply 新对象会自动覆盖旧对象;
  • CREATE / UPDATE:按类型分发到apply_project/apply_data_source/apply_entity/apply_feature_service/apply_feature_view/apply_permission;其中 FeatureView 家族的 apply 会透传no_promote参数——当no_promote=True时,新版本快照只保存、不提升为激活定义(与 Feast 的 FeatureView 版本管理机制联动);
  • 最后根据commit参数决定是否调用registry.commit()一次性持久化。

四、infra_diff:基础设施的差异计算与更新

注册表差异解决"元数据"问题,基础设施差异则解决"物理资源"问题。infra_diff.py 目前支持两类在线存储表对象:datastore table(Google Datastore)与sqlite table,通过InfraObjectProto = TypeVar(..., DatastoreTableProto, SqliteTableProto)泛型约束(infra_diff.py)。

diff_infra_protos(current_infra_proto, new_infra_proto, project=None)(infra_diff.py)是入口,要点如下:

  1. 按类型分组:通过get_infra_object_protos_by_typeinfra_object_class_typeInfraproto 中筛选出对应类型的对象列表;
  2. 项目前缀过滤:当使用共享在线存储时(project参数非空),由于表名带有{project}_前缀,会先按前缀过滤,避免跨项目互相干扰;
  3. 三分类对比tag_infra_proto_objects_for_keep_delete_add按名称把对象分为 keep / delete / add——注意基础设施对象没有单独的 update 集合,UPDATE 是通过"keep 集合中的新旧对象成对比较"得到的;
  4. 属性级对比diff_between(infra_diff.py)与 registry 层同理,遍历 proto 字段、跳过project字段、生成PropertyDiff列表。

InfraDiff.update(progress_ctx=None)(infra_diff.py)是执行入口:DELETE/UPDATE对象先teardown()旧资源,CREATE/UPDATE对象再update()新资源,全程可通过progress_ctx.update_phase_progress上报"正在拆除/创建 xxx"的进度。InfraDiff.to_string()RegistryDiff.to_string()结构一致,无变更时输出No changes to infrastructure

五、apply_progress:apply 过程的双进度条追踪

feast plan可以纯预演,但feast apply是实打实的多阶段执行。为了让用户看到执行进度,apply_progress.py 提供了ApplyProgressContext

  • 双进度条模型:position 0 是"整体进度"(Applying changes,默认total_phases = 3个阶段),position 1 是"阶段内进度"(当前阶段的操作数);
  • 阶段流转start_overall_progress初始化总进度 →start_phase(phase_name, operations_count)开启新阶段 →update_phase_progress(description)推进当前阶段并更新 postfix 描述 →complete_phase关闭阶段条并推进总进度条(附带(n/3 phases)计数)→cleanupfinally块中统一收尾;
  • 健壮性设计:所有 tqdm 操作都被try/except (TypeError, AttributeError)包裹,且通过is_tty_available()检测终端能力——非 TTY(CI、日志重定向)或progress_utils导入失败时直接跳过进度条,不影响主流程。

六、端到端调用链:plan → apply 的完整协作

把四个子模块串起来的正是FeatureStore与 CLI 层。

6.1 FeatureStore.plan:预演阶段

FeatureStore.plan()(feature_store.py)是 dry-run 入口,流程为:

  1. 校验 FeatureView 并做类型推断(_validate_all_feature_views_make_inferences);
  2. registry_diff = diff_between(self.registry, self.project, desired_repo_contents)—— 计算元数据差异;
  3. 刷新注册表、清空 FeatureService 缓存,取得current_infra_proto
  4. new_infra = self.provider.plan_infra(...)让 provider 基于期望注册表 proto 规划目标基础设施;
  5. infra_diff = diff_infra_protos(current_infra_proto, new_infra_proto, project=self.project)—— 计算基础设施差异;
  6. 返回(registry_diff, infra_diff, new_infra)三元组。

CLI 侧的feast plan命令(repo_operations.py 起的plan函数)拿到该三元组后调用click.echo(registry_diff.to_string())输出彩色差异摘要,全程不修改注册表。

6.2 _apply_diffs:执行阶段

FeatureStore._apply_diffs()(feature_store.py)按两个阶段落地:

  1. 基础设施阶段progress_ctx.start_phase("Updating infrastructure", infra_ops_count)infra_diff.update(progress_ctx)
  2. 注册表阶段progress_ctx.start_phase("Updating registry", 2)apply_diff_to_registry(self.registry, registry_diff, self.project, commit=False, no_promote=no_promote)

CLI 的apply_total_with_repo_instance(repo_operations.py)会先调用store.plan(...)打印预演结果,再创建ApplyProgressContext并调用_apply_diffs,从而让一次feast apply呈现"先看差异、再逐阶段执行"的完整体验。此外,apply 后的 registry diff 还会被用于 MLflow 与 OpenLineage 集成(_mlflow_log_apply_diffs_emit_openlineage_apply_diffs,见 feature_store.py),说明 diff 结构是 Feast 观测性生态的共享底座。

6.3 测试验证

diff 逻辑有专门的单元测试保障:

  • test_registry_diff.py:用tag_objects_for_keep_delete_update_add构造"新增 to_add / 删除 to_delete / 修改 fv2 的 tags"场景,断言 keep=2、delete=1、update=2、add=1 的分类结果;并用diff_registry_objects验证属性级对比能识别出tags字段从{"when": "before"}{"when": "after"}的变化;
  • test_infra_diff.py:对应验证基础设施 proto 的 keep / delete / add 分类与属性差异。

这两组测试文件是理解各函数输入输出约定的最佳"可执行文档"。

七、小结:diff 机制的设计要点

回顾整个feast.diff包,可以提炼出四条贯穿始终的设计原则:

  1. 统一的变更语言TransitionType(CREATE / DELETE / UPDATE / UNCHANGED)与PropertyDiff(属性名 + 新旧值)同时用于注册表层与基础设施层,让两层差异可共享相同的渲染与执行逻辑;
  2. proto 作为对比基准:无论 registry 还是 infra 对象,最终都序列化为 protobuf 后按DESCRIPTOR.fields逐字段对比,天然支持新增字段的向前兼容,并通过FIELDS_TO_IGNORE = {"project"}屏蔽命名空间噪音;
  3. 预演与执行分离plan只计算(diff_between / diff_infra_protos)不落库,apply才执行(apply_diff_to_registry / InfraDiff.update),配合commit=False的延迟提交模式实现事务化落地;
  4. 面向可观测性与健壮性:彩色to_string摘要服务于 CLI 展示,ApplyProgressContext的双进度条与 TTY 降级保证了大仓库 apply 时的可读性与 CI 兼容性。

无论你是想理解feast plan输出中每一条Updated fv_name: xxx -> yyy的来源,还是准备为 Feast 接入新的在线存储类型(只需扩展infra_diff中的InfraObjectProto与类型映射),sdk/python/feast/diff/ 都是必读的第一站。

【免费下载链接】feastThe Open Source Feature Store for AI/ML项目地址: https://gitcode.com/GitHub_Trending/fe/feast

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

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

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

立即咨询