题干与适用场景
公司有 30,000 个数据集,每天通过 Airflow、Spark、dbt 和 Kafka 运行 200,000 次数据任务。团队需要回答三个 问题:某个数据集或字段从哪里来;准备修改它时会影响什么;某次具体运行究竟读取和产出了什么。请设计一套数据 血缘系统,采集表级与列级依赖,支持上下游遍历和历史时点查询,正确处理重试、失败与部分写入,遵守元数据访问 权限,并告诉使用者当前血缘图是否完整到足以支撑决策。
本题假设事件写入峰值为每秒 500 条,三跳查询通常应在 2 秒内完成,详细运行历史保留 1 年。这些数字是面试场景 输入,不是行业基准;方案还要说明上线后如何用真实流量重新校准容量和目标。
2026 年公开的数据工程面试材料直接询问如何设计数据血缘,并覆盖表、列、任务三个层级,以及元数据采集、统一 命名和变更影响。OpenLineage 1.50.0 可作为事实底座:核心实体是 Job、Run、Dataset;Job 与 Dataset 由命名空间和 名称标识;Run 使用 UUID;运行事件有明确状态;facet 可扩展 schema 和列依赖等元数据。核心考察能力是元数据 建模、数据任务语义、影响分析和数据治理,因此 category 为 data。
面试官考察点
第一,候选人会不会先处理标识和语义。同一张表如果同时以 orders、prod.orders 和仓库 URL 出现,或把一次 重试误当成新任务,图数据库再快也没有意义。必须先定义资源主键、环境边界、任务定义、运行 ID、字段路径和版本 语义。
第二,血缘是否贴近真实运行。代码或编译产物提供的声明血缘适合发布前分析;某次 Run 上报的观测血缘证明该次 执行实际读写了什么。失败运行可能已留下临时或部分输出,而执行成功也不等于数据正确。高质量方案会区分事件 证据、Run 状态、声明边、观测边和输出发布状态。
第三,系统设计是否闭环。答案应覆盖生产端集成、持久且幂等的写入链路、不可变原始事件、标准化、时态图物化、 遍历索引、历史重放、覆盖率、新鲜度和权限控制。只说“用图数据库存”跳过了真正困难的部分。
最后,能否诚实表达可信度。如果关键任务只有 60% 接入,界面却画出一张完整的图,影响分析会误导上线决策。 每条边都应显示来源、观测时间、推导方式和缺口,让使用者知道证据能支持什么结论。
回答前需要澄清的问题
- 血缘要支撑哪些决策? 故障排查、发布前影响分析、数据发现、PII 传播追踪和审计,对新鲜度、历史与精度的
要求不同。
- 什么算一个 Dataset? 表、视图、文件、对象前缀、Kafka Topic、物化视图、看板和特征都要定粒度。把每个
文件都建成永久节点可能让图失去可用性。
- 需要声明血缘、观测血缘,还是两者都要? 前者能描述尚未运行的改动,后者能关联一次具体 Run;界面不能
悄悄混为同一种事实。
- 列级精度到哪里? 字段直接参与值计算,与字段只参与 JOIN、过滤、分组、排序、窗口或条件判断,影响语义不同。
有些引擎能提供逻辑计划,有些只能可靠提供表级血缘。
- 跨环境如何标识 Dataset 和 Job? 需要定义 namespace、规范名称、别名、大小写、重命名和所有者;展示名称
不能充当稳定主键。
- 失败或部分完成后如何处理? 部分输出是已发布、隔离还是回滚?影响查询是否既要保留尝试记录,又不能把它
当成当前可信事实?
- 历史查询要保留多久? 保留 1 年 Run 事件,不代表所有展开后的列级边都必须在低延迟层保存 1 年。
- 哪些元数据敏感? SQL、字段名、所有者、PII 标签甚至数据集是否存在都可能受保护,遍历必须服从目录权限。
- 容量和服务目标是什么? 需要确认事件率、图规模、遍历深度、延迟分位数、恢复目标和运行结束到血缘可见的
最大延迟。
30 秒回答框架
“我会先定义规范的 Job、Run、Dataset 和 Field 标识,再区分声明与观测、表级与列级血缘。生产端通过认证、幂等 且有持久日志的网关上报带版本事件;消费者保留原始证据,并构建上下游时态索引。失败输出先作为诊断证据,确认 发布后才进入当前血缘。遍历受深度、时点和权限约束,并显示来源与缺口。我会监控延迟、预期任务覆盖率、终态 完整率、未解析标识、陈旧边和抽样路径正确率,先覆盖关键任务的表级血缘,再增加可靠的列级提取。”
分步骤深入解答
第一步:先定义真相模型,再选存储。
至少需要四类记录:
| 记录 | 稳定标识 | 用途 | |---|---|---| | Dataset | (namespace, name) 加环境 | 表、Topic、视图或刻意选择的逻辑数据集 | | Field | Dataset 标识加规范字段路径 | 某个 schema 版本内的列或嵌套字段 | | Job | (namespace, name) 加定义版本 | 周期性转换、任务、查询或模型 | | Run | 客户端生成的 UUID | Job 的一次执行;只有真正独立执行才把重试建成新 Run |
Dataset 的 namespace 通常来自数据源,Job 的 namespace 来自调度器或处理系统。别名单独保存并带有效期。把 analytics.orders 改名为 analytics.sales_orders 时,应产生明确的重命名或别名事件,不能靠字符串相似度自动 合并或拆分。
编译 SQL、dbt manifest 或配置得到的声明血缘,与 Run 实际发出的观测血缘分别建模;表级边与字段级边也 分别建模。字段边记录输出字段、输入字段、转换类型,以及直接值推导或间接影响。OpenLineage 的列级模型把直接 依赖分为原值、转换、聚合,把间接影响分为 JOIN、GROUP BY、FILTER、SORT、WINDOW 和 CONDITIONAL。字段改值、 改类型或删除时,这些差异会改变影响判断。
每条边至少带 validFrom、可选 validTo、observedAt、producer、源事件、Job 版本、Run ID、Run 状态、血缘种类 以及置信或推导方式。这样一条箭头才回答得了“何时有效”和“证据来自哪里”。
第二步:尽量在执行位置采集。
优先使用引擎或社区维护的集成:调度器监听任务生命周期;Spark 读取逻辑计划;dbt 使用 manifest 与 run results; 仓库和流处理系统读取连接器、查询历史或执行计划。不要把正则解析 SQL 当主路径。动态 SQL、宏、存储过程、临时 对象、UDF 和运行时分支都会让纯文本推断漏报或误报。
在血缘载荷外定义一层带版本的本平台写入信封:
{
"eventId": "producer-unique-id",
"producer": "spark-prod-eu",
"schemaVersion": "1.0",
"emittedAt": "2026-07-19T00:00:00Z",
"job": { "namespace": "spark-prod", "name": "daily_orders" },
"runId": "53ee3770-86fa-4cb9-8c31-a09072dd88f7",
"state": "COMPLETE",
"inputs": [{ "namespace": "warehouse-prod", "name": "raw.orders" }],
"outputs": [{ "namespace": "warehouse-prod", "name": "mart.daily_orders" }]
}这里的 eventId 是本平台为幂等规定的信封字段,不能说成所有外部血缘标准都强制包含它。生产端重试时复用同一 ID。网关认证生产端,校验 schema 兼容性和大小,补充接收时间,并在确认前写入分区持久日志。非法事件进入带 原因、生产端和安全载荷引用的隔离流,不能只留一行日志。
按 Run ID 分区,可以保持单次运行内的局部顺序,又让无关 Run 并行消费。事件可能迟到或时钟漂移,因此同时保存 发出时间和接收时间,并按状态机处理。OpenLineage 定义 START、RUNNING、COMPLETE、ABORT、FAIL、 OTHER 六种状态;终态不能被迟到的 START 重新打开,但原始事件仍完整保留。
第三步:标准化时保留来源。
标准化消费者把不同集成转换成规范标识和边语义,解析已登记别名、大小写、环境、临时 Dataset 和嵌套字段路径。 未知标识进入待解析队列,禁止猜测。原始事件、标准化结果、解析器版本和警告保持关联,映射错误才能修复并重放。
schema 变更产生带版本的字段定义。删除后重新创建同名 customer_id,不能默认它拥有连续历史。schema hash 或 目录版本配合有效期,可以让历史查询选中正确字段。流任务用 Topic 和转换 Job 作为稳定节点;分区与 offset 更适合 保存在 Run 证据中,避免把每个分区-offset 对永久展开成图节点。
执行状态和发布状态要分离。COMPLETE 代表执行结束,不代表业务数据正确。FAIL 或 ABORT 也可能上报输入和 尝试输出:这些边用于排障,但只有满足原子提交标记或独立质量门禁后,才进入当前已发布血缘,且必须明确标注。
第四步:从不可变事件构建时态图投影。
持久日志和不可变对象存储是恢复真源,消费者生成三个投影:
- 元数据存储:规范 Job、Dataset、Field、schema、别名、所有者和权限策略。
- 时态边存储:声明与观测血缘、有效期和执行来源。
- Run 存储:生命周期事件、输入输出快照、状态和诊断信息。
先做一笔可复算的容量估算,避免把每日 Run 数误当成事件吞吐。每天 200,000 次 Run 各自至少发一个 START 和一个 终态,每天就是 400,000 条事件,平均约每秒 4.6 条。若典型 Run 发一个 START、两个 RUNNING 和一个终态, 则每天 800,000 条,平均约每秒 9.3 条;题设每秒 500 条是突发目标,不是平均值。再假设原始载荷平均 20 KB, 800,000 条约占每天 16 GB、每年 5.8 TB,尚未计算压缩和副本。上线后必须实测单条大小与每 Run 事件数,因为 列级 facet 会显著改变估算。
低延迟查询同时维护上游和下游邻接索引,键包含规范节点 ID 与有效版本或时间桶。广度优先遍历必须限制深度、节点 数、边类型、环境和历史时点;触达上限时返回“结果不完整”与续查信息。图数据库可以实现,但不是必选项。当前 规模也可能用有索引的关系边表或键值邻接服务更简单,应以真实扇出和时态条件基准测试为准。
列级边远多于表级边。表级边和常用、近期的字段邻接放热层,低频旧列边放压缩历史层。不要预计算全图传递闭包: 高扇出、版本变化和权限都会让更新代价失控。缓存键应包含节点、方向、深度、时点、边类型和权限范围,相关边版本 变化后失效。
第五步:把查询契约说清楚。
接口至少支持:Dataset 或 Field 的上下游限深遍历;区分直接和间接依赖的 schema 变更影响分析;显示某次 Run 的 准确输入、输出、Job 版本、生命周期和发布状态;为每条边返回声明或观测、来源和最后观测时间;对未接入 Job、 未解析标识、陈旧 producer 与截断遍历返回缺口标记。
权限不能等遍历完成后才过滤。隐藏 Dataset 的名称和存在本身也可能敏感。扩展邻居时就要执行策略,按治理规则 省略或替换受保护节点,避免度数泄露隐藏邻居,并审计敏感查询。缓存键包含授权范围,防止把一个人的图返回给 另一个人。
针对三跳 2 秒目标,按方向、深度、扇出、历史条件以及表级或列级,分别测 P50、P95、P99。超过节点预算时返回 续查 token 或明确截断,不能悄悄给出残缺图。
第六步:支持重放、回填和恢复。
消费者记录持久日志 offset。处理是至少一次,因此投影写入用 eventId 和投影版本幂等。标准化逻辑出错时,发布 新解析器版本,从原始事件重建影子投影,对比数量并抽样验证路径,验证通过后再切读流量,不能直接覆盖唯一在线图。
原始事件按题设加密保留 1 年并执行生命周期策略。快照可以缩短恢复时间,但必须证明“快照加后续事件”能得到同一 投影。定义恢复目标,演练写入区域故障,并确认生产端重试不会增加重复边。
历史血缘要依据边的有效期和观测时间,不能在当前图上随便贴一个过去时间。查询上个月时,要解析当时有效的标识、 schema 版本和授权边。生产端从未提供历史时,应如实返回限制。
第七步:把可信度做成产品能力。
至少监控以下信号:
| 信号 | 能发现什么 | |---|---| | 各 producer 写入延迟和拒收率 | 图是否新鲜,协议是否仍兼容 | | 预期 Job 上报覆盖率 | 哪些已调度 Job 没有血缘事件 | | 终态事件完整率 | 只有 START、没有终态的 Run | | 标识解析失败率 | 因未知或冲突名称而悬空的边 | | 观测边新鲜度 | 多久没有由近期成功发布再次证实 | | 分关键等级的表级与列级覆盖 | 重要资产是否达到需要的精度 | | 抽样路径正确率 | 已知输入输出和真实运行能否返回预期路径 | | 遍历截断率与延迟 | 高扇出是否让服务目标失真 |
覆盖率必须有独立分母。用调度器清单或仓库查询历史中的 Run 对比实际血缘事件,不能只统计已收到多少条。界面展示 “12 分钟前观测”“仅声明”“无列级血缘”“17 个上游 Job 中 2 个未接入”等具体标签,不用一个不透明总分掩盖问题。
准备包含原值、聚合、JOIN、过滤、重命名、重试、失败和部分发布的确定性样例。生产抽样时,从引擎计划、发出事件、 标准化边到查询结果端到端核对;每次投影发布都对账节点数与边数。
第八步:按决策价值渐进上线。
先覆盖关键业务域和表级血缘,登记规范标识与所有者,接入影响最大的调度器和引擎,并先公开新鲜度与覆盖缺口。之后 再为能可靠输出逻辑计划的引擎增加列级血缘、发布前声明血缘、历史查询和 PII 传播。
成功指标应对应真实决策:关键改动中有可用发布前影响报告的比例;故障中能定位第一个异常上游边界的比例;未解析 标识下降幅度;关键 producer 覆盖率。节点很多、图看起来很密都不是成功指标。
高质量示范回答
“我先定义决策和标识。Dataset 和 Job 在环境内用规范 namespace 与 name 标识,Field 再加规范路径和 schema 版本, Run 是一次 UUID 标识的执行。别名和重命名是带时间范围的显式映射。编译计划得到的声明血缘与运行产生的观测血缘 分开,表依赖和字段依赖也分开。
采集侧使用 Airflow、Spark、dbt 和 Kafka 相关处理框架的维护型集成。Spark 与 SQL 引擎尽量读取逻辑计划,因为 正则无法可靠处理动态 SQL、宏、JOIN 和运行时分支。平台信封包含稳定 event ID、producer、协议版本、Job、Run ID、 生命周期和输入输出。网关认证并校验后,先追加到持久日志再确认;同 event ID 重投幂等,非法事件进入可见隔离流。
原始事件不可变。标准化层解析已登记别名,产出带版本的 Job、Dataset、Field 和边,同时保留源事件及解析器版本。 未知标识绝不猜测。START、RUNNING 能补充证据,COMPLETE、ABORT、FAIL 是终态。失败 Run 仍可排障,但 尝试输出不会自动成为当前已发布血缘;COMPLETE 也只表示执行结束,数据质量另算。
消费者构建规范元数据、时态边和 Run 三类存储,并维护上下游邻接索引。每条边带有效期、观测时间、producer、Job 版本、Run、状态、声明或观测类型,以及字段直接或间接转换。历史查询选择当时有效的标识和边。我不会预计算全图 传递闭包,因为高扇出、版本变化和权限会让它难以更新。
对 30,000 个数据集、每日 200,000 次 Run 和每秒 500 条峰值事件,我会先采用分区持久日志和有索引的关系或键值 投影,再用真实扇出评测专用图存储。列级边是主要膨胀项,所以常用近期邻接保热,旧明细压缩。三跳 2 秒要按 P50、 P95、P99 和扇出分别验证;每次查询有深度和节点预算,超限必须明确截断或续查。
查询在扩图时执行权限,不能泄露隐藏节点的名称、存在或邻居数量,缓存也带权限范围。结果显示来源与缺口:仅声明、 最近观测时间、未解析标识、陈旧 producer、缺少列级血缘或未接入 Job。
投影必须能重建。原始事件保留 1 年,消费者按 offset 推进且幂等写入。解析器或 schema 出错时,构建影子投影, 对账并验证已知路径后再切换。快照只用于缩短恢复,仍要证明加上后续事件能确定性还原。
最后,我用预期 Job 覆盖率、终态完整率、标识解析失败、边新鲜度、关键表列覆盖、拒收事件和抽样路径正确率衡量 可信度,分母来自调度器清单和查询历史。先上线财务、客户等关键域的表级血缘并公开缺口,再在抽取可靠时补列级和 声明血缘。系统价值体现在更安全地改动和用证据定位故障,不体现在图里有多少节点。”
常见错误与改进建议
- 先选图数据库 → 存储解决不了标识、运行状态、历史和缺失接入 → 先定义实体、证据、生命周期和查询契约。
- 把展示名当主键 → 别名、大小写、环境和重命名会拆错或合错节点 → **使用规范 namespace/name,并显式维护
带有效期别名。**
- 把声明和观测血缘混为一谈 → 编译可能路径与实际运行路径可能不同 → 每条边保存类型和来源,并允许筛选。
- 把失败 Run 的尝试输出都升级为当前边 → 部分文件或表会变成虚假真相 → **保留排障证据,但满足发布语义后
才激活。**
- 认为 COMPLETE 等于数据正确 → 执行可以成功地产出重复或无效数据 → Run 生命周期与数据质量分别建模。
- 所有 SQL 都用正则解析 → 动态 SQL、方言、宏和嵌套表达式会产生错误依赖 → **优先执行计划和可靠解析器,
并公开未支持覆盖。**
- 没有事件协议就按载荷 hash 去重 → 两个进度事件可能内容相同,重投时间又可能不同 → **在平台信封中要求
producer 稳定 event ID。**
- 只保留当前图 → 历史影响和故障还原无法完成 → 保留不可变事件和时态边版本。
- 预计算全部传递路径 → 扇出、版本和权限导致失效代价过高 → 使用受限遍历和定向缓存。
- 最终响应才做权限过滤 → 扩图和缓存阶段已可能泄露隐藏节点 → 扩展邻居时执行策略,并隔离缓存范围。
- 用收到的事件数当覆盖率 → 静默 producer 在事件和指标里同时消失 → 与调度器清单或查询历史对比。
- 缺口未知却画成完整图 → 使用者会做出不安全的变更判断 → 返回新鲜度、来源、未解析标识和缺失 producer。
延伸追问
追问一:Spark Run 失败前已经写出一个表分区,这条边要显示吗?
保留 Run 和尝试输入输出边作为观测排障证据,标记为 FAIL 且未发布。是否进入当前生产图取决于存储提交与发布 策略:分区已经可见时,先显示成失败或可疑版本,直到回滚或验证;原子写已中止时,不激活这条边。把执行证据和 发布状态同时保留,既不丢失取证信息,也不把部分输出伪装成可信事实。
追问二:如何发现一个 producer 悄悄停止上报?
只看已收事件无法发现完全缺席的 producer。应从 Airflow 调度、Spark 历史、dbt run results、仓库查询日志等独立 控制面建立预期 Run 清单,在迟到窗口内按规范 Job 和 Run 标识关联血缘事件。分别告警缺少开始、缺少终态和关键 等级覆盖率下降,并区分任务已下线与集成故障。
追问三:任务尚未运行,怎么做发布前影响分析?
从待发布的编译计划或 manifest 提取声明血缘,与当前定义做差异,再从被删除或修改的输出向下游遍历。结果明确 标注为发布前声明证据,并展示历史观测血缘是否支持或冲突。关键资产可在 CI 中要求所有者审核,但不能把未来的 动态分支说成已经观测到的事实。
追问四:为什么不把所有内容放进一个图数据库?
单库在基准测试通过后可以采用,但 Run 事件历史、不可变重放、元数据搜索和低延迟邻接的访问模式不同。把持久事件 真源与可重建投影分开,有利于恢复和独立演进。先用满足需求的最少存储,测量扇出和时态查询成本,再决定专用存储 是否值得其运维成本。
追问五:列级血缘让边数增加几百倍,先降级什么?
优先保护正确性和关键决策。保留表级血缘及高等级业务域的近期列级血缘,把旧明细移到压缩历史层,低频字段路径 异步计算。遍历设置预算并明确返回部分结果,不能悄悄用表级猜测冒充列级答案。持续按引擎和业务域统计列覆盖率, 确保降级可测且可恢复。