一、背景
hive、spark、flink是hadoop最常用的,三个计算入口。hive最古老,同时有metastore,spark算的快,flink流技术支持最好。目前hive和spark融合度高,flink较为独行。
1.1 spark、hive关系:
hive和spark相互依存,如hive使用spark引擎,进行计算(当然也能使用tez引擎),spark连接hive metastore,获取表的元数据关系。本文不讨论tez引擎的问题。仅讨论hive使用spark引擎,和spark连接spark元数据情形。
hive使用spark引擎:
虽然hive使用spark引擎,但是不会触发spark的钩子函数的。仅作为引擎使用。
spark连接hive metastore。
spark内执行ddl的时候,spark监听ddl可以捕捉到。同时由于修改hive metastore,hive metastore的钩子也能监听到。所以spark 和 hive都能监听到,如果都监听务必仅向数据库写入一份,或者直接都用merge方式写入。
1.2 目前已经有框架
1.2.1、datahub
datahub
领英,创建,完全开源。社区较为活跃,依赖图数据库,文档详尽,支持很多数据源。定义为数据发现平台(Data Discovery Platform ),数据管理平台,集成了元数据管理和数据血缘功能。拥有UI界面。由python和java编写完成,元数据导入等使用python脚本完成。
数据血缘的表示如下:
source
1.2.2、atlas
atlas
阿特拉斯,由apache主导,社区活跃略低,较重,依赖hbase。集成数据发现功能,数据定义提较为灵活。
支持
1.3、血缘粒度
hive在不太老的版本上可以实现column级血缘,下文有介绍。
spark目前较难实现column级血缘。只能实现table级。
1.4、项目架构
其中有些是比较特别的,比如SQL形式的create table as
,create view
,和SPARK的save into datasource
,都是又是ddl也是dml的,会被执行两次。但由于项目中数据模型的一致性且存储模型均使用merge方式,所以最终存储到图数据的数据不会受到影响。
二、hive数据血缘实现思路
2.1 hive数据血缘捕捉实现
实现org.apache.hadoop.hive.ql.hooks.ExecuteWithHookContext
可以获取到任意类型的sql,比如insert,create table as,explain,use database等等。
需要注意的是:
(1)此类监测的hiveserver2,因此spark、flink的ddl等都不会被此类监测到。
(2)此类能检测到sql文本内容。
此类只有一个方法
void run(HookContext var1) throws Exception;
从HookContext中可以获取到
// 查询计划
val plan: QueryPlan = hookContext.getQueryPlan
// 查询的operation,即查询类型
// 例如 org.apache.hadoop.hive.ql.plan.HiveOperation#QUERY 通过 getOperationName方法可以和op进行匹配
val op = plan.getOperationName
// 通过SessionState可以获取到当前的SessionId和当前数据库名,还有当前Session内的所有临时temporary表名称,还有当前的HiveConf。
val ss: SessionState = SessionState.get
// 获取当前Session的用户名称
val userName = hookContext.getUgi.getUserName
// 执行时间
val queryTime = getQueryTime(plan)
// 执行的Sql语句
val sql = plan.getQueryStr.trim
// 通过HiveMetaStoreClient 可以实时再查询库表的元数据。
// 使用ms需要注意:如果此表已经drop或者alter就不要使用ms查询了,否则程序报异常。
val ms: HiveMetaStoreClient = getMetastore(ss.getConf)
和血缘有关系的操作有一些内容:
本例中view相关的也纳入血缘范畴。
op match {
case dml if (Set(HiveOperation.QUERY.getOperationName // insert into
, HiveOperation.CREATETABLE_AS_SELECT.getOperationName// create table as
, HiveOperation.CREATEVIEW.getOperationName // create view
, HiveOperation.ALTERVIEW_RENAME.getOperationName // alter view as
).contains(op)) => {
// 下文将详细讲解
}
case load if Set(HiveOperation.LOAD.getOperationName).contains(load) => {
LOG.info(s"lineage event: ${
op}!")
// 封装对象
}
case truncate if Set(HiveOperation.TRUNCATETABLE.getOperationName).contains(truncate) => {
// 封装对象
}
case other=> LOG.info(s"lineage event: ${
op} passed!")
}
这里着重说明case dml
,情形较为复杂:
主要分为finalSelOps是否为空两种情况。
def toScalaLinkedHashMap[K, V](input: java.util.Map[K, V]): LinkedHashMap[K, V] = {
val output: LinkedHashMap[K, V] = LinkedHashMap.empty
output.putAll(input)
output
}
// index是指每一列都对应已改索引,血缘信息需要通过index查找到
val index: LineageCtx.Index = hookContext.getIndex
// finalSelOps就是select最终获取的列的信息
// LinkedHashMap[列名称,[SelectOperator,sink表]]
val finalSelOps: mutable.LinkedHashMap[String, ObjectPair[SelectOperator, Table]] = toScalaLinkedHashMap(index.getFinalSelectOps)
// hive2.3.9以后修复了finalSelOps为空的bug,能获取列的血缘信息。
// 具体参考:https://issues.apache.org/jira/browse/HIVE-14706
if (finalSelOps.values.isEmpty) {
// 无法获取最终选取的列的信息,就只能获取表的血缘了。
// plan.getInputs获取数据的source,可能是表,视图,临时表,也可能是 insert into values(我是临时表) 生成