映射 Karrot 的数据:我们如何构建列级血缘
Source: 당근마켓 Tech Blog
问题:当你看不到数据流向时
在 Karrot,许多不同的团队在 BigQuery 中创建派生表。数据工程师、分析师、产品经理——很多人把数据转换成他们需要的形态。
但我们遇到了一个重大问题。很难了解
- 哪些表会影响其他表?
- 这个表最终依赖哪些上游来源?
由于整体的数据流向不可见,继续工作或排查问题常常变得痛苦。
案例 1:级联故障的噩梦
单个表的管道失败了,但该表被许多其他表引用,导致一连串的故障。我们没有快速的方法查看受影响的表,于是只能手动逐个查询。花了数小时才弄清楚影响范围并修复问题。
案例 2:MySQL 模式变更的连锁效应
当我们想要从 MySQL 中删除一个列时,无法可靠地回答“如果删除此列,哪些表和列会受影响?”评估影响耗时很长。
这些经历让我们清楚地认识到,需要一个能够可靠追踪和管理数据流向的系统。这个系统就是数据血缘(data lineage)。
为什么要进行列级血缘?
一旦我们决定构建血缘,接下来的问题是:“我们应该追踪到多细的粒度?” 我们是只追踪表级血缘,还是一直追踪到列级?
表级血缘的局限性
表级血缘相对容易构建。BigQuery 提供的 JOBS 视图包含被引用的表和目标表,可用于推断表之间的依赖关系。然而,它不足以支撑日常工作,因为:
- 它不告诉你下游表中具体受影响的列是哪一列。
- 它无法回答删除或更改单个列的影响。
- 它隐藏了敏感字段(如 PII)的传播路径。
换句话说,单纯的表级血缘粒度太粗。
列级血缘的价值
列级血缘能够实现:
- 细粒度影响分析——例如,修改
users.email时,立即显示下游的user_stats.email或marketing.user_email等列。 - 数据安全与 PII 跟踪——追踪 PII 列的来源及其在数据仓库中的流向。
- 更快的根因分析——快速将下游数据质量问题追溯到源列。
剩下的问题是:我们如何可靠地提取如此详细的列级血缘?
评估血缘提取方法
我们考虑了几种方法,重点关注两个标准:
- 在列级别上能够准确提取血缘。
- 能够解析 Karrot 各团队的查询,无论其风格如何多样。
1) 基于 BigQuery INFORMATION_SCHEMA 的方法
限制:
- 没有列级追踪——BigQuery 内建的元数据不提供细粒度的列依赖信息。
- 没有视图级血缘——视图不会被记录为被引用对象,只会出现底层基表,导致难以理解基于视图构建的管道。
因此,仅凭 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_time、end_time、user_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;
(实现细节和更多代码示例将在后续章节中给出,保持代码块原样不变。)