3、血缘存储与建模:图数据库选型与元数据模型设计

血缘分析,说白了就是搞清楚数据从哪来、到哪去、中间经历了什么。这个关系天然就是个图结构——表是节点,ETL 作业是边,字段映射是属性。所以选对图数据库、设计好元数据模型,是整个血缘系统能不能跑起来的关键。

我这些年折腾过好几套血缘方案,踩过不少坑。今天就把图数据库选型、模型设计、节点与关系定义这些核心问题,掰开了讲清楚。

3.1 图数据库选型:Neo4j vs JanusGraph

市面上图数据库不少,但做血缘分析,主流就两个方向:Neo4j 和 JanusGraph。我个人的习惯是,先看场景再选工具,别盲目追新。

对比维度 Neo4j JanusGraph
存储引擎 本地文件 / 单机 HBase / Cassandra / Bigtable
查询语言 Cypher(声明式,易上手) Gremlin(函数式,灵活但陡峭)
事务支持 ACID,强一致性 最终一致性(依赖后端)
水平扩展 只读副本,写扩展弱 原生分布式,可横向扩展
部署复杂度 单机部署,5 分钟搞定 依赖 Hadoop 生态,运维成本高
血缘场景适配 中小规模(千万节点以内) 超大规模(亿级节点+)

我的建议:如果你的血缘系统要管理的数据资产在 5000 万节点以内,团队图数据库经验不多,直接上 Neo4j。我见过太多团队一上来就上 JanusGraph,结果半年了还在调 HBase 参数,血缘查询一次要 10 秒,得不偿失。

避坑指南:我曾经在一个项目里选了 JanusGraph,因为觉得「分布式就是好」。结果业务方只需要查单表血缘,每次查询都要跨 Region Server 扫描,延迟根本扛不住。后来切回 Neo4j,单机 64G 内存,血缘链路查询从 8 秒降到了 200 毫秒。

为什么会这样?因为血缘分析的核心操作是「从某个节点出发,沿着边遍历上下游」。这种模式在单机内存里跑得飞快,分布式反而因为网络开销和分区策略拖慢速度。除非你的血缘数据真的到了亿级,否则 Neo4j 是更务实的选择。

3.2 血缘元数据模型设计

模型设计是血缘系统的地基。我见过最糟糕的设计,是把所有东西都塞进一个节点类型里,结果查询时根本分不清谁是表、谁是字段、谁是作业。

一个成熟的元数据模型,至少需要区分三个层次:

  • 资产层:表、视图、文件、Kafka Topic 等数据载体
  • 字段层:列、属性、Schema 字段
  • 流程层:ETL 作业、SQL 脚本、数据管道

我习惯用「三层五类」来概括:

层级 节点类型 示例 关键属性
资产层 Table / View / File ods_order, dws_sales database, schema, owner, create_time
字段层 Column order_id, user_name data_type, comment, nullable
流程层 ETLJob / SQLScript dwd_order_etl, daily_agg job_type, cron_expr, last_run_time
流程层 Process insert_overwrite, join_transform operation, sql_text, start_time
辅助层 Application / System datax, flink_job version, cluster, config

注意:不要把字段层和资产层混在一起。我见过有人把 Column 作为 Table 的属性存成 JSON 数组,结果想查「某个字段被哪些下游表引用」时,只能全表扫描。字段必须独立成节点,才能做细粒度血缘追踪。

3.3 节点与关系定义

节点定义好了,关系才是灵魂。血缘分析的本质就是「关系遍历」。我总结了一套常用的关系类型,基本覆盖了 90% 的血缘场景:

3.3.1 核心关系类型

  • DERIVED_FROM:数据从上游来。比如 dws_sales 的 sale_amt 字段 DERIVED_FROM ods_order 的 amount 字段。
  • CONSUMED_BY:数据被下游消费。和 DERIVED_FROM 是反向关系,但建议双向都存,避免遍历时反向查询性能差。
  • EXECUTED_BY:数据由某个作业产生。比如 dwd_order 表 EXECUTED_BY dwd_order_etl 作业。
  • DEPENDS_ON:作业依赖上游表。比如 dwd_order_etl 作业 DEPENDS_ON ods_order 表。
  • CONTAINS:表包含字段。比如 dws_sales 表 CONTAINS sale_amt 字段。

关键原则:关系一定要带属性。我习惯在每条边上记录 lineage_type(direct / transform / filter)、confidence(0.0 ~ 1.0)、update_time。这样在做质量问题溯源时,可以快速过滤出「低置信度的血缘路径」,优先排查。

3.3.2 关系属性设计示例

// Neo4j Cypher 示例:创建字段级血缘关系
MATCH (src:Column {name: 'amount', table: 'ods_order'})
MATCH (tgt:Column {name: 'sale_amt', table: 'dws_sales'})
CREATE (tgt)-[:DERIVED_FROM {
    lineage_type: 'transform',
    confidence: 0.95,
    transform_expr: 'SUM(amount)',
    update_time: datetime('2024-01-15T10:00:00')
}]->(src)

个人经验:confidence 字段特别有用。有一次我们自动解析 SQL 时,发现某个字段的血缘关系置信度只有 0.6,因为 SQL 里用了子查询嵌套,解析器没完全搞懂。我们就把这条边标灰,在血缘图上用虚线展示,提醒用户「这条路径可能不准」。后来果然发现是解析 bug,修复后置信度升到了 0.98。

3.4 整体模型结构图

下面这张图是我常用的血缘元数据模型结构,你可以看到节点、关系以及它们之间的层次关系:

血缘元数据模型结构图 Table / View File / Topic Column Field / Attribute ETLJob / SQLScript Process CONTAINS DERIVED_FROM EXECUTED_BY DEPENDS_ON 说明: 1. 资产层和字段层通过 CONTAINS 关系连接,形成「表含字段」的层级结构。 2. 字段之间通过 DERIVED_FROM 关系追踪细粒度血缘,这是质量溯源的基石。 3. 流程层通过 EXECUTED_BY 和 DEPENDS_ON 与资产层关联,形成完整的作业血缘链路。

3.5 模型设计中的常见坑

嗯,这里我要多说几句。模型设计看着简单,实际落地时全是细节。我列几个我踩过的坑:

  • 坑一:关系方向搞反。DERIVED_FROM 的方向应该是「下游指向上游」,但有人习惯写成「上游指向下游」。结果查询时所有遍历逻辑都得反向,代码可读性极差。我建议统一用「数据流向的反方向」来定义关系名,这样遍历时顺着关系走就是溯源。
  • 坑二:忽略时间维度。血缘关系不是一成不变的。同一个字段,昨天可能从 A 表来,今天改了 ETL 逻辑就从 B 表来了。如果不加时间版本,查历史血缘时全是错的。我习惯在节点和关系上都加 valid_from 和 valid_to 字段,支持时间旅行查询。
  • 坑三:过度建模。有人把每个 SQL 里的每个子查询都建模成一个 Process 节点,结果一个简单查询生成了上百个节点。血缘图变得一团乱麻,根本没法用。我的原则是:只建模「有业务意义的转换步骤」,中间临时结果直接跳过。

我曾经犯过的错:在一个金融项目里,我把每个字段的每个中间计算步骤都建模成节点,结果一个字段的血缘路径上有 20 多个节点。业务方看血缘图时直接崩溃,说「这比我的交易链路还复杂」。后来我砍掉了所有中间临时字段,只保留最终字段和直接上游,血缘图一下子清爽了。

3.6 小结

图数据库选型上,中小规模无脑 Neo4j,大规模再考虑 JanusGraph。模型设计上,三层五类结构基本够用,字段必须独立成节点。关系定义上,DERIVED_FROM、CONTAINS、EXECUTED_BY 是核心,别忘了加 confidence 和 time 属性。