AI生成的图片
你好 again,各位技术爱好者!
在我的上一篇文章文章中,我提供了一个关于如何使用Spark和Apache Iceberg等技术搭建数据工程项目系统的分步指南。这一次,我想要更进一步,利用该系统构建一个全流程没有使用Spark的数据管道。
虽然 Spark 是一个流行的大规模数据处理分析引擎,但它在处理小到中等规模的数据集时(如小于 2TB 的数据)可能会变得相当慢。另一方面,Polars 在处理类似规模的数据集时已经证明要快得多。可以看看 这篇分析。因此,我们将用 Polars 来处理数据,并通过 PyIceberg 访问 Iceberg 表,无需依赖 JVM。
此外,在之前的部分中,我使用了Hadoop目录作为我们数据湖的目录。目录是表格式的关键组成部分,使我们能够确保多个读取者和写入者之间的一致,并发现环境中有可用的哪些表。
在选择使用哪个目录时,有很多选择,比如 Hadoop、Hive、AWS Glue、REST、Nessie 等。在这些选项里,我发现 Project Nessie 尤其有趣,因为它采用了类似 Git 的语义,这种语义使增强了数据湖的能力,使 数据即代码 的理念得以实现。
因此,在这篇帖子中,我们将使用Nessie及其分支功能特性来应用WAP设计到我们的数据管道流程中(受Bauplan启发)。我们将利用PyIceberg的REST目录与Nessie进行端到端的交互,从而理解其实际好处及其如何让数据管道流程更顺畅。
到本文结束时,你会有一个专为小型和中型数据集(、)优化的功能数据管道的基础架构,利用Polars的速度优势以及Project Nessie和PyIceberg的创新功能。
咱们现在就开始吧。
这个项目首先,我们需要开始一个数据工程项目的核心要素是——正如你所料——数据。
虽然在线上有许多数据集可供使用,但抓取数据或找到适合你演示的数据集可能非常令人沮丧。因此,我将生成一些合成数据集,以复制Olist巴西电商数据集。此数据集提供了来自领先的巴西电商平台的有关销售和客户互动的全面信息,非常适合用来展示我们的数据处理管道。
复制的Olist电子商务数据架构图(作者创建,原图可在此处找到 此处)
为了高效地处理和管理这类数据,我们将在这个项目中使用medallion架构(注:也称为层级架构)。
勋章式的建筑风格勋章架构模式是一种数据设计模式,将数据分成增量层——这三个阶段分别是铜,银,和金——从而提高数据质量和简化数据治理。每一层在数据处理管道中都有特定的功能。通过分成这些增量层,数据质量得以提高,数据治理也变得更加简化。
- 青铜层(原始数据): 存储来自源头的原始数据的初始层,仅进行基本处理。
- 白银层(清洗/转换数据): 中间层,数据在这里被清洗、转换,并准备好进行分析。
- 黄金层(聚合分析数据): 最终层,包含完全清洗和聚合的数据,准备用于分析和机器学习模型。
通过这种方法分层,我们能够:
- 系统地提高数据质量: 每一层都应用转换和验证来提高数据质量。
- 提高数据管道效率: 通过分阶段处理数据,我们可以尽早识别和解决这些问题。
- 促进数据治理: 清晰地划分数据阶段有助于合规和审计。
(下图由作者创作)数据转换过程在Medallion架构中(注:此处指代特定的数据架构)
通过采用勋章式架构,我们确保在每个阶段都提升数据质量,从而促进高效的数据处理并确保可靠的分析结果。这将帮助我们搭建一个符合项目需求的稳健且可扩展的数据流。
数据流水线,架构
下面这张图展示了我们数据管道的架构,以及我们如何从消费原始数据到达到黄金层的过程。由于这三个管道在结构上非常相似,我将主要描述加载器组件,在该组件中使用了Project Nessie。所有管道的代码可以在这里找到:here。
数据管道结构(由作者绘制的图片)
管道设计和分支策略在我们数据管道中,我们遵循一个符合Write-Audit-Publish (WAP)模式的结构化流程。这种模式确保了每个阶段的数据质量和完整性。具体来说:
数据变换:
- 数据转换完成后,我们进入 Loader 模块(在 Mage 中称为 Exporter)。
- 这一阶段代表了 WAP 模式中的 写 阶段,此时准备将转换后的数据加载。
命名空间和表的建立:
- 在加载器中,如果发现命名空间和表不存在,我们就会创建它们。
- 我们定义一个命名空间作为数据库,并让它对应数据的内容,例如
ecommerce
。 - 这种组织结构有助于高效地管理和访问数据。
分支版本控制策略:
我们采用分支策略来隔离变更并以保持数据的完整性:
- 层级分支: 我们分别为青铜层、白银层和主层(即黄金层)创建了各自的分支。
- 表分支: 在每一层的分支内,我们分别为每个新表生成分支,用于执行数据转换和质量检查。
这相当于 WAP 模式的审计阶段,在发布之前验证数据。
数据质量检查和合并:
在每个表的子表中,我们会对数据质量进行审核。
如果一切顺利:
- 我们将表分支合并回相应的层级分支(例如,将
bronze_orders_table
合并到bronze
)。 - 然后我们将层级分支合并到下一层次 — bronze 合并到 silver,并 silver 合并到 main。
- 这代表了 WAP 模式中的 发布,经过验证的数据在这一阶段被提升到下一阶段。
如果检查不通过:
- 我们发送警报并且不合并该分支。
- 这样可以防止损坏或无效的数据继续在管道中流动。
注意:命名空间、表和分支的结构方式有很多种。例如,可以将所有层都放在主分支内,或者为每个层使用单独的命名空间。
注意:实际上,最好在脚本之外定义命名空间和表,这样可以更容易地定义额外属性。
这种方法的优点,
- 数据隔离处理: 通过在表和层级别管理分支,确保每个分支中的数据与最终用户和其他层次隔离不产生影响。
- 安全测试环境: 这种隔离使我们可以查询和检查分支内的数据,发现问题并重新运行管道,而不会影响主分支或其它层次。
- 数据完整性: 我们确保只有经过验证和质量保证的数据才能推进到下一个管道阶段,保持数据管道的完整性和可靠性。
- 符合WAP模式: 这种分支策略体现了写-审核-发布模式:
- 写: 数据被写入隔离的分支。
- 审核: 在这些分支中进行数据质量的检查。
- 发布: 经过验证的数据会被合并到下一个层次的分支中。
通过利用Project Nessie的类似Git的分支和合并功能,我们可以高效地管理和转换数据,从而在整个数据处理流程中保持高标准的数据质量。
整合所有内容:搭建数据管道现在咱们看看一切运作起来。
首先,我创建了两个类:NessieBranchManager
用于分支管理,IcebergTableManager
用于表格管理。Bauplan 将这两个功能结合到一个类中,通过一个方便的补丁,但我发现这有些复杂,因此为了简洁明了,因此在我们的演示中将它们分开来。
尼斯分支经理
以下展示的是 NessieBranchManager
类,该类用于管理 Project Nessie 中分支的功能。下面是该类的代码,包括创建、删除、合并和检索分支的方法等等,等功能。
from pynessie import init as nessie_init
import time
import os
class NessieBranchManager:
def __init__(self, verify: bool = False):
"""初始化Nessie客户端,用于分支管理。"""
self.endpoint = os.environ.get('NESSIE_ENDPOINT', "http://nessie:19120/api/v1/")
self.nessie_client = nessie_init(config_dict={"endpoint": self.endpoint, "verify": verify})
def create_branch(self, branch_name: str, from_branch: str = "main"):
"""从源分支创建一个新的分支,确保使用源分支的哈希值。"""
try:
# 检查分支是否已存在
existing_branch = self.nessie_client.get_reference(branch_name)
print(f"分支 '{branch_name}' 已存在。跳过。")
return existing_branch.name # 返回已存在的分支名称
except Exception:
# 如果分支不存在,创建它
try:
from_branch_ref = self.nessie_client.get_reference(from_branch)
from_branch_hash = from_branch_ref.hash_
new_branch = self.nessie_client.create_branch(branch_name, ref=from_branch, hash_on_ref=from_branch_hash)
print(f"分支 '{branch_name}' 已从 '{from_branch}' 创建,哈希值:'{from_branch_hash}'。")
return new_branch.name # 返回新创建的分支名称
except Exception as e:
print(f"创建分支 '{branch_name}' 失败,错误信息:{e}")
raise
# 完整代码请参阅仓库。
注:为了清晰阅读,仅包括类定义和
create_branch
方法。完整代码中还包括其他方法,例如generate_custom_branch_name
、delete_branch
、merge_branch
和get_branch
方法。
IcebergTableManager 类
The IcebergTableManager
类处理 Apache Iceberg 中的表操作。除了标准的 PyIceberg 功能以外,它还提供了将 Polars DataFrames 转换为 PyArrow Tables 的工具,便于数据交换和处理,因为 Mage 目前还不支持在不同脚本之间导出 Arrow 表。
from pyiceberg.catalog import load_catalog
import pyarrow as pa
import polars as pl
import os
class IcebergTableManager:
def __init__(self):
self.minio_endpoint = os.getenv("MINIO_ENDPOINT", "minio:9000")
self.minio_access_key = os.getenv("MINIO_ACCESS_KEY")
self.minio_secret_key = os.getenv("MINIO_SECRET_KEY")
def create_namespace_if_not_exists(self, catalog, namespace):
try:
# 检查命名空间是否已存在
existing_namespaces = [ns[0] for ns in catalog.list_namespaces()]
if namespace not in existing_namespaces:
namespace_str = f"命名空间 '{namespace}'"
print(namespace_str + " 不存在。正在创建。")
catalog.create_namespace(namespace)
print(namespace_str + " 已创建成功。")
else:
print(f"命名空间 '{namespace}' 已存在。")
return namespace
except Exception as e:
print(f"创建或列出命名空间时出错: {e}")
raise e
# 请参阅仓库中的完整代码
注:该类包括
initialize_rest_catalog
、create_iceberg_table
、polars_to_pyarrow_schema
和polars_to_arrow_with_schema
等方法。
在加载器块中整合各类
在定义了这些类之后,我们现在可以将它们集成到 Loader 模块中来,以实现 写入、审计、发布 (WAP) 模式。
如下脚本展示了Loader模块是如何通过Nessie来进行分支的,以便促进WAP流程。尽管示例中使用了银层的导出器作为示例,但所有管道中,结构基本相似,只需要一些小的调整。
if 'data_exporter' not in globals():
from mage_ai.data_preparation.decorators import data_exporter
import pyarrow as pa
import polars as pl
from datetime import datetime
from pyiceberg.catalog import load_catalog
import os
from minio import Minio
import time
from mage.utils.nessie_branch_manager import NessieBranchManager
from mage.utils.iceberg_table_manager import IcebergTableManager
def data_quality_check(table):
arrow_table = table.scan().to_arrow()
for column_name in arrow_table.schema.names:
column = arrow_table[column_name]
# 检查每个列的空值数量
null_count = column.null_count
if null_count > 0:
print(f"列 '{column_name}' 包含 {null_count} 个空值。")
return False
else:
print(f"没有空值。")
return True
def write_data(data, NAMESPACE, branch_manager, table_manager, tbl_name, silver_br, BUCKET_NAME):
table_name = tbl_name
schema = data.schema
arrow_schema = table_manager.polars_to_pyarrow_schema(schema)
arrow_table = table_manager.polars_to_arrow_with_schema(data, arrow_schema)
# 初始化 Nessie 和 Iceberg 的 REST catalog
main_catalog = table_manager.initialize_rest_catalog(silver_br)
# 创建命名空间
namespace = table_manager.create_namespace_if_not_exists(main_catalog, NAMESPACE)
# 如果表不存在,则创建 Iceberg 表
table_manager.create_iceberg_table(main_catalog, namespace, table_name, arrow_schema, f"s3a://{BUCKET_NAME}/{NAMESPACE}")
branch_name = branch_manager.generate_custom_branch_name(table_name, NAMESPACE)
new_branch_name = branch_manager.create_branch(branch_name, silver_br)
# 从层分支合并以确保表存在于新分支上
branch_manager.merge_branch(from_branch=silver_br, to_branch=new_branch_name)
# 为特定分支重新初始化目录
catalog = table_manager.initialize_rest_catalog(new_branch_name)
# 从分支加载表
table_identifier = f"{NAMESPACE}.{table_name}"
_table = catalog.load_table(f"{table_identifier}")
# 将 Arrow 表数据追加到 Iceberg 表中
_table.append(arrow_table)
_pass = data_quality_check(_table)
if _pass:
branch_manager.merge_branch(from_branch=new_branch_name, to_branch=silver_br)
branch_manager.delete_branch(new_branch_name)
else:
raise ValueError(f"表 {table_name} 和分支 {new_branch_name} 的质量测试未通过。")
@data_exporter
def export_data(orders_fct, order_items_fct, sellers_dim, customers_dim, products_dim, *args, **kwargs):
"""
导出数据到某些源。
参数:
data: 上游父块的输出
args: 任何附加的上游块的输出(如果适用)
输出(可选):
可选返回任何对象,它将被记录并在检查块运行时显示。
"""
# 在这里指定您的数据导出逻辑
BUCKET_NAME = kwargs["bucket_name"]
NAMESPACE = kwargs["namespace"]
TABLE_NAME = kwargs['table_name']
DATA_LAYER = kwargs['data_layer']
table_name = f'{TABLE_NAME}_{DATA_LAYER}'
branch_manager = NessieBranchManager()
table_manager = IcebergTableManager()
# 创建数据层分支
silver_br = branch_manager.create_branch(DATA_LAYER)
_ = write_data(orders_fct, NAMESPACE, branch_manager, table_manager, 'orders_fct_silver', silver_br, BUCKET_NAME)
_ = write_data(order_items_fct, NAMESPACE, branch_manager, table_manager, 'order_items_fct_silver', silver_br, BUCKET_NAME)
_ = write_data(sellers_dim, NAMESPACE, branch_manager, table_manager, 'sellers_dim_silver', silver_br, BUCKET_NAME)
_ = write_data(customers_dim, NAMESPACE, branch_manager, table_manager, 'customers_dim_silver', silver_br, BUCKET_NAME)
_ = write_data(products_dim, NAMESPACE, branch_manager, table_manager, 'products_dim_silver', silver_br, BUCKET_NAME)
通过将这些组件整合,我们成功地实现了数据流管道,使用Project Nessie进行分支管理,并且用PyIceberg管理表。
这种设置采用了Write-Audit-Publish (WAP) 模式,即写-审核-发布流程。
- 写: 我们使用
write_data
函数将数据写入专门针对表和数据层的新独立分支。 - 审计: 我们使用
data_quality_check
函数进行数据质量检查。 - 发布: 数据通过检查后,我们将该分支合并回该层的主要分支(例如,青铜层、银层)。
通过遵循WAP(Web Application Protocol)模式,我们确保了整个流程中的数据质量和完整性,防止受损的数据流入主干,并保持可靠的数据基础设施稳定。
使用 Webhooks 添加提醒通知为了提高管道的健壮性,我们可以设置警报通知,以便在故障发生时及时得到提醒。Mage 提供了一个内置的解决方案,当故障发生时可以通过 webhook 发送消息。这使我们能够及时处理问题并确保数据管道的可靠性。
设置 Webhook(Webhook 接口)
首先,在您的流程的元数据文件(metadata.yaml
)中定义 webhook 的 URL。这里是如何配置它:
notification_config:
alert_on:
- trigger_failure
slack_config:
webhook_url: "{{ env_var('MAGE_SLACK_WEBHOOK_URL') }}"
message_templates:
failure:
details: >
管道执行失败: {pipeline_run_url}.
管道 UUID: {pipeline_uuid}. 触发器名: {pipeline_schedule_name}.
错误详情: {stacktrace}.
在这个设置里:
**alert_on**
:指定触发警报的事件。在这种情况下,会在管道失败时发送警报。**slack_config**
:包含 Slack 的 webhook URL。我们使用环境变量MAGE_SLACK_WEBHOOK_URL
以保持 webhook 的安全,避免将其置于代码库中。**message_templates**
:允许自定义警报消息。你可以添加如{pipeline_run_url}
、{pipeline_uuid}
、{stacktrace}
等占位符来提供更详细的警报信息。
和WAP模式集成
虽然Mage提供功能来测试每个脚本的输出,但在将数据写入目标之前,我们需要实现自己的数据质量检查。这时,这种模式就显得很有用了。Write-Audit-Publish (WAP) 模式直接使用英文缩写。
发送失败通知
一旦数据有问题,我们可以在代码里抛异常。
# 这是数据导出器的最后一部分。
if _pass:
branch_manager.merge_branch(from_branch=new_branch_name, to_branch=branch_lyr)
branch_manager.delete_branch(new_branch_name)
else:
raise ValueError(f"表 {table_name} 和分支 {new_branch_name} 的质量测试未通过")
通过抛出异常,我们触发了Mage的故障处理机制。因为设置了notification_config
,当异常发生时,Mage会自动通过webhook发送报警信息。
魔法师警报:Slack(消息截断)
这将确保:
- 我们会立即得知任何数据质量问题。
- 我们可以立即采取行动来调查并解决这些问题。
- 我们的数据管道通过防止无效数据的发布,保持了高数据完整性。
我们一步一步来看运行脚本时都做了些什么。注意:请设置环境变量
MAGE_SLACK_WEBHOOK_URL
,使用你实际的Slack webhook URL或你偏好的消息应用的webhook URL。
在运行 Loader 脚本之前,我们从一个空桶开始项目,该桶位于 MinIO 存储,如下所示:
MinIO 图像(作者提供):空的桶
同样,来看看Nessie的用户界面是什么样的:
Nessie界面分支创建前(作者供图)
我们可以看到Nessie有一个默认的分支叫main
,除此之外没有其他分支。
执行了青铜层的加载脚本后,我们发现了一些变化。
我们生成的数据存储在我们 MinIO 存储桶的名为 ecommerce
的命名空间中。
运行青铜管道后,MinIO存储的情况(图片来源:作者)
我们的Nessie仓库现在包含了ecommerce
命名空间,该命名空间位于bronze
分支下。在这个命名空间下,我们可以看到与青铜层相关的表。
尼斯茜的分支和表格(或桌子)
Nessie的一个有趣的地方是它可以查看提交记录。当你在Nessie UI中点击提交记录时,可以看到以下内容:
我们可以观察到它创建了一个空表,然后,由于数据质量检查通过了,它将表与数据合并到bronze
分支(因为其他分支在合并后被删除了,所以我们看不到它们)。
然后,我们来运行Silver流程,并故意让质量检查不通过(例如,修改质量检查的设置)。
我们现在看到silver
分支,还有一个额外的分支对应于失败的数据的分支。我们也会收到如上所示的slack警报(不过这里省略显示了)。
尼斯湖生态系统中的分支结构
我们来看看 silver
分支的提交历史吧。
尼斯湖怪物(尼斯IE)的提交记录在白银管道失败后(作者供图)
我们注意到最新的更改包括创建了 ecommerce
命名空间(namespace)和 orders_fct_silver
表。但是,与青铜层不同的是,我们没有看到合并的部分,因为数据质量检查没有通过。
如果我们切换到新创建的分支(或新分支),专门针对失败的Silver管道,可以看到:
- 来自银枝的变化: 继承自上次成功提交的数据。
- 尝试追加数据: 尝试提交数据的尝试。
尼斯湖水怪提交历史记录(作者供图),失败的 Silver 管道
由于我们的数据质量检查未通过,我们需要使用一个SQL引擎来调查数据。具体来说,虽然Nessie支持与多个SQL引擎的交互,较好的选择有Dremio、Spark和Trino。不过,在这次演示里,我们将使用StarRocks来查询这些数据(更多关于如何在Dremio里操作的信息,你可以参考这个示例。Docker Compose文件中也包含了Dremio的镜像,你可以试试)。
注: 在 StarRocks 中,我们可以从任何分叉读取数据,但不能创建或合并分叉,至少目前还行不通。
按照上一篇帖子中的过程,我们需要创建一个Iceberg目录(Catalog)。这次我们将使用REST类型而不是Hadoop。但是这里有一个不便之处,如果我们想调查特定分支的数据(就像我们现在所做的那样),我们需要创建另一个具有该分支特定URI的目录。
CREATE EXTERNAL CATALOG iceberg_catalog_silver
PROPERTIES
(
"type"="iceberg",
"iceberg.catalog.type"="rest",
"iceberg.catalog.uri"="http://nessie:19120/iceberg/silver",
"iceberg.catalog.warehouse"="warehouse",
"aws.s3.access_key"="admin",
"aws.s3.secret_key"="admin",
"aws.s3.endpoint"="http://minio:9000",
"aws.s3.enable_path_style_access"="true",
"aws.s3.enable_ssl"="false",
"client.factory"="com.starrocks.connector.iceberg.IcebergAwsClientFactory"
);
下面的查询创建了一个能够访问 silver
分支数据的目录。
注意: 我使用
'冰山目录仓库'='warehouse'
而不是"iceberg-demo-nessie"
,因为我们在 Docker Compose 文件中启动 Nessie 时提供的默认设置是'warehouse'
。
通过检查此目录中的数据库,我们果然只发现了ecommerce
名称空间。
现在如果我们要查看 silver
命名空间里的 orders_fct
表:
SELECT COUNT(*) FROM ecommerce.orders_fct_silver;
-- 该查询应该会返回 0
我们看到表格为空,这是因为质量检查未通过,数据未能合并到 silver
分支。
为了调查失败分支的数据,我们需要创建一个专门用于该分支的带有特定URI的目录。
CREATE 外部目录 iceberg_catalog_branch
属性
(
"type"="iceberg",
"iceberg.catalog.type"="rest",
"iceberg.catalog.uri"="http://nessie:19120/iceberg/ecommerce-orders_fct_silver-20241003-194424",
"iceberg.catalog.warehouse"="warehouse",
"aws.s3.access_key"="admin",
"aws.s3.secret_key"="admin",
"aws.s3.endpoint"="http://minio:9000",
"aws.s3.enable_path_style_access"="true",
"aws.s3.enable_ssl"="false",
"client.factory"="com.starrocks.connector.iceberg.IcebergAwsClientFactory"
);
查询新目录中的 orders_fct_silver
表:
SELECT COUNT(*) FROM ecommerce.orders_fct_silver;
-- 这应该返回我们添加的行数,应该是18条。
我们得到的结果等于追加的行数(也就是现有行数与追加行数的总和)。这让我们能够展开调查,找出数据未能通过质量检查的原因。
这是如何在你的管道中使用Nessie为数据添加类似Git的语义的方法。通过利用分支和提交历史,你可以更好地管理数据变更,隔离变更,进行数据质量检查,并排查问题而不会影响主分支或Gold分支或影响最终用户的数据体验。
例如,当银管道故障时,该批次的数据不会被合并到银层,也不会传递到金层。这确保只有经过验证的数据才会通过管道,从而确保每一层数据的完整性。
结论注意:采用此架构,您会在 MinIO 中看到所有层的所有表都位于同一个命名空间(ecommerce,电子商务)中,但您需要分别创建三个独立的目录数据库(每个层一个)以便进行查询。
在这个项目中,我们建立了一个端到端的数据管道,使用了MinIO、Iceberg、Nessie、Polars、StarRocks、Mage和Docker等工具。通过采用分层架构并利用Nessie的分支和合并特性,我们有效管理了数据转换,并通过采用Write-Audit-Publish (WAP) 模式,我们确保了数据质量。
我们的示例展示了Nessie在Olist数据集的相互关联的表上实现的层级版本控制,这就像在更大的数据湖部署中,一个表的变化可能会影响到其他表一样。我们根据数据层构建了分支策略,这使我们能够更高效地应对相互关联的数据所带来的复杂性。
通过整合这些现代工具和概念,我们不仅构建了一个功能性的数据管道,还为可扩展和可维护的数据架构奠定了基础。尽管Nessie相对较新,在没有Spark或Dremio等工具辅助时使用起来不够直观,但它在数据版本管理和数据管理实践方面展现出了巨大的潜力。
代码库你可以在这篇帖子和之前的帖子中找到代码here
参考文献共同学习,写下你的评论
评论加载中...
作者其他优质文章