使用 Databricks Lakehouse 构建现代数据应用程序——使用 Unity Catalog 查看数据血缘

308 阅读19分钟

在本章中,我们将深入探讨数据血缘在 Databricks 数据智能平台中的关键作用。您将学习如何追溯数据的来源,可视化数据集的转化过程,识别上游和下游的依赖关系,并使用目录资源管理器的血缘图功能来记录血缘信息。到本章结束时,您将掌握确保数据来自可信来源,并在变化发生之前识别潜在问题的技能。

本章将涵盖以下主要内容:

  • 介绍 Unity Catalog 中的数据血缘
  • 使用数据血缘 REST API 追溯数据来源
  • 可视化上游和下游的数据转化过程
  • 识别依赖关系和影响
  • 动手实验 - 跨组织记录数据血缘

技术要求

要跟随本章的示例,您需要具有 Databricks 工作区权限来创建和启动通用集群,以便导入并执行本章的配套笔记本。所有代码示例可以从本章的 GitHub 仓库下载,地址为:GitHub 仓库。本章将使用通用集群创建和运行几个新笔记本,预计消耗约 5-10 个 Databricks 单位(DBUs)。

介绍 Unity Catalog 中的数据血缘

数据血缘是指在 Unity Catalog(UC)中追踪可安全存储对象之间关系的能力,用户可以查看数据资产如何从上游源头形成,并验证下游的依赖关系。

image.png

在 Databricks 中,用户可以几乎实时地追踪数据资产的血缘,数据管理员可以确保他们正在使用最新的资产。此外,Unity Catalog 中的数据血缘跨越了多个连接到同一 Unity Catalog 元存储的工作区,使数据专业人员能够全面、整体地了解数据集是如何转化的,并且它们之间的关系如何。

数据血缘可以跨越 Databricks 数据智能平台中的多种可安全存储对象进行追踪,包括以下对象:

  • 查询
  • 表列
  • 笔记本
  • 工作流
  • 机器学习模型
  • Delta Live Tables(DLT)管道
  • 仪表板

像 Databricks 数据智能平台中的许多对象一样,您可以通过多种机制追踪血缘,包括使用 Databricks UI 的目录资源管理器,或者通过使用数据血缘 REST API。事实上,数据血缘会被 Databricks 数据智能平台自动捕获,并记录在系统表中(本章第 5 章介绍)。像其他在 Databricks 系统表中保存的系统信息一样,血缘信息也会积累不少。为了节省存储成本,这些信息默认会保留一年。对于需要更长时间存储血缘信息的需求,建议设置一个替代流程,将血缘信息追加到长期存档存储中。例如,假设一个组织需要保留多年级别的系统审计信息,那么就需要一个长期存档的 ETL 管道,将血缘数据复制到归档存储中。

在接下来的章节中,我们将介绍如何通过各种方式查看 Databricks 中数据资产的血缘。

使用数据血缘 REST API 追溯数据来源

像 Databricks 数据智能平台中的许多可安全存储对象一样,有多种方式可以检索与对象相关的详细血缘信息。在 Databricks 中,检索特定对象的血缘信息的常见模式之一是通过数据血缘 REST API。目前,数据血缘 REST API 限制为仅检索表的血缘信息以及列的血缘信息的只读视图。

UC 对象HTTP 动词端点描述
GET/api/2.0/lineage-tracking/table-lineage给定一个 UC 表名,检索上游和下游表连接的列表,并提供相关笔记本连接信息
GET/api/2.0/lineage-tracking/column-lineage给定一个 UC 表名和列名,检索上游和下游列连接的列表

表 7.1 – 数据血缘 REST API 获取与 UC 表和列对象相关的上游和下游连接信息

然而,预计数据血缘 REST API 将随着时间的推移不断发展,增加更多功能,使数据管理员能够检索信息,甚至操控平台内数据资产的端到端血缘。

让我们看看如何使用数据血缘跟踪 API 来检索由本章配套 GitHub 仓库中的数据集生成器笔记本创建的表的上游和下游连接信息,仓库地址为:GitHub 仓库

首先,我们将在 Databricks 工作区中创建一个全新的笔记本,并导入 requests Python 库。我们将专门使用 Python requests 库来向 Databricks REST API 发送数据血缘请求,并解析来自 Databricks 控制平面的响应:

import requests

创建并启动一个通用集群,将笔记本附加到该集群并运行笔记本单元。您需要生成一个个人访问令牌(PAT)以便与 Databricks REST 端点进行身份验证并发送数据血缘 API 请求。强烈建议将 PAT 存储在 Databricks 秘密对象中(Databricks 秘密文档),以避免意外泄露身份验证详细信息。

重要提示 以下代码示例仅用于说明目的。您需要更新工作区名称以匹配您的 Databricks 工作区的名称,以及 API 令牌的值。

我们使用 requests 库通过指定完全限定的端点向数据血缘 API 发送请求:

response = requests.get(
    f"https://{WORKSPACE_NAME}.cloud.databricks.com/api/2.0/lineage-tracking/table-lineage",
    headers={
        "Authorization": f"Bearer {API_TOKEN}"
    },
    json={
        "table_name": FULLY_QUALIFIED_TABLE_NAME,
        "include_entity_lineage": "true"
    }
)
print(response.json())

接下来,我们添加一些辅助函数来解析数据血缘 API 的响应,并以易于理解的格式打印连接信息。将以下辅助函数添加到笔记本的新单元中:

def print_table_info(conn_type, table_info_json):
    info = table_info_json["tableInfo"]
    print(f"""
        +---------------------------------------------+
        | {conn_type.upper()} Table Connection Info
        |---------------------------------------------|
        | Table name: {info['name']}
        |---------------------------------------------|
        | Catalog name: {info['catalog_name']}
        |---------------------------------------------|
        | Table type: {info['table_type']}
        |---------------------------------------------|
        | Lineage timestamp: {info['lineage_timestamp']}
        +---------------------------------------------+
    """)
    if conn_type.upper() == "UPSTREAMS":
        print(f"""
                                |
                               |/
        """)
def print_notebook_info(conn_type, notebook_info):
    print(f"""
        +---------------------------------------------+
        | {conn_type.upper()} Notebook Connection Info:
        |---------------------------------------------|
        | Workspace id: {str(notebook_info['workspace_id'])}
        |---------------------------------------------|
        | Notebook id: {str(notebook_info['notebook_id'])}
        |---------------------------------------------|
        | Timestamp: {notebook_info['lineage_timestamp']}
        +---------------------------------------------+
    """)

现在,让我们更新之前代码片段中的响应部分,用于获取表的血缘信息,但这次我们将调用这些辅助函数:

if response.status_code == 200:
    connection_flows = ["upstreams", "downstreams"]
    for flow in connection_flows:
        if flow in response.json():
            connections = response.json()[flow]
            for conn in connections:
                if "tableInfo" in conn:
                    print_table_info(flow, conn)
                elif "notebookInfos" in conn:
                    for notebook_info in conn["notebookInfos"]:
                        print_notebook_info(flow, notebook_info)

现在,您应该能够看到从 Unity Catalog 中的表的上游和下游连接信息,且响应会以更易读的格式呈现。

image.png

数据血缘 API 非常适合追溯 Unity Catalog 中数据集之间的连接。然而,我们也可以检索关于表列的更细粒度的血缘信息。在下一个示例中,让我们检索表中 description 列的相关信息。我们还将定义另一个辅助函数,以便以清晰的方式显示列的连接信息:

def print_column_info(conn_type, column_info):
    print(f"""
        Connection flow: {conn_type.upper()}
        Column name: {column_info['name']}
        Catalog name: {column_info['catalog_name']}
        Schema name: {column_info['schema_name']}
        Table name: {column_info['table_name']}
        Table type: {column_info['table_type']}
        Lineage timestamp: {column_info['lineage_timestamp']}
    """)

然后,设定我们要查询的列名并发送请求:

column_name = "description"
response = requests.get(
    f"https://{WORKSPACE_NAME}.cloud.databricks.com/api/2.0/lineage-tracking/column-lineage",
    headers={
        "Authorization": f"Bearer {API_TOKEN}"
    },
    json={
        "table_name": FULLY_QUALIFIED_TABLE_NAME,
        "column_name": column_name
    }
)

如果响应状态码为 200,我们将继续查看上游和下游列连接信息,并使用我们定义的辅助函数打印这些信息:

if response.status_code == 200:
    if "upstream_cols" in response.json():
        print("| Upstream cols:")
        for column_info in response.json()['upstream_cols']:
            print_column_info("Upstream", column_info)
    if "downstream_cols" in response.json():
        print("| Downstream cols:")
        for column_info in response.json()['downstream_cols']:
            print_column_info("Downstream", column_info)

在这个场景中,我们的 description 列特别有意思,因为它是通过将一个文本字符串与两个不同的列连接而生成的。如果您更新之前的列血缘请求,使用不同的列名,您会注意到上游源的数量会变化,以反映特定于该列的连接数。

image.png

到目前为止,您应该已经能够熟练使用 Databricks 数据血缘 API 来追溯数据集之间的连接,甚至细粒度的数据转换,例如列连接。如您所见,来自数据血缘 API 的请求和响应需要处理 JSON 负载的经验。对于一些响应,我们需要创建辅助函数来将响应解析为更易读的格式。

在接下来的部分中,我们将介绍如何使用 Databricks UI 来追溯数据集关系,这使得非技术数据管理员能够通过点击按钮轻松查看上游和下游数据源。

可视化上游和下游转换

在本节中,我们将利用数据集生成器笔记本,在 Unity Catalog 中创建几个数据集,以便使用 Databricks UI 来追踪数据集血缘。如果您还没有这样做,请克隆本章的配套 GitHub 仓库,地址为:GitHub 仓库。接下来,启动一个现有的通用集群,或者创建一个新的集群,并将数据生成器笔记本附加到集群上。点击 Databricks 工作区右上角的 Run all 按钮,执行所有笔记本单元,确保所有单元成功执行。如果遇到运行时错误,请确认您具有在 Unity Catalog 元存储中创建新目录、模式和表的正确权限。

重要提示 您需要被授予在 Unity Catalog 元存储中创建新目录和模式的权限。如果这不可能,请随时重用现有的目录和模式来生成示例表。您需要相应地更新 DDL 和 DML 语句,以匹配您自己 Databricks 工作区中的值。

数据生成器笔记本的结果应该是三张表:youtube_channelsyoutube_channel_artistscombined_table。在 Databricks 数据智能平台中,数据血缘可以通过多种方式轻松追踪。在此示例中,我们将使用 Databricks UI 来追溯一个数据资产的血缘——combined_table 表。首先,从 Databricks 工作区中,点击左侧导航菜单中的 Catalog Explorer 选项卡。接下来,可以深入到目录和模式中找到 combined_table 表,或者直接在 Catalog Explorer 顶部的搜索框中输入 combined_table,这将过滤匹配该文本字符串的数据资产列表。点击 combined_table 表,将会在 UI 右侧的单独面板中打开该数据资产的概览详情。

image.png

在 UI 面板中,点击 Lineage 选项卡以查看我们表的 数据血缘 详情。进入 Lineage 选项卡后,您应该会看到与 combined_table 数据集相关的所有连接的摘要,清晰地标识出构建此表所使用的所有上游数据源,以及任何利用此表的下游依赖关系。

image.png

在这种情况下,应该有两行包含关于上游数据源的信息——youtube_channels 父表和 youtube_channel_artists 表。由于我们刚刚使用数据生成器笔记本创建了这个表,因此目前不应该有任何具有下游依赖关系的行。如您所想,这个表将会几乎实时地更新,列出所有以某种方式使用该数据集的对象,清晰地标识出数据的任何下游依赖项。

最后,让我们可视化一下我们表的血缘关系图。点击标有 See lineage graph 的蓝色按钮,打开血缘可视化图。

现在,您应该能够清晰地看到,两张上游表连接形成了 combined_table 表。

image.png

接下来,点击连接上游表与下游表 combined_table 的箭头,以显示有关血缘连接的更多详细信息。您会注意到,一个侧边面板会打开,显示血缘连接的信息,例如源表和目标表,但它还会展示这些数据资产如何在 Databricks 数据智能平台中的其他各个对象中被使用。例如,UI 面板会列出这些数据集当前如何在笔记本、工作流、DLT 管道和 DBSQL 查询中被利用。在这种情况下,我们仅通过数据生成器笔记本生成了这些表,因此它是血缘连接信息中唯一列出的对象。

image.png

列血缘也可以通过目录资源管理器进行追踪。在相同的血缘图中,点击 combined_table 表中的各个列,以显示血缘信息。例如,点击 description 列时,血缘图将更新,清晰地可视化 description 列是如何计算的。在这种情况下,该列是通过将一段文本字符串与父表中的 category 列以及子表中的 artist’s name 列连接在一起计算得出的。

image.png

识别依赖关系和影响

在本节中,我们将再次利用目录资源管理器中的血缘图 UI,更好地理解更改特定列的数据类型和值将如何影响下游数据集和下游过程,例如笔记本和工作流,在我们的 Databricks 工作区中进行操作。

首先,我们将创建一个新的笔记本,包含我们新 DLT 管道的定义。我们 DLT 管道中的第一个数据集将摄取包含商业航空公司航班信息的原始 CSV 文件,这些文件存储在默认的 Databricks 文件系统(DBFS)中的 /databricks-datasets 目录下。每个 Databricks 工作区都可以访问此数据集。创建一个新的笔记本单元,并添加以下代码片段,以定义我们数据管道中的铜级表:

import dlt
@dlt.table(
    name="commercial_airliner_flights_bronze",
    comment="位于`/databricks-datasets/`中的商业航空公司航班数据集"
)
def commercial_airliner_flights_bronze():
    path = "/databricks-datasets/airlines/"
    return (spark.readStream
            .format("csv")
            .schema(schema)
            .option("header", True)
            .load(path))

我们希望通过添加有关商业航空喷气机的信息来增强航班数据。创建一个新的笔记本单元,并添加以下代码片段,定义一个静态引用表,包含关于流行商业航空喷气机的信息,包括制造商名称、飞机型号、原产国和燃油容量等:

commercial_airliners = [
    ("Airbus A220", "Canada", 2, 2013, 2016, 287, 287, 5790),
    ("Airbus A330neo", "Multinational", 2, 2017, 2018, 123, 123, 36744),
    ("Airbus A350 XWB", "Multinational", 2, 2013, 2014, 557, 556, 44000),
    ("Antonov An-148/An-158", "Ukraine", 2, 2004, 2009, 37, 8, 98567),
    ("Boeing 737", "United States", 2, 1967, 1968, 11513, 7649, 6875),
    ("Boeing 767", "United States", 2, 1981, 1982, 1283, 764, 23980),
    ("Boeing 777", "United States", 2, 1994, 1995, 1713, 1483, 47890),
    ("Boeing 787 Dreamliner", "United States", 2, 2009, 2011, 1072, 1069, 33340),
    ("Embraer E-Jet family", "Brazil", 2, 2002, 2004, 1671, 1443, 3071),
    ("Embraer E-Jet E2 family", "Brazil", 2, 2016, 2018, 81, 23, 3071)
]
commercial_airliners_schema = "jet_model string, Country_of_Origin string, Engines int, First_Flight int, Airline_Service_Entry int, Number_Built int, Currently_In_Service int, Fuel_Capacity int"
airliners_df = spark.createDataFrame(
    data=commercial_airpliners,
    schema=commercial_airliners_schema
)

接下来,我们将商业航空喷气机参考表保存到 Unity Catalog 中先前创建的模式:

airliners_table_name = f"{catalog_name}.{schema_name}.{table_name}"
(airliners_df.write
    .format("delta")
    .mode("overwrite")
    .option("mergeSchema", True)
    .saveAsTable(airliners_table_name))

让我们为数据管道添加另一个步骤,将我们的静态商业喷气机航空参考表与航空航班数据流进行连接。在新的笔记本单元中,创建以下用户定义函数(UDF),该函数将为商业航空数据集中的每个条目生成一个尾号:

from pyspark.sql.types import StringType
from pyspark.sql.functions import udf
@udf(returnType=StringType())
def generate_jet_model():
    import random
    commercial_jets = [
        "Airbus A220",
        "Airbus A320",
        "Airbus A330",
        "Airbus A330neo",
        "Airbus A350 XWB",
        "Antonov An-148/An-158",
        "Boeing 737",
        "Boeing 767",
        "Boeing 777",
        "Boeing 787 Dreamliner",
        "Comac ARJ21 Xiangfeng",
        "Comac C919",
        "Embraer E-Jet family",
        "Embraer E-Jet E2 family",
        "Ilyushin Il-96",
        "Sukhoi Superjet SSJ100",
        "Tupolev Tu-204/Tu-214"
    ]
    random_index = random.randint(0, 16)
    return commercial_jets[random_index]

最后,再创建一个笔记本单元,并添加以下 DLT 数据集定义,创建我们的银级表:

@dlt.table(
    name="commercial_airliner_flights_silver",
    comment="增强了随机生成喷气机型号和使用的燃料量的商业航空公司航班数据"
)
def commercial_airliner_flights_silver():
    return (dlt.read_stream(
            "commercial_airliner_flights_bronze")
            .withColumn("jet_model", generate_jet_model())
            .join(spark.table(airliners_table_name),
                  ["jet_model"], "left"))

在提示时,点击笔记本单元输出底部的 Create pipeline 蓝色按钮创建一个新的 DLT 管道。给管道起一个有意义的名字,例如 Commercial Airliner Flights Pipeline。选择 Triggered 作为执行模式,选择 Core 作为产品版本。接下来,选择上一段代码示例中的目标目录和模式,作为我们 DLT 管道的目标数据集位置。最后,点击 Start 按钮触发管道更新。

image.png

让我们假设有一个外部过程,旨在计算每次商业航班的碳足迹。在这个例子中,过程是另一个 Databricks 笔记本,它读取我们的银级表的输出,并计算每个航班在美国境内的二氧化碳排放量。

在 Databricks 工作区内创建另一个笔记本,并为其起一个有意义的名字,如 Calculating Commercial Airliner Carbon Footprint。接下来,添加一个新的笔记本单元,读取银级表并使用简单的公式计算二氧化碳输出:

碳足迹 = 燃烧的燃料量 * 系数 / 乘客人数

在这个例子中,我们只关心计算每个航空喷气机的碳足迹;因此,我们将避免除以乘客人数。将以下代码片段添加到新创建的笔记本中,这将为每个航班条目分配一个计算出的碳足迹:

# 每燃烧1公斤燃料产生3.1公斤的CO2
# 因此,我们将上述燃料质量乘以3.1来估算排放的CO2
# 来源:https://ecotree.green/en/calculate-flight-co2
# 1加仑喷气燃料大约重3.03907公斤
def calc_carbon_footprint(fuel_consumed_gallons):
    return (fuel_consumed_gallons * 3.03907) * 3.1

再假设我们 DLT 管道中银级表的燃料容量量度单位目前是以加仑为单位的。然而,我们的欧洲业务伙伴希望使用升作为数据集的单位。让我们使用 Catalog Explorer 来探索我们银级表的血缘图,更好地理解将 fuel_capacity 列的单位转换为升将对数据集消费者产生什么样的影响。通过点击 Databricks 数据智能平台左侧导航栏中的 Catalog Explorer,在搜索框中输入目录名称进行过滤,然后点击 silvercommercial_airliner_flights_silver 来导航到血缘图。

image.png

通过生成血缘图,我们能够几乎实时地看到所有可能依赖于该列的下游列。此外,我们还可以看到所有依赖于该列的 Unity Catalog 对象的实时列表,例如笔记本、工作流、DLT 管道和机器学习模型。因此,实际上,我们可以快速了解更改单位衡量标准可能对共享此数据集的整个组织产生的影响。

在接下来的部分,我们将继续这个示例,确定一种替代方法来更新数据集,包括燃料容量、旅行距离和到达时间,以便使其更符合欧洲需求,而不会影响我们数据的现有消费者。

动手实验 - 跨组织记录数据血缘

在本节中,我们将探讨 Databricks 中的系统表如何自动记录数据集和其他数据资产之间关系随时间变化的情况。如前所述,Unity Catalog 会保留所有连接到同一 Unity Catalog 元存储的工作区之间的数据血缘。这在组织需要对其数据资产进行强有力的端到端审计时尤其有用。

让我们再次开始,在 Databricks 工作区中创建一个新的笔记本,并给它一个有意义的标题,例如 Viewing Documented Data Lineage。接下来,创建一个新的通用集群,或将笔记本附加到一个已运行的集群上,以开始执行笔记本单元。

像数据血缘 API 一样,Unity Catalog 中有两个系统表提供了血缘信息的只读视图——system.access.table_lineage 表和 system.access.column_lineage 表。数据血缘系统表自动记录与 UC 表和列对象的上游和下游连接相关的信息。

UC 对象表名称描述
system.access.table_lineage包含上游和下游表连接的列表,以及与其相关的笔记本连接信息
system.access.column_lineage包含上游和下游列连接的列表

表 7.2 – 数据血缘系统表捕获有关表和列连接的信息

让我们查询上一示例中的上游和下游血缘信息。在新的笔记本单元中,添加以下查询并执行单元:

SELECT *
  FROM system.access.table_lineage
  WHERE source_table_name LIKE '%commercial_airliners_silver%';

我们将得到以下输出:

image.png

从输出中可以看出,系统表自动记录了有关上游和下游数据源的连接信息。此外,系统表还会自动捕获审计信息,包括数据集创建者的信息和对象创建的事件时间戳。这是一个很好的方式,可以在组织的数据集之间文档化、审查甚至报告数据血缘。

总结

在本章中,我们探讨了在 Databricks 数据智能平台中追溯数据血缘的各种方式。我们看到,数据血缘 REST API 使我们能够快速查看 Unity Catalog 中特定表或列的上游和下游连接。接下来,我们了解了如何使用 Unity Catalog 中的目录资源管理器轻松生成血缘图。血缘图对于深入了解数据集的变化如何影响数据集下游消费者至关重要。最后,我们看到了 Unity Catalog 中的系统表如何为我们的组织提供文档化数据资产关系流的方式。

在下一章中,我们将关注如何使用 Terraform 等工具以自动化方式部署我们的数据管道及其所有依赖项。