一、引言
随着企业信息化和业务的发展,数据资产日益庞大,数据仓库构建越来越复杂,在数仓构建的过程中,常遇到数据溯源困难,数据模型修改导致业务分析困难等难题,此类问题主要是由于数据血缘分析不足造成的,只有强化血缘关系,才能帮助企业更好的发挥数据价值。
SQL血缘关系是数据仓库模型构建的核心依赖。通过对SQL语句进行梳理与解析,得到各个业务层表之间依赖关系和属性依赖关系,并进行可视化展示,形成数据表和属性血缘层次关系图,可以充分展示原始字段数据与数据模型的映射关系。拥有良好的SQL血缘关系系统,不仅有利于数据分析师对业务场景的梳理,还极大帮助对数仓分层的构建,同时对企业数据质量控制方面起到很好的朔源作用,对构造数据链路图,监控数据变化起到很好的辅助作用。
本文将主要介绍Hive数据血缘追踪技术以及如何对数据血缘进行可视化展示。
二、数据血缘介绍
2.1 数据血缘的定义
数据血缘,又称数据血统、数据起源、数据谱系,是指数据的全生命周期中,数据从产生、处理、加工、融合、流转到最终消亡,数据之间自然形成一种关系。其记录了数据产生的链路关系,这些关系与人类的血缘关系比较相似,所以被成为数据血缘关系。
2.2 数据血缘的特点
归属性:一般来说,特定的数据归属于特定的组织或者个人。
多源性:同一个数据可以有多个来源;一个数据也可以是多个数据经过加工生成的,而且这种加工过程可以是多个。
可追溯性:数据的血缘关系体现了数据的生命周期,体现了数据从产生到消亡的整个过程,具备可追溯性。
层次性:数据的血缘关系是有层次的。对数据进行分类、归纳、总结等描述信息又会形成新的数据,不同程度的描述信息形成了数据的层次。
2.3 大数据平台现状
在我们的大数据平台中,任务处理主要依赖于Hive、Spark和Doris等组件,其中Hive承担着大规模数据处理和批量计算的核心任务。目前对于Hive血缘的分析还停留在基于静态SQL人工识别并维护的层面,并未实现血缘数据自动化提取和动态更新。因此下文主要对Hive中数据血缘进行研究,并基于此实现血缘数据的提取、解析以及可视化的全链路流程。
三、Hive数据血缘
3.1 Hive数据血缘方案介绍
Hive作为离线数仓分析工具,自带了数据血缘分析的解决方案:
表级别:org.apache.hadoop.hive.ql.tools 下的LineageInfo类
列级别:org.apache.hadoop.hive.ql.hooks.LineageLogger类
3.2 Hive数据血缘实现原理
这一章节我将从HiveSQL的解析过程、Hook函数来介绍Hive是如何实现数据血缘的。
3.2.1 Hive的解析过程
Hive的解析过程包括词法分析、语法分析、语义分析、生成逻辑计划、优化和生成物理计划。具体展开如下:
- Hive 根据 Antlr 定义的词法、语法规则完成词法、语法分析将 HQL 解析为 AST Tree 即抽象语法树;检查 AST 中的表名、列名和数据类型,确保所有引用的对象在 Hive 元数据中存在;
- 深度遍历AST进行语义解析,将 AST 替换成 QueryBlock(QB),QB 可以理解为最小的语法单元,将 AST 每一个节点单独取出来并封装成一个个 QB,同时在这里替换元数据里的信息;
- 遍历 Query Block,解析为操作树 OperatorTree,生成逻辑执行计划;
- 创建逻辑执行计划优化器和物理执行计划优化器、处理视图、生成表的统计信息;
- 逻辑优化器进行操作树变换,合并多余的 ReduceSinkOperator,减少 shuffle,即对应的列剪枝、分区剪枝以及 Join 顺序优化等操作。所有的优化操作都对应一个 Transform 对象,并被放置在 Optimizer 的一个 List 中;
- 遍历 Operator Tree,将操作树转变为对应的 MapReduce 任务,生成物理执行计划;
- 物理优化器进行 MapReduce 任务变换,针对最后生成 DAG 图进行优化,生成最终的执行计划。
3.2.2 Hook函数
Hook是一种在处理过程中拦截事件,消息或函数调用的机制。Hive hook是绑定到了Hive内部的工作机制,无需重新编译Hive。Hive hook提供了使用Hive扩展和集成外部功能的能力,相当于一个可以在查询处理的各个步骤中运行/注入代码的插件。根据钩子的类型,它可以在查询处理期间的不同点调用。
3.2.2.1 Hook函数类型
Pre-execution hook:在执行引擎执行查询之前,将调用Pre-execution hooks。需要在Hive的查询计划优化后才会调用。
Post-execution hook:在查询执行完成之后以及将结果返回给用户之前,将调用Post-execution hooks 。
Failure-execution hook:当查询执行失败时,将调用Failure-execution hooks 。
Pre-driver-run 和Post-driver-run hook:在Driver执行查询之前和之后分别调用。
Pre-semantic-analyzer 和 Post-semantic-analyzer hook:在Hive在查询字符串上运行语义分析器之前和之后分别调用。
3.2.2.2 Hook函数的应用
Hive源码中实现了一些Hook,具体有以下几个例子:
DriverTestHook:实现了HiveDriverRunHook的preDriverRun方法(对postDriverRun是空实现),用于打印输出的命令。
PreExecutePrinter和PostExecutePrinter:分别实现了pre execute和 post execute hook,它将参数打印到标准输出。
LineageLogger:是一个ExecuteWithHookContext,实现了post execute hook,它将查询的血缘信息记录到日志文件中。 LineageInfo包含有关query血统的所有信息。
3.3 生成血缘数据
这一章节我将介绍如何使用Hive提供的类生成血缘数据。
3.3.1 表血缘生成
表血缘数据基于LineageInfo实现,下面是LineageInfo的main方法:
public static void main(String[] args) throws IOException, ParseException,
SemanticException {
String query = args[0];
LineageInfo lep = new LineageInfo();
lep.getLineageInfo(query);
for (String tab : lep.getInputTableList()) {
System.out.println("InputTable=" + tab);
}
for (String tab : lep.getOutputTableList()) {
System.out.println("OutputTable=" + tab);
}
}
可以看出LineageInfo会根据输入的SQL查询,输出对应的上下游数据表。下面我们传入一个测试sql:
insert overwrite table vesync_warehouse.test_1 select * from t2 join t3
返回结果如下:
InputTable=t2
InputTable=t3
OutputTable=vesync_warehouse.test_1
3.3.2 列血缘生成
列血缘基于LineageLogger实现,前面提到LineageLogger实现了post execute hook,因此需要在SQL执行完成后才会将血缘数据写入到日志文件中。使用方法如下:
1.hive-site.xml文件中增加以下配置(此处为全局配置,也可在执行SQL时动态指定该配置)
<property>
<name>hive.exec.post.hooks</name>
<value>org.apache.hadoop.hive.ql.hooks.LineageLogger<value/>
</property>
2.后续SQL执行结束后,血缘数据JSON就会打印在终端上。生成的血缘JSON如下所示:
"version":"1,
"user":"appuser",
"timestamp":1695030974,
"duration":9777,
"jobIds":[],
"engine":"tez",
"database":"default",
"hash":"192462ad588d8bd7b0dfcaea1c462f51",
"queryText":"sql",
"edges":[ { "sources":[], "targets":[0], "expression":"'118694510'", "edgeType":"PROJECTION" }, { "sources":[17,18], "targets":[0,1], "expression":"((ods_track_event.event = 'ViewDevicePage') and (ods_track_event.partition_date = '2023-09-18')", "edgeType":"PREDICATE" }],
"vertices":[ { "id":0, "vertexType":"COLUMN", "vertexId":"vesync_warehouse.ods_track_event.distinct_id" }, { "id":1, "vertexType":"COLUMN", "vertexId":"vesync_warehouse.ods_track_event.time" } ]
参数说明:
version:自定义版本号
user:当前用户
timestamp:时间戳
duration:当前系统时间与timestamp的差值
jobIds:任务列表
engine:计算引擎
database:当前数据库
hash:根据sql语句生成的md5哈希
queryText:SQL语句
vertices:顶点。代表参与DAG的节点元素。id从0开始自增,vertexType有COLUMN和TABLE两个值,vertexId为对应的列名或表名
edges:边。代表DAG的流向,由sources指向targets,edgeType有PROJECTION(投影)和PREDICATE(谓语)两种类型。PROJECTION对应的edge即为我们需要的血缘数据 ,PREDICATE为过滤逻辑
3.若需要保存血缘数据,则还需要在hive-log4j.properties文件下添加以下配置。默认会输出至hive的运行日志中。
log4j.logger.org.apache.hadoop.hive.ql.hooks.LineageLogger=INFO
四、数据血缘在大数据平台的实现和应用
4.1 背景
在我们的大数据平台中,代码托管和版本管理使用GitLab进行维护,所有的任务代码都存放在sqlManager模块的task目录下。由于业务需求的变化和数据处理流程的不断调整,团队每天都会向GitLab进行新的代码提交(push),并且这些提交可能包含新的任务逻辑或者对现有代码的修改。这些变化直接影响到数据处理流程。为了确保数据处理的准确性和可追溯性,我们需要及时感知到每次代码变更,自动更新相关的血缘数据。如果不能动态跟踪和更新这些变化,可能会导致血缘关系的失真,从而影响到数据质量管理、调试和故障排查。基于此,我们需要建立一种机制,能够监控GitLab的代码变动,自动化地更新数据血缘信息,以确保系统的稳定性和高效性。
4.2 数据血缘全链路的实现
改写LineageLogger类
上文提到LineageLogger生成的列级血缘数据中,数据列使用对应的ID来标识,可读性较差,不利于后续血缘解析。故对源码进行修改,将ID替换为对应的列名。
生成血缘数据到文件
上文中提到生成血缘数据需要执行SQL,为了不影响线上数据,以下操作均在测试环境中进行。
1.为了确保血缘信息与最新的SQL代码同步,首先需要定时从Git仓库中获取有变更的文件。通过以下命令我们可以筛选出前一天合并到dev分支的提交,并获取这些提交中有添加、修改或重命名操作的文件。
git log --since="${yesterday} 16:00:00" --until="${today} 15:59:59" --merges --pretty=format:"%h" --grep="into 'dev'" | xargs -I {} git diff --name-status {}^ {} | grep -E '^(A|M|R)' | awk '{if (substr($1,1,1) == "R") print $3; else print $2}'
2.鉴于每日变更文件数量可能较多,一次性执行耗时较长,因此分目标表提取变更文件中的SQL语句,写入临时SQL文件中。
3.指定hive.exec.post.hooks为改写后的血缘类com.etekcity.hive.hooks.CustomHook,执行以上临时SQL文件即可生成血缘数据到文件中。
4.血缘数据默认会生成在Hive日志中,不利于下游读取解析,故需要对hive-log4j.properties文件修改,实现血缘和日志的分离存储。以下配置会将血缘数据写入到hive默认日志目录下的hive-lineage.log文件中
appenders = console, DRFA, lineage
appender.lineage.type = File
appender.lineage.name = lineage
appender.lineage.fileName = ${sys:hive.log.dir}/hive-lineage.log
# Use %pid in the filePattern to append <process-id>@<host-name> to the filename if you want separate log files for different CLI session
appender.lineage.layout.type = PatternLayout
appender.lineage.layout.pattern = %d{ISO8601} %5p [%t] %c{2}: %m%n
# list of all loggers
loggers = NIOServerCnxn, ClientCnxnSocketNIO, DataNucleus, Datastore, JPOX, PerfLogger, AmazonAws, ApacheHttp,CustomHook
logger.CustomHook.name = com.etekcity.hive.hooks.CustomHook
logger.CustomHook.level = INFO
logger.CustomHook.additivity = false
logger.CustomHook.appenderRef.CustomHook.ref = lineage
血缘可视化
前面步骤中我们已经将血缘数据写入到hive-lineage.log,但仅有血缘数据无法清晰呈现数据表和数据列之间的关系,因此还需要对其解析,实现可视化。
血缘数据解析
以下方法用于从日志中提取出每个表和列的血缘关系,并将其存储在 column_result 和 table_result 字典中。
def analyze_lineage_data():
column_result = {} # 存储列级血缘关系
table_result = {} # 存储表级关系
with open('/data/logs/hive/hive-lineage.log', 'r') as file:
for line in file:
json_data = json.loads(line.split(' hooks.CustomHook: ')[1])
edges = json_data.get('edges', []) # 获取 edges 信息
for edge in edges:
sources = edge.get('sources', [])
targets = edge.get('targets', [])
edge_type = edge.get('edgeType')
for target in targets:
# 提取目标表名
target_table_name = '.'.join(target.split('.')[:-1])
if edge_type == 'PROJECTION':
pattern = re.compile(r'\w+\.\w+\.\w+')
if not pattern.match(target):
continue
target_table_column_source_dict = column_result.get(target_table_name, {})
source_column_set = target_table_column_source_dict.get(target, set())
# 添加列血缘
for source in sources:
source_column_set.add(source)
target_table_column_source_dict[target] = source_column_set
column_result[target_table_name] = target_table_column_source_dict
source_table_set = table_result.get(target_table_name, set())
# 添加表血缘
for source in sources:
source_table_set.add('.'.join(source.split('.')[:-1]))
table_result[target_table_name] = source_table_set
return column_result, table_result
血缘数据推送
基于解析出的血缘关系,我们构建详细的列级血缘信息 (fine_grained_lineage_list) 和表级血缘信息 (upstream_tables_list ),并推送至DataHub,关键代码如下。详细可参考datahub官方文档给出的提交细粒度血缘的脚本:lineage_emitter_dataset_finegrained.py
def send_lineage_data():
for target_table_name, lineage in column_result.items():
upstream_tables_list = []
source_table_set = table_result.get(target_table_name)
for source_table_name in source_table_set:
upstream_tables_list.append(Upstream(datasetUrn(source_table_name), type=DatasetLineageType.TRANSFORMED))
# 列级血缘list
fine_grained_lineage_list = []
for target_column, source_column_set in lineage.items():
# 上游list
upstream_str_list = []
# 下游list
downstream_str_list = []
target_column_split = target_column.split('.')
downstream_field_name = target_column_split[-1]
downstream_table_name = '.'.join(target_column_split[0:-1])
downstream_str_list.append(fieldUrn(downstream_table_name, downstream_field_name))
for source_column in source_column_set:
source_column_split = source_column.split('.')
upstream_field_name = source_column_split[-1]
upstream_table_name = '.'.join(source_column_split[0:-1])
upstream_str_list.append(fieldUrn(upstream_table_name, upstream_field_name))
fine_grained_lineage = FineGrainedLineage(upstreamType=FineGrainedLineageUpstreamType.DATASET,
upstreams=upstream_str_list,
downstreamType=FineGrainedLineageDownstreamType.FIELD_SET,
downstreams=downstream_str_list)
fine_grained_lineage_list.append(fine_grained_lineage)
# upstreams为表血缘,fine_grained_lineages为列级血缘
field_lineages = UpstreamLineage(upstreams=upstream_tables_list, fineGrainedLineages=fine_grained_lineage_list)
lineage_mcp = MetadataChangeProposalWrapper(
entityType="dataset",
changeType=ChangeTypeClass.UPSERT,
entityUrn=datasetUrn(target_table_name),
aspect=field_lineages
)
# 调用datahub REST API推送血缘
datahub_emitter = DatahubRestEmitter('localhost:8080')
datahub_emitter.emit_mcp(lineage_mcp)
血缘数据可视化
数据推送至DataHub之后即可实现可视化,在此界面我们可以清楚的看到各个数据表以及字段之间的关系:
4.3 数据血缘的应用
1.影响分析,当我们对表结构做变更的时候,在事前需要感知这个变更的影响。处于血缘上游的负责人在修改对应的任务的时候,需要通过血缘来查看自己的下游,来判断这个修改的影响,针对修改兼容性或者链路重要性,来对应的做一些操作。在此前没有数据血缘的时候,仅能在模块代码中根据表名去搜索下游,有时会出现遗漏的情况,从而导致下游数据不完整或者不可用,现在通过数据血缘监控没有再发生过类似的线上问题。
2.归因分析,当数据表出现问题时,通过查询血缘的上游,逐级寻找到血缘上游改动的任务节点来排查出造成问题的根因。在定位到问题后,我们会去修复数据,在修复数据的时候,又可以通过血缘来查找表的依赖关系,对受影响的下游任务进行重跑。在数仓中维度表的用途非常广泛,如果维度表出现问题,在没有血缘的情况下,仅仅是梳理下游依赖便要耗费大量时间,在修复数据时也有可能遗漏。而基于可视化血缘可以帮助数据开发快速定位问题,高效修复数据,在提升效率的同时又确保数据的完整性和一致性。
3.数仓治理。数仓规范化治理包括清理数仓中分层不合理的引用,或者是数仓分层整体不规范,存在一些冗余的表。比如,两个表来自同一个上游表,但是它们在不同层级,这些冗余的表就需要被清理掉,而在没有血缘的情况下便很难发现该问题。
五、总结与展望
5.1 总结
本文首先介绍了数据血缘的定义和特点,随后详细探讨了Hive数据血缘的实现原理及其生成过程,分析了表血缘和列血缘的生成方式。接着讨论了数据血缘在大数据平台中的实现,涵盖了数据血缘全链路的实现过程,包括LineageLogger类的改写、血缘数据生成与推送、数据解析与可视化。最后,本文探讨了数据血缘在数据开发、数据安全等领域的应用,全面阐述了数据血缘在现代数据管理中的重要价值。
5.2 展望
1.Hive血缘数据生成需要基于SQL执行,实时性较差且较为耗费资源。为了弥补当前方法的局限,数据血缘生成可以从更底层的技术实现入手,通过对SQL进行词法分析和语法分析,结合元数据实现无需实际执行SQL,即可自动生成数据血缘信息。
2.目前我们仅实现了Hive 单数据源血缘全链路,但在Doris可以访问和分析来自多个异构数据源的数据,如何生成Doris的数据血缘,这部分待后续调研解决中。
3.DataHub在V0.12.0版本后提供了基于数据库查询日志的血缘生成方案,可通过SQL查询连接器,从查询日志中提取列级数据血缘和表使用情况统计,在SDK中也提供了parse_sql_lineage()方法,解析SQL获取血缘。后续考虑基于此来实现血缘的全链路流程。
4.除了可解析的 SQL ,日常还会涉及到代码类型的任务,如Flink 或者 Spark 的 JAR 任务。未来会对非侵入式的非 SQL 类型血缘采集的技术进行调研,从而在任务运行过程中拿到血缘数据。