映射 Karrot 的数据:我们如何构建列级血缘

发布: (2025年12月5日 GMT+8 11:25)
7 min read

Source: 당근마켓 Tech Blog

问题:当你看不到数据流向时

在 Karrot,许多不同的团队在 BigQuery 中创建派生表。数据工程师、分析师、产品经理——很多人把数据转换成他们需要的形态。

但我们遇到了一个重大问题。很难了解

  • 哪些表会影响其他表?
  • 这个表最终依赖哪些上游来源?

由于整体的数据流向不可见,继续工作或排查问题常常变得痛苦。

案例 1:级联故障的噩梦

单个表的管道失败了,但该表被许多其他表引用,导致一连串的故障。我们没有快速的方法查看受影响的表,于是只能手动逐个查询。花了数小时才弄清楚影响范围并修复问题。

案例 2:MySQL 模式变更的连锁效应

当我们想要从 MySQL 中删除一个列时,无法可靠地回答“如果删除此列,哪些表和列会受影响?”评估影响耗时很长。

这些经历让我们清楚地认识到,需要一个能够可靠追踪和管理数据流向的系统。这个系统就是数据血缘(data lineage)。

为什么要进行列级血缘?

一旦我们决定构建血缘,接下来的问题是:“我们应该追踪到多细的粒度?” 我们是只追踪表级血缘,还是一直追踪到列级

表级血缘的局限性

表级血缘相对容易构建。BigQuery 提供的 JOBS 视图包含被引用的表和目标表,可用于推断表之间的依赖关系。然而,它不足以支撑日常工作,因为:

  • 它不告诉你下游表中具体受影响的列是哪一列。
  • 它无法回答删除或更改单个列的影响。
  • 它隐藏了敏感字段(如 PII)的传播路径。

换句话说,单纯的表级血缘粒度太粗。

列级血缘的价值

列级血缘能够实现:

  1. 细粒度影响分析——例如,修改 users.email 时,立即显示下游的 user_stats.emailmarketing.user_email 等列。
  2. 数据安全与 PII 跟踪——追踪 PII 列的来源及其在数据仓库中的流向。
  3. 更快的根因分析——快速将下游数据质量问题追溯到源列。

剩下的问题是:我们如何可靠地提取如此详细的列级血缘?

评估血缘提取方法

我们考虑了几种方法,重点关注两个标准:

  • 在列级别上能够准确提取血缘。
  • 能够解析 Karrot 各团队的查询,无论其风格如何多样。

1) 基于 BigQuery INFORMATION_SCHEMA 的方法

限制:

  1. 没有列级追踪——BigQuery 内建的元数据不提供细粒度的列依赖信息。
  2. 没有视图级血缘——视图不会被记录为被引用对象,只会出现底层基表,导致难以理解基于视图构建的管道。

因此,仅凭 BigQuery 元数据无法获得所需的细节。

2) 使用类似 OpenLineage 的开源框架

Karrot 的团队通过多种机制执行查询(Airflow、cron 作业、Notebook、内部批处理系统等)。若要采用 OpenLineage,我们需要:

  • 在每个执行环境中安装客户端库或插件。
  • 为每个作业加入代码,以发出血缘事件。

从中心化数据团队的视角来看,这会带来以下挑战:

  • 要求众多团队在工作流中添加新依赖。
  • 有可能干扰各团队的业务逻辑和部署模式。
  • 需要持续验证新管道是否已正确植入血缘采集。

OpenLineage 功能强大,但集成工作量大且我们迫切需要列级解析,使其不太适合我们的场景。

我们的选择:SQL 解析

Karrot 的主要数据仓库是 BigQuery,几乎所有与 DW 相关的工作负载都在其上运行。我们决定 直接解析存储在 BigQuery 中的查询日志,而不是在每个执行器上埋点。

换句话说,我们从 BigQuery 的作业元数据中提取所有查询,并直接从 SQL 中抽取血缘。这种方式对我们的环境更简单、更可靠。

我们解析什么?

Karrot 的大多数数据工作都是在 BigQuery 中使用 SQL 完成的。BigQuery 将所有执行过的查询保存在 INFORMATION_SCHEMA.JOBS 中,其中包括:

  • query —— 原始 SQL 文本。
  • creation_timeend_timeuser_email 等字段。

我们把 query 字段作为 SQL 解析器的输入,解析器负责识别源表、目标表以及列级映射。

-- Example placeholder for parsing logic
SELECT
  src_table,
  src_column,
  dest_table,
  dest_column
FROM
  parsed_lineage
WHERE
  job_id = @job_id;

(实现细节和更多代码示例将在后续章节中给出,保持代码块原样不变。)

Back to Blog

相关文章

阅读更多 »