一、背景
数据血缘是实时数据治理的核心能力,能够追踪数据从产生、流转到消费的完整链路。在实时数仓与流批一体架构普及的背景下,数据链路复杂度持续提升,数据血缘的价值愈发凸显:
- 问题定位:当下游指标异常时,可快速溯源上游数据源与转换逻辑,缩短故障排查时间
- 影响分析:表结构变更前,可评估下游所有关联作业的影响范围,降低变更风险
- 数据治理:支撑元数据管理、数据质量、数据安全等治理场景,完善实时数据资产地图
- 合规审计:满足数据流转可追溯的监管合规要求
之前的文章讲过Flink datastream/SQL/Table从代码编写到作业提交运行的全过程,今天聊聊在不同阶段实现血缘的几种方案。
二、Calcite RelNode 阶段(SQL/Table API 逻辑计划)
时机:SQL 经 Parser 解析 → Validator 校验后,Calcite 生成 RelNode语义树,在优化规则应用之前或之后。
ini
// 核心思路:从 TableEnvironment 获取优化后的 RelNode 树
TableEnvironmentImpl tEnv = (TableEnvironmentImpl) TableEnvironment.create(settings);
// 注册 source/sink,执行 SQL
tEnv.executeSql("CREATE TABLE ...");
String sql = "INSERT INTO sink_table SELECT a, b FROM source_table";
// 获取解析后的逻辑计划
List<Operation> operations = tEnv.getParser().parse(sql);
if (operations.get(0) instanceof SinkModifyOperation) {
SinkModifyOperation sinkOp = (SinkModifyOperation) operations.get(0);
// 获取 sink 表信息
ObjectIdentifier sinkIdentifier = sinkOp.getContextResolvedTable()
.getIdentifier();
// 递归遍历 QueryOperation 树获取 source 表
extractSources(sinkOp.getChild());
}
优势:
- 列级血缘最完整的捕获点:RelNode 保留了完整的投影、过滤、Join 语义,可通过 RelShuttle 遍历追踪每个字段的来源和去向
- 天然支持 Flink SQL 和 Table API
- 逻辑计划不依赖运行时,可在客户端离线解析
劣势:
- 仅覆盖 SQL/Table API 作业,DataStream API 作业不经过 Calcite
- 经过优化规则后 RelNode 结构可能被改写(如子查询展开、Join 重排),需决定在优化前还是后捕获
- 数据集(表/Topic)的物理定位信息缺失,需要额外关联 Catalog
三、StreamGraph 阶段(API 层 DAG)
时机:用户代码的 Transformation 列表被 StreamExecutionEnvironment.execute() 转换为 StreamGraph时。
scss
// 方式 A:直接获取 StreamGraph
StreamExecutionEnvironment env = ...;
// 编写业务逻辑后
StreamGraph streamGraph = env.getStreamGraph();
// 遍历所有 StreamNode
for (Integer nodeId : streamGraph.getStreamNodes().stream()
.map(StreamNode::getId).collect(Collectors.toList())) {
StreamNode node = streamGraph.getStreamNode(nodeId);
String operatorName = node.getOperatorName();
// Source 节点识别
if (node.getInEdges().isEmpty()) {
// 这是一个 source
}
// Sink 节点识别
if (node.getOutEdges().isEmpty()) {
// 这是一个 sink
}
// 获取边(上下游关系)
for (StreamEdge edge : node.getOutEdges()) {
int targetId = edge.getTargetId();
// nodeId -> targetId 构成一条边
}
}
// 方式 B:自定义 PipelineExecutor 拦截(非侵入式)
public class LineagePipelineExecutorFactory implements PipelineExecutorFactory {
@Override
public PipelineExecutor createExecutor(Configuration config, ...) {
PipelineExecutor delegate = realFactory.createExecutor(config, ...);
return new LineagePipelineExecutor(delegate);
}
}
public class LineagePipelineExecutor implements PipelineExecutor {
@Override
public CompletableFuture<JobClient> execute(Pipeline pipeline, ...) {
// pipeline 实际上就是 StreamGraph
StreamGraph streamGraph = (StreamGraph) pipeline;
// 解析血缘后再提交
extractLineage(streamGraph);
return delegate.execute(pipeline, configuration, classLoader);
}
}
//通过 SPI 注册自定义的 `PipelineExecutorFactory`,对业务代码无侵入。
优势:
- 算子级血缘最完整的捕获点:保留了每个算子的原始语义(map、filter、window 等),未被算子链合并
- 同时覆盖 DataStream API 和 Table API(Table API 最终也会翻译为 DataStream)
- Source/Sink 算子的输入输出信息可在此提取
劣势:
- 算子粒度太细,下游血缘系统难以消费
- 不含并行度、分区策略等物理信息
- 需要在客户端进程内拦截,异步存储需自行实现
四、JobGraph 阶段(客户端优化后)
时机:StreamGraph 经 StreamingJobGraphGenerator 算子链合并后生成 JobGraph,提交给 JobManager 之前。
优势:
- 最常用的血缘捕获点:算子链已合并,粒度适中;JobVertex 包含了 Source/Sink 标识
- 包含分区策略(Hash、Rebalance、Forward 等)信息
- JSON 格式的执行计划可可视化,方便调试
- 在客户端侧即可获取,不侵入 JobManager 运行时
劣势:
- 算子链合并后,中间算子的语义被折叠,无法直接提取列级血缘
- Source/Sink 的具体数据集信息(如 Kafka topic、文件路径)仍需从算子配置中解析
- 需要反射或解析用户代码来提取数据集元数据(旧版 OpenLineage 就是这么做的,维护成本高)
五、ExecutionGraph 阶段(JobManager 并行化后)
时机:JobGraph 到达 JobManager 后,被 DefaultExecutionGraphBuilder 展开为带并行度的 ExecutionGraph。
优势:
- 包含完整的物理执行信息:并行度、Task 部署位置、数据分区映射
- 可结合 Checkpoint 信息追踪运行时数据流
劣势:
- 血缘信息反而更难提取:并行化后一个 JobVertex 对应多个 ExecutionVertex,血缘拓扑被稀释到物理层面
- 需要侵入 JobManager 运行时,增加集群风险
- 运行时才可用,无法做提交前审计
- 图结构可变(Failover/Rescaling 会重建),血缘不稳定
六、原生血缘机制(Flink 2.0+ 推荐)
时机:作业提交时,Flink 自动从 Source/Sink 的 LineageVertexProvider 接口收集血缘元数据,构建 LineageGraph,通过 JobStatusChangedListener 异步通知。
优势:
- 官方标准方案:Source/Sink 自身暴露血缘数据集,无需反射破解私有字段
- 天然支持 Flink SQL(Table API 的 Source/Sink 自动适配)
- 通过 JobStatusChangedListener 回调机制,天然支持异步存储
- OpenLineage 社区已实现对应的 Listener,可直接集成
- 代码变更极小:仅配置 execution.job-status-changed-listeners 即可
劣势:
- Flink 2.0 才正式可用,目前只有 Kafka 连接器实现了 LineageVertexProvider
- 不支持列级血缘(当前只覆盖表级)
- 依赖连接器厂商适配,未实现的连接器无法提取血缘
- 无法提取中间算子(map/filter 等)的变换语义
七、选型建议
| 需求 | 推荐 | 理由 |
|---|---|---|
| 列级血缘(SQL 作业) | Calcite RelNode | 唯一能追踪字段变换的点 |
| 表级血缘 + 运维友好 | Flink 2.0+原生血缘 | 官方标准,异步回调,零侵入 |
| 跨 API 统一血缘 | JobGraph | 同时覆盖 DataStream 和 SQL |
| 运行时血缘 + 状态关联 | ExecutionGraph | 可结合 Checkpoint 追踪 |
还有诸如SQL Parser方案(纯SQL解析)、REST API 获取 Job Plan(/jobs/:jobid/plan)等方案都可以作为血缘实现的路径,但在实施过程中也各有利弊,很多时候也会搭配使用。