PySpark 大规模数据分析精要(一)
原文:
annas-archive.org/md5/b3bdd515fb6b21fd6dc5f2c481c3d18f译者:飞龙
前言
Apache Spark 是一个统一的数据分析引擎,旨在以快速高效的方式处理海量数据。PySpark 是 Apache Spark 的 Python 语言 API,为 Python 开发人员提供了一个易于使用且可扩展的数据分析框架。
可扩展数据分析的必备 PySpark 通过探索分布式计算范式开始,并提供 Apache Spark 的高层次概述。然后,你将开始数据分析之旅,学习执行数据摄取、数据清洗和大规模数据集成的过程。
本书还将帮助你构建实时分析管道,使你能够更快速地获得洞察。书中介绍了构建基于云的数据湖的技术,并讨论了 Delta Lake,它为数据湖带来了可靠性和性能。
本书介绍了一个新兴的范式——数据湖仓(Data Lakehouse),它将数据仓库的结构和性能与基于云的数据湖的可扩展性结合起来。你将学习如何使用 PySpark 执行可扩展的数据科学和机器学习,包括数据准备、特征工程、模型训练和模型生产化技术。还介绍了如何扩展标准 Python 机器学习库的技术,以及在 PySpark 上提供的类似 pandas 的新 API,名为 Koalas。
本书适合人群
本书面向那些已经在使用数据分析探索分布式和可扩展数据分析世界的实践数据工程师、数据科学家、数据分析师、公民数据分析师和数据爱好者。建议你具有数据分析和数据处理领域的知识,以便获得可操作的见解。
本书内容概述
第一章,分布式计算概述,介绍了分布式计算范式。它还讲述了随着过去十年数据量的不断增长,分布式计算为何成为一种必需,并最终介绍了基于内存的数据并行处理概念,包括 Map Reduce 范式,并介绍了 Apache Spark 3.0 引擎的最新功能。
第二章,数据摄取,涵盖了各种数据源,如数据库、数据湖、消息队列,以及如何从这些数据源中摄取数据。你还将了解各种数据存储格式在存储和处理数据方面的用途、差异和效率。
第三章,数据清洗与集成,讨论了各种数据清洗技术,如何处理不良输入数据、数据可靠性挑战以及如何应对这些挑战,以及数据集成技术,以构建单一的集成数据视图。
第四章,实时数据分析,解释了如何进行实时数据的获取和处理,讨论了实时数据集成所面临的独特挑战及其解决方法,以及它所带来的好处。
第五章,使用 PySpark 进行可扩展的机器学习,简要讲解了扩展机器学习的需求,并讨论了实现这一目标的各种技术,从使用原生分布式机器学习算法,到令人尴尬的并行处理,再到分布式超参数搜索。它还介绍了 PySpark MLlib 库,并概述了其各种分布式机器学习算法。
第六章,特征工程——提取、转换与选择,探讨了将原始数据转化为适合机器学习模型使用的特征的各种技术,包括特征缩放和转换技术。
第七章,监督式机器学习,探讨了用于机器学习分类和回归问题的监督学习技术,包括线性回归、逻辑回归和梯度提升树。
第八章,无监督式机器学习,介绍了无监督学习技术,如聚类、协同过滤和降维,以减少应用监督学习前的特征数量。
第九章,机器学习生命周期管理,解释了仅仅构建和训练模型是不够的,在现实世界中,同一个模型会构建多个版本,并且不同版本适用于不同的应用。因此,有必要跟踪各种实验、它们的超参数、指标,以及它们训练所用的数据版本。还需要在一个集中可访问的库中跟踪和存储各种模型,以便能够轻松地将模型投入生产并进行共享;最后,还需要机制来自动化这一重复出现的过程。本章通过一个端到端的开源机器学习生命周期管理库 MLflow 介绍了这些技术。
第十章*, 使用 PySpark 进行单节点机器学习的横向扩展*,解释了在第五章**, 使用 PySpark 进行可扩展机器学习中,您学习了如何利用 Apache Spark 的分布式计算框架在大规模上训练和评分机器学习模型。Spark 的本地机器学习库很好地覆盖了数据科学家通常执行的标准任务;然而,标准的单节点 Python 库提供了多种功能,但这些库并非为分布式方式而设计。本章介绍了如何将标准 Python 数据处理和机器学习库(如 pandas、scikit-learn 和 XGBoost)进行横向扩展。本章将涵盖常见数据科学任务的横向扩展,如探索性数据分析、模型训练、模型推理,最后还将介绍一个可扩展的 Python 库——Koalas,它使您能够使用非常熟悉且易于使用的类似 pandas 的语法轻松编写 PySpark 代码。
第十一章*,* 使用 PySpark 进行数据可视化,介绍了数据可视化,这是从数据中传递意义并获得洞察力的重要方面。本章将介绍如何使用最流行的 Python 可视化库与 PySpark 结合。
第十二章*, Spark SQL 入门*,介绍了 SQL,这是一种用于临时查询和数据分析的表达语言。本章将介绍 Spark SQL 用于数据分析,并展示如何交替使用 PySpark 进行数据分析。
第十三章*, 将外部工具与 Spark SQL 集成*,解释了当我们在高效的数据湖中拥有干净、整理过且可靠的数据时,未能将这些数据普及到组织中的普通分析师将是一个错失的机会。最流行的方式是通过各种现有的商业智能(BI)工具。本章将讨论 BI 工具集成的需求。
第十四章*, 数据湖屋*,解释了传统的描述性分析工具,如商业智能(BI)工具,是围绕数据仓库设计的,并期望数据以特定方式呈现,而现代的高级分析和数据科学工具则旨在处理可以轻松访问的大量数据,这些数据通常存储在数据湖中。将冗余数据存储在单独的存储位置以应对这些独立的用例既不实际也不具成本效益。本章将介绍一种新的范式——数据湖屋,它试图克服数据仓库和数据湖的局限性,通过结合两者的最佳元素来弥合差距。
要最大限度地利用本书
预计读者具备数据工程、数据科学和 SQL 分析的基础至中级知识。能够使用任何编程语言,特别是 Python,并且具备使用 pandas 和 SQL 等框架进行数据分析的基本知识,将有助于你从本书中获得最大收益。
本书使用 Databricks Community Edition 来运行所有代码:community.cloud.databricks.com。注册说明可在databricks.com/try-databricks找到。
本书中使用的整个代码库可以从github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/blob/main/all_chapters/ess_pyspark.dbc下载。
本章使用的数据集可以在github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
如果你正在使用本书的数字版本,我们建议你自己输入代码,或者从本书的 GitHub 仓库获取代码(链接将在下一个章节提供)。这样做有助于避免由于复制和粘贴代码而导致的潜在错误。
下载示例代码文件
你可以从 GitHub 上下载本书的示例代码文件:github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics。如果代码有更新,GitHub 仓库会进行更新。
我们的书籍和视频丰富目录中还提供了其他代码包,可以在github.com/PacktPublishing/查看!
下载彩色图片
我们还提供了一个包含本书中使用的截图和图表的彩色图片的 PDF 文件。你可以在这里下载:static.packt-cdn.com/downloads/9781800568877_ColorImages.pdf
使用的约定
本书中使用了多种文本约定。
文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账户名。例如:“DataStreamReader 对象的 readStream() 方法用于创建流式 DataFrame。”
代码块按如下方式设置:
lines = sc.textFile("/databricks-datasets/README.md")
words = lines.flatMap(lambda s: s.split(" "))
word_tuples = words.map(lambda s: (s, 1))
word_count = word_tuples.reduceByKey(lambda x, y: x + y)
word_count.take(10)
word_count.saveAsTextFile("/tmp/wordcount.txt")
任何命令行输入或输出都按如下方式书写:
%fs ls /FileStore/shared_uploads/delta/online_retail
粗体:表示一个新术语、一个重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的词汇通常是粗体。例如:“可以有多个Map阶段,接着是多个Reduce阶段。”
提示或重要注意事项
以这种方式显示。
联系我们
我们始终欢迎读者的反馈。
customercare@packtpub.com 并在邮件主题中注明书名。
勘误:尽管我们已尽力确保内容的准确性,但错误难免发生。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/support/err… 并填写表格。
copyright@packt.com 并附上相关材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且有兴趣撰写或为书籍贡献内容,请访问 authors.packtpub.com。
分享您的想法
阅读完 Essential PySpark for Scalable Data Analytics 后,我们非常期待听到您的想法!请访问 packt.link/r/1-800-56887-8 为本书留下评价并分享您的反馈。
您的评价对我们以及技术社区都至关重要,它将帮助我们确保提供优质的内容。
第一部分:数据工程
本节介绍了分布式计算范式,并展示了 Spark 如何成为大数据处理的事实标准。
完成本节后,你将能够从各种数据源摄取数据,进行清洗、集成,并以可扩展和分布式的方式将其写入持久化存储,例如数据湖。你还将能够构建实时分析管道,并在数据湖中执行变更数据捕获(CDC)。你将理解 ETL 和 ELT 数据处理方式的关键区别,以及 ELT 如何为云端数据湖世界的发展演变。本节还介绍了 Delta Lake,以使基于云的数据湖更加可靠和高效。你将理解 Lambda 架构的细微差别,作为同时执行批处理和实时分析的一种手段,以及 Apache Spark 结合 Delta Lake 如何大大简化 Lambda 架构。
本节包括以下章节:
-
第一章*,分布式计算基础*
-
第二章*,数据摄取*
-
第三章*,数据清洗与集成*
-
第四章*,实时数据分析*
第一章:分布式计算入门
本章介绍了分布式计算范式,并展示了分布式计算如何帮助你轻松处理大量数据。你将学习如何使用MapReduce范式实现数据并行处理,并最终了解如何通过使用内存中的统一数据处理引擎(如 Apache Spark)来提高数据并行处理的效率。
接下来,你将深入了解 Apache Spark 的架构和组件,并结合代码示例进行讲解。最后,你将概览 Apache Spark 最新的 3.0 版本中新增的功能。
在本章中,你将掌握的关键技能包括理解分布式计算范式的基础知识,以及分布式计算范式的几种不同实现方法,如 MapReduce 和 Apache Spark。你将学习 Apache Spark 的基本原理及其架构和核心组件,例如 Driver、Executor 和 Cluster Manager,并了解它们如何作为一个整体协同工作以执行分布式计算任务。你将学习 Spark 的弹性分布式数据集(RDD)API,及其高阶函数和 lambda 表达式。你还将了解 Spark SQL 引擎及其 DataFrame 和 SQL API。此外,你将实现可运行的代码示例。你还将学习 Apache Spark 数据处理程序的各个组件,包括转换和动作,并学习惰性求值的概念。
在本章中,我们将涵盖以下主要内容:
-
介绍 分布式计算
-
使用 Apache Spark 进行分布式计算
-
使用 Spark SQL 和 DataFrames 进行大数据处理
技术要求
在本章中,我们将使用 Databricks Community Edition 来运行代码。你可以在community.cloud.databricks.com找到该平台。
注册说明可在databricks.com/try-databricks找到。
本章使用的代码可以从github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter01下载。
本章使用的数据集可以在github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
原始数据集可以从以下来源获取:
分布式计算
在本节中,你将了解分布式计算,它的需求,以及如何利用它以快速且高效的方式处理大量数据。
分布式计算简介
分布式计算是一类计算技术,我们通过使用一组计算机作为一个整体来解决计算问题,而不是仅仅依赖单台机器。
在数据分析中,当数据量变得太大,无法在单台机器中处理时,我们可以将数据拆分成小块并在单台机器上迭代处理,或者可以在多台机器上并行处理数据块。虽然前者可以完成任务,但可能需要更长时间来迭代处理整个数据集;后者通过同时使用多台机器,能够在更短的时间内完成任务。
有多种分布式计算技术;然而,在数据分析中,一种流行的技术是 数据并行处理。
数据并行处理
数据并行处理包含两个主要部分:
-
需要处理的实际数据。
-
需要应用于数据的代码片段或业务逻辑,以便进行处理。
我们可以通过将大量数据拆分成小块,并在多台机器上并行处理它们,从而处理大规模数据。这可以通过两种方式实现:
-
首先,将数据带到运行代码的机器上。
-
第二步,将我们的代码带到数据实际存储的位置。
第一种技术的一个缺点是,随着数据量的增大,数据移动所需的时间也会成比例增加。因此,我们最终会花费更多的时间将数据从一个系统移动到另一个系统,从而抵消了并行处理系统带来的任何效率提升。同时,我们还会在数据复制过程中产生多个数据副本。
第二种技术要高效得多,因为我们不再移动大量数据,而是可以将少量代码移到数据实际存放的位置。这种将代码移到数据所在位置的技术称为数据并行处理。数据并行处理技术非常快速和高效,因为它节省了之前在不同系统之间移动和复制数据所需的时间。这样的数据并行处理技术之一被称为 MapReduce 模式。
使用 MapReduce 模式进行数据并行处理
MapReduce 模式将数据并行处理问题分解为三个主要阶段:
-
Map 阶段
-
Shuffle 阶段
-
Reduce 阶段
(key, value)对,应用一些处理,将它们转换为另一组(key, value)对。
从 Map 阶段获取的(key, value)对,将其洗牌/排序,使得具有相同key的对最终聚集在一起。
从 Shuffle 阶段获取的(key, value)对,经过归约或聚合,产生最终结果。
可以有多个Map阶段,后跟多个Reduce阶段。然而,Reduce阶段仅在所有Map阶段完成后开始。
让我们看一个例子,假设我们想要计算文本文件中所有不同单词的计数,并应用MapReduce范式。
以下图表展示了 MapReduce 范式的一般工作原理:
图 1.1 – 使用 MapReduce 计算单词计数
之前的例子按照以下方式工作:
-
在图 1.1中,我们有一个包含三台节点的集群,标记为M1、M2和M3。每台机器上都有几个文本文件,包含若干句子的纯文本。在这里,我们的目标是使用 MapReduce 来计算文本文件中所有单词的数量。
-
我们将所有文本文件加载到集群中;每台机器加载本地的文档。
-
(word, count)对。 -
从Map 阶段获取的
(word, count)对,进行洗牌/排序,使得具有相同关键词的单词对聚集在一起。 -
Reduce 阶段将所有关键字分组,并对其计数进行汇总,得到每个单独单词的最终计数。
MapReduce 范式由Hadoop框架推广,曾在大数据工作负载处理领域非常流行。然而,MapReduce 范式提供的是非常低级别的 API 来转换数据,且要求用户具备如 Java 等编程语言的熟练知识。使用 Map 和 Reduce 来表达数据分析问题既不直观也不灵活。
MapReduce 被设计成可以在普通硬件上运行,由于普通硬件容易发生故障,因此对于硬件故障的容错能力至关重要。MapReduce 通过将每个阶段的结果保存到磁盘上来实现容错。每个阶段结束后必须往返磁盘,这使得 MapReduce 在处理数据时相对较慢,因为物理磁盘的 I/O 性能普遍较低。为了克服这个限制,下一代 MapReduce 范式应运而生,它利用比磁盘更快的系统内存来处理数据,并提供了更灵活的 API 来表达数据转换。这个新框架叫做 Apache Spark,接下来的章节和本书其余部分你将学习到它。
重要提示
在分布式计算中,你会经常遇到集群这个术语。集群是由一组计算机组成的,它们共同作为一个单一的单元来解决计算问题。集群的主要计算机通常被称为主节点,负责集群的协调和管理,而实际执行任务的次要计算机则被称为工作节点。集群是任何分布式计算系统的关键组成部分,在本书中,你将会遇到这些术语。
使用 Apache Spark 进行分布式计算
在过去的十年中,Apache Spark 已发展成为大数据处理的事实标准。事实上,它是任何从事数据分析工作的人手中不可或缺的工具。
在这里,我们将从 Apache Spark 的基础知识开始,包括其架构和组件。接着,我们将开始使用 PySpark 编程 API 来实际实现之前提到的单词计数问题。最后,我们将看看最新的 Apache Spark 3.0 版本有什么新功能。
Apache Spark 介绍
Apache Spark 是一个内存中的统一数据分析引擎,相较于其他分布式数据处理框架,它的速度相对较快。
它是一个统一的数据分析框架,因为它可以使用单一引擎处理不同类型的大数据工作负载。这些工作负载包括以下内容:
-
批量数据处理
-
实时数据处理
-
机器学习和数据科学
通常,数据分析涉及到之前提到的所有或部分工作负载来解决单一的业务问题。在 Apache Spark 出现之前,没有一个单一的框架能够同时容纳所有三种工作负载。通过 Apache Spark,参与数据分析的各个团队可以使用同一个框架来解决单一的业务问题,从而提高团队之间的沟通与协作,并显著缩短学习曲线。
我们将在本书中深入探索每一个前述的工作负载,从第二章**,数据摄取,到第八章**,无监督机器学习。
此外,Apache Spark 在两个方面都非常快:
-
它在数据处理速度方面非常快。
-
它在开发速度方面很快。
Apache Spark 具有快速的作业/查询执行速度,因为它将所有数据处理都在内存中进行,并且还内置了一些优化技术,如惰性求值、谓词下推和分区修剪,仅举几例。我们将在接下来的章节中详细介绍 Spark 的优化技术。
其次,Apache Spark 为开发者提供了非常高级的 API,用于执行基本的数据处理操作,例如 过滤、分组、排序、连接 和 聚合。通过使用这些高级编程结构,开发者能够非常轻松地表达数据处理逻辑,使开发速度大大提高。
Apache Spark 的核心抽象,正是使其在数据分析中既快速又富有表现力的,是 RDD。我们将在下一节中介绍这个概念。
使用 RDD 进行数据并行处理
RDD 是 Apache Spark 框架的核心抽象。可以将 RDD 看作是任何一种不可变数据结构,它通常存在于编程语言中,但与一般情况下只驻留在单台机器的不同,RDD 是分布式的,驻留在多台机器的内存中。一个 RDD 由多个分区组成,分区是 RDD 的逻辑划分,每个机器上可能会有一些分区。
下图帮助解释 RDD 及其分区的概念:
图 1.2 – 一个 RDD
在之前的示意图中,我们有一个由三台机器或节点组成的集群。集群中有三个 RDD,每个 RDD 被分成多个分区。每个节点包含一个或多个分区,并且每个 RDD 通过分区在集群的多个节点之间分布。
RDD 抽象伴随着一组可以操作 RDD 的高阶函数,用于操作存储在分区中的数据。这些函数被称为 高阶函数,你将在接下来的章节中了解它们。
高阶函数
高阶函数操作 RDD,并帮助我们编写业务逻辑来转换存储在分区中的数据。高阶函数接受其他函数作为参数,这些内部函数帮助我们定义实际的业务逻辑,转换数据并并行应用于每个 RDD 的分区。传递给高阶函数的内部函数称为 lambda 函数 或 lambda 表达式。
Apache Spark 提供了多个高阶函数,例如 map、flatMap、reduce、fold、filter、reduceByKey、join 和 union 等。这些函数是高级函数,帮助我们非常轻松地表达数据操作逻辑。
例如,考虑我们之前展示的字数统计示例。假设你想将一个文本文件作为 RDD 读取,并根据分隔符(例如空格)拆分每个单词。用 RDD 和高阶函数表示的代码可能如下所示:
lines = sc.textFile("/databricks-datasets/README.md")
words = lines.flatMap(lambda s: s.split(" "))
word_tuples = words.map(lambda s: (s, 1))
在之前的代码片段中,发生了以下情况:
-
我们使用内置的
sc.textFile()方法加载文本文件,该方法会将指定位置的所有文本文件加载到集群内存中,将它们按行拆分,并返回一个包含行或字符串的 RDD。 -
然后,我们对新的行 RDD 应用
flatMap()高阶函数,并提供一个函数,指示它将每一行按空格分开。我们传递给flatMap()的 Lambda 函数只是一个匿名函数,它接受一个参数,即单个StringType行,并返回一个单词列表。通过flatMap()和lambda()函数,我们能够将一个行 RDD 转换为一个单词 RDD。 -
最后,我们使用
map()函数为每个单词分配1的计数。这相对简单,且比使用 Java 编程语言开发 MapReduce 应用程序直观得多。
总结你所学的内容,Apache Spark 框架的主要构建块是 RDD。一个 RDD 由分布在集群各个节点上的 分区 组成。我们使用一种叫做高阶函数的特殊函数来操作 RDD,并根据我们的业务逻辑转化 RDD。这些业务逻辑通过高阶函数以 Lambda 或匿名函数的形式传递到 Worker 节点。
在深入探讨高阶函数和 Lambda 函数的内部工作原理之前,我们需要了解 Apache Spark 框架的架构以及一个典型 Spark 集群的组件。我们将在接下来的章节中进行介绍。
注意
RDD 的 Resilient 特性来源于每个 RDD 知道它的血统。任何时候,一个 RDD 都拥有它上面执行的所有操作的信息,追溯到数据源本身。因此,如果由于某些故障丢失了 Executors,且其某些分区丢失,它可以轻松地通过利用血统信息从源数据重新创建这些分区,从而使其对故障具有 Resilient(韧性)。
Apache Spark 集群架构
一个典型的 Apache Spark 集群由三个主要组件组成,即 Driver、若干 Executors 和 Cluster Manager:
图 1.3 – Apache Spark 集群组件
让我们仔细看看这些组件。
Driver – Spark 应用程序的核心
Spark Driver 是一个 Java 虚拟机进程,是 Spark 应用程序的核心部分。它负责用户应用程序代码的声明,同时创建 RDD、DataFrame 和数据集。它还负责与 Executors 协调并在 Executors 上运行代码,创建并调度任务。它甚至负责在失败后重新启动 Executors,并最终将请求的数据返回给客户端或用户。可以把 Spark Driver 想象成任何 Spark 应用程序的 main() 程序。
重要说明
Driver 是 Spark 集群的单点故障,如果 Driver 失败,整个 Spark 应用程序也会失败;因此,不同的集群管理器实现了不同的策略以确保 Driver 高可用。
执行器 – 实际的工作节点
Spark 执行器也是 Java 虚拟机进程,它们负责在 RDD 上运行实际的转换数据操作。它们可以在本地缓存数据分区,并将处理后的数据返回给 Driver,或写入持久化存储。每个执行器会并行运行一组 RDD 分区的操作。
集群管理器 – 协调和管理集群资源
集群管理器是一个在集群上集中运行的进程,负责为 Driver 提供所请求的资源。它还监控执行器的任务进度和状态。Apache Spark 自带集群管理器,称为 Standalone 集群管理器,但它也支持其他流行的集群管理器,如 YARN 或 Mesos。在本书中,我们将使用 Spark 自带的 Standalone 集群管理器。
开始使用 Spark
到目前为止,我们已经学习了 Apache Spark 的核心数据结构 RDD、用于操作 RDD 的函数(即高阶函数)以及 Apache Spark 集群的各个组件。你还见识了一些如何使用高阶函数的代码片段。
在这一部分,你将把所学知识付诸实践,编写你的第一个 Apache Spark 程序,你将使用 Spark 的 Python API —— PySpark 来创建一个词频统计应用程序。然而,在开始之前,我们需要准备以下几样东西:
-
一个 Apache Spark 集群
-
数据集
-
词频统计应用程序的实际代码
我们将使用免费的 Databricks 社区版 来创建我们的 Spark 集群。所使用的代码可以通过本章开头提到的 GitHub 链接找到。所需资源的链接可以在本章开头的 技术要求 部分找到。
注意
尽管本书中使用的是 Databricks Spark 集群,但提供的代码可以在任何运行 Spark 3.0 或更高版本的 Spark 集群上执行,只要数据位于 Spark 集群可以访问的位置。
现在你已经理解了 Spark 的核心概念,如 RDD、高阶函数、Lambda 表达式以及 Spark 架构,让我们通过以下代码实现你的第一个 Spark 应用程序:
lines = sc.textFile("/databricks-datasets/README.md")
words = lines.flatMap(lambda s: s.split(" "))
word_tuples = words.map(lambda s: (s, 1))
word_count = word_tuples.reduceByKey(lambda x, y: x + y)
word_count.take(10)
word_count.saveAsTextFile("/tmp/wordcount.txt")
在上一个代码片段中,发生了以下操作:
-
我们使用内建的
sc.textFile()方法加载文本文件,该方法会读取指定位置的所有文本文件,将其拆分为单独的行,并返回一个包含行或字符串的 RDD。 -
接着,我们对 RDD 中的行应用
flatMap()高阶函数,并传入一个函数,该函数指示它根据空格将每一行拆分开来。我们传给flatMap()的 lambda 函数只是一个匿名函数,它接受一个参数——一行文本,并将每个单词作为列表返回。通过flatMap()和lambda()函数,我们能够将行的 RDD 转换成单词的 RDD。 -
然后,我们使用
map()函数为每个单独的单词分配一个1的计数。 -
最后,我们使用
reduceByKey()高阶函数对多次出现的相似单词进行计数求和。 -
一旦计算出单词的计数,我们使用
take()函数展示最终单词计数的一个示例。 -
尽管展示一个示例结果集通常有助于确定我们代码的正确性,但在大数据环境下,将所有结果展示到控制台上并不现实。因此,我们使用
saveAsTextFile()函数将最终结果持久化到持久存储中。重要提示
不推荐使用
take()或collect()等命令将整个结果集展示到控制台。这在大数据环境下甚至可能是危险的,因为它可能试图将过多的数据带回驱动程序,从而导致驱动程序因OutOfMemoryError失败,进而使整个应用程序失败。因此,建议你在结果集非常小的情况下使用
take(),而仅在确信返回的数据量确实非常小时,才使用collect()。
让我们深入探讨下面这一行代码,了解 lambda 的内部工作原理,以及它们如何通过高阶函数实现数据并行处理:
words = lines.flatMap(lambda s: s.split(" "))
在之前的代码片段中,flatMmap() 高阶函数将 lambda 中的代码打包,并通过一种叫做 序列化 的过程将其发送到网络上的 Worker 节点。这个 序列化的 lambda 随后会被发送到每个执行器,每个执行器则将此 lambda 并行应用到各个 RDD 分区上。
重要提示
由于高阶函数需要能够序列化 lambda,以便将你的代码发送到执行器。因此,lambda 函数需要是 可序列化 的,如果不满足这一要求,你可能会遇到 任务不可序列化 的错误。
总结来说,高阶函数本质上是将你的数据转换代码以序列化 lambda 的形式传递到 RDD 分区中的数据上。因此,我们并不是将数据移动到代码所在的位置,而是将代码移动到数据所在的位置,这正是数据并行处理的定义,正如我们在本章前面所学的那样。
因此,Apache Spark 及其 RDD 和高阶函数实现了内存中版本的数据并行处理范式。这使得 Apache Spark 在分布式计算环境中,进行大数据处理时既快速又高效。
Apache Spark 的 RDD 抽象相较于 MapReduce 确实提供了一个更高级的编程 API,但它仍然需要一定程度的函数式编程理解,才能表达最常见的数据转换类型。为了克服这个挑战,Spark 扩展了已有的 SQL 引擎,并在 RDD 上增加了一个叫做 DataFrame 的抽象。这使得数据处理对于数据科学家和数据分析师来说更加容易和熟悉。接下来的部分将探索 Spark SQL 引擎的 DataFrame 和 SQL API。
使用 Spark SQL 和 DataFrame 处理大数据
Spark SQL 引擎支持两种类型的 API,分别是 DataFrame 和 Spark SQL。作为比 RDD 更高层次的抽象,它们更加直观,甚至更具表现力。它们带有更多的数据转换函数和工具,你作为数据工程师、数据分析师或数据科学家,可能已经熟悉这些内容。
Spark SQL 和 DataFrame API 提供了一个低门槛进入大数据处理的途径。它们让你可以使用现有的数据分析知识和技能,轻松开始分布式计算。它们帮助你开始进行大规模数据处理,而无需处理分布式计算框架通常带来的复杂性。
本节内容将教你如何使用 DataFrame 和 Spark SQL API 来开始你的可扩展数据处理之旅。值得注意的是,这里学到的概念在本书中会非常有用,并且是必须掌握的。
使用 Spark DataFrame 转换数据
从 Apache Spark 1.3 开始,Spark SQL 引擎被作为一层添加到 RDD API 之上,并扩展到 Spark 的每个组件,以提供一个更易于使用且熟悉的 API 给开发者。多年来,Spark SQL 引擎及其 DataFrame 和 SQL API 变得更加稳健,并成为了使用 Spark 的事实标准和推荐标准。在本书中,你将专门使用 DataFrame 操作或 Spark SQL 语句来进行所有数据处理需求,而很少使用 RDD API。
可以把 Spark DataFrame 想象成一个 Pandas DataFrame 或一个具有行和命名列的关系型数据库表。唯一的区别是,Spark DataFrame 存储在多台机器的内存中,而不是单台机器的内存中。下图展示了一个具有三列的 Spark DataFrame,分布在三台工作机器上:
图 1.4 – 分布式 DataFrame
Spark DataFrame 也是一种不可变的数据结构,类似于 RDD,包含行和命名列,其中每一列可以是任何类型。此外,DataFrame 提供了可以操作数据的功能,我们通常将这些操作集合称为 领域特定语言 (DSL)。Spark DataFrame 操作可以分为两大类,即转换(transformations)和动作(actions),我们将在接下来的章节中进行探讨。
使用 DataFrame 或 Spark SQL 相较于 RDD API 的一个优势是,Spark SQL 引擎内置了一个查询优化器,名为 Catalyst。这个 Catalyst 优化器分析用户代码,以及任何可用的数据统计信息,以生成查询的最佳执行计划。这个查询计划随后会被转换为 Java 字节码,原生运行在 Executor 的 Java JVM 内部。无论使用哪种编程语言,这个过程都会发生,因此在大多数情况下,使用 Spark SQL 引擎处理的任何代码都能保持一致的性能表现,不论是使用 Scala、Java、Python、R 还是 SQL 编写的代码。
转换
read、select、where、filter、join 和 groupBy。
动作
write、count 和 show。
懒评估
Spark 转换是懒评估的,这意味着转换不会在声明时立即评估,数据也不会在内存中展现,直到调用一个动作。这样做有几个优点,因为它给 Spark 优化器提供了评估所有转换的机会,直到调用一个动作,并生成最优化的执行计划,以便从代码中获得最佳性能和效率。
懒评估与 Spark 的 Catalyst 优化器相结合的优势在于,你可以专注于表达数据转换逻辑,而不必过多担心按特定顺序安排转换,以便从代码中获得最佳性能和效率。这可以帮助你在任务中更高效,不至于被新框架的复杂性弄得困惑。
重要提示
与 Pandas DataFrame 相比,Spark DataFrame 在声明时不会立即加载到内存中。它们只有在调用动作时才会被加载到内存中。同样,DataFrame 操作不一定按你指定的顺序执行,因为 Spark 的 Catalyst 优化器会为你生成最佳的执行计划,有时甚至会将多个操作合并成一个单元。
让我们以之前使用 RDD API 实现的词频统计为例,尝试使用 DataFrame DSL 来实现。
from pyspark.sql.functions import split, explode
linesDf = spark.read.text("/databricks-datasets/README.md")
wordListDf = linesDf.select(split("value", " ").alias("words"))
wordsDf = wordListDf.select(explode("words").alias("word"))
wordCountDf = wordsDf.groupBy("word").count()
wordCountDf.show()
wordCountDf.write.csv("/tmp/wordcounts.csv")
在前面的代码片段中,发生了以下情况:
-
首先,我们从 PySpark SQL 函数库中导入几个函数,分别是 split 和 explode。
-
然后,我们使用
SparkSession的read.text()方法读取文本,这会创建一个由StringType类型的行组成的 DataFrame。 -
接着,我们使用
split()函数将每一行拆分为独立的单词;结果是一个包含单一列的 DataFrame,列名为value,实际上是一个单词列表。 -
然后,我们使用
explode()函数将每一行中的单词列表分解为每个单词独立成行;结果是一个带有word列的 DataFrame。 -
现在我们终于准备好计算单词数量了,因此我们通过
word列对单词进行分组,并统计每个单词的出现次数。最终结果是一个包含两列的 DataFrame,即实际的word和它的count。 -
我们可以使用
show()函数查看结果样本,最后,使用write()函数将结果保存到持久化存储中。
你能猜出哪些操作是动作操作吗?如果你猜测是show()或write(),那你是对的。其他所有函数,包括select()和groupBy(),都是转换操作,不会触发 Spark 任务的执行。
注意
尽管read()函数是一种转换操作,但有时你会注意到它实际上会执行一个 Spark 任务。之所以这样,是因为对于某些结构化和半结构化的数据格式,Spark 会尝试从底层文件中推断模式信息,并处理实际文件的小部分以完成此操作。
使用 Spark 上的 SQL
SQL 是一种用于临时数据探索和商业智能查询的表达性语言。因为它是一种非常高级的声明式编程语言,用户只需关注输入、输出以及需要对数据执行的操作,而无需过多担心实际实现逻辑的编程复杂性。Apache Spark 的 SQL 引擎也提供了 SQL 语言 API,并与 DataFrame 和 Dataset APIs 一起使用。
使用 Spark 3.0,Spark SQL 现在符合 ANSI 标准,因此,如果你是熟悉其他基于 SQL 的平台的数据分析师,你应该可以毫不费力地开始使用 Spark SQL。
由于 DataFrame 和 Spark SQL 使用相同的底层 Spark SQL 引擎,它们是完全可互换的,通常情况下,用户会将 DataFrame DSL 与 Spark SQL 语句混合使用,尤其是在代码的某些部分,用 SQL 表达更加简洁。
现在,让我们使用 Spark SQL 重写我们的单词计数程序。首先,我们创建一个表,指定我们的文本文件为 CSV 文件,并以空格作为分隔符,这是一个巧妙的技巧,可以读取文本文件的每一行,并且同时将每个文件拆分成单独的单词:
CREATE TABLE word_counts (word STRING)
USING csv
OPTIONS("delimiter"=" ")
LOCATION "/databricks-datasets/README.md"
现在我们已经有了一个包含单列单词的表格,我们只需要对word列进行GROUP BY操作,并执行COUNT()操作来获得单词计数:
SELECT word, COUNT(word) AS count
FROM word_counts
GROUP BY word
在这里,你可以观察到,解决相同的业务问题变得越来越容易,从使用 MapReduce 到 RRDs,再到 DataFrames 和 Spark SQL。每一个新版本发布时,Apache Spark 都在增加更多的高级编程抽象、数据转换和实用函数,以及其他优化。目标是让数据工程师、数据科学家和数据分析师能够将时间和精力集中在解决实际的业务问题上,而无需担心复杂的编程抽象或系统架构。
Apache Spark 最新版本 3 的主要发布包含了许多增强功能,使数据分析专业人员的工作变得更加轻松。我们将在接下来的章节中讨论这些增强功能中最突出的部分。
Apache Spark 3.0 有什么新特性?
在 Apache Spark 3.0 中有许多新的显著特性;不过这里只提到了其中一些,你会发现它们在数据分析初期阶段非常有用:
-
速度:Apache Spark 3.0 比其前版本快了几个数量级。第三方基准测试表明,Spark 3.0 在某些类型的工作负载下速度是前版本的 2 到 17 倍。
-
自适应查询执行:Spark SQL 引擎根据用户代码和之前收集的源数据统计信息生成一些逻辑和物理查询执行计划。然后,它会尝试选择最优的执行计划。然而,有时由于统计信息过时或不存在,Spark 可能无法生成最佳的执行计划,导致性能不佳。通过自适应查询执行,Spark 能够在运行时动态调整执行计划,从而提供最佳的查询性能。
-
动态分区修剪:商业智能系统和数据仓库采用了一种名为 维度建模 的数据建模技术,其中数据存储在一个中央事实表中,周围是几个维度表。利用这些维度模型的商业智能类型查询涉及多个维度表和事实表之间的连接,并带有各种维度表的筛选条件。通过动态分区修剪,Spark 能够根据应用于这些维度的筛选条件,过滤掉任何事实表分区,从而减少内存中读取的数据量,进而提高查询性能。
-
Kubernetes 支持:之前,我们了解到 Spark 提供了自己的独立集群管理器,并且还可以与其他流行的资源管理器如 YARN 和 Mesos 配合使用。现在,Spark 3.0 原生支持 Kubernetes,这是一个流行的开源框架,用于运行和管理并行容器服务。
总结
在本章中,你学习了分布式计算的概念。我们发现,分布式计算变得非常重要,因为生成的数据量正在快速增长,使用单一专用系统处理所有数据既不切实际也不现实。
然后,你学习了数据并行处理的概念,并通过 MapReduce 范式回顾了它的实际实现示例。
接着,你了解了一个名为 Apache Spark 的内存中统一分析引擎,并学习了它在数据处理方面的速度和高效性。此外,你还了解到,它非常直观且容易上手,适合开发数据处理应用程序。你还了解了 Apache Spark 的架构及其组件,并理解了它们如何作为一个框架结合在一起。
接下来,你学习了 RDD(弹性分布式数据集)的概念,它是 Apache Spark 的核心抽象,了解了它如何在一群机器上以分布式方式存储数据,以及如何通过高阶函数和 lambda 函数结合使用 RDD 实现数据并行处理。
你还学习了 Apache Spark 中的 Spark SQL 引擎组件,了解了它提供了比 RDD 更高层次的抽象,并且它有一些你可能已经熟悉的内置函数。你学会了利用 DataFrame 的 DSL 以更简单且更熟悉的方式实现数据处理的业务逻辑。你还了解了 Spark 的 SQL API,它符合 ANSI SQL 标准,并且允许你高效地在大量数据上执行 SQL 分析。
你还了解了 Apache Spark 3.0 中一些显著的改进,例如自适应查询执行和动态分区修剪,这些都大大提高了 Spark 3.0 的性能,使其比前几版本更快。
现在你已经学习了使用 Apache Spark 进行大数据处理的基础知识,接下来你可以开始利用 Spark 踏上数据分析之旅。一个典型的数据分析旅程从从各种源系统获取原始数据开始,将其导入到历史存储组件中,如数据仓库或数据湖,然后通过清洗、集成和转换来处理原始数据,以获得一个统一的真实数据源。最后,你可以通过干净且集成的数据,利用描述性和预测性分析获得可操作的业务洞察。我们将在本书的后续章节中讨论这些方面,首先从下一章的数据清洗和数据导入过程开始。
第二章:数据摄取
数据摄取是将数据从不同的操作系统迁移到中央位置(如数据仓库或数据湖)以便进行处理,并使其适用于数据分析的过程。这是数据分析过程的第一步,对于创建可以集中访问的持久存储至关重要,在这里,数据工程师、数据科学家和数据分析师可以访问、处理和分析数据,以生成业务分析。
你将了解 Apache Spark 作为批量和实时处理的数据摄取引擎的功能。将介绍 Apache Spark 支持的各种数据源以及如何使用 Spark 的 DataFrame 接口访问它们。
此外,你还将学习如何使用 Apache Spark 的内置函数,从外部数据源访问数据,例如关系数据库管理系统(RDBMS)以及消息队列,如 Apache Kafka,并将其摄取到数据湖中。还将探讨不同的数据存储格式,如结构化、非结构化和半结构化文件格式,以及它们之间的主要差异。Spark 的实时流处理引擎——结构化流(Structured Streaming)也将被介绍。你将学习如何使用批处理和实时流处理创建端到端的数据摄取管道。最后,我们将探索一种将批处理和流处理统一的技术——Lambda 架构,并介绍如何使用 Apache Spark 实现它。
在本章中,你将学习执行批量和实时数据摄取所需的基本技能,使用 Apache Spark。此外,你将掌握构建端到端可扩展且高效的大数据摄取管道所需的知识和工具。
本章将涵盖以下主要主题:
-
企业决策支持系统简介
-
从数据源摄取数据
-
向数据接收端摄取数据
-
使用文件格式进行数据湖中的数据存储
-
构建批量和实时数据摄取管道
-
使用 Lambda 架构统一批量和实时数据摄取
技术要求
在本章中,我们将使用 Databricks 社区版来运行我们的代码。你可以在community.cloud.databricks.com找到它。
注册说明请参见databricks.com/try-databricks。
本章使用的代码可以从github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter02下载。
本章使用的数据集可以在github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
企业决策支持系统简介
企业决策支持系统(Enterprise DSS)是一个端到端的数据处理系统,它将企业组织生成的运营和交易数据转换为可操作的洞察。每个企业决策支持系统都有一些标准组件,例如数据源、数据接收器和数据处理框架。企业决策支持系统以原始交易数据为输入,并将其转化为可操作的洞察,如运营报告、企业绩效仪表板和预测分析。
以下图示展示了在大数据环境中典型的企业决策支持系统的组成部分:
图 2.1 – 企业决策支持系统架构
大数据分析系统也是一种企业决策支持系统,但其处理的规模要大得多,涉及的数据种类更多,且数据到达速度更快。作为企业决策支持系统的一种类型,大数据分析系统的组件与传统企业决策支持系统相似。构建企业决策支持系统的第一步是从数据源摄取数据并将其传送到数据接收器。您将在本章中学习这一过程。让我们从数据源开始,详细讨论大数据分析系统的各个组件。
从数据源摄取数据
在本节中,我们将了解大数据分析系统使用的各种数据源。典型的数据源包括事务系统(如 RDBMS)、基于文件的数据源(如数据湖)以及消息队列(如Apache Kafka)。此外,您将学习 Apache Spark 内置的连接器,以便从这些数据源摄取数据,并编写代码以查看这些连接器的实际操作。
从关系型数据源摄取数据
事务系统,或称操作系统,是一种数据处理系统,帮助组织执行日常的业务功能。这些事务系统处理单个业务交易,例如零售自助服务亭的交易、在线零售门户下单、航空票务预订或银行交易。这些交易的历史汇总构成了数据分析的基础,分析系统摄取、存储并处理这些交易数据。因此,这类事务系统构成了分析系统的数据源,并且是数据分析的起点。
交易系统有多种形式;然而,最常见的是关系型数据库管理系统(RDBMS)。在接下来的章节中,我们将学习如何从 RDBMS 中摄取数据。
关系型数据源是一组关系型数据库和关系型表,这些表由行和命名列组成。用于与 RDBMS 进行通信和查询的主要编程抽象称为 结构化查询语言 (SQL)。外部系统可以通过 JDBC 和 ODBC 等通信协议与 RDBMS 进行通信。Apache Spark 配备了一个内置的 JDBC 数据源,可以用来与存储在 RDBMS 表中的数据进行通信和查询。
让我们来看一下使用 PySpark 从 RDBMS 表中摄取数据所需的代码,如以下代码片段所示:
dataframe_mysql = spark.read.format("jdbc").options(
url="jdbc:mysql://localhost:3306/pysparkdb",
driver = "org.mariadb.jdbc.Driver",
dbtable = "authors",
user="#####",
password="@@@@@").load()
dataframe_mysql.show()
在前面的代码片段中,我们使用 spark.read() 方法通过指定格式为 jdbc 来加载来自 JDBC 数据源的数据。在这里,我们连接到一个流行的开源 RDBMS,名为 url,该 URL 指定了 MySQL 服务器的 jdbc url,并包含其 hostname、port number 和 database name。driver 选项指定了 Spark 用来连接并与 RDBMS 通信的 JDBC 驱动程序。dtable、user 和 password 选项指定了要查询的表名以及进行身份验证所需的凭证。最后,show() 函数从 RDBMS 表中读取示例数据并显示在控制台上。
重要提示
前面的代码片段使用了虚拟的数据库凭证,并以纯文本形式显示。这会带来巨大的数据安全风险,并且不推荐这种做法。处理敏感信息时,应遵循适当的最佳实践,比如使用配置文件或其他大数据软件供应商提供的机制,如隐藏或模糊化敏感信息。
要运行此代码,您可以使用自己的 MySQL 服务器并将其配置到您的 Spark 集群中,或者可以使用本章提供的示例代码来设置一个简单的 MySQL 服务器。所需的代码可以在 github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/blob/main/Chapter02/utils/mysql-setup.ipynb 中找到。
提示
Apache Spark 提供了一个 JDBC 数据源,能够连接几乎所有支持 JDBC 连接并具有 JDBC 驱动程序的关系型数据库管理系统(RDBMS)。然而,它并不自带任何驱动程序;需要从相应的 RDBMS 提供商处获取,并且需要将驱动程序配置到 Spark 集群中,以便 Spark 应用程序可以使用。
从基于文件的数据源进行数据摄取
文件型数据源在不同数据处理系统之间交换数据时非常常见。举个例子,假设一个零售商想要用外部数据(如邮政服务提供的邮政编码数据)来丰富他们的内部数据源。这两个组织之间的数据通常通过文件型数据格式进行交换,如 XML 或 JSON,或者更常见的方式是使用分隔符的纯文本或 CSV 格式。
Apache Spark 支持多种文件格式,如纯文本、CSV、JSON 以及二进制文件格式,如 Apache Parquet 和 ORC。这些文件需要存储在分布式文件系统上,如Hadoop 分布式文件系统(HDFS),或者是基于云的数据湖,如AWS S3、Azure Blob或ADLS存储。
让我们来看一下如何使用 PySpark 从 CSV 文件中读取数据,如下面的代码块所示:
retail_df = (spark
.read
.format("csv")
.option("inferSchema", "true")
.option("header","true")
.load("dbfs:/FileStore/shared_uploads/snudurupati@outlook.com/")
)
retail_df.show()
在前面的代码片段中,我们使用 spark.read() 函数来读取 CSV 文件。我们将 inferSchema 和 header 选项设置为 true,这有助于 Spark 通过读取数据样本来推断列名和数据类型信息。
重要提示
文件型数据源需要存储在分布式文件系统中。Spark 框架利用数据并行处理,每个 Spark Executor 尝试将数据的一个子集读取到其本地内存中。因此,文件必须存储在分布式文件系统中,并且所有 Executor 和 Driver 都能够访问该文件。HDFS 和基于云的数据湖,如 AWS S3、Azure Blob 和 ADLS 存储,都是分布式数据存储层,是与 Apache Spark 一起使用的理想文件型数据源。
在这里,我们从dbfs/位置读取 CSV 文件,这是 Databricks 的专有文件系统,称为Databricks 文件系统(DBFS)。DBFS 是一个抽象层,实际上使用的是 AWS S3、Azure Blob 或 ADLS 存储。
提示
由于每个 Executor 只尝试读取数据的一个子集,因此文件类型必须是可拆分的。如果文件不能拆分,Executor 可能会尝试读取比其可用内存更大的文件,从而导致内存溢出并抛出“内存不足”错误。一个不可拆分文件的例子是gzipped的 CSV 文件或文本文件。
从消息队列中读取数据
另一种在实时流处理分析中常用的数据源是消息队列。消息队列提供了一种发布-订阅模式的数据消费方式,其中发布者将数据发布到队列,而多个订阅者可以异步地消费数据。在分布式计算环境中,消息队列需要是分布式的、容错的,并且可扩展的,以便作为分布式数据处理系统的数据源。
其中一个消息队列是 Apache Kafka,它在与 Apache Spark 一起处理实时流式工作负载时非常突出。Apache Kafka 不仅仅是一个消息队列;它本身是一个端到端的分布式流处理平台。然而,在我们的讨论中,我们将 Kafka 视为一个分布式、可扩展且容错的消息队列。
让我们看一下使用 PySpark 从 Kafka 导入数据的代码,如下面的代码块所示:
kafka_df = (spark.read
.format("kafka")
.option("kafka.bootstrap.servers", "localhost:9092")
.option("subscribe", "wordcount")
.option("startingOffsets", "earliest")
.load()
)
kafka_df.show()
在前面的代码示例中,我们使用spark.read()通过提供主机名和端口号,从 Kafka 服务器加载数据,主题名为wordcount。我们还指定了 Spark 应该从队列的最开始处读取事件,使用StartingOffsets选项。尽管 Kafka 通常用于与 Apache Spark 一起处理流式用例,但这个前面的代码示例将 Kafka 作为批量处理数据的数据源。你将在使用结构化流实时导入数据部分学习如何使用 Kafka 和 Apache Spark 处理流。
提示
在 Kafka 术语中,单个队列称为主题,而每个事件称为偏移量。Kafka 是一个队列,因此它按照事件发布到主题的顺序处理事件,个别消费者可以选择自己的起始和结束偏移量。
现在你已经熟悉了使用 Apache Spark 从不同类型的数据源导入数据,在接下来的部分中,让我们学习如何将数据导入到数据汇中。
导入数据到数据汇
数据汇,顾名思义,是用于存储原始或处理过的数据的存储层,既可以用于短期暂存,也可以用于长期持久存储。尽管数据汇一词通常用于实时数据处理,但没有特定的限制,任何存储层中存放导入数据的地方都可以称为数据汇。就像数据源一样,数据汇也有不同的类型。你将在接下来的章节中学习一些最常见的类型。
导入到数据仓库
数据仓库是一种特定类型的持久数据存储,最常用于商业智能类型的工作负载。商业智能和数据仓库是一个完整的研究领域。通常,数据仓库使用 RDBMS 作为其数据存储。然而,数据仓库与传统数据库不同,它遵循一种特定的数据建模技术,称为维度建模。维度模型非常直观,适合表示现实世界的业务属性,并有利于用于构建商业报告和仪表板的商业智能类型查询。数据仓库可以建立在任何通用 RDBMS 上,或者使用专业的硬件和软件。
让我们使用 PySpark 将 DataFrame 保存到 RDBMS 表中,如下面的代码块所示:
wordcount_df = spark.createDataFrame(
[("data", 10), ("parallel", 2), ("Processing", 30), ("Spark", 50), ("Apache", 10)], ("word", "count"))
在前面的代码块中,我们通过编程方式从一个 Python List 对象创建了一个包含两列的 DataFrame。然后,我们使用 spark.write() 函数将 Spark DataFrame 保存到 MySQL 表中,如下所示的代码片段所示:
wordcount_df.write.format("jdbc").options(
url="jdbc:mysql://localhost:3306/pysparkdb",
driver = "org.mariadb.jdbc.Driver",
dbtable = "word_counts",
user="######",
password="@@@@@@").save()
前面的代码片段写入数据到 RDBMS 与读取数据的代码几乎相同。我们仍然需要使用 MySQL JDBC 驱动程序,并指定主机名、端口号、数据库名和数据库凭据。唯一的区别是,在这里,我们需要使用 spark.write() 函数,而不是 spark.read()。
向数据湖中注入数据
数据仓库非常适合直观地表示现实世界的业务数据,并以有利于商业智能类型工作负载的方式存储高度结构化的关系型数据。然而,当处理数据科学和机器学习类型工作负载所需的非结构化数据时,数据仓库就显得不足。数据仓库不擅长处理大数据的高体积和速度。这时,数据湖就填补了数据仓库留下的空白。
从设计上讲,数据湖在存储各种类型的数据时具有高度的可扩展性和灵活性,包括高度结构化的关系型数据以及非结构化数据,如图像、文本、社交媒体、视频和音频。数据湖也擅长处理批量数据和流数据。随着云计算的兴起,数据湖如今变得非常普遍,并且似乎是所有大数据分析工作负载的持久存储的未来。数据湖的一些例子包括 Hadoop HDFS、AWS S3、Azure Blob 或 ADLS 存储以及 Google Cloud 存储。
基于云的数据湖相较于本地部署的版本有一些优势:
-
它们是按需的,并且具有无限的可扩展性。
-
它们是按使用量计费的,从而节省了前期投资。
-
它们与计算资源完全独立;因此,存储可以独立于计算资源进行扩展。
-
它们支持结构化数据和非结构化数据,并同时支持批处理和流式处理,使得同一存储层可以用于多个工作负载。
由于上述优势,基于云的数据湖在过去几年变得越来越流行。Apache Spark 将这些数据湖视为另一种基于文件的数据存储。因此,使用 Spark 操作数据湖就像操作任何其他基于文件的数据存储层一样简单。
让我们来看一下使用 PySpark 将数据保存到数据湖是多么简单,如下所示的代码示例:
(wordcount_df
.write
.option("header", "true")
.mode("overwrite")
.save("/tmp/data-lake/wordcount.csv")
)
在前面的代码块中,我们将前一节中创建的 wordcount_df DataFrame 保存到数据湖中的 CSV 格式,使用的是 DataFrame 的 write() 函数。mode 选项指示 DataFrameWriter 替换指定文件位置中任何现有的数据;请注意,你也可以使用 append 模式。
向 NoSQL 和内存数据存储中注入数据
数据仓库一直以来都是数据分析用例的传统持久存储层,而数据湖作为新的选择,正在崛起,旨在满足更广泛的工作负载。然而,还有其他涉及超低延迟查询响应时间的大数据分析用例,这些用例需要特定类型的存储层。两种这样的存储层是 NoSQL 数据库和内存数据库,本节将详细探讨这两种存储层。
用于大规模操作分析的 NoSQL 数据库
NoSQL 数据库是传统关系型数据库的替代方案,主要用于处理杂乱且非结构化的数据。NoSQL 数据库在以 键值 对的形式存储大量非结构化数据方面表现非常出色,并且能够在高并发情况下,以常数时间高效地检索任何给定 键 对应的 值。
假设一个业务场景,其中一家企业希望通过毫秒级的查询响应时间,以高度并发的方式向单个客户提供预计算的、超个性化的内容。像 Apache Cassandra 或 MongoDB 这样的 NoSQL 数据库将是这个用例的理想选择。
注意
Apache Spark 并不自带针对 NoSQL 数据库的连接器。然而,这些连接器由各自的数据库提供商构建和维护,可以从相应的提供商处下载,并与 Apache Spark 配置使用。
内存数据库用于超低延迟分析
内存数据库仅将数据存储在内存中,不涉及磁盘等持久存储。正是由于这一特点,内存数据库在数据访问速度上比基于磁盘的数据库更快。一些内存数据库的例子包括 Redis 和 Memcached。由于系统内存有限且存储在内存中的数据在断电后无法持久保存,因此内存数据库不适合用于存储大量历史数据,这在大数据分析系统中是典型需求。然而,它们在涉及超低延迟响应时间的实时分析中有其应用。
假设有一家在线零售商希望在顾客结账时,在其在线门户上展示产品的预计运输交付时间。大多数需要估算交货时间的参数可以预先计算。然而,某些参数,如客户邮政编码和位置,只有在客户结账时提供时才能获得。在这种情况下,需要立即从 Web 门户收集数据,利用超快的事件处理系统进行处理,然后将结果计算并存储在超低延迟的存储层中,以便通过 Web 应用程序访问并返回给客户。所有这些处理应该在几秒钟内完成,而像 Redis 或 Memcached 这样的内存数据库将充当超低延迟数据存储层的角色。
到目前为止,你已经学习了如何访问来自不同数据源的数据并将其导入到各种数据接收端。此外,你已经了解到你对数据源的控制有限,但你对数据接收端有完全的控制。为某些高并发、超低延迟的用例选择正确的数据存储层非常重要。然而,对于大多数大数据分析用例,数据湖已成为首选的持久数据存储层,几乎成了事实上的标准。
另一个优化数据存储的关键因素是数据的实际格式。在接下来的部分,我们将探讨几种数据存储格式及其相对优点。
使用文件格式在数据湖中存储数据
你选择用于在数据湖中存储数据的文件格式对数据存储和检索的便捷性、查询性能以及存储空间有关键影响。因此,选择一个可以平衡这些因素的最佳数据格式至关重要。数据存储格式大致可以分为结构化、非结构化和半结构化格式。在本节中,我们将通过代码示例探讨这几种类型。
非结构化数据存储格式
非结构化数据是指没有预定义数据模型表示的数据,可以是人工或机器生成的。例如,非结构化数据可以是存储在纯文本文件、PDF 文件、传感器数据、日志文件、视频文件、图像、音频文件、社交媒体流等中的数据。
非结构化数据可能包含重要的模式,提取这些模式可能带来有价值的见解。然而,由于以下原因,以非结构化格式存储数据并不十分有用:
-
非结构化数据可能并不总是具有固有的压缩机制,并且可能占用大量存储空间。
-
对非结构化文件进行外部压缩可以节省空间,但会消耗用于文件压缩和解压的处理能力。
-
存储和访问非结构化文件比较困难,因为它们本身缺乏任何模式信息。
鉴于上述原因,摄取非结构化数据并在将其存储到数据湖之前将其转换为结构化格式是合理的。这样可以使后续的数据处理更加轻松和高效。让我们看一个例子,我们将一组非结构化的图像文件转换为图像属性的 DataFrame,然后使用 CSV 文件格式存储它们,如下所示的代码片段所示:
Raw_df = spark.read.format("image").load("/FileStore/FileStore/shared_uploads/images/")
raw_df.printSchema()
image_df = raw_df.select("image.origin", "image.height", "image.width", "image.nChannels", "image.mode", "image.data")
image_df.write.option("header", "true").mode("overwrite").csv("/tmp/data-lake/images.csv")
在之前的代码块中,发生了以下情况:
-
我们使用 Spark 内置的
image格式加载一组图像文件,结果是一个包含图像属性的 Spark DataFrame。 -
我们使用
printSchema()函数查看 DataFrame 的模式,并发现 DataFrame 有一个名为image的单一嵌套列,其中包含origin、height、width、nChannels等作为其内部属性。 -
然后,我们使用
image前缀将内部属性提升到顶层,例如image.origin,并创建一个新的 DataFrame,命名为image_df,其中包含图像的所有单独属性作为顶层列。 -
现在我们已经得到了最终的 DataFrame,我们将其以 CSV 格式写入数据湖。
-
在浏览数据湖时,你可以看到该过程向数据湖写入了几个 CSV 文件,文件大小大约为 127 字节。
提示
写入存储的文件数量取决于 DataFrame 的分区数量。DataFrame 的分区数量取决于执行器核心的数量以及
spark.sql.shuffle.partitions的 Spark 配置。每当 DataFrame 进行洗牌操作时,这个数量也会发生变化。在 Spark 3.0 中,自适应查询执行会自动管理最优的洗牌分区数量。
文件大小和查询性能是考虑文件格式时的两个重要因素。因此,我们进行一个快速测试,在 DataFrame 上执行一个适度复杂的操作,如以下代码块所示:
from pyspark.sql.functions import max, lit
temp_df = final_df.withColumn("max_width", lit(final_df.agg(max("width")).first()[0]))
temp_df.where("width == max_width").show()
前面的代码块首先创建了一个新列,其中每一行的值为所有行中最大的宽度。然后,它过滤掉具有width列最大值的行。这个查询是适度复杂的,典型的数据分析查询类型。在我们的示例测试中,在一个非结构化二进制文件上运行的查询大约花费了5.03 秒。在接下来的章节中,我们将查看其他文件格式上的相同查询,并比较查询性能。
半结构化数据存储格式
在前面的示例中,我们能够获取一个二进制图像文件,提取其属性,并将其存储为 CSV 格式,这使得数据结构化,但仍保持人类可读格式。CSV 格式是另一种数据存储格式,称为半结构化数据格式。半结构化数据格式与非结构化数据格式类似,没有预定义的数据模型。然而,它们以一种方式组织数据,使得从文件本身推断模式信息变得更加容易,而无需提供外部元数据。它们是不同数据处理系统之间交换数据的流行数据格式。半结构化数据格式的示例包括 CSV、XML 和 JSON。
让我们看看如何使用 PySpark 处理半结构化数据的示例,如以下代码块所示:
csv_df = spark.read.options(header="true", inferSchema="true").csv("/tmp/data-lake/images.csv")
csv_df.printSchema()
csv_df.show()
前面的代码示例使用在先前图像处理示例中生成的 CSV 文件,并将其加载为 Spark 数据框。我们启用了从实际数据推断列名和数据类型的选项。printSchema()函数显示 Spark 能够正确推断所有列的数据类型,除了来自半结构化文件的二进制数据列。show()函数显示数据框已经从 CSV 文件中正确重建,并且包含列名。
我们将在csv_df数据框上运行一个适度复杂的查询,如下所示代码块:
from pyspark.sql.functions import max, lit
temp_df = csv_df.withColumn("max_width", lit(csv_df.agg(max("width")).first()[0]))
temp_df.where("width == max_width").show()
在前面的代码块中,我们执行了一些数据框操作,以获取width列最大值的行。使用 CSV 数据格式执行的代码花费了1.24 秒,而我们在非结构化数据存储格式部分执行的类似代码大约花费了5 秒。因此,显然,半结构化文件格式比非结构化文件更适合数据存储,因为从这种数据存储格式中推断模式信息相对更容易。
然而,请注意前面代码片段中show()函数的结果。包含二进制数据的数据列被错误地推断为字符串类型,并且列数据被截断。因此,需要注意的是,半结构化格式并不适合表示所有数据类型,并且在从一种数据格式转换到另一种数据格式时,某些数据类型可能会丢失信息。
结构化数据存储格式
结构化数据遵循预定义的数据模型,具有表格格式,具有明确定义的行和命名列以及定义的数据类型。结构化数据格式的一些示例包括关系数据库表和事务系统生成的数据。请注意,还有一些完全结构化数据及其数据模型的文件格式,如 Apache Parquet、Apache Avro 和 ORC 文件,它们可以轻松存储在数据湖中。
Apache Parquet是一种二进制、压缩的列式存储格式,旨在提高数据存储效率和查询性能。Parquet 是 Apache Spark 框架的一级公民,Spark 的内存存储格式Tungsten旨在充分利用 Parquet 格式。因此,当你的数据存储在 Parquet 格式中时,你将从 Spark 中获得最佳的性能和效率。
注意
Parquet 文件是一种二进制文件格式,意味着文件的内容经过二进制编码。因此,它们不可供人类阅读,不像基于文本的文件格式,如 JSON 或 CSV。然而,这种格式的一个优点是,机器可以轻松解析这些文件,并且在编码和解码过程中不会浪费时间。
让我们将image_df DataFrame(包含来自未结构化数据存储格式部分的图像属性数据)转换为 Parquet 格式,如下所示的代码块所示:
final_df.write.parquet("/tmp/data-lake/images.parquet")
parquet_df = spark.read.parquet("/tmp/data-lake/images.parquet")
parquet_df.printSchema()
parquet_df.show()
上一个代码块将二进制图像文件加载到 Spark DataFrame 中,并将数据以 Parquet 格式写回数据湖。show()函数的结果显示,data列中的二进制数据并未被截断,并且已经从源图像文件中如实保留。
让我们执行一个中等复杂度的操作,如下所示的代码块:
temp_df = parquet_df.withColumn("max_width", lit(parquet_df.agg(max("width")).first()[0]))
temp_df.where("width == max_width").show()
上述代码块提取了列名为width的最大值所在的行。该查询大约需要4.86 秒来执行,而使用原始未结构化的图像数据时则需要超过5 秒。因此,这使得结构化的 Parquet 文件格式成为在 Apache Spark 数据湖中存储数据的最佳格式。表面上看,半结构化的 CSV 文件执行查询所需时间较短,但它们也截断了数据,导致它们并不适合所有使用场景。作为一个通用的经验法则,几乎所有 Apache Spark 的使用场景都推荐使用 Parquet 数据格式,除非某个特定的使用场景需要其他类型的数据存储格式。
到目前为止,你已经看到选择合适的数据格式会影响数据的正确性、易用性、存储效率、查询性能和可扩展性。此外,无论你使用哪种数据格式,将数据存储到数据湖中时,还有另一个需要考虑的因素。这个技术叫做数据分区,它可以真正决定你的下游查询性能是成功还是失败。
简而言之,数据分区是将数据物理地划分到多个文件夹或分区中的过程。Apache Spark 利用这些分区信息,只将查询所需的相关数据文件加载到内存中。这一机制称为分区剪枝。你将会在第三章,数据清理与整合中了解更多关于数据分区的内容。
到目前为止,你已经了解了企业决策支持系统(DSS)的各个组成部分,即数据源、数据目标和数据存储格式。此外,在上一章中,你对 Apache Spark 框架作为大数据处理引擎也有了一定的了解。现在,让我们运用这些知识,构建一个端到端的数据摄取管道。
构建批处理和实时数据摄取管道
一个端到端的数据摄取管道涉及从数据源读取数据,并将其摄取到数据目标中。在大数据和数据湖的背景下,数据摄取通常涉及大量数据源,因此需要一个高可扩展性的数据处理引擎。市场上有一些专门的工具,旨在处理大规模数据摄取,例如 StreamSets、Qlik、Fivetran、Infoworks 等第三方供应商提供的工具。此外,云服务提供商也有其自有的本地工具,例如 AWS 数据迁移服务、Microsoft Azure 数据工厂和 Google Dataflow。还有一些免费的开源数据摄取工具可以考虑使用,例如 Apache Sqoop、Apache Flume、Apache Nifi 等。
提示
Apache Spark 足够适合用于临时数据摄取,但将 Apache Spark 作为专门的数据摄取引擎并不是行业中的常见做法。相反,您应该考虑选择一个专门的、为数据摄取需求量身定制的工具。您可以选择第三方供应商提供的工具,或者选择自己管理一个开源工具。
在本节中,我们将探讨 Apache Spark 在批量处理和流处理方式下的数据摄取能力。
批量处理的数据摄取
批量处理是指一次处理一组或一批数据。批量处理通常是在预定的时间间隔内运行的,且不需要用户干预。通常情况下,批量处理会安排在夜间、业务时间之外运行。其简单的原因在于,批量处理通常需要从操作系统中读取大量的事务数据,这会给操作系统带来很大的负担。这是不可取的,因为操作系统对于企业的日常运营至关重要,我们不希望给事务系统带来那些对日常业务操作没有关键影响的工作负载。
此外,批量处理任务通常是重复性的,因为它们会在固定的时间间隔内运行,每次都会引入自上次成功的批处理以来生成的新数据。批量处理可以分为两种类型,分别是 完整数据加载 和 增量数据加载。
完整数据加载
完全数据加载涉及完全覆盖现有数据集。这对于数据量相对较小且变化不频繁的数据集非常有用。它也是一个更容易实现的过程,因为我们只需要扫描整个源数据集并完全覆盖目标数据集。无需维护任何关于上次数据导入作业的状态信息。以数据仓库中的维度表为例,例如日历表或包含所有零售商实体店数据的表。这些表变化不大且相对较小,非常适合进行完全数据加载。虽然实现简单,但在处理非常大的源数据集并且数据经常变化时,完全数据加载有其缺点。
假设我们考虑一个大型零售商的交易数据,该零售商在全国拥有超过一千家门店,每家门店每月产生大约 500 笔交易。换算下来,大约每天有 15,000 笔交易被导入数据湖。考虑到历史数据,这个数字会迅速增加。假设我们刚开始构建数据湖,目前只导入了大约 6 个月的交易数据。即使在这种规模下,我们的数据集中已经有了 300 万条交易记录,完全清空并重新加载数据集并非一项轻松的任务。
另一个需要考虑的重要因素是,通常操作系统只保留小时间间隔的历史数据。在这里,完全加载意味着也会丢失数据湖中的历史数据。此时,您应考虑增量加载来进行数据导入。
增量数据加载
在增量数据加载过程中,我们只导入在上次成功的数据导入后,数据源中新创建的一组数据。这个增量数据集通常被称为 delta(增量集)。与完全加载相比,增量加载导入的数据集更小,并且由于我们已经在 delta 湖中维护了完整的历史数据,因此增量加载不需要依赖数据源来维护完整的历史记录。
基于前面提到的零售商示例,假设我们每晚运行一次增量批量加载。在这种情况下,我们每天只需要将 15,000 笔交易导入数据湖,这相对容易管理。
设计增量数据导入管道并不像设计完全加载管道那么简单。需要维护增量作业上次运行的状态信息,以便我们能够识别所有来自数据源的尚未导入到数据湖中的新记录。这个状态信息被存储在一个特殊的数据结构中,称为水印表。这个水印表需要由数据导入作业来更新和维护。一个典型的数据导入管道如下图所示:
图 2.2 – 数据摄取
上面的图示展示了一个典型的使用 Apache Spark 的数据摄取管道,并且包含了用于增量加载的水印表。在这里,我们使用 Spark 内建的数据源从源系统摄取原始交易数据,使用 DataFrame 操作进行处理,然后将数据发送回数据湖。
在扩展上一节的零售示例时,让我们使用 PySpark 构建一个端到端的数据摄取管道,采用批处理方式。构建数据管道的一个前提条件当然是数据,对于这个示例,我们将使用UC Irvine 机器学习库提供的在线零售数据集。该数据集以 CSV 格式存放在本章技术要求部分提到的 GitHub 仓库中。在线零售数据集包含了一个在线零售商的交易数据。
我们将下载包含两个 CSV 文件的数据集,并通过笔记本文件菜单中的上传接口将它们上传到Databricks Community Edition笔记本环境。一旦数据集上传完成,我们将记录文件位置。
注意
如果你使用的是自己的 Spark 环境,请确保数据集存放在 Spark 集群可以访问的位置。
现在,我们可以开始实际的代码部分,构建数据摄取管道,如以下代码示例所示:
retail_df = (spark
.read
.option("header", "true")
.option("inferSchema", "true")
.csv("/FileStore/shared_uploads/online_retail/online_retail.csv")
)
在前面的代码块中,我们启用了header和inferSchema选项来加载 CSV 文件。这会创建一个包含八个列及其相应数据类型和列名的 Spark DataFrame。现在,让我们将这些数据以 Parquet 格式摄取到数据湖中,如以下代码块所示:
(retail_df
.write
.mode("overwrite")
.parquet("/tmp/data-lake/online_retail.parquet")
)
在这里,我们将包含原始零售交易数据的retail_df Spark DataFrame 使用 DataFrameWriter 的write()函数保存到数据湖中,以 Parquet 格式存储。我们还将mode选项设置为overwrite,基本上执行的是全量数据加载。
需要注意的一点是,整个数据摄取作业仅仅是10行代码,而且它可以轻松扩展到数千万条记录,处理多达数个 PB 的数据。这就是 Apache Spark 的强大与简洁,它使得 Apache Spark 在极短的时间内成为大数据处理的事实标准。那么,你将如何扩展前述的数据摄取批处理作业,并最终将其投入生产环境呢?
Apache Spark 是从头开始构建的,旨在具备可扩展性,其可扩展性完全依赖于集群上作业可用的核心数量。因此,要扩展你的 Spark 作业,你只需为作业分配更多的处理核心。大多数商业化的 Spark 作为托管服务的提供方案都提供了便捷的自动扩展功能。通过此自动扩展功能,你只需指定集群的最小和最大节点数,集群管理器就能动态计算出为你的作业分配的最优核心数。
大多数商业化的 Spark 提供方案也配备了内置的作业调度器,并支持将笔记本直接作为作业进行调度。外部调度器,从简单的crontab到复杂的作业协调器如Apache Airflow,也可以用来将 Spark 作业生产化。这大大简化了集群容量规划的过程,帮助你腾出时间,专注于实际的数据分析,而不是在容量规划、调优和维护 Spark 集群上耗费时间和精力。
到目前为止,在本节中,你已经查看了一个完整加载批处理摄取作业的示例,该作业从数据源加载整个数据集,并覆盖数据湖中的数据集。你需要添加一些业务逻辑,以在水印数据结构中维护摄取作业的状态,然后计算增量进行增量加载。你可以自己构建所有这些逻辑,或者,也可以简单地使用 Spark 的结构化流处理引擎来为你完成繁重的工作,正如接下来的部分将讨论的那样。
使用结构化流处理进行实时数据摄取
企业通常需要在实时做出战术决策的同时进行战略决策,以保持竞争力。因此,实时将数据摄取到数据湖的需求应运而生。然而,跟上大数据的快速数据速度需要一个强大且可扩展的流处理引擎。Apache Spark 就有这样一个流处理引擎,叫做结构化流处理,我们接下来将探讨它。
结构化流处理入门
结构化流处理是一个基于 Spark SQL 引擎的 Spark 流处理引擎。与 Spark 的其他所有组件一样,结构化流处理也具备可扩展性和容错性。由于结构化流处理基于 Spark SQL 引擎,你可以使用与批处理一样的 Spark DataFrame API 来进行流处理。结构化流处理支持 DataFrame API 所支持的所有函数和构造。
结构化流处理将每个传入的数据流视为一批小数据,称为微批次(micro-batch),并不断将每个微批次附加到目标数据集。结构化流处理的编程模型持续处理微批次,将每个微批次视为一个批处理作业。因此,现有的 Spark 批处理作业可以通过少量的修改轻松转换为流处理作业。结构化流处理旨在提供最大吞吐量,这意味着结构化流处理作业可以扩展到集群中的多个节点,并以分布式方式处理大量传入数据。
结构化流处理还具备额外的故障容忍能力,保证精确一次语义(exactly-once semantics)。为了实现这一点,结构化流处理跟踪数据处理进度。它通过**检查点(checkpointing)和预写日志(write-ahead logs)**等概念,跟踪任何时刻处理的偏移量或事件。预写日志是关系型数据库中的一个概念,用来保证数据库的原子性和持久性。在此技术中,记录会先写入日志,然后再写入最终的数据库。检查点是结构化流处理中另一种技术,它将当前读取的偏移量位置记录在持久化存储系统中。
通过这些技术,结构化流处理能够记录流中最后一个处理过的偏移量的位置,使其具备在流处理作业失败时从中断点恢复处理的能力。
注意
我们建议将检查点存储在具有高可用性和分区容忍支持的持久化存储中,例如基于云的数据湖。
这些技术(检查点、预写日志和可重放的流式数据源)以及支持重新处理数据的流式数据接收器,使得结构化流处理能够保证每个流事件都被处理一次且仅处理一次。
注意
结构化流处理的微批处理模型不适合处理源端事件发生时立即进行处理。像 Apache Flink 或 Kafka Streams 这样的其他流处理引擎,更适合超低延迟的流处理。
增量加载数据
由于结构化流处理(Structured Streaming)内置了机制,帮助你轻松维护增量加载所需的状态信息,因此你可以简单地选择结构化流处理来处理所有增量加载,真正简化你的架构复杂性。让我们构建一个管道,以实时流式方式执行增量加载。
通常,我们的数据摄取从已经加载到数据源的数据开始,例如数据湖或像 Kafka 这样的消息队列。在这里,我们首先需要将一些数据加载到 Kafka 主题中。你可以从一个已经在主题中包含数据的现有 Kafka 集群开始,或者你可以设置一个快速的 Kafka 服务器,并使用github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/blob/main/Chapter02/utils/kafka-setup.ipynb中提供的代码加载在线零售数据集。
让我们来看一下如何使用结构化流处理从 Kafka 实时摄取数据到数据湖,以下是相关的代码片段:
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, TimestampType, DoubleType
eventSchema = ( StructType()
.add('InvoiceNo', StringType())
.add('StockCode', StringType())
.add('Description', StringType())
.add('Quantity', IntegerType())
.add('InvoiceDate', StringType())
.add('UnitPrice', DoubleType())
.add('CustomerID', IntegerType())
.add('Country', StringType())
)
在前面的代码块中,我们声明了所有我们打算从 Kafka 事件中读取的列及其数据类型。结构化流处理要求数据模式必须提前声明。一旦定义了模式,我们就可以开始从 Kafka 主题中读取数据,并将其加载到 Spark DataFrame 中,如以下代码块所示:
kafka_df = (spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers",
"localhost:9092")
.option("subscribe", "retail_events")
.option("startingOffsets", "earliest")
.load()
)
在前面的代码块中,我们开始从一个 Kafka 主题 retail_events 中读取事件流,并告知 Kafka 我们希望从流的开始处加载事件,使用 startingOffsets 选项。Kafka 主题中的事件遵循键值对模式。这意味着我们的实际数据被编码在 value 列中的 JSON 对象内,我们需要提取这些数据,如下代码块所示:
from pyspark.sql.functions import col, from_json, to_date
retail_df = (kafka_df
.select(from_json(col("value").cast(StringType()), eventSchema).alias("message"), col("timestamp").alias("EventTime"))
.select("message.*", "EventTime")
)
在前面的代码块中,我们通过传递之前定义的数据模式对象,使用 from_json() 函数提取数据。这样会得到一个 retail_df DataFrame,其中包含我们需要的事件所有列。此外,我们从 Kafka 主题中附加了一个 EventTime 列,它显示了事件实际到达 Kafka 的时间。这个信息在之后的进一步数据处理过程中可能会有所帮助。由于这个 DataFrame 是通过 readStream() 函数创建的,Spark 本身就知道这是一个流式 DataFrame,并为该 DataFrame 提供了结构化流处理 API。
一旦我们从 Kafka 流中提取了原始事件数据,就可以将其持久化到数据湖中,如以下代码块所示:
base_path = "/tmp/data-lake/retail_events.parquet"
(retail_df
.withColumn("EventDate", to_date(retail_df.EventTime))
.writeStream
.format('parquet')
.outputMode("append")
.trigger(once=True)
.option('checkpointLocation', base_path + '/_checkpoint')
.start(base_path)
)
在前面的代码块中,我们利用了 writeStream() 函数,这是流式 DataFrame 提供的功能,可以以流式方式将数据保存到数据湖中。在这里,我们以 Parquet 格式写入数据,结果数据湖中的数据将是一组 .parquet 文件。保存后,这些 Parquet 文件与任何其他由批处理或流处理创建的 Parquet 文件没有区别。
此外,我们将outputMode设置为append,以表明我们将其视为一个无界数据集,并将继续追加新的 Parquet 文件。checkpointLocation选项存储结构化流处理的写前日志及其他检查点信息。这使其成为一个增量数据加载作业,因为流处理仅基于存储在检查点位置的偏移信息处理新的和未处理的事件。
注意
结构化流处理支持complete和update模式,除了append模式外。关于这些模式的描述以及何时使用它们,可以在 Apache Spark 的官方文档中找到,网址为spark.apache.org/docs/latest/structured-streaming-programming-guide.html#output-modes。
那如果你需要将增量数据加载作业作为较少频繁的批处理作业运行,而不是以持续流处理方式运行呢?
结构化流处理也支持这种情况,方法是通过trigger选项。我们可以将once=True用于该选项,流处理作业将在外部触发时处理所有新的和未处理的事件,然后在没有新的事件需要处理时停止流处理。我们可以根据时间间隔安排该作业定期运行,它的行为就像一个批处理作业,但具备增量加载的所有优势。
总结来说,Spark SQL 引擎的 DataFrame API 在批量数据处理和流处理方面都非常强大且易于使用。静态 DataFrame 和流式 DataFrame 在功能和工具方面有一些微小的差别。然而,在大多数情况下,使用 DataFrame 的批处理和流处理编程模型非常相似。这减少了学习曲线,有助于使用 Apache Spark 的统一分析引擎来统一批处理和流处理。
现在,在下一节中,让我们探讨如何使用 Apache Spark 实现一个统一的数据处理架构,采用的概念就是Lambda 架构。
使用 Lambda 架构统一批量数据和实时数据
批量数据处理和实时数据处理是现代企业决策支持系统(DSS)中的重要组成部分,能够无缝实现这两种数据处理技术的架构有助于提高吞吐量、减少延迟,并使您能够更快速地获得最新数据。这样的架构被称为Lambda 架构,我们接下来将详细探讨。
Lambda 架构
Lambda 架构是一种数据处理技术,用于以单一架构摄取、处理和查询历史数据与实时数据。在这里,目标是提高吞吐量、数据新鲜度和容错性,同时为最终用户提供历史数据和实时数据的统一视图。以下图示展示了一个典型的 Lambda 架构:
图 2.3 – Lambda 架构
如前图所示,Lambda 架构由三个主要组件组成,即 批处理层、速度层 和 服务层。我们将在接下来的章节中讨论这几个层。
批处理层
批处理层就像任何典型的 ETL 层,涉及从源系统批量处理数据。这个层通常涉及定期运行的调度作业,通常在晚上进行。
Apache Spark 可用于构建批处理作业或按计划触发的结构化流式作业,也可用于批处理层构建数据湖中的历史数据。
速度层
速度层持续从数据湖中与批处理层相同的数据源中摄取数据,生成实时视图。速度层不断提供批处理层尚未提供的最新数据,这是由于批处理层固有的延迟。Spark Structured Streaming 可用于实现低延迟的流式作业,持续从源系统中摄取最新数据。
服务层
服务层将批处理层中的历史数据和速度层中的最新数据合并为一个视图,以支持终端用户的临时查询。Spark SQL 是服务层的一个优秀候选,它可以帮助用户查询批处理层中的历史数据以及速度层中的最新数据,并为用户呈现一个统一的数据视图,以便进行低延迟的临时查询。
在前面的章节中,你实现了批处理和流式处理的数据摄取作业。现在,让我们探讨如何将这两种视图结合起来,向用户提供一个统一的视图,如下所示的代码片段所示:
batch_df = spark.read.parquet("/tmp/data-lake/online_retail.parquet")
speed_df = spark.read.parquet("/tmp/data-lake/retail_events.parquet").drop("EventDate").drop("EventTime")
serving_df = batch_df.union(speed_df)
serving_df.createOrReplaceGlobalTempView("serving_layer")
在前面的代码块中,我们创建了两个 DataFrame,一个是通过union函数将这两个 DataFrame 合并,然后使用合并后的 DataFrame 创建一个 Spark Global Temp View。结果是一个可以在集群中所有 Spark Sessions 中访问的视图,它为你提供了跨批处理层和速度层的数据统一视图,如下代码所示:
%sql
SELECT count(*) FROM global_temp.serving_layer;
前面的代码行是一个 SQL 查询,它查询来自 Spark 全局视图的数据,Spark 全局视图充当 服务层,并可以呈现给终端用户,以便跨最新数据和历史数据进行临时查询。
通过这种方式,您可以利用 Apache Spark SQL 引擎的 DataFrame、结构化流处理和 SQL API 来构建一个 Lambda 架构,从而提高数据的新鲜度、吞吐量,并提供统一的数据视图。然而,Lambda 架构的维护较为复杂,因为它有两个独立的数据导入管道,分别用于批处理和实时处理,并且有两个独立的数据接收端。实际上,有一种更简单的方式可以使用开源存储层 Delta Lake 来统一批处理和实时层。您将在第三章,数据清洗与整合中学习到这一点。
总结
在本章中,您了解了在大数据分析背景下的企业决策支持系统(DSS)及其组成部分。您学习了各种类型的数据源,如基于 RDBMS 的操作系统、消息队列、文件源,以及数据接收端,如数据仓库和数据湖,并了解了它们的相对优缺点。
此外,您还探索了不同类型的数据存储格式,如非结构化、结构化和半结构化数据,并了解了使用结构化格式(如 Apache Parquet 与 Spark)带来的好处。您还了解了数据批量导入和实时导入的方式,并学习了如何使用 Spark DataFrame API 来实现它们。我们还介绍了 Spark 的结构化流处理框架,用于实时流数据处理,您学习了如何使用结构化流处理来实现增量数据加载,同时减少编程负担。最后,您探索了 Lambda 架构,将批处理和实时数据处理进行统一,并了解了如何使用 Apache Spark 来实现它。您在本章中学到的技能将帮助您通过 Apache Spark 实施可扩展且高性能的分布式数据导入管道,支持批处理和流处理模式。
在下一章中,您将学习如何处理、清洗和整合在本章中导入数据湖的原始数据,将其转化为干净、整合且有意义的数据集,供最终用户进行业务分析并生成有价值的洞察。
第三章:数据清洗与集成
在上一章中,你了解了数据分析过程的第一步——即将来自各个源系统的原始事务数据导入云数据湖。一旦获得原始数据,我们需要对其进行处理、清洗,并转换为有助于提取有意义的、可操作的商业洞察的格式。这个清洗、处理和转换原始数据的过程被称为数据清洗与集成。本章将讲解这一过程。
来自运营系统的原始数据,在其原始格式下并不适合进行数据分析。在本章中,你将学习各种数据集成技术,这些技术有助于整合来自不同源系统的原始事务数据,并将它们合并以丰富数据,向最终用户展示一个统一的、经过整合的真实版本。接着,你将学习如何使用数据清洗技术清理和转换原始数据的形式和结构,使其适合数据分析。数据清洗主要涉及修复数据中的不一致性,处理坏数据和损坏数据,消除数据中的重复项,并将数据标准化以符合企业的数据标准和惯例。你还将了解使用云数据湖作为分析数据存储所面临的挑战。最后,你将了解一种现代数据存储层——Delta Lake,以克服这些挑战。
本章将为你提供将原始数据整合、清洗和转换为适合分析结构的核心技能,并为你提供在云中构建可扩展、可靠且适合分析的数据湖的有用技术。作为开发者,本章的内容将帮助你随时让业务用户访问所有数据,让他们能够更快速、更轻松地从原始数据中提取可操作的洞察。
本章将涵盖以下主要内容:
-
将原始数据转换为丰富的有意义数据
-
使用云数据湖构建分析数据存储
-
使用数据集成整合数据
-
使用数据清洗使原始数据准备好进行分析
技术要求
在本章中,我们将使用 Databricks 社区版来运行我们的代码(community.cloud.databricks.com)。注册说明可以在databricks.com/try-databricks找到。
本章中的代码可以从github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/Chapter03下载。
本章的数据集可以在github.com/PacktPublishing/Essential-PySpark-for-Data-Analytics/tree/main/data找到。
将原始数据转换为有意义的丰富数据
每个数据分析系统都包括几个关键阶段,包括数据摄取、数据转换以及加载到数据仓库或数据湖中。只有在数据经过这些阶段后,才能准备好供最终用户进行描述性和预测性分析。有两种常见的行业实践用于进行此过程,广泛称为提取、转换、加载(ETL)和提取、加载、转换(ELT)。在本节中,你将探讨这两种数据处理方法,并理解它们的主要区别。你还将了解在云端大数据分析背景下,ELT 相比 ETL 所具备的主要优势。
提取、转换和加载数据
这是几乎所有数据仓库系统遵循的典型数据处理方法。在这个方法中,数据从源系统中提取,并存储在临时存储位置,如关系数据库,称为暂存区。然后,暂存区中的数据会被整合、清洗和转换,最后加载到数据仓库中。下图展示了典型的 ETL 过程:
图 3.1 – 提取、转换和加载
如前面的图所示,ETL 过程由三个主要阶段组成。我们将在接下来的章节中讨论这些阶段。
从运营系统中提取数据
ETL 阶段涉及从多个源系统中提取选择性的原始事务数据,并将其暂存于临时存储位置。此步骤相当于数据摄取过程,你可以在第二章《数据摄取》中学习到。ETL 过程通常处理大量数据,尽管直接在源系统上运行可能会对它们造成过重的负担。运营系统对日常业务功能至关重要,因此不建议不必要地增加其负担。因此,提取过程会在非工作时间从源系统中提取数据,并将其存储在暂存区。此外,ETL 处理可以在暂存区中的数据上进行,从而使运营系统能够处理其核心功能。
转换、清洗和整合数据
这一阶段涉及各种数据转换过程,如数据集成、数据清洗、连接、过滤、拆分、标准化、验证等。此步骤将原始事务数据转化为清晰、集成和丰富的版本,准备进行业务分析。我们将在本章的使用数据集成整合数据和使用数据清洗使原始数据具备分析能力部分深入探讨这一阶段。
将数据加载到数据仓库
这是 ETL 过程的最后阶段,经过转换的数据最终被加载到持久的历史数据存储层中,如数据仓库。通常,ETL 处理系统会在一个单一的流程中完成转换和加载步骤,其中暂存区的原始数据经过清洗、集成和根据业务规则转换后加载到数据仓库中。
ETL 和数据仓库的优缺点
ETL 方法的某些优势在于,数据被转换并加载到一个结构化的分析数据存储中,如数据仓库,这使得数据的分析既高效又具有较高的性能。由于 ETL 模式已经存在了几十年,现在市场上有一些成熟的平台和工具,可以非常高效地在单一统一的流程中执行 ETL。
ETL 的另一个优点是,由于数据在加载到最终存储之前已经处理过,因此可以有机会省略不需要的数据或掩盖敏感数据。这在满足数据合规性和监管要求方面非常有帮助。
然而,ETL 过程以批处理方式运行,通常每天晚上执行一次。因此,只有在 ETL 批处理成功完成后,最终用户才能访问新数据。这就产生了对数据工程师的依赖,需要他们高效地运行 ETL 过程,同时最终用户在获取最新数据之前会有相当的延迟。
每次在下一次计划的 ETL 负载开始之前,暂存区的数据几乎都会被完全清除。而且,操作系统通常不会保留超过几年的事务数据的历史记录。这意味着,最终用户无法访问历史原始数据,除了数据仓库中的已处理数据。对于某些类型的数据分析(如预测分析)来说,这些历史原始数据可能非常有用,但数据仓库通常不会保留这些数据。
ETL 过程围绕数据仓库概念演变,更多适用于本地环境下的商业智能工作负载。数据仓库高度结构化且相对僵化的特性使得 ETL 不太适合数据科学和机器学习,这两者都涉及大量非结构化数据。此外,ETL 过程的批处理特性使其不适用于实时分析。而且,ETL 和数据仓库没有充分利用云技术及基于云的数据湖。因此,一种新的数据处理方法 提取、加载和转换(ELT)应运而生,接下来的章节将详细介绍这一方法。
提取、加载和转换数据
在 ELT 方法中,来自源系统的事务性数据以其原始、未经处理的格式被摄取到数据湖中。摄取到数据湖中的原始数据随后按需或定期进行转换。在 ELT 过程中,原始数据直接存储在数据湖中,通常不会被删除。因此,数据可能会以巨大的规模增长,并且几乎需要无限的存储和计算能力。传统的本地数据仓库和数据湖并未设计用来处理如此庞大的数据规模。因此,ELT 方法的实现仅能依赖现代云技术,这些技术提供了高度可扩展和弹性的计算与存储资源。下图展示了典型的 ELT 过程:
图 3.2 – 提取、加载和转换
在前述图示中,原始数据从多个源系统连续或定期地摄取到数据湖中。然后,数据湖中的原始数据被集成、清洗并转换,之后再存储回数据湖中。数据湖中的清洗和聚合数据作为所有类型下游分析的单一事实来源。
使用 ELT,几乎可以保留任何量的历史数据,且数据可以在源系统中创建后立即提供。无需在摄取数据之前进行预处理,并且由于数据湖对数据格式或结构没有严格要求,ELT 可以摄取并存储各种结构化、非结构化和半结构化的数据。因此,ETL 过程使得所有历史原始数据都可用,从而使数据转换完全按需进行。
选择 ELT 而非 ETL 的优势
ELT 方法学的一些优势在于数据可以以更快的速度进行摄取,因为不需要预处理步骤。它在数据摄取的灵活性方面也更强,有助于解锁如数据科学和机器学习等新的分析用例。ETL 利用云数据湖提供的弹性存储,帮助组织维护事务数据的副本,并保存几乎无限的历史记录。作为云端技术,ELT 还消除了数据复制和归档管理的麻烦,因为大多数云提供商都提供了这些托管服务,并保证服务水平协议(SLAs)。
ELT 方法学正在迅速成为云端大数据处理的事实标准,特别适用于处理大量事务数据的组织。对于已经进入云端或未来有云端战略的组织,推荐采用 ELT 方法学进行数据处理。
然而,云端的 ELT 方法学仍处于起步阶段,云数据湖并未提供其数据仓库对应物所具备的任何事务性或可靠性保障。在下一节中,您将探索构建基于云的数据湖所涉及的一些挑战,并探讨克服这些挑战的方法。
使用云数据湖构建分析数据存储
在本节中,您将探讨基于云的数据湖为大数据分析系统提供的优势,并了解在利用基于云的数据分析系统时,大数据分析系统面临的一些挑战。您还将编写几个PySpark代码示例,亲自体验这些挑战。
云数据湖的挑战
基于云的数据湖提供无限的、可扩展的、相对廉价的数据存储。它们由各大云提供商作为托管服务提供,具备高可用性、可扩展性、高效性和较低的总拥有成本。这帮助组织加速数字创新,缩短上市时间。然而,云数据湖作为对象存储,主要是为了解决存储可扩展性的问题而发展起来的。它们并非为了存储高度结构化、强类型的分析数据而设计。因此,使用基于云的数据湖作为分析存储系统存在一些挑战。
数据湖的可靠性挑战
数据湖并不是基于任何底层文件系统,而是基于对象存储机制,将数据作为对象进行管理。对象存储将数据表示为具有唯一标识符及其相关元数据的对象。对象存储并非为管理频繁变化的事务数据而设计,因此在作为分析数据存储和数据处理系统时,存在一些限制,比如最终一致性、缺乏事务性保障等。我们将在接下来的章节中探讨这些问题。
数据的最终一致性
基于云的数据湖是分布式存储系统,数据存储分布在多台机器上,而不是单一机器上。分布式存储系统受到一个称为 CAP 定理的理论的约束。CAP 定理表明,分布式存储系统只能在一致性、可用性和分区容忍性这三者中选择其中的两个来进行调优。不保证强一致性和分区容忍性可能导致数据丢失或错误,因此基于云的数据湖优先保证这两者,以便使其最终一致。
最终一致性意味着写入云数据湖的数据可能不会立即可用。这可能会导致数据分析系统中的FileNotFound错误,尤其是在下游的商业分析过程试图在 ELT 过程写入数据的同时读取数据时。
缺乏事务性保证
一个典型的关系型数据库在数据写入时提供事务性保证。这意味着数据库操作要么完全成功,要么完全失败,并且任何同时尝试读取数据的消费者都不会因为数据库操作失败而读取到不一致或错误的数据。
数据湖不提供任何此类原子事务或持久性保证。这意味着开发人员需要清理并手动回滚任何失败作业中半写入的不完整数据,并重新处理这些数据。
请考虑以下代码片段,我们正在摄取 CSV 数据,将其转换为 Parquet 格式,并将其保存到数据湖中:
(spark
.read
.csv("/FileStore/shared_uploads/online_retail/")
.write
.mode("overwrite")
.format("parquet")
.save("/tmp/retail.parquet")
)
在这里,我们尝试在工作过程中中断任务,以模拟 Spark 作业失败。在浏览/tmp/retail.parquet数据湖时,你会注意到一些半写入的 Parquet 文件。接下来,我们尝试通过另一个 Spark 作业读取这些 Parquet 文件,代码如下所示:
(spark
.read
.format("parquet")
.load("dbfs:/tmp/retail.parquet/part-00006-tid-6775149024880509486-a83d662e-809e-4fb7-beef-208d983f0323-212-1-c000.snappy.parquet")
.count()
)
在前面的代码块中,我们读取了一个 Parquet 文件,它是一个数据摄取作业未完全完成时的结果。当我们尝试在支持原子事务的数据存储上读取这些数据时,预期的结果是查询要么不返回任何结果,要么因为数据不正确而失败。然而,在前述的 Spark 作业中,我们却得到了一些几千条记录,这是错误的。这是因为 Apache Spark 及数据湖缺乏原子事务保障。
缺乏模式强制执行
数据湖作为对象存储,并不关心数据的结构和模式,能够存储任何数据,而不会执行任何检查来确保数据的一致性。Apache Spark 也没有内建的机制来强制执行用户定义的模式。这导致了损坏和不良数据的产生,数据类型不匹配的数据最终进入数据湖。这会降低数据质量,而数据质量对于最终用户的分析应用至关重要。
看一下以下代码示例,我们已经创建了一个包含几列的初始 DataFrame。第一列的数据类型是IntegerType,第二列的数据类型是StringType。我们将第一个 DataFrame 写入数据湖,格式为 Parquet。接着,我们生成了第二个 DataFrame,两个列的数据类型都是IntegerType。然后,我们尝试将第二个 DataFrame 追加到已经存在于数据湖中的原 Parquet 数据集,如下所示:
from pyspark.sql.functions import lit
df1 = spark.range(3).withColumn("customer_id", lit("1"))
(df1
.write
.format("parquet")
.mode("overwrite")
.save("/tmp/customer")
)
df2 = spark.range(2).withColumn("customer_id", lit(2))
(df2
.write
.format("parquet")
.mode("append")
.save("/tmp/customer"))
在强类型分析数据存储(如数据仓库)上,预期的结果应该是数据类型不匹配错误。然而,Apache Spark、数据湖或 Parquet 数据格式本身并不会在我们尝试执行此操作时抛出错误,事务似乎成功完成。这是不可取的,因为我们允许不一致的数据进入数据湖。然而,对 Parquet 数据集执行读取操作时会因类型不匹配而失败,这可能令人困惑且相当难以调试。如果数据湖或 Apache Spark 具备数据验证支持,这个错误本来可以在数据加载过程中就被捕获。在将数据提供给业务分析之前,始终验证数据的正确性和一致性非常重要,因为业务决策者依赖这些数据。
统一批处理和流处理
现代大数据分析系统的一个关键要求是实时访问最新数据和洞察。Apache Spark 提供了结构化流处理功能,能够处理所有实时分析需求。尽管流处理是核心,但批处理依然是大数据分析的一个重要方面,而 Apache Spark 通过其 Spark SQL 引擎将实时和批处理分析统一,Spark SQL 引擎作为批处理和流处理 Spark 作业的核心抽象层,表现得非常好。
然而,数据湖不支持任何级别的原子事务或同一表或数据集上不同事务之间的隔离。因此,像Lambda 架构这样的技术,就需要被用来统一批处理和流处理管道,这个架构你在第二章中学习过,数据摄取。这就导致了需要维护不同的数据处理管道、不同的代码库以及不同的表,一个用于批处理,另一个用于流处理。你大数据分析系统的架构设计和维护非常复杂。
更新和删除数据
在 ELT 方法论中,你是持续地将新数据摄取到数据湖,并在其中维护源交易的副本以及一段时间内的历史记录。操作系统不断生成交易。然而,时不时你需要更新和删除记录。
以一个客户在在线零售商处下单的例子为例。交易经历不同的阶段,从下订单、订单处理中、订单准备发货、订单已发货、订单运输中,到订单已交付。这一交易状态的变化必须在数据湖中得到反映。
捕获数据状态变化的过程被称为数据湖中的 UPDATE 和 DELETE 操作。数据湖是仅附加的系统,并不设计用来处理大量的任意更新和删除。因此,实施任意更新和删除会增加你 ELT 应用程序的复杂性。
回滚错误数据
之前,你了解到数据湖不支持任何关于写操作的原子事务保证。数据工程师需要识别错误记录,清理它们,并在失败的任务中重新处理数据。对于较小的数据集,这个清理过程可能只是简单的截断并重新加载整个数据集。然而,对于大规模数据集,包含数千 TB 数据的情况下,截断并加载数据根本不可行。数据湖和 Apache Spark 都没有便捷的回滚选项,这就要求数据工程师构建复杂的机制来处理失败的任务。
一类新的现代数据存储格式应运而生,旨在克服上一节提到的数据湖挑战。这些技术的一些例子包括 Apache Hudi、Apache Iceberg 和 Delta Lake。在接下来的部分,我们将探索 Delta Lake,并看看它如何帮助克服各种数据湖挑战。
使用 Delta Lake 克服数据湖挑战
在这一部分中,你将了解 Delta Lake,并理解它如何帮助克服数据湖的一些挑战。你还将编写一些代码示例,看看 Delta Lake 如何实际应用。
Delta Lake 简介
Delta Lake 是一个开源的数据存储层,旨在为基于云的数据湖带来可靠性、ACID 事务保证、架构验证和演进。Delta Lake 还帮助统一批处理和流处理。Delta Lake 由 Databricks 创建,Databricks 是 Apache Spark 的原始开发者,且它完全兼容所有 Apache Spark API。
Delta Lake 由一组版本化的 Parquet 文件组成,并配有一个称为 事务日志 的写前日志。Delta 事务日志有助于实现 Delta Lake 的所有功能。让我们深入了解 Delta 事务日志的内部工作原理,以便更好地理解 Delta Lake 的运作方式。
Delta Lake 事务日志
Delta 事务日志基于一种流行的技术,该技术应用于关系型数据库,被称为预写日志(WAL)。这种技术保证了数据库写操作的原子性和持久性。这是通过在数据写入数据库之前,将每个写操作作为事务记录到预写日志中来实现的。Delta 事务日志基于与 WAL 相同的技术,但在这里,WAL 以及已写入的数据存储在数据湖中的文件里。
让我们尝试通过一个简单的 Spark 作业来理解 Delta 事务日志,该作业将 CSV 数据以 Delta 格式导入数据湖,如以下代码块所示:
(spark
.read
.option("header", True)
.option("inferSchema", True)
.csv("/FileStore/shared_uploads/online_retail/")
.write
.format("delta")
.save("/FileStore/shared_uploads/delta/online_retail")
)
上述代码从数据湖读取 CSV 文件,推断底层数据的模式以及表头,将数据转换为 Delta 格式,并将数据保存到数据湖的不同位置。现在,让我们使用以下命令探索数据湖中 Delta 文件的位置:
%fs ls /FileStore/shared_uploads/delta/online_retail
执行上述命令后,您将注意到 Delta 位置的文件夹结构,如下图所示:
图 3.3 – Delta 文件夹结构
在前面的截图中,您可以看到一个 Delta Lake 位置包含两部分:一个名为 _delta_log 的文件夹和一组 Parquet 文件。_delta_log 文件夹包含 Delta 事务日志的文件。我们可以通过以下命令来探索事务日志:
%fs ls dbfs:/FileStore/shared_uploads/delta/online_retail/_delta_log/
上述命令显示了 _delta_log 文件夹的内容,如下图所示:
图 3.4 – Delta 事务日志
在前面的截图中,我们可以看到文件夹中包含几种不同类型的文件。还有一些带有 .json 扩展名的文件。这些 JSON 文件是实际的 Delta 事务日志文件,包含对 Delta 表执行的所有成功事务的有序记录。
注意
之前使用的 %fs 文件系统命令仅适用于 Databricks 平台。您需要使用适合您 Spark 和数据湖分发版的命令来浏览数据湖。
Delta Lake 事务可以是对 Delta 表执行的任何操作,如插入、更新、删除,甚至是元数据操作,如重命名表、修改表架构等。每次操作发生时,Delta 事务日志都会附加一条新记录,记录诸如添加文件、删除文件、更新元数据等操作。这些操作是原子单位,并按发生顺序记录下来,它们被称为提交。
每 10 次提交后,Delta Lake 会生成一个 Parquet 格式的检查点文件,其中包含到该时刻为止的所有事务。这些周期性的 Parquet 检查点文件使得 Spark 作业能够快速、轻松地读取并重建表的状态。以下 Spark 代码可以轻松地说明这一点:
spark.read.json("/FileStore/shared_uploads/delta/online_retail/_delta_log/").show()
在前面的代码行中,我们像读取其他 JSON 文件一样,使用spak.read()函数读取 Delta 事务日志,并创建了一个 Spark 数据帧。每次在 Delta Lake 表上运行spak.read()命令时,都会执行一个小的 Spark 作业来读取表的状态,从而使对 Delta Lake 的元数据操作完全可扩展。
注意
用于在数据湖中浏览文件的%fs文件系统命令仅在 Databricks 平台上可用。你需要为你的 Spark 环境和数据湖选择合适的机制。
现在你已经了解了 Delta Lake 的组件以及 Delta 事务日志的内部工作原理,接下来我们来看一下 Delta Lake 如何帮助解决数据湖面临的挑战。
使用 Delta Lake 提高数据湖的可靠性
Delta Lake 及其事务日志保证了写入数据湖的数据的原子性和持久性。只有当操作的所有数据完全写入数据湖时,Delta Lake 才会将事务提交到事务日志中。任何读取 Delta 表数据的 Delta 感知消费者都会首先解析 Delta 事务日志,以获取 Delta 表的最新状态。
这样,如果数据摄取任务在中途失败,Delta 事务日志感知消费者会解析事务日志,获取表的最后稳定状态,并只读取事务日志中有提交的数据。任何半写入的脏数据(可能存在于数据湖中)都会被完全忽略,因为这些数据在事务日志中没有任何提交。因此,Delta Lake 与其事务日志结合,通过提供事务的原子性和持久性保证,使数据湖更加可靠。
提示
数据读取器和数据写入器需要是Delta 事务日志感知的,才能获得 Delta Lake 的 ACID 事务保证。任何使用 Apache Spark 的读取器或写入器,只需在 Spark 集群中包含适当版本的 Delta Lake 库,就可以完全Delta 事务日志感知。Delta Lake 还具有与外部数据处理系统的连接器,例如 Presto、Athena、Hive、Redshift 和 Snowflake。
启用 Delta Lake 的模式验证
干净且一致的数据是任何商业分析应用程序的基本要求。确保只有干净数据进入数据湖的一个简单方法是确保在数据摄取过程中验证模式。Delta Lake 内置了模式验证机制,确保写入 Delta Lake 的任何数据都符合用户定义的 Delta 表模式。让我们通过创建一个新的 Delta 表并尝试插入数据类型不匹配的数据来探索此功能,如下所示:
from pyspark.sql.functions import lit
df1 = spark.range(3).withColumn("customer_id", lit("1"))
(df1
.write
.format("delta")
.mode("overwrite")
.save("/tmp/delta/customer"))
df2 = spark.range(2).withColumn("customer_id", lit(2))
(df2
.write
.format("delta")
.mode("append")
.save("/tmp/delta/customer"))
在前面的代码片段中,我们创建了一个名为 df1 的 Spark DataFrame,它有两列,且两列的数据类型均为 StringType。我们使用 Delta Lake 格式将此 DataFrame 写入数据湖中。然后,我们创建了另一个名为 df2 的 Spark DataFrame,同样包含两列,但它们的数据类型分别设置为 LongType 和 IntegerType。
接下来,我们尝试将第二个 DataFrame 附加到原始的 Delta 表中。正如预期的那样,Delta Lake 失败了该操作并抛出了 无法合并不兼容的数据类型 StringType 和 IntegerType 异常。通过这种方式,Delta Lake 在数据湖中通过提供模式验证和强制执行,确保数据质量。
Delta Lake 支持模式演变
在数据摄取和 ELT 过程中,另一个常见的用例是源模式可能会随着时间的推移发生变化,并且需要在数据湖中进行处理。一个这样的场景是可能会向源系统表中添加新的列。希望将这些新列引入我们的数据湖表中,而不影响我们已有的数据。这个过程通常被称为 模式演变,而 Delta Lake 已内建对此的支持。让我们通过以下代码示例来探讨 Delta Lake 中的模式演变:
from pyspark.sql.functions import lit
df1 = spark.range(3)
(df1
.write
.format("delta")
.mode("overwrite")
.save("/tmp/delta/customer"))
df2 = spark.range(2).withColumn("customer_id", lit(2))
(df2
.write
.format("delta")
.option("mergeSchema", True)
.mode("append")
.save("/tmp/delta/customer"))
在前面的代码片段中,我们创建了一个名为 df1 的 Spark DataFrame,它只有一个名为 id 的列。然后,我们将此 DataFrame 以 Delta Lake 格式保存到数据湖中。接着,我们创建了第二个名为 df2 的 Spark DataFrame,包含两个名为 id 和 customer_id 的列。之后,我们将第二个 DataFrame 附加到由 df1 创建的原始 Delta 表中。这次,我们使用了 mergeSchema 选项。该 mergeSchema 选项指定我们期望将新列写入 Delta Lake,并需要将这些列附加到现有表中。我们可以通过对 Delta 表运行以下命令轻松验证这一点:
spark.read.format("delta").load("/tmp/delta/customer").show()
在前面的代码块中,我们将 Delta 表中的数据加载到 Spark DataFrame 中,并调用 show() 操作来显示 DataFrame 的内容,如下图所示:
图 3.5 – Delta Lake 模式演变
如你所见,启用新的mergeSchema后,Delta Lake 会自动将新列添加到现有表中,并将之前不存在的行的值标记为null值。
Delta Lake 中的任意更新和删除
事务不仅会被插入到操作系统中——它们还会时常被更新和删除。在 ELT 过程中,源系统数据的副本会被保存在数据湖中。因此,能够不仅将数据插入到数据湖中,还能更新和删除它变得非常必要。然而,数据湖是仅追加的存储系统,几乎没有或完全没有支持任何更新或删除的功能。Delta Lake,然而,完全支持插入、更新和删除记录。
让我们看一个例子,演示如何从 Delta Lake 更新和删除任意数据,如下代码块所示:
from pyspark.sql.functions import lit
df1 = spark.range(5).withColumn("customer_id", lit(2))
df1.write.format("delta").mode("overwrite").save("/tmp/df1")
在前面的代码块中,我们创建了一个 Spark DataFrame,包含两个列:id 和 customer_id。id 的值从 1 到 5。我们使用 Delta Lake 格式将此表保存到数据湖中。现在,让我们更新id列大于2的customer_id列,如下代码块所示:
%sql
UPDATE delta.`/tmp/df1` SET customer_id = 5 WHERE id > 2;
SELECT * FROM delta.`/tmp/df1`;
在前面的代码块中,我们使用UPDATE SQL 子句更新了customer_id列,并通过WHERE子句指定了条件,就像你在任何关系型数据库管理系统(RDBMS)中操作一样。
提示
%sql 魔法命令指定我们打算在当前笔记本单元格中执行 SQL 查询。尽管我们没有明确创建表,但我们仍然可以使用delta.`path-to-delta-table`语法将 Delta Lake 位置视为一个表来引用。
第二个 SQL 查询从 Delta 表中读取数据,并使用SELECT SQL 子句显示出来,如下图所示:
图 3.6 – 使用 Delta Lake 进行更新
在这里,我们可以验证所有id列值大于2的 Delta 表中的行都已成功更新。因此,Delta Lake 完全支持使用简单的类似 SQL 的语法,在大规模上更新多个任意记录。
提示
Delta 表的元数据完全存储在 Delta 事务日志中。这使得将 Delta 表注册到外部元存储(如Hive)成为完全可选的。这样,直接将 Delta 表保存到数据湖中,并通过 Spark 的 DataFrame 和 SQL API 无缝使用变得更加容易。
现在,让我们看一下 Delta 如何通过以下代码块来支持删除:
%sql
DELETE FROM delta.`/tmp/df1` WHERE id = 4;
SELECT * FROM delta.`/tmp/df1`;
在前面的代码片段中,我们使用DELETE命令删除了所有id值为4的记录。第二个查询,我们使用SELECT子句,显示了DELETE操作后的 Delta 表内容,如下图所示:
图 3.7 – 使用 Delta Lake 进行删除
在这里,我们可以轻松验证我们不再拥有任何 id 值为 4 的行。因此,Delta Lake 也支持大规模删除任意记录。
提示
Delta Lake 同时支持 SQL 和 DataFrame 语法来执行 DELETES、UPDATES 和 UPSERTS。有关语法的参考可以在开源文档中找到,文档链接为:docs.delta.io/latest/delta-update.html#table-deletes-updates-and-merges。
即使是 DELETE 和 UPDATE 操作,Delta Lake 也能像写入操作一样支持原子性和持久性的事务保证。然而,值得注意的是,每次执行 DELETE 或 UPDATE 操作时,Delta Lake 并不是直接更新或删除任何数据,而是生成一个包含更新或删除记录的新文件,并将这些新文件附加到现有的 Delta 表中。然后,Delta Lake 在事务日志中为此次写入事务创建一个新的 提交,并将删除或更新记录的旧 提交 标记为无效。
因此,Delta Lake 实际上并没有删除或更新数据湖中的实际数据文件;它只是为每个操作附加新文件并更新事务日志。更新较小的事务日志文件比更新大量非常大的数据文件要快得多,效率也更高。使用 Delta Lake 更新和删除记录的过程非常高效,并且可以扩展到 PB 级数据。这一功能对于需要识别并删除客户任意记录的用例非常有用,例如 GDPR 合规的用例。
这种始终附加数据文件而从不删除文件的技术的另一个有趣副作用是,Delta Lake 保留了所有数据变化的历史审计记录。这个审计日志保存在 Delta 事务日志 中,借助它,Delta Lake 可以回溯到过去,重现某一时刻的 Delta 表快照。我们将在下一节中探讨这个功能。
Delta Lake 的时间旅行与回滚
Delta Lake 在其事务日志中保留数据如何随时间变化的审计日志。每次数据发生变化时,它还会保持旧版本的 Parquet 数据文件。这使得 Delta Lake 能够在某一时刻重现整个 Delta 表的快照。这个功能叫做 时间旅行。
你可以通过以下 SQL 查询轻松浏览 Delta 表的审计记录:
%sql DESCRIBE HISTORY delta.`/tmp/df1`
在前面的 Spark SQL 查询中,我们使用了 DESCRIBE HISTORY 命令来重现 Delta 表上发生的所有变更的审计日志,如下所示:
图 3.8 – Delta Lake 的时间旅行
在前面的截图中,你可以看到这个 Delta 表发生了三次变化。首先,数据被插入到表中,然后表被更新,最后从表中删除了记录。Delta Lake 将所有这些事件记录为称为提交的事务。提交事件的时间戳和版本号也会记录在变更审计日志中。时间戳或表的版本号可以通过 SQL 查询,用于回溯到 Delta 表的特定快照,示例如下:
%sql SELECT * from delta.`/tmp/delta/df1` VERSION AS OF 0
在前面的 SQL 查询中,我们执行了 Delta Time Travel,回到了表的原始版本。Time Travel 在数据工程和 ELT 处理过程中非常有用,可以在数据摄取过程失败时执行回滚。Delta Time Travel 可以用于将 Delta 表恢复到先前的状态,如下所示:
%sql
INSERT OVERWRITE delta.`/tmp/df1`
SELECT * from delta.`/tmp/df1` VERSION AS OF 0
在前面的 SQL 查询中,我们使用来自表的先前版本的快照覆盖了 Delta 表,并充分利用了Delta Time Travel特性。
另一个 Delta Time Travel 很有用的场景是数据科学和机器学习的应用场景。数据科学家通常通过修改用于实验的数据集来进行多个机器学习实验。在这个过程中,他们最终会维护多个物理版本的相同数据集或表。Delta Lake 可以通过 Time Travel 帮助消除这些物理版本的表,因为 Delta 内置了数据版本管理。你将在第九章《机器学习生命周期管理》中更详细地探讨这种技术。
提示
Delta 会在每次修改数据的操作中保持 Parquet 数据文件的版本。这意味着旧版本的数据文件会不断积累,且 Delta Lake 不会自动删除它们。这可能会导致数据湖的大小随着时间的推移显著增加。为了解决这一问题,Delta Lake 提供了 VACUUM 命令来永久删除不再被 Delta 表引用的旧文件。有关 VACUUM 命令的更多信息,请参见 docs.delta.io/latest/delta-utility.html#vacuum。
使用 Delta Lake 统一批处理和流处理
批处理和实时流处理是任何现代大数据架构中的关键组件。在第二章《数据摄取》中,你学习了如何使用 Apache Spark 进行批处理和实时数据摄取。你还学习了 Lambda 架构,利用它可以实现同时的批处理和流处理。使用 Apache Spark 实现 Lambda 架构仍然相对复杂,因为需要为批处理和实时处理分别实现两个独立的数据处理管道。
这种复杂性来源于数据湖的局限性,因为它们本质上不提供任何写操作的事务性、原子性或持久性保障。因此,批处理和流处理无法将数据写入数据湖的同一个表或位置。由于 Delta Lake 已经解决了数据湖面临的这一挑战,可以将单一的 Delta Lake 与多个批处理和实时管道结合使用,从而进一步简化 Lambda 架构。你将在第四章中进一步探讨这一点,实时数据分析。
总结来说,在本节中,你学到了数据湖在支持真正可扩展的大数据处理系统中的重要作用。然而,它们并非为数据分析存储系统而构建,存在一些不足之处,例如缺乏 ACID 事务保障,以及无法支持更新或删除记录、保持数据质量的模式执行或批处理与流处理的统一。你还学到了现代数据存储层(如 Delta Lake)如何帮助克服数据湖的挑战,并使其更接近真正的数据分析存储系统。
现在,既然你已经了解了如何让基于云的数据湖更加可靠并适合数据分析,你已经准备好学习将原始事务数据转化为有意义的商业洞察的过程。我们将从整合来自不同来源的数据并创建统一的单一视图开始。
使用数据集成进行数据整合
数据集成是 ETL 和 ELT 数据处理模式中的一个重要步骤。数据集成是将来自不同数据源的数据进行组合和融合,生成代表单一事实版本的丰富数据的过程。数据集成不同于数据摄取,因为数据摄取只是将数据从不同来源收集并带到一个中心位置,例如数据仓库。另一方面,数据集成将这些不同的数据源结合起来,创建一个有意义的统一版本的数据,代表数据的所有维度。数据集成有多种实现方式,本节将探讨其中的一些。
通过 ETL 和数据仓库进行数据整合
提取、转换和加载数据到数据仓库是过去几十年来数据集成的最佳技术之一。数据整合的主要目标之一是减少数据存储位置的数量。ETL 过程从各种源系统中提取数据,然后根据用户指定的业务规则对数据进行合并、过滤、清洗和转换,最后将其加载到中心数据仓库。
通过 ETL 和数据仓库技术以及专门为此构建的工具和技术支持数据整合和数据集成。虽然 ELT 过程与 ETL 略有不同,并且使用 Apache Spark,我们打算构建一个数据湖,但数据集成和数据整合的技术仍然保持相同,即使是 ETL 也是如此。
让我们使用 PySpark 实现数据整合过程。作为第一步,将本章提供的所有数据集上传到可以被您的 Spark 集群访问的位置。在 Databricks Community Edition 的情况下,可以直接从笔记本的File菜单中将数据集上传到数据湖中。数据集和代码文件的链接可以在本章开头的Technical requirements部分找到。
让我们使用以下代码块探索标记为online_retail.csv和online_retail_II.csv的两个交易数据集的架构信息:
from pyspark.sql.types import StructType, StructField, IntegerType, TimestampType, StringType, DoubleType
schema = (StructType()
.add("InvoiceNo", StringType(), True)
.add("StockCode", StringType(), True)
.add("Description", StringType(), True)
.add("Quantity", StringType(), True)
.add("InvoiceDate", StringType(), True)
.add("UnitPrice", StringType(), True)
.add("CustomerID", StringType(), True)
.add("Country", StringType(), True))
df1 = spark.read.schema(schema).option("header", True).csv("dbfs:/FileStore/shared_uploads/online_retail/online_retail.csv")
df2 = spark.read.schema(schema).option("header", True).csv("dbfs:/FileStore/shared_uploads/online_retail/online_retail_II.csv")
df1.printSchema()
df2.printSchema()
在前面的代码片段中,我们执行了以下操作:
-
我们将 Spark DataFrame 的架构定义为由多个 StructField 组成的
StructType。PySpark 提供了这些内置结构来编程地定义 DataFrame 的架构。 -
然后,我们将两个 CSV 文件加载到单独的 Spark DataFrames 中,同时使用
schema选项指定我们在Step 1中创建的数据模式。我们仍然将头部选项设置为True,因为 CSV 文件的第一行有一个定义好的标题,我们需要忽略它。 -
最后,我们打印了在Step 2中创建的两个 Spark DataFrames 的架构信息。
现在我们已经将来自 CSV 文件的零售数据集加载到 Spark DataFrames 中,让我们将它们整合成一个单一数据集,如以下代码所示:
retail_df = df1.union(df2)
retail_df.show()
在前述代码中,我们简单地使用union()函数将包含在线零售交易数据的两个 Spark DataFrames 组合成一个单一的 Spark DataFrame。联合操作将这两个不同的 DataFrame 合并成一个 DataFrame。合并后的数据集被标记为retail_df。我们可以使用show()函数验证结果。
提示
union()函数是一种转换操作,因此它是延迟评估的。这意味着当您在两个 Spark DataFrames 上调用union()时,Spark 会检查这两个 DataFrame 是否具有相同数量的列,并且它们的数据类型是否匹配。它不会立即将 DataFrame 映射到内存中。show()函数是一个动作操作,因此 Spark 会处理转换并将数据映射到内存中。然而,show()函数仅在 DataFrame 的少量分区上工作,并返回一组样本结果给 Spark Driver。因此,这个动作帮助我们快速验证我们的代码。
接下来,我们有一些描述国家代码和名称的数据存储在country_codes.csv文件中。让我们使用以下代码块将其与前一步中创建的retail_df DataFrame 集成:
df3 = spark.read.option("header", True).option("delimiter", ";").csv("/FileStore/shared_uploads/countries_codes.csv")
country_df = (df3
.withColumnRenamed("OFFICIAL LANG CODE", "CountryCode")
.withColumnRenamed("ISO2 CODE", "ISO2Code")
.withColumnRenamed("ISO3 CODE", "ISO3Code")
.withColumnRenamed("LABEL EN", "CountryName")
.withColumnRenamed("Geo Shape", "GeoShape")
.drop("ONU CODE")
.drop("IS ILOMEMBER")
.drop("IS RECEIVING QUEST")
.drop("LABEL FR")
.drop("LABEL SP")
.drop("geo_point_2d")
)
integrated_df = retail_df.join(country_df, retail_df.Country == country_df.CountryName, "left_outer")
在前面的代码片段中,我们做了以下操作:
-
我们将
country_codes.csv文件加载到一个 Spark 数据框中,并将header选项设置为True,文件分隔符设置为";"。 -
我们重命名了一些列名,以遵循标准命名约定,使用了
withColumnRenamed()函数。我们删除了几个我们认为对任何业务用例都不必要的列。这导致生成了一个名为country_df的数据框,其中包含了国家代码和其他描述性列。 -
然后,我们将这个数据框与之前步骤中的
retail_df数据框进行了连接。我们使用的是retail_df数据框,无论它们是否在country_df数据框中有匹配记录。 -
结果生成的
integrated_df数据框包含了来自country_codes.csv数据集的描述性列,并对在线零售交易数据进行了增强。
我们还有一个名为adult.data的数据集,其中包含了来自美国人口普查的收入数据集。我们将这个数据集与已经集成和增强的零售交易数据集进行集成,代码如下所示:
from pyspark.sql.functions import monotonically_increasing_id
income_df = spark.read.schema(schema).csv("/FileStore/shared_uploads/adult.data").withColumn("idx", monotonically_increasing_id())
retail_dfx = retail_df.withColumn("CustomerIDx", monotonically_increasing_id())
income_dfx = income_df.withColumn("CustomerIDx", monotonically_increasing_id())
income_df = spark.read.schema(schema).csv("/FileStore/shared_uploads/adult.data").withColumn("idx", monotonically_increasing_id())
retail_dfx = integrated_df.withColumn("RetailIDx", monotonically_increasing_id())
income_dfx = income_df.withColumn("IncomeIDx", monotonically_increasing_id())
retail_enriched_df = retail_dfx.join(income_dfx, retail_dfx.RetailIDx == income_dfx.IncomeIDx, "left_outer")
在前面的代码片段中,我们做了以下操作:
-
我们使用
csv()函数从收入数据集中创建了一个 Spark 数据框。该文件是逗号分隔的,并且有一个头部,因此我们使用了适当的选项。最终,我们得到了一个名为income_df的数据框,包含了与消费者人口统计和收入水平相关的一些列。 -
然后,我们添加了两个
income_df和integrated_df数据框,以便可以进行连接。我们使用了monotonically_increasing_id()函数,它生成唯一的递增数字。 -
然后,两个数据框基于新生成的
integrated_df数据框进行了连接,无论它们是否在income_df数据框中有对应的匹配行。结果是集成的、增强的零售交易数据,包含了国家、客户人口统计信息和收入信息,所有数据都统一在一个数据集中。
这个中间数据集对于执行retail_enriched.delta非常有用,下面的代码展示了如何使用它:
(retail_enriched_df
.coalesce(1)
.write
.format("delta", True)
.mode("overwrite")
.save("/FileStore/shared_uploads/retail.delta"))
在前面的代码块中,我们使用coalesce()函数将retailed_enriched_df数据框的分区数量减少到一个分区。这样就生成了一个单一的可移植 Parquet 文件。
注意
学习和实验大数据分析的最大挑战之一是找到干净且有用的数据集。在前面的代码示例中,我们必须引入一个代理键来连接两个独立的数据集。在实际应用中,除非数据集之间相关且存在公共连接键,否则你永远不会强行连接数据集。
因此,使用 Spark 的数据框操作或 Spark SQL,你可以从不同来源集成数据,创建一个增强的、有意义的数据集,表示单一版本的真实数据。
使用数据虚拟化技术进行数据集成
数据虚拟化,顾名思义,是一种虚拟过程,在该过程中,数据虚拟化层作为所有不同数据源之上的逻辑层。这个虚拟层充当业务用户的通道,使他们能够实时无缝访问所需数据。与传统的ETL和ELT过程相比,数据虚拟化的优势在于它不需要任何数据移动,而是直接向业务用户展示集成的数据视图。当业务用户尝试访问数据时,数据虚拟化层会查询底层数据集并实时获取数据。
数据虚拟化层的优势在于,它完全绕过了数据移动,节省了通常需要投入到这个过程中的时间和资源。它能够实时展示数据,几乎没有延迟,因为它直接从源系统获取数据。
数据虚拟化的缺点是它并不是一种广泛采用的技术,而且提供这项技术的产品价格通常较高。Apache Spark 并不支持纯粹意义上的数据虚拟化。然而,Spark 支持一种称为数据联邦的数据虚拟化技术,您将在下一节中学习到。
通过数据联邦实现数据集成
数据联邦是一种数据虚拟化技术,它使用虚拟数据库(也称为联邦数据库)来提供异构数据源的统一和同质化视图。这里的思路是通过单一的数据处理和元数据层访问任何地方的数据。Apache Spark SQL 引擎支持数据联邦,Spark 的数据源可以用来定义外部数据源,从而在 Spark SQL 中实现无缝访问。使用 Spark SQL 时,可以在单一的 SQL 查询中使用多个数据源,而不需要先合并和转换数据集。
让我们通过一个代码示例来学习如何使用 Spark SQL 实现数据联邦:
%sql
CREATE TABLE mysql_authors IF NOT EXISTS
USING org.apache.spark.sql.jdbc
OPTIONS (
url "jdbc:mysql://localhost:3306/pysparkdb",
dbtable "authors",
user "@@@@@@",
password "######"
);
在前一块代码中,我们创建了一个以 MySQL 为数据源的表。这里,我们使用 Spark 创建的表只是指向 MySQL 中实际表的指针。每次查询这个 Spark 表时,它都会通过 JDBC 连接从底层的 MySQL 表中获取数据。接下来,我们将从 Spark DataFrame 创建另一个表,并将其保存为 CSV 格式,如下所示:
from pyspark.sql.functions import rand, col
authors_df = spark.range(16).withColumn("salary", rand(10)*col("id")*10000)
authors_df.write.format("csv").saveAsTable("author_salary")
在前面的代码块中,我们生成了一个包含 16 行和 2 列的 Spark DataFrame。第一列标记为id,它只是一个递增的数字;第二列标记为salary,它是使用内置的rand()函数生成的随机数。我们将 DataFrame 保存到数据湖中,并使用saveAsTable()函数将其注册到 Spark 内置的 Hive 元数据存储中。现在我们有了两个表,它们分别存在于不同的数据源中。接下来,看看我们如何在 Spark SQL 中通过联邦查询将它们一起使用,如下所示:
%sql
SELECT
m.last_name,
m.first_name,
s.salary
FROM
author_salary s
JOIN mysql_authors m ON m.uid = s.id
ORDER BY s.salary DESC
在之前的 SQL 查询中,我们将 MySQL 表与位于数据湖中的 CSV 表在同一查询中连接,生成了数据的集成视图。这展示了 Apache Spark 的数据联合功能。
提示
某些专门的数据处理引擎纯粹设计为联合数据库,例如 Presto。Presto 是一个分布式的大数据大规模并行处理(MPP)查询引擎,旨在在任何数据上提供非常快速的查询性能。使用 Apache Spark 而不是 Presto 的一个优势是,它支持数据联合,并且能够处理其他用例,如批处理和实时分析、数据科学、机器学习和交互式 SQL 分析,所有这些都由单一的统一引擎支持。这使得用户体验更加无缝。然而,组织在不同用例中采用多种大数据技术也是非常常见的。
总结来说,数据集成是将来自不同数据源的数据进行整合和结合,生成有意义的数据,提供单一版本的真实情况。数据集成围绕着多种技术,包括使用 ETL 或 ELT 技术整合数据以及数据联合。在本节中,您学习了如何利用这些技术通过 Apache Spark 实现数据的集成视图。数据分析旅程的下一步是学习如何通过称为数据清洗的过程来清理混乱和脏数据。
使用数据清洗使原始数据适合分析
原始事务数据可能存在多种不一致性,这些不一致性可能是数据本身固有的,或是在不同数据处理系统之间传输过程中、数据摄取过程中产生的。数据集成过程也可能引入数据不一致性。这是因为数据正在从不同系统中整合,而这些系统有各自的数据表示机制。这些数据并不十分干净,可能包含一些坏记录或损坏的记录,在生成有意义的业务洞察之前,需要通过称为数据清洗的过程进行清理。
数据清洗是数据分析过程的一部分,通过修复不良和损坏的数据、删除重复项,并选择对广泛业务用例有用的数据集来清理数据。当来自不同来源的数据被合并时,可能会出现数据类型的不一致,包括错误标签或冗余数据。因此,数据清洗还包括数据标准化,以便将集成数据提升至企业的标准和惯例。
数据清洗的目标是生成干净、一致、完美的数据,为最终一步的生成有意义和可操作的洞察力做好准备,这一步骤来自原始事务数据。在本节中,您将学习数据清洗过程中的各种步骤。
数据选择以消除冗余
一旦来自不同源的数据被整合,集成数据集中可能会出现冗余项。可能有些字段对于你的业务分析团队来说并不必要。数据清洗的第一步就是识别这些不需要的数据元素并将其移除。
让我们对我们在通过 ETL 和数据仓库进行的数据整合部分中生成的集成数据集进行数据选择。我们首先需要查看表模式,了解有哪些列以及它们的数据类型。我们可以使用以下代码行来做到这一点:
retail_enriched_df.printSchema()
前一行代码的结果显示了所有列,我们可以轻松发现Country和CountryName列是冗余的。数据集中还有一些为了数据集成而引入的列,这些列对后续分析并没有太大用处。让我们清理集成数据集中不需要的冗余列,如下所示的代码块所示:
retail_clean_df = (retail_enriched_df
.drop("Country")
.drop("ISO2Code")
.drop("ISO3Code")
.drop("RetailIDx")
.drop("idx")
.drop("IncomeIDx")
)
在前面的代码片段中,我们使用了drop() DataFrame 操作来删除不需要的列。现在我们已经从集成数据集中选择了正确的数据列,接下来的步骤是识别并消除任何重复的行。
去重数据
去重过程的第一步是检查是否有任何重复的行。我们可以通过组合 DataFrame 操作来做到这一点,如下所示的代码块所示:
(retail_enriched_df
.select("InvoiceNo", "InvoiceDate")
.groupBy("InvoiceNo", "InvoiceDate")
.count()
.show())
前面的代码行显示了在根据InvoiceNo、InvoiceDate和StockCode列对行进行分组后,所有行的计数。在这里,我们假设InvoiceNo、InvoiceDate和StockCode的组合是唯一的,并且它们构成了1。然而,在结果中,我们可以看到一些行的计数大于1,这表明数据集中可能存在重复行。这应该在你抽样检查了一些显示重复的行后手动检查,以确保它们确实是重复的。我们可以通过以下代码块来做到这一点:
(retail_enriched_df.where("InvoiceNo in ('536373', '536382', '536387') AND StockCode in ('85123A', '22798', '21731')")
.display()
)
在前面的查询中,我们检查了InvoiceNo和StockCode值的示例,以查看返回的数据是否包含重复项。通过目视检查结果,我们可以看到数据集中存在重复项。我们需要消除这些重复项。幸运的是,PySpark 提供了一个叫做drop_duplicates()的便捷函数来实现这一点,如下所示的代码行所示:
retail_nodupe = retail_clean_df.drop_duplicates(["InvoiceNo", "InvoiceDate", "StockCode"])
在前一行代码中,我们使用了drop_duplicates()函数,根据一组列来消除重复项。让我们通过以下代码行来检查它是否成功删除了重复行:
(retail_nodupe
.select("InvoiceNo", "InvoiceDate", "StockCode")
.groupBy("InvoiceNo", "InvoiceDate", "StockCode")
.count()
.where("count > 1")
.show())
之前的代码根据复合键对行进行了分组,并检查了每组的计数。结果是一个空数据集,这意味着所有重复项已经成功消除。
到目前为止,我们已经从集成的数据集中删除了不需要的列并消除了重复。在数据选择步骤中,我们注意到所有列的数据类型都是string,且列名称遵循不同的命名惯例。这可以通过数据标准化过程进行修正。
数据标准化
数据标准化是指确保所有列都遵循其适当的数据类型。这也是将所有列名称提升到我们企业命名标准和惯例的地方。可以通过以下 DataFrame 操作在 PySpark 中实现:
retail_final_df = (retail_nodupe.selectExpr(
"InvoiceNo AS invoice_num", "StockCode AS stock_code",
"description AS description", "Quantity AS quantity",
"CAST(InvoiceDate AS TIMESTAMP) AS invoice_date",
"CAST(UnitPrice AS DOUBLE) AS unit_price",
"CustomerID AS customer_id",
"CountryCode AS country_code",
"CountryName AS country_name", "GeoShape AS geo_shape",
"age", "workclass AS work_class",
"fnlwgt AS final_weight", "education",
"CAST('education-num' AS NUMERIC) AS education_num",
"'marital-status' AS marital_status", "occupation",
"relationship", "race", "gender",
"CAST('capital-gain' AS DOUBLE) AS capital_gain",
"CAST('capital-loss' AS DOUBLE) AS capital_loss",
"CAST('hours-per-week' AS DOUBLE) AS hours_per_week",
"'native-country' AS native_country")
)
在前面的代码块中,实际上是一个 SQL SELECT 查询,它将列转换为其适当的数据类型,并为列名称指定别名,以便它们遵循合适的 Python 命名标准。结果是一个最终数据集,包含来自不同来源的数据,已集成成一个清洗、去重和标准化的数据格式。
这个最终的数据集,是数据集成和数据清洗阶段的结果,已经准备好向业务用户展示,供他们进行业务分析。因此,将这个数据集持久化到数据湖并提供给最终用户使用是有意义的,如下所示的代码行所示:
retail_final_df.write.format("delta").save("dbfs:/FileStore/shared_uploads/delta/retail_silver.delta")
在前面的代码行中,我们将最终版本的原始事务数据以 Delta Lake 格式保存到数据湖中。
注意
在业界惯例中,从源系统直接复制的事务数据被称为铜数据,经过清洗和集成的事务数据被称为银数据,而聚合和汇总后的数据被称为金数据。数据分析过程,简而言之,就是一个不断摄取铜数据并将其转化为银数据和金数据的过程,直到它可以转化为可执行的业务洞察。
为了总结数据清洗过程,我们获取了数据集成过程的结果集,移除了任何冗余和不必要的列,消除了重复的行,并将数据列提升到企业标准和惯例。所有这些数据处理步骤都是通过 DataFrame API 实现的,该 API 由 Spark SQL 引擎提供支持。它可以轻松地将此过程扩展到数 TB 甚至 PB 的数据。
提示
在本章中,数据集成和数据清洗被视为两个独立且互相排斥的过程。然而,在实际使用案例中,将这两个步骤作为一个数据处理管道共同实现是非常常见的做法。
数据集成和数据清洗过程的结果是可用的、干净的且有意义的数据,已准备好供业务分析用户使用。由于我们在这里处理的是大数据,因此数据必须以一种提高业务分析查询性能的方式进行结构化和呈现。你将在接下来的章节中了解这一点。
通过数据分区优化 ELT 处理性能
数据分区是一个将大数据集物理拆分为较小部分的过程。这样,当查询需要大数据集的一部分时,它可以扫描并加载分区的子集。这种排除查询不需要的分区的技术被称为分区修剪。
谓词下推是另一种技术,将查询中的一些过滤、切片和切割数据的部分,即谓词,下推到数据存储层。然后,由数据存储层负责过滤掉所有查询不需要的分区。
传统的关系型数据库管理系统(RDBMS)和数据仓库一直都支持数据分区、分区修剪和谓词下推。像 CSV 和 JSON 这样的半结构化文件格式支持数据分区和分区修剪,但不支持谓词下推。Apache Spark 完全支持这三种技术。通过谓词下推,Spark 可以将过滤数据的任务委派给底层数据存储层,从而减少需要加载到 Spark 内存中的数据量,并进一步进行处理。
结构化数据格式如 Parquet、ORC 和 Delta Lake 完全支持分区修剪和谓词下推。这有助于 Spark 的 Catalyst 优化器生成最佳的查询执行计划。这是优先选择像 Apache Parquet 这样结构化文件格式而不是半结构化数据格式的一个有力理由。
假设你的数据湖中包含跨越数年的历史数据,而你的典型查询通常只涉及几个月到几年之间的数据。你可以选择将数据完全不分区,所有数据存储在一个文件夹中。或者,你可以按照年份和月份属性对数据进行分区,如下图所示:
图 3.9 – 数据分区
在前面图示的右侧,我们有未分区的数据。这样的数据存储模式使得数据存储变得稍微简单一些,因为我们只是反复将新数据追加到同一个文件夹中。然而,到了一定程度后,数据会变得难以管理,也使得执行任何更新或删除操作变得困难。此外,Apache Spark 需要将整个数据集读取到内存中,这样会丧失分区修剪和谓词下推可能带来的优势。
在图表的右侧,数据按照年份分区,然后按照月份分区。这使得写入数据稍微复杂一些,因为 Spark 应用程序每次写入数据之前都需要选择正确的分区。然而,与更新、删除以及下游查询带来的效率和性能相比,这只是一个小代价。对这种分区数据的查询将比未分区的数据快几个数量级,因为它们充分利用了分区修剪和谓词下推。因此,推荐使用适当的分区键对数据进行分区,以从数据湖中获得最佳的性能和效率。
由于数据分区在决定下游分析查询性能方面起着至关重要的作用,因此选择合适的分区列非常重要。一般的经验法则是,选择一个基数较低的分区列。至少为一千兆字节的分区大小是实际可行的,通常,基于日期的列是一个很好的分区键候选。
注意
云端对象存储的递归文件列出通常较慢且费用昂贵。因此,在云端对象存储上使用层次分区并不是很高效,因此不推荐使用。当需要多个分区键时,这可能会成为性能瓶颈。Databricks 的专有版本 Delta Lake 以及他们的 Delta Engine 支持动态文件修剪和Z-order多维索引等技术,帮助解决云端数据湖中层次分区的问题。你可以在docs.databricks.com/delta/optimizations/dynamic-file-pruning.html了解更多信息。然而,这些技术目前还没有在 Delta Lake 的开源版本中提供。
总结
在本章中,你了解了两种著名的数据处理方法——ETL和ELT,并看到了使用 ETL 方法能解锁更多分析用例的优势,这些用例是使用 ETL 方法无法实现的。通过这样做,你理解了 ETL 的可扩展存储和计算需求,以及现代云技术如何帮助实现 ELT 的数据处理方式。接着,你了解了将基于云的数据湖作为分析数据存储的不足之处,例如缺乏原子事务和持久性保证。然后,你被介绍到 Delta Lake,这是一种现代数据存储层,旨在克服基于云的数据湖的不足。你学习了数据集成和数据清洗技术,这些技术有助于将来自不同来源的原始事务数据整合起来,生成干净、纯净的数据,这些数据准备好呈现给最终用户以生成有意义的见解。你还学习了如何通过 DataFrame 操作和 Spark SQL 实现本章中使用的每一种技术。你获得了将原始事务数据转换为有意义的、丰富的数据的技能,这些技能对于使用 ELT 方法在大规模大数据中进行处理至关重要。
通常,数据清洗和集成过程是性能密集型的,且以批处理方式实现。然而,在大数据分析中,你必须在事务数据在源头生成后尽快将其传递给最终用户。这对战术决策非常有帮助,并且通过实时数据分析得以实现,实时数据分析将在下一章中讲解。
第四章:实时数据分析
在现代大数据世界中,数据生成的速度非常快,快到过去十年中的任何技术都无法处理,例如批处理 ETL 工具、数据仓库或商业分析系统。因此,实时处理数据并从中提取洞察力对于企业做出战术决策至关重要,以帮助他们保持竞争力。因此,迫切需要能够实时或近实时处理数据的实时分析系统,帮助最终用户尽可能快地获取最新数据。
本章中,您将探索实时大数据分析处理系统的架构和组件,包括作为数据源的消息队列、作为数据汇聚点的 Delta 和作为流处理引擎的 Spark Structured Streaming。您将学习使用有状态处理的 Structured Streaming 处理迟到数据的技巧。还将介绍使用 变更数据捕获(CDC)技术,在数据湖中保持源系统的精确副本。您将学习如何构建多跳流处理管道,逐步改进从原始数据到已清洗和丰富数据的质量,这些数据已准备好进行数据分析。您将掌握使用 Apache Spark 实现可扩展、容错且近实时的分析系统的基本技能。
本章将涵盖以下主要主题:
-
实时分析系统架构
-
流处理引擎
-
实时分析行业应用案例
-
使用 Delta Lake 简化 Lambda 架构
-
CDC
-
多跳流处理管道
技术要求
本章中,您将使用 Databricks Community Edition 来运行您的代码。可以通过以下链接找到:community.cloud.databricks.com:
-
注册说明请参见:
databricks.com/try-databricks。 -
本章中使用的代码和数据可以从以下链接下载:
github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/tree/main/Chapter04。
在我们深入探讨如何使用 Apache Spark 实现实时流处理数据管道之前,首先,我们需要了解实时分析管道的一般架构及其各个组件,具体内容将在以下部分中描述。
实时分析系统架构
实时数据分析系统,顾名思义,是实时处理数据的系统。由于数据在源头生成,使其可以以最小的延迟提供给业务用户。它由几个重要组件组成,即流数据源、流处理引擎、流数据汇聚点以及实际的实时数据消费者,如下图所示:
图 4.1 – 实时数据分析
上图展示了一个典型的实时数据分析系统架构。在接下来的部分,我们将更详细地探讨各个组件。
流数据源
类似于其他企业决策支持系统,实时数据分析系统也从数据源开始。企业在实时中持续生成数据;因此,任何被批处理系统使用的数据源也是流数据源。唯一的区别在于你从数据源摄取数据的频率。在批处理模式下,数据是周期性摄取的,而在实时流式系统中,数据是持续不断地从同一数据源摄取的。然而,在持续摄取数据之前,有几个需要注意的事项。这些可以描述如下:
-
数据源能否跟上实时流式分析引擎的需求?否则,流引擎是否会给数据源带来压力?
-
数据源能否异步与流引擎进行通信,并按流引擎要求的任意顺序重放事件?
-
数据源能否以它们在源端发生的精确顺序重放事件?
上述三点提出了关于流数据源的一些重要要求。流数据源应该是分布式且可扩展的,以便跟上实时流式分析系统的需求。需要注意的是,它必须能够以任何任意顺序重放事件。这样,流引擎可以灵活地按任何顺序处理事件,或者在发生故障时重新启动处理。对于某些实时使用场景,如 CDC,必须按事件发生的精确顺序重放事件,以保持数据完整性。
由于前述原因,没有操作系统适合做流数据源。在云和大数据领域,建议使用可扩展、容错且异步的消息队列,如 Apache Kafka、AWS Kinesis、Google Pub/Sub 或 Azure Event Hub。云端数据湖如 AWS S3、Azure Blob 和 ADLS 存储,或者 Google Cloud Storage 在某些使用场景下也适合作为流数据源。
现在我们已经了解了流数据源,让我们来看一下如何以流的方式从数据源(如数据湖)摄取数据,如以下代码片段所示:
stream_df = (spark.readStream
.format("csv")
.option("header", "true")
.schema(eventSchema)
.option("maxFilesPerTrigger", 1)
.load("/FileStore/shared_uploads/online_retail/"))
在之前的代码中,我们定义了一个流式数据框架,该框架一次从数据湖位置读取一个文件。DataStreamReader对象的readStream()方法用于创建流式数据框架。数据格式指定为 CSV,并且使用eventSchema对象定义了模式信息。最后,通过load()函数指定了数据湖中 CSV 文件的位置。maxFilesPerTrigger选项指定流每次只能读取一个文件。这对于控制流处理速率非常有用,尤其是在计算资源有限的情况下。
一旦我们创建了流式数据框架,它可以使用数据框架 API 中的任何可用函数进行进一步处理,并持久化到流式数据接收器中,例如数据湖。我们将在接下来的章节中介绍这一部分内容。
流式数据接收器
一旦数据流从各自的流式数据源中读取并处理完毕,它们需要存储到某种持久存储中,以供下游进一步消费。尽管任何常规的数据接收器都可以作为流式数据接收器,但在选择流式数据接收器时需要考虑许多因素。以下是一些考虑因素:
-
数据消费的延迟要求是什么?
-
消费者将消费数据流中的哪种类型的数据?
延迟是选择流式数据源、数据接收器和实际流式引擎时的重要因素。根据延迟要求,您可能需要选择完全不同的端到端流式架构。根据延迟要求,流式用例可以分为两大类:
-
实时事务系统
-
接近实时的分析系统
实时事务系统
实时事务系统是操作系统,通常关注于一次处理与单个实体或事务相关的事件。让我们考虑一个在线零售业务的例子,其中一个顾客访问了一个电商网站并在某个会话中浏览了几个产品类别。一个操作系统将专注于捕捉该会话的所有事件,并可能实时向该用户展示折扣券或进行特定推荐。在这种场景下,延迟要求是超低的,通常在亚秒级范围内。这类用例需要一个超低延迟的流式引擎,以及一个超低延迟的流式接收器,例如内存数据库,如Redis或Memcached。
另一个实时事务型用例的例子是 CRM 系统,其中客户服务代表试图向在线客户进行追加销售或交叉销售推荐。在这种情况下,流处理引擎需要从数据存储中获取针对特定客户的某些预先计算好的指标,而数据存储包含关于数百万客户的信息。它还需要从 CRM 系统本身获取一些实时数据点,以生成针对该客户的个性化推荐。所有这些操作都需要在几秒钟内完成。一个CustomerID。
重要提示
Spark 的结构化流处理(Structured Streaming)采用微批次的流处理模型,这对于实时流处理用例并不理想,尤其是在需要超低延迟来处理源头发生的事件时。结构化流处理的设计目标是最大化吞吐量和可扩展性,而非追求超低延迟。Apache Flink 或其他为此目的专门设计的流处理引擎更适合实时事务型用例。
现在,您已经了解了实时分析引擎,并且掌握了一个实时分析用例的示例,在接下来的部分,我们将深入探讨一种处理接近实时分析的更突出的、实际可行的方式。
接近实时分析系统
接近实时的分析系统是那些在接近实时的状态下处理大量记录并具有从几秒钟到几分钟的延迟要求的分析系统。这些系统并不关注单个实体或交易的事件处理,而是为一组交易生成指标或关键绩效指标(KPI),以实时展示业务状态。有时,这些系统也可能为单个交易或实体生成事件会话,但供后续离线使用。
由于这种类型的实时分析系统处理的数据量非常庞大,因此吞吐量和可扩展性至关重要。此外,由于处理后的输出要么被输入到商业智能系统中进行实时报告,要么被存储到持久化存储中以供异步消费,数据湖或数据仓库是此类用例的理想数据接收端。在实时分析行业用例部分,详细介绍了接近实时分析的用例。Apache Spark 的设计旨在处理需要最大吞吐量的大量数据的接近实时分析用例,并具有良好的可扩展性。
现在,您已经理解了流数据源、数据接收端以及 Spark 的结构化流处理更适合解决的实时用例,让我们进一步深入了解实际的流处理引擎。
流处理引擎
流处理引擎是任何实时数据分析系统中最关键的组件。流处理引擎的作用是持续处理来自流数据源的事件,并将其摄取到流数据接收端。流处理引擎可以实时处理传入的事件,或者将事件分组为一个小批量,每次处理一个微批量。
以接近实时的方式进行处理。引擎的选择在很大程度上取决于使用案例的类型和处理延迟的要求。现代流处理引擎的一些例子包括 Apache Storm、Apache Spark、Apache Flink 和 Kafka Streams。
Apache Spark 配备了一个流处理引擎,叫做结构化流处理,该引擎基于 Spark 的 SQL 引擎和 DataFrame API。结构化流处理采用微批量处理方式,将每个传入的小批量数据视为一个小的 Spark DataFrame。它对每个微批量应用 DataFrame 操作,就像对待任何其他 Spark DataFrame 一样。结构化流处理的编程模型将输出数据集视为一个无限制的表,并将传入的事件作为连续微批流进行处理。结构化流处理为每个微批生成查询计划,处理它们,然后将其附加到输出数据集,就像处理一个无限制的表一样,具体请参见下图:
图 4.2 – 结构化流处理编程模型
如前面的图所示,结构化流处理将每个传入的小批量数据视为一个小的 Spark DataFrame,并将其附加到现有的流处理 DataFrame 末尾。关于结构化流处理编程模型的详细解释,并附带示例,已在第二章的实时数据摄取部分中介绍。
结构化流处理可以简单地处理传入的小批量流事件,并将输出持久化到流数据接收端。然而,在实际应用中,由于数据延迟到达,流处理的简单模型可能不太实用。结构化流处理还支持有状态的处理模型,以应对延迟到达或无序的数据。关于如何处理延迟到达数据,您将在处理延迟到达数据部分中学习更多内容。
实时数据消费者
实时数据分析系统的最终组件是实际的数据消费者。数据消费者可以是通过临时的 Spark SQL 查询、交互式操作仪表板或其他系统来消费实时数据的实际业务用户,这些系统会接收流引擎的输出并进一步处理。实时业务仪表板由业务用户使用,通常这些仪表板对延迟的要求较高,因为人类大脑只能在一定的速率下理解数据。结构化流处理(Structured Streaming)非常适合这些用例,可以将流输出写入数据库,并进一步将其提供给商业智能系统。
流引擎的输出还可以被其他业务应用程序消费,例如移动应用或 Web 应用。在这种情况下,使用场景可能是超个性化的用户推荐,其中流引擎的处理输出可以进一步传递给在线推理引擎,用于生成个性化的用户推荐。只要延迟要求在几秒钟到几分钟的范围内,结构化流处理也可以用于这些用例。
总结来说,实时数据分析包含几个重要的组件,比如流数据源和数据接收端、实际的流处理引擎以及最终的实时数据消费者。在架构中,数据源、数据接收端和实际引擎的选择依赖于你的实际实时数据消费者、要解决的用例、处理延迟以及吞吐量要求。接下来,在以下章节中,我们将通过一些现实世界的行业用例来了解如何利用实时数据分析。
实时数据分析行业用例
实时处理数据确实有需求,并且具有优势,因此公司正在迅速从批处理转向实时数据处理。在本节中,我们将通过行业垂直的几个示例来了解实时数据分析。
制造业中的实时预测分析
随着物联网(IoT)的到来,制造业及其他行业从其机器和重型设备中产生了大量的物联网数据。这些数据可以通过几种不同的方式来提升行业的工作方式,帮助它们节省成本。一个这样的例子是预测性维护,其中物联网数据不断从工业设备和机械中获取,应用数据科学和机器学习技术对数据进行分析,以识别可以预测设备或部件故障的模式。当这一过程在实时情况下执行时,可以在故障发生之前预测设备和部件的故障。通过这种方式,可以主动进行维护,防止停机,从而避免任何损失的收入或错过的生产目标。
另一个例子是建筑行业,其中 IoT 数据(如设备正常运行时间、燃料消耗等)可以被分析,以识别任何使用不足的设备,并实时调整设备以实现最佳利用率。
汽车行业中的联网车辆
现代车辆配备了大量的联网功能,显著提高了消费者的生活便利性。车辆遥感技术,以及由这些车辆生成的用户数据,可用于多种应用场景,或进一步为终端用户提供便利功能,如实时个性化车载内容和服务、先进的导航与路线指引以及远程监控。制造商可以利用遥感数据解锁诸如预测车辆维修时间窗或零部件故障,并主动提醒附属供应商和经销商等应用场景。预测零部件故障并更好地管理车辆召回,能帮助汽车制造商节省巨额成本。
财务欺诈检测
现代个人财务正迅速从传统的物理方式转向数字化,并由此带来了诸如欺诈和身份盗窃等数字金融威胁。因此,金融机构需要主动评估数百万笔交易的实时欺诈行为,并向个人消费者发出警告并保护其免受此类欺诈。为了在如此大规模下检测和防止金融欺诈,要求具备高度可扩展、容错性强的实时分析系统。
IT 安全威胁检测
消费电子产品制造商和在线联网设备的公司必须不断监控其终端用户设备中的任何恶意活动,以保障用户身份和资产的安全。监控 PB 级别的数据需要实时分析系统,能够每秒处理数百万条记录。
根据前述的行业应用案例,你可能会注意到实时数据分析日益显得尤为重要。然而,实时数据分析系统并不一定意味着可以完全代替批处理数据的需求。批处理依然是非常必要的,特别是在用静态数据丰富实时数据流、生成为实时数据提供上下文的查找表,以及为实时数据科学和机器学习应用场景生成特征方面。在第二章,《数据摄取》中,你学习了一个可以高效统一批处理和实时处理的架构,称为Lambda 架构。在接下来的部分,你将学习如何结合 Delta Lake 使用结构化流处理进一步简化 Lambda 架构。
使用 Delta Lake 简化 Lambda 架构
一个典型的 Lambda 架构有三个主要组件:批处理层、流处理层和服务层。在 第二章,数据摄取 中,你已经查看了使用 Apache Spark 的统一数据处理框架来实现 Lambda 架构的例子。Spark DataFrames API、Structured Streaming 和 SQL 引擎有助于简化 Lambda 架构。然而,仍然需要多个数据存储层来分别处理批量数据和流数据。这些单独的数据存储层可以通过使用 Spark SQL 引擎作为服务层轻松合并。但这可能仍然会导致数据的多重副本,并且可能需要通过额外的批处理作业进一步整合数据,以便为用户呈现一个一致的集成视图。这个问题可以通过将 Delta Lake 作为 Lambda 架构的持久数据存储层来解决。
由于 Delta Lake 内建了 ACID 事务和写操作隔离特性,它能够提供批量数据和流数据的无缝统一,从而进一步简化 Lambda 架构。如下图所示:
](tos-cn-i-73owjymdk6/2fbd27beb21a4c59bb29f2d56d1fc883)
图 4.3 – 带有 Apache Spark 和 Delta Lake 的 Lambda 架构
在前面的图中,展示了一个简化的 Lambda 架构。在这里,批量数据和流数据分别通过 Apache Spark 的批处理和 Structured Streaming 进行同时处理。将批量数据和流数据同时注入到一个 Delta Lake 表中,大大简化了 Lambda 架构。一旦数据被注入到 Delta Lake 中,它便可以立即用于进一步的下游用例,如通过 Spark SQL 查询进行的临时数据探索、近实时的商业智能报告和仪表盘,以及数据科学和机器学习的用例。由于处理后的数据是持续流入 Delta Lake 的,因此可以以流式和批量方式进行消费:
-
让我们看看如何使用 Apache Spark 和 Delta Lake 来实现这个简化的 Lambda 架构,如以下代码块所示:
retail_batch_df = (spark .read .option("header", "true") .option("inferSchema", "true") .csv("/FileStore/shared_uploads/online_retail/online_retail.csv"))在前面的代码片段中,我们通过使用
read()函数从数据湖中读取存储的 CSV 文件来创建一个 Spark DataFrame。我们指定选项,从半结构化的 CSV 文件中推断出头部和模式。结果是一个名为retail_batch_df的 Spark DataFrame,它指向存储在 CSV 文件中的零售数据的内容和结构。 -
现在,让我们将这些 CSV 数据转换为 Delta Lake 格式,并将其作为 Delta 表存储在数据湖中,如以下代码块所示:
(retail_batch_df .write .mode("overwrite") .format("delta") .option("path", "/tmp/data-lake/online_retail.delta") .saveAsTable("online_retail"))在前面的代码片段中,我们使用
write()函数和saveAsTable()函数将retail_batch_dfSpark DataFrame 保存为数据湖中的 Delta 表。格式指定为delta,并通过path选项指定表的位置。结果是一个名为online_retail的 Delta 表,其数据以 Delta Lake 格式存储在数据湖中。提示
当一个 Spark DataFrame 作为表保存时,指定了位置,则该表被称为外部表。作为最佳实践,建议始终创建外部表,因为即使删除表定义,外部表的数据仍然会被保留。
在前面的代码块中,我们使用 Spark 的批处理进行了数据的初始加载:
-
现在,让我们使用 Spark 的结构化流处理将一些增量数据加载到之前定义的同一个 Delta 表中,名为
online_retail。这一过程在以下代码块中有所展示:retail_stream_df = (spark .readStream .schema(retailSchema) .csv("/FileStore/shared_uploads/online_retail/"))在前面的代码片段中,我们使用
readStream()函数以流的方式读取存储在数据湖中的一组 CSV 文件。结构化流处理要求在读取数据时必须提前指定数据的模式,这可以通过schema选项来提供。结果是一个名为retail_stream_df的结构化流处理 DataFrame。 -
现在,让我们将这一数据流注入到之前在初始加载时创建的同一个 Delta 表中,名为
online_retail。这个过程展示在以下代码块中:(retail_stream_df .writeStream .outputMode("append") .format("delta") .option("checkpointLocation", "/tmp/data-lake/online_retail.delta/") .start("/tmp/data-lake/online_retail.delta"))在前面的代码块中,结构化流处理的
retail_stream_dfDataFrame 被加载到名为online_retail的现有 Delta 表中,使用的是结构化流的writeStream()函数。outputMode选项指定为append。这是因为我们希望将新数据持续追加到现有的 Delta 表中。由于结构化流处理保证必须指定checkpointLocation,以便在发生故障或流处理重新启动时,能够跟踪处理数据的进度,并从中断点精确恢复。注意
Delta 表将所有必需的模式信息存储在 Delta 事务日志中。这使得将 Delta 表注册到元数据存储(metastore)成为完全可选的,只有在通过外部工具或 Spark SQL 访问 Delta 表时,才需要进行注册。
从之前的代码块中,你可以看到,Spark 的统一批处理和流处理的结合,已经通过使用单一的统一分析引擎简化了 Lambda 架构。随着 Delta Lake 事务和隔离特性以及批处理和流处理统一性的加入,你的 Lambda 架构可以进一步简化,提供一个强大且可扩展的平台,让你能够在几秒钟到几分钟内访问最新的数据。一个流数据摄取的显著用例是,在数据湖中维护源事务系统数据的副本。该副本应包括源系统中发生的所有删除、更新和插入操作。通常,这个用例被称为 CDC,并遵循类似本节描述的模式。在接下来的部分,我们将深入探讨如何使用 Apache Spark 和 Delta Lake 实现 CDC。
数据变更捕捉(Change Data Capture)
一般来说,操作系统不会长期保留历史数据。因此,必须在数据湖中维护事务系统数据的精确副本,并保留其历史记录。这有几个优点,包括为你提供所有事务数据的历史审计日志。此外,这一大量的数据可以帮助你解锁新的商业用例和数据模式,推动业务迈向更高的水平。
在数据湖中维护事务系统的精确副本意味着捕获源系统中发生的每一笔交易的所有变更,并将其复制到数据湖中。这个过程通常被称为 CDC。CDC 不仅要求你捕获所有的新交易并将其追加到数据湖中,还要捕获源系统中对交易的任何删除或更新。这在数据湖中并非易事,因为数据湖通常不支持更新或删除任意记录。然而,通过 Delta Lake 完全支持插入、更新和删除任意数量的记录,CDC 在数据湖上成为可能。此外,Apache Spark 和 Delta Lake 的结合使得架构变得简单。
让我们实现一个使用 Apache Spark 和 Delta Lake 的 CDC 过程,如下一个代码块所示:
(spark
.read
.option("header", "true")
.option("inferSchema", "true")
.csv("/FileStore/shared_uploads/online_retail/online_retail.csv")
.write
.mode("overwrite")
.format("delta")
.option("path", "/tmp/data-lake/online_retail.delta")
.saveAsTable("online_retail"))
在前面的代码片段中,我们使用 Spark 的批处理处理初始加载一组静态数据到 Delta 表中。我们简单地使用 Spark DataFrame 的 read() 函数读取一组静态的 CSV 文件,并使用 saveAsTable() 函数将其保存到 Delta 表中。这里,我们使用 path 选项将表定义为外部表。结果是一个包含源表初始静态数据的 Delta 表。
这里的问题是,如何将来自操作系统(通常是关系型数据库管理系统 RDBMS)的事务数据最终转化为数据湖中的一组文本文件?答案是使用专门的工具集,这些工具专门用于从操作系统读取 CDC 数据,并将其转换并暂存到数据湖、消息队列或其他数据库中。像 Oracle 的 Golden Gate 和 AWS 数据库迁移服务就是此类 CDC 工具的一些示例。
注意
Apache Spark 可以处理 CDC 数据并将其无缝地导入 Delta Lake;然而,它不适合构建端到端的 CDC 流水线,包括从操作源加载数据。专门为此目的构建的开源和专有工具,如 StreamSets、Fivetran、Apache Nifi 等,可以帮助完成这一工作。
现在,我们已经将一组静态的事务数据加载到 Delta 表中,让我们将一些实时数据加载到同一个 Delta 表中,代码如下所示:
retail_stream_df = (spark
.readStream
.schema(retailSchema)
.csv("/FileStore/shared_uploads/online_retail/"))
在前面的代码片段中,我们从数据湖中的一个位置定义了一个流式 DataFrame。这里的假设是一个第三方 CDC 工具正在不断地将包含最新事务数据的新文件添加到数据湖中的该位置。
现在,我们可以将变更数据合并到现有的 Delta 表中,如下代码所示:
from delta.tables import *
deltaTable = DeltaTable.forPath(spark, "/tmp/data-lake/online_retail.delta")
def upsertToDelta(microBatchOutputDF, batchId):
deltaTable.alias("a").merge(
microBatchOutputDF.dropDuplicates(["InvoiceNo", "InvoiceDate"]).alias("b"),
"a.InvoiceNo = b.InvoiceNo and a.InvoiceDate = b.InvoiceDate") \
.whenMatchedUpdateAll() \
.whenNotMatchedInsertAll() \
.execute()
在前面的代码块中,发生了以下操作:
-
我们使用 Delta Lake 位置和
DeltaTable.forPath()函数重新定义现有 Delta 表的定义。结果是指向 Spark 内存中 Delta 表的指针,命名为deltaTable。 -
然后,我们定义了一个名为
upsertToDelta()的函数,它执行实际的merge或upsert操作,将数据合并到现有的 Delta 表中。 -
现有的 Delta 表被使用字母
a作为别名,包含来自每个流式微批的最新更新的 Spark DataFrame 被别名为字母b。 -
从流式微批处理传入的更新可能实际上包含重复数据。重复的原因是,在数据到达结构化流处理(Structured Streaming)时,某个给定的事务可能已经经历了多次更新。因此,在将数据合并到 Delta 表之前,需要对流式微批处理数据进行去重。通过在流式微批处理 DataFrame 上应用
dropDuplicates()函数来实现去重。 -
然后,流式更新通过在现有 Delta 表上应用
merge()函数将其合并到 Delta 表中。对两个 DataFrame 的关键列应用相等条件,并使用whenMatchedUpdateAll()函数将所有与流式微批更新匹配的记录更新到现有的 Delta 表中。 -
来自流式微批处理的任何记录,如果尚未存在于目标 Delta 表中,将通过
whenNotMatchedInsertAll()函数进行插入。注意
需要对以微批次形式到达的流式更新进行去重,因为在我们的流处理任务实际处理数据时,某个事务可能已经经历了多次更新。业界的常见做法是基于键列和最新时间戳选择每个事务的最新更新。如果源表中没有这样的时间戳列,大多数 CDC 工具具备扫描记录的功能,按创建或更新的正确顺序插入它们自己的时间戳列。
通过使用一个简单的merge()函数,可以轻松地将变更数据合并到存储在任何数据湖中的现有 Delta 表中。这一功能大大简化了实时分析系统中实现 CDC 场景的架构复杂性。
重要提示
对于 CDC 场景,确保事件按其在源端创建的准确顺序到达至关重要。例如,删除操作不能在插入操作之前执行,否则会导致数据错误。某些消息队列无法保持事件到达队列时的顺序,因此在处理时应特别注意保持事件的顺序。
在幕后,Spark 会自动扩展合并过程,使其能够处理 PB 级别的数据。通过这种方式,Delta Lake 将类似数据仓库的功能引入到本来并未设计用于处理分析类用例的基于云的数据湖中。
提示
随着目标 Delta 表中数据量的增加,Delta 合并可能会逐渐变慢。通过使用合适的数据分区方案,并在合并子句中指定数据分区列,可以提高 Delta 合并的性能。这样,Delta 合并只会选择那些确实需要更新的分区,从而大大提高合并性能。
另一个在实时流分析场景中独特的现象是延迟到达的数据。当某个事件或事件更新比预期稍晚到达流处理引擎时,就称为延迟到达的数据。一个强大的流处理引擎需要能够处理延迟到达的数据或乱序到达的数据。在接下来的部分,我们将更详细地探讨如何处理延迟到达的数据。
处理延迟到达的数据
延迟到达的数据是实时流分析中的一种特殊情况,其中与同一事务相关的事件未能及时到达以便一起处理,或者它们在处理时是乱序到达的。结构化流处理支持有状态流处理来处理此类场景。我们接下来将进一步探讨这些概念。
使用窗口和水印的有状态流处理
假设我们考虑一个在线零售交易的例子,用户正在浏览电子零售商网站。我们希望根据以下两种事件之一计算用户会话:用户退出电子零售商门户或发生超时。另一个例子是用户下订单后又更新订单,由于网络或其他延迟,我们首先接收到更新事件,然后才接收到原始订单创建事件。在这种情况下,我们希望等待接收任何迟到或乱序的数据,然后再将数据保存到最终存储位置。
在前面提到的两种场景中,流引擎需要能够存储和管理与每个事务相关的某些状态信息,以便处理迟到的数据。Spark 的结构化流处理可以通过使用窗口化概念实现有状态处理,从而自动处理迟到的数据。
在深入探讨结构化流处理中的窗口化概念之前,您需要理解事件时间(event time)的概念。事件时间是指事务事件在源端生成时的时间戳。例如,订单创建事件的事件时间就是订单下单的时间戳。同样,如果同一事务在源端进行了更新,则更新的时间戳成为该事务更新事件的事件时间。事件时间是任何有状态处理引擎中的一个重要参数,用于确定哪个事件先发生。
使用窗口化(windowing)时,结构化流处理(Structured Streaming)会为每个键维护一个状态,并在相同键的新的事件到达时更新该键的状态,如下图所示:
图 4.4 – 有状态流处理
在上面的示意图中,我们有一个订单放置的事务事件流。O1、O2和O3分别表示订单号,而T、T+03等则表示订单创建的时间戳。输入流有一个稳定的订单相关事件生成流。我们定义了一个持续10分钟的有状态窗口,并且每5分钟滑动一次窗口。我们在窗口中想要实现的是更新每个唯一订单的计数。如你所见,在每个5分钟的间隔内,同一订单的任何新事件都会更新计数。这个简单的示意图描述了有状态处理在流处理场景中的工作原理。
然而,这种类型的状态处理有一个问题;即状态似乎被永久维护,随着时间的推移,状态数据本身可能变得过大,无法适应集群内存。永久维护状态也不现实,因为实际场景中很少需要长期维护状态。因此,我们需要一种机制来在一定时间后使状态过期。结构化流处理具有定义水印的能力,水印控制每个键的状态维护时间,一旦水印过期,系统将删除该键的状态。
注意
尽管定义了水印,状态可能仍然会变得非常大,并且结构化流处理有能力在需要时将状态数据溢出到执行器的本地磁盘。结构化流处理还可以配置使用外部状态存储,例如 RocksDB,以维护数百万个键的状态数据。
以下代码块展示了使用 Spark 的结构化流式处理,通过事件时间、窗口函数和水印函数进行任意状态处理的实现细节:
-
让我们通过将
InvoiceDate列从StringType转换为TimestampType来实现InvoiceTime的概念。 -
接下来,我们将在
raw_stream_df流式数据框上执行一些状态处理操作,通过在其上定义窗口函数和水印函数,如下所示的代码块:aggregated_df = ( raw_stream_df.withWatermark("InvoiceTime", "1 minutes") .groupBy("InvoiceNo", window("InvoiceDate", "30 seconds", "10 seconds", "0 seconds")) .agg(max("InvoiceDate").alias("event_time"), count("InvoiceNo").alias("order_count")) )从前面的代码片段可以得出以下观察结论:
-
我们在
raw_stream_df流式数据框上定义了一个水印,持续时间为1分钟。这意味着结构化流处理应当为每个键维护一个状态,仅持续1分钟。水印的持续时间完全取决于你的用例以及数据预期到达的延迟时间。 -
我们在键列
InvoiceNo上定义了一个分组函数,并为我们的状态操作定义了所需的窗口,窗口大小为30秒,滑动窗口为每10秒一次。这意味着我们的键将在初始的30秒窗口后,每10秒进行一次聚合。 -
我们定义了聚合函数,其中对时间戳列使用
max函数,对键列使用count函数。 -
一旦水印过期,流处理过程会立即将数据写入流式接收器。
-
-
一旦使用窗口函数和水印函数定义了状态流,我们可以快速验证流是否按预期工作,如下所示的代码片段所示:
(aggregated_df .writeStream .queryName("aggregated_df") .format("memory") .outputMode("complete") .start())上述代码块将状态处理流式数据框的输出写入内存接收器,并指定了一个
queryName属性。该流被注册为一个内存表,使用指定的查询名称,可以通过 Spark SQL 轻松查询,以便快速验证代码的正确性。
通过利用结构化流式处理提供的窗口功能和水印功能,可以实现有状态的流处理,并且可以轻松处理迟到的数据。在本章之前所有的代码示例中,另一个需要注意的方面是流数据如何逐步从原始状态转化为处理后的状态,再进一步转化为聚合后的状态。这种使用多个流式过程逐步转化数据的方法通常被称为多跳架构。在接下来的部分,我们将进一步探讨这种方法。
多跳管道
多跳管道是一种架构,用于构建一系列链式连接的流式作业,使得管道中的每个作业处理数据并逐步提升数据的质量。一个典型的数据分析管道包括多个阶段,包括数据摄取、数据清洗与整合、数据聚合等。随后,它还包括数据科学和机器学习相关的步骤,如特征工程、机器学习训练和评分。这个过程逐步提高数据质量,直到它最终准备好供终端用户使用。
使用结构化流式处理,所有这些数据分析管道的阶段可以被链式连接成一个有向无环图(DAG)的流式作业。通过这种方式,新的原始数据持续进入管道的一端,并通过管道的每个阶段逐步处理。最终,经过处理的数据从管道的尾端输出,准备供终端用户使用。以下是一个典型的多跳架构:
图 4.5 – 多跳管道架构
上面的图示代表了一个多跳管道架构,其中原始数据被摄取到数据湖中,并通过数据分析管道的每个阶段进行处理,从而逐步提高数据的质量,直到最终准备好供终端用户使用。终端用户的使用场景可能是商业智能与报告,或者进一步处理为预测分析的使用场景,利用数据科学和机器学习技术。
虽然这看起来是一个简单的架构实现,但为了无缝实现多跳管道,必须满足一些关键的前提条件,以避免频繁的开发者干预。前提条件如下:
-
为了使管道的各个阶段能够无缝连接,数据处理引擎需要支持“恰好一次”数据处理保证,并且能够在故障发生时对数据丢失具有恢复能力。
-
数据处理引擎需要具备维护水印数据的能力。这样可以确保它能够在给定的时间点了解数据处理的进度,并且能够无缝地接收以流式方式到达的新数据并进行处理。
-
底层数据存储层需要支持事务性和隔离性保障,以便在作业失败时,无需开发人员干预处理任何错误或不正确的数据清理。
Apache Spark 的结构化流式处理解决了前面提到的1和2问题,因为它保证了精确一次的数据处理语义,并且内建支持检查点。这是为了跟踪数据处理进度,并帮助在作业失败后从停止的地方重新启动。3问题由 Delta Lake 提供支持,其提供 ACID 事务保障,并支持同时进行批处理和流式作业。
-
让我们实现一个多跳管道示例,使用结构化流式处理和 Delta Lake,如下面的代码块所示:
raw_stream_df = (spark .readStream .schema(retailSchema) .option("header", True) .csv("/FileStore/shared_uploads/online_retail/")) (raw_stream_df .writeStream .format("delta") .option("checkpointLocation", "/tmp/delta/raw_stream.delta/checkpoint") .start("/tmp/delta/raw_stream.delta/"))在前面的代码块中,我们通过将源数据从其原始格式摄取到 Delta Lake 格式的数据湖中,创建了一个原始流式 DataFrame。
checkpointLocation为流式作业提供了容错性,而 Delta Lake 作为目标位置则为write操作提供了事务性和隔离性保障。 -
现在,我们可以使用另一个作业进一步处理原始摄取的数据,进一步提高数据质量,如下面的代码块所示:
integrated_stream_df = (raw_stream_df .withColumn("InvoiceTime", to_timestamp("InvoiceDate", 'dd/M/yy HH:mm'))) (integrated_stream_df .writeStream .format("delta") .option("checkpointLocation", "/tmp/delta/int_stream.delta/checkpoint") .start("/tmp/delta/int_stream.delta/"))在前面的代码块中,我们将一个字符串列转换为时间戳列,并将清理后的数据持久化到 Delta Lake。这是我们多跳管道的第二阶段,通常,这个阶段从前一阶段的原始数据摄取所生成的 Delta 表中读取数据。同样,这里使用的检查点位置有助于执行数据的增量处理,并在新的记录到达时处理添加到原始 Delta 表中的数据。
-
现在我们可以定义管道的最终阶段,在这个阶段,我们将数据汇总为高度摘要的数据,准备供最终用户消费,如下面的代码片段所示:
aggregated_stream_df = (integrated_stream_df .withWatermark("InvoiceTime", "1 minutes") .groupBy("InvoiceNo", window("InvoiceTime", "30 seconds", "10 seconds", "0 seconds")) .agg(max("InvoiceTime").alias("event_time"), count("InvoiceNo").alias("order_count"))) (aggregated_stream_df .writeStream .format("delta") .option("checkpointLocation", "/tmp/delta/agg_stream.delta/checkpoint") .start("/tmp/delta/agg_stream.delta/"))在前面的代码块中,已集成并清理过的数据被汇总成最高级别的摘要数据。这个数据可以进一步供商业智能或数据科学与机器学习使用。管道的这一阶段也使用了检查点位置和 Delta 表,以确保作业失败时的容错性,并跟踪到达时需要处理的新数据。
因此,通过结合 Apache Spark 的结构化流和 Delta Lake,实现多跳架构变得无缝且高效。多跳架构的不同阶段可以实现为一个包含多个流处理过程的单一整体作业。作为最佳实践,管道中每个阶段的单独流处理过程被拆分为多个独立的流作业,这些作业可以通过外部调度器(如 Apache Airflow)进一步链接成一个 DAG。后者的优点在于更易于维护各个流作业,并且在需要更新或升级管道的某个阶段时,可以最大限度地减少整个管道的停机时间。
总结
本章介绍了实时数据分析系统的需求以及它们在向业务用户提供最新数据、帮助企业提高市场响应速度并最小化任何机会损失方面的优势。展示了典型实时分析系统的架构,并描述了主要组件。还展示了一个使用 Apache Spark 结构化流的实时分析架构。描述了实时数据分析的几个突出行业应用案例。此外,还介绍了一个简化的 Lambda 架构,使用结构化流和 Delta Lake 的组合。介绍了 CDC 的应用案例,包括其要求和好处,并展示了如何利用结构化流实现 CDC 用例的技术。
最后,你学习了一种通过多跳管道逐步改善数据质量的技术,从数据摄取到高度聚合和汇总的数据,几乎实时完成。你还研究了使用结构化流和 Delta Lake 强大组合实现的多跳管道的简单实现。
本书的数据工程部分到此结束。你迄今为止学到的技能将帮助你开始数据分析之旅,从操作源系统的原始事务数据开始,摄取到数据湖中,进行数据清洗和整合。此外,你应该熟悉构建端到端的数据分析管道,这些管道能够以实时流的方式逐步提高数据质量,并最终生成可以供商业智能和报告使用的、清晰且高度聚合的数据。
在接下来的章节中,你将基于迄今为止学到的数据工程概念,深入探索利用 Apache Spark 的数据科学和机器学习功能进行预测分析的领域。在下一章中,我们将从探索性数据分析和特征工程的概念开始。
第二部分:数据科学
一旦我们在数据湖中获得了清洗后的数据,就可以开始对历史数据进行数据科学和机器学习处理。本节帮助你理解可扩展机器学习的重要性和需求。本节的各个章节展示了如何使用 PySpark 以可扩展和分布式的方式进行探索性数据分析、特征工程和机器学习模型训练。本节还介绍了 MLflow,一个开源的机器学习生命周期管理工具,适用于跟踪机器学习实验和生产化机器学习模型。本节还向你介绍了一些基于标准 Python 扩展单机机器学习库的技术。
本节包括以下章节:
第五章*,使用 PySpark 进行可扩展机器学习*
第六章*,特征工程 – 提取、转换与选择*
第七章*,有监督机器学习*
第八章*,无监督机器学习*
第九章*,机器学习生命周期管理*
第十章*,使用 PySpark 扩展单节点机器学习*
第五章:使用 PySpark 进行可扩展机器学习
在前几章中,我们已经建立了现代数据以惊人的速度增长,且其体量、速度和准确性是传统系统无法跟上的。因此,我们学习了分布式计算,以跟上日益增长的数据处理需求,并通过实际案例了解如何摄取、清洗和整合数据,将其处理到适合商业分析的水平,充分利用 Apache Spark 统一的数据分析平台的强大功能和易用性。本章及后续章节将探索数据科学和机器学习(ML)在数据分析中的应用。
如今,人工智能(AI)和机器学习(ML)计算机科学领域正经历大规模复兴,并且无处不在。各行各业的企业都需要利用这些技术来保持竞争力,扩大客户群,推出新产品线,并保持盈利。然而,传统的机器学习和数据科学技术是为了处理有限的数据样本而设计的,本身并不具备扩展性。
本章为你提供了传统机器学习算法的概述,包括有监督和无监督的 ML 技术,并探索了机器学习在商业应用中的实际案例。接着,你将了解可扩展机器学习的必要性。将介绍一些以分布式方式扩展 ML 算法、处理非常大的数据样本的技术。然后,我们将深入探讨 Apache Spark 的 ML 库——MLlib,并结合代码示例,使用 Apache Spark 的 MLlib 进行数据整理,探索、清洗并操作数据,为机器学习应用做好准备。
本章涵盖以下主要内容:
-
机器学习概述
-
扩展机器学习
-
使用 Apache Spark 和 MLlib 进行数据整理
到本章结束时,你将会对可扩展的机器学习(ML)及其商业应用有所了解,并掌握 Apache Spark 的可扩展 ML 库——MLlib 的基本知识。你将掌握使用 MLlib 清洗和转换数据的技能,为大规模机器学习应用做准备,帮助你减少数据清洗任务所需的时间,使你的整体 ML 生命周期更加高效。
技术要求
在本章中,我们将使用 Databricks Community Edition 来运行我们的代码:community.cloud.databricks.com。
-
注册说明可以在
databricks.com/try-databricks找到。 -
本章使用的代码和数据可以从
github.com/PacktPublishing/Essential-PySpark-for-Scalable-Data-Analytics/tree/main/Chapter05下载。
机器学习概述
机器学习是人工智能和计算机科学的一个领域,利用统计模型和计算机算法学习数据中固有的模式,而无需显式编程。机器学习由能够自动将数据中的模式转换为模型的算法组成。当纯数学或基于规则的模型一遍又一遍地执行相同任务时,机器学习模型则从数据中学习,并且通过暴露于大量数据,性能可以大大提高。
一个典型的机器学习过程涉及将机器学习算法应用于已知数据集(称为训练数据集),以生成新的机器学习模型。这个过程通常被称为模型训练或模型拟合。一些机器学习模型是在包含已知正确答案的数据集上进行训练的,目的是在未知数据集中预测这些答案。训练数据集中已知的正确值称为标签。
一旦模型训练完成,生成的模型将应用于新数据以预测所需的值。这个过程通常被称为模型推断或模型评分。
提示
与其训练单一模型,最佳做法是使用不同的模型参数(称为超参数)训练多个模型,并根据定义明确的准确性指标从所有训练过的模型中选择最佳模型。这个基于不同参数训练多个模型的过程通常被称为超参数调优或交叉验证。
机器学习算法的示例包括分类、回归、聚类、协同过滤和降维。
机器学习算法的类型
机器学习算法可以分为三大类,即监督学习、无监督学习和强化学习,以下部分将详细讨论这些内容。
监督学习
监督学习是一种机器学习方法,其中模型在已知标签的训练数据集上进行训练。该标签是在训练数据集中标记的,并代表我们尝试解决问题的正确答案。我们进行监督学习的目的是在模型在已知数据集上经过训练后,预测未知数据集中的标签。
监督学习算法的示例包括线性回归、逻辑回归、朴素贝叶斯分类器、K 近邻、决策树、随机森林、梯度提升树和支持向量机。
监督学习可以分为两大类,即回归问题和分类问题。回归问题涉及预测一个未知标签,而分类问题则尝试将训练数据集分类到已知类别中。使用Apache Spark MLlib进行监督学习的详细实现将在第六章,“监督学习”中介绍。
无监督学习
无监督学习是机器学习的一种类型,其中训练数据对算法是未知的,且没有提前标注正确答案。无监督学习涉及学习一个未知、未标注的数据集的结构,没有用户的任何指导。在这种情况下,机器的任务是根据某些相似性或差异将数据分组或归类,而无需任何预先的训练。
无监督学习可以进一步分为聚类和关联问题。聚类问题涉及发现训练数据集中的类别,而关联问题则涉及发现数据中描述实体关系的规则。无监督学习的例子包括 K-means 聚类和协同过滤。无监督学习将在第七章中详细探讨,无监督机器学习,并通过 Apache Spark MLlib 提供编码示例。
强化学习
强化学习被软件系统和机器用于在给定情境下找到最优的行为或路径。与已经在训练数据集中包含正确答案的监督学习不同,在强化学习中没有固定答案,强化学习代理通过反复试验来决定结果,并且旨在从经验中学习。强化学习代理会根据选择的路径获得奖励或受到惩罚,目标是最大化奖励。
强化学习应用于自驾车、机器人技术、工业自动化以及用于聊天机器人代理的自然语言处理等领域。Apache Spark MLlib 中没有现成的强化学习实现,因此深入探讨这一概念超出了本书的范围。
注意
数据科学和机器学习的另一个分支是深度学习,它利用了先进的机器学习技术,如神经网络,这些技术近年来也变得非常突出。虽然 Apache Spark 确实支持某些深度学习算法,但这些概念过于先进,无法在本书的范围内涉及。
机器学习的业务应用案例
到目前为止,我们讨论了机器学习的不同类别,并简要介绍了机器学习模型可以执行的任务。在本节中,您将了解一些机器学习算法在现实生活中如何帮助解决不同行业的实际商业问题。
客户流失预防
使用机器学习建立客户流失模型对于识别那些可能停止与您的业务互动的客户非常有用,并且还可以帮助您深入了解导致客户流失的因素。流失模型可以简单地是一个回归模型,用于估算每个个体的风险评分。客户流失模型可以帮助企业识别面临流失风险的客户,从而实施客户保持策略。
客户终生价值建模
零售企业的收入大部分来自少数高价值客户,这些客户带来了重复购买。客户终生价值模型可以估算一个客户的生命周期,即客户可能流失的时期。它们还可以预测一个客户在其生命周期内可能带来的总收入。因此,估算潜在高价值客户在其生命周期内可能带来的收入,对于重新分配营销资金来吸引和留住这些客户至关重要。
需求预测
实体店和在线商店都有有限的实际存储空间,无论是在商店内还是在仓库里。因此,如何将这些有限的存储空间填充上实际需求的产品是非常重要的。您可以基于季节性和每年的月份开发一个简单的模型。然而,建立一个更复杂的机器学习模型,不仅包括季节性和历史数据,还包括外部数据,例如社交媒体上的当前趋势、天气预报数据和社交媒体上的客户情绪,可能会导致更准确的需求预测,并因此帮助最大化收入。
运输交货时间预测
任何涉及配送和物流操作的企业,无论是在线零售商还是食品配送平台,都需要能够估算订单送达客户所需的时间。通常,运输的交货时间是客户在选择是否与您合作时做决策的一个关键因素,客户可能会因此选择与竞争对手合作。回归模型可以根据产品的起始地和目的地、天气及其他季节性数据准确估算产品交付到客户邮政编码所需的时间。
市场篮分析
市场篮分析是一种根据客户购物篮中已有的商品向其推荐其他产品的技术。通过利用协同过滤算法,机器学习(ML)可以发现产品类别之间的关联规则,从而根据客户购物车中的商品和过去的购买记录向在线客户推荐产品。这是几乎所有电子零售商常用的一个重要应用。
财务欺诈检测
机器学习具备从数据中检测模式的固有能力。因此,可以利用机器学习构建能够检测财务交易异常的模型,以标记某些交易为欺诈行为。传统上,金融机构已经在利用基于规则的模型进行欺诈检测,但将机器学习模型纳入其中会使欺诈检测模型更加有效,从而帮助发现新的欺诈模式。
使用自然语言处理进行信息提取
制药公司和产生大量知识的企业面临着一个特定于其行业的独特挑战。在拥有数万名员工的组织中,尝试识别某个知识点是否已经由另一个小组创建,并非一件简单的事情。机器学习的自然语言处理技术可以用来整理、分组、分类和标注大量文档,从而使用户能够轻松搜索是否已有类似的知识点存在。
到目前为止,你已经了解了机器学习的基本知识、不同类型的机器学习算法以及它们在实际商业案例中的应用。在接下来的部分中,我们将讨论可扩展机器学习的需求,以及扩展机器学习算法的一些技术,并介绍 Apache Spark 的原生可扩展机器学习库MLlib及其在数据整理中的应用。
扩展机器学习
在前面的章节中,我们了解到,机器学习是一套算法,而不是显式编程,它能够自动学习数据中隐藏的模式。因此,暴露给更大数据集的机器学习算法,可能会导致更好的模型表现。然而,传统的机器学习算法设计是基于有限的数据样本并在单台机器上训练的。这意味着现有的机器学习库本身并不具备扩展性。解决这一问题的一个方法是将较大的数据集下采样,以适应单台机器的内存,但这也可能意味着最终得到的模型不如可能的最优模型准确。
此外,通常会在同一数据集上构建多个机器学习模型,仅仅是改变提供给算法的参数。在这些模型中,选择最优的模型用于生产环境,这个过程叫做超参数调优。在单台机器上按顺序构建多个模型,需要很长时间才能得到最佳模型,这导致生产周期更长,从而也增加了推向市场的时间。
鉴于传统机器学习算法的可扩展性挑战,迫切需要扩展现有的机器学习算法或开发新的可扩展机器学习算法。我们将在接下来的部分中探索一些扩展机器学习算法的技术。
扩展机器学习的技术
以下部分将介绍两种扩展机器学习算法的主要技术。
令人尴尬的并行处理
令人尴尬的并行处理是一种并行计算技术,在这种技术中,几乎不需要任何努力就可以将给定的计算问题分解成较小的并行任务。当并行化的任务之间没有任何相互依赖,且所有任务都可以完全独立执行时,这种方式就可以实现。
现在,让我们尝试将这个方法应用到在非常大的数据集上扩展单机机器学习算法的问题上,乍一看,这似乎不是一个简单的任务。然而,考虑到超参数调优或交叉验证的问题,我们可以运行多个并行模型,每个模型都有不同的参数,但它们都可以在一个单机的内存中处理相同的小数据集。在这种情况下,我们可以通过调整模型参数轻松地在相同的数据集上训练多个模型。因此,通过利用令人尴尬的并行处理技术,我们可以将模型构建过程加速数个数量级,帮助我们在数小时内而非数周或数月内找到最优的模型,从而加速你的业务价值实现。你将进一步了解如何在第十章**中应用这一技术,使用 PySpark 扩展单节点机器学习。
可扩展的机器学习算法
尽管令人尴尬的并行计算技术帮助我们在更短时间内得到更好的模型,并提高了准确性,但它仍然受限于较小的数据集大小。这意味着我们可能会因为数据的下采样而错失潜在的数据模式。为了解决这个问题,我们需要能够天然扩展至多个机器的机器学习算法,并能够在分布式的方式下训练非常大的数据集。Apache Spark 的原生 ML 库,名为 MLlib,包含了这些本质上可扩展的机器学习算法,接下来的部分我们将进一步探讨 MLlib。
Apache Spark 的 ML 库简介
MLlib 是 Apache Spark 的原生机器学习库。作为一个原生库,MLlib 与 Spark 的其他 API 和库紧密集成,包括 Spark SQL 引擎、DataFrame API、Spark SQL API,甚至结构化流处理。这个特性使得 Apache Spark 成为一个真正统一的数据分析平台,可以执行所有与数据分析相关的任务,从数据摄取到数据转换,再到数据的临时分析、构建复杂的机器学习模型,甚至将这些模型应用于生产环境中。在接下来的部分,你将更深入地了解 Spark MLlib 及其核心组件。
Spark MLlib 概览
在 Apache Spark 的早期版本中,MLlib 基于 Spark 的 RDD API。自 Spark 2.0 版本开始,推出了一个基于 DataFrame API 的新 ML 库。现在,在 Spark 3.0 及更高版本中,基于 DataFrame API 的 MLlib 是标准,而旧的基于 RDD 的 MLlib 处于维护模式,未来不会再进行扩展。
基于 DataFrame 的 MLlib 与传统的基于 Python 的单机 ML 库(如 scikit-learn)高度相似,包含三个主要组件,分别是转换器、估算器和管道,具体内容将在接下来的章节中介绍。
转换器
转换器 是一种算法,它接受一个 DataFrame 作为输入,对 DataFrame 列进行处理,并返回另一个 DataFrame。使用 Spark MLlib 训练的 ML 模型是一个转换器,它接受一个原始 DataFrame,并返回一个包含原始数据和新预测列的 DataFrame。典型的转换器管道如下图所示:
图 5.1 – 一个转换器管道
在前面的图中,展示了一个典型的转换器管道,其中一系列的转换器阶段,包括 VectorIndexer 和已训练的 线性回归模型,被应用到原始的 DataFrame。结果是一个新的 DataFrame,包含了所有原始列,并新增了包含预测值的新列。
注
转换操作与 Spark 的 MLlib 中的转换器是不同的概念。虽然两者都将一个 DataFrame 转换为另一个 DataFrame,并且都是惰性计算,但前者是对 DataFrame 执行的操作,而后者则是一个实际的 ML 算法。
估算器
估算器是另一种算法,它接受一个 DataFrame 作为输入,并生成一个转换器。任何 ML 算法都是一个估算器,因为它将包含原始数据的 DataFrame 转换为包含实际预测结果的 DataFrame。估算器管道在下图中描述:
图 5.2 – 一个估算器管道
在上图中,首先将 Transformer 应用于一个包含原始数据的 DataFrame,生成一个 特征向量 DataFrame。然后,将 估算器(以 线性回归 算法 的形式)应用于包含 特征向量 的 DataFrame,生成一个新的 线性回归模型,该模型作为 Transformer 返回。
注
特征向量是 Spark MLlib 库中的一种特殊数据结构。它是一个 DataFrame 列,包含实际的浮动点类型向量对象。由于 ML 基于数学和统计学,所有 ML 算法只对浮动点值的向量进行操作。原始数据通过特征提取和特征工程技术转换为特征向量。
管道
Spark MLlib 中的 ML 管道将多个转换器和估算器的阶段链在一起,形成一个执行端到端 ML 操作的有向无环图(DAG),该操作从数据清理到特征工程,再到实际的模型训练。一个管道可以是仅包含转换器的管道,也可以是仅包含估算器的管道,或者两者的结合。
使用 Spark MLlib 中的可用转换器和估算器,可以构建一个完整的端到端机器学习(ML)管道。一个典型的 ML 管道由多个阶段组成,从数据清理、特征工程、模型训练到模型推理。你将在接下来的章节中学习更多关于数据清理的技术。
使用 Apache Spark 和 MLlib 进行数据清理
数据清理,在数据科学社区中也称为 数据清洗 或简而言之 数据准备,是典型数据科学过程中的第一步。数据清理涉及采样、探索、选择、操作和清洗数据,以使其准备好进行机器学习应用。数据清理占整个数据科学过程的 60% 到 80%,是确保构建的 ML 模型准确性的最关键步骤。接下来的章节将使用 Apache Spark 和 MLlib 探讨数据清理过程。
数据预处理
数据预处理是数据清理过程中的第一步,涉及收集、探索和选择对于解决当前问题有用的数据元素。数据科学过程通常继承数据工程过程,假设数据湖中已经存在干净和集成的数据。然而,足够干净的数据可能对于商业智能(BI)来说是合适的,但可能并不适合数据科学应用。同时,数据科学应用需要额外的数据集,这些数据集可能对其他分析用例无用,因此可能尚未清理。
在开始操作和清理数据之前,我们需要将其加载到 Spark DataFrame 中,并探索数据以了解其结构。以下代码示例将使用在 第三章《数据清理与集成》末尾产生的集成数据集,名为 retail_silver.delta:
raw_data = spark.read.format("delta").load("dbfs:/FileStore/shared_uploads/delta/retail_silver.delta")
raw_data.printSchema()
(select_data = raw_data.select("invoice_num", "stock_code",
"quantity", "invoice_date",
"unit_price","country_code",
"age", "work_class",
"final_weight")
select_data.describe().show()
在前面的代码片段中,我们执行了以下操作:
-
我们使用
spark.read()函数将数据从数据湖加载到 Spark DataFrame 中。 -
我们使用
printSchema()函数打印其架构,以检查列的数据类型。 -
我们使用
select()操作显示 DataFrame 中的几个列,以检查它们的值。 -
我们使用
describe()操作生成 DataFrame 的基本统计信息。
数据清理
在上一节的代码示例中,你应该注意到大多数数据类型只是字符串类型。数据集可能还包含重复项,并且数据中也可能存在 NULL 值。让我们解决数据集中的这些不一致性,如下所示的代码片段所示:
dedupe_data = select_data.drop_duplicates(["invoice_num",
"invoice_date",
"stock_code"])
interim_data = (select_data
.withColumn("invoice_time", to_timestamp("invoice_date",
'dd/M/yy HH:mm'))
.withColumn("cust_age", col("age").cast(FloatType()))
.withColumn("working_class",
col("work_class").cast(FloatType()))
.withColumn("fin_wt",
col("final_weight").cast(FloatType()))
)
clean_data = interim_data.na.fill(0)
在上面的代码片段中,我们执行了以下操作:
-
我们使用
dropduplicates()操作通过键列去重数据。 -
然后,我们使用
to_timestamp()函数将 datetime 列转换为正确的时间戳类型,通过提供正确的时间戳格式。 -
我们使用
CAST()方法更改 DataFrame 的数据类型。 -
我们使用
na.fill()操作将缺失值和NULL值替换为0。
本节展示了如何使用 PySpark 进行大规模的数据清洗。下一节将展示如何执行数据处理步骤,如过滤和重命名。
数据操作
一旦你有了更干净的数据集,你可以执行操作来过滤掉任何不需要的数据、重命名列以遵循你的命名约定,并删除任何不需要的数据列,如下代码块所示:
final_data = (clean_data.where("year(invoice_time) = 2009")
.withColumnRenamed("working_class",
"work_type")
.withColumnRenamed("fin_wt",
"final_weight")
.drop("age")
.drop("work_class")
.drop("fn_wt"))
pd_data = final_data.toPandas()
在上面的代码片段中,我们执行了以下操作:
-
我们使用
where()函数过滤、切片和处理数据。 -
我们使用
withColumnsRenamed()函数重命名列,并使用drop()函数删除不需要的列。 -
我们使用
toPandas()函数将 Spark DataFrame 转换为 PySpark DataFrame。
有时,Spark MLlib 中没有可用的 ML 算法,或者有一个使用单节点 Python 库构建的自定义算法。对于这些用例,你可以将 Spark DataFrame 转换为 pandas DataFrame,如前面的步骤 3所示。
注意
将 Spark DataFrame 转换为 pandas DataFrame 涉及将所有数据从 Executor 收集到 Spark 驱动程序。因此,需要注意此转换仅应用于较小的数据集,否则可能会导致驱动节点的OutOfMemory错误。
总结
在本章中,你了解了机器学习(ML)的概念和不同类型的 ML 算法。你还学习了 ML 在现实世界中的一些应用,帮助企业减少损失、最大化收入并加速上市时间。你还了解了可扩展 ML 的必要性,以及两种不同的技术来扩展 ML 算法。介绍了 Apache Spark 的本地 ML 库 MLlib 及其主要组件。
最后,你学习了一些执行数据清理、处理和转换的技术,使数据更适合数据科学流程。在接下来的章节中,你将学习机器学习(ML)流程的发送阶段,称为特征提取和特征工程,你将学习如何应用各种可扩展的算法来转换单个数据字段,使其更适合数据科学应用。