Spark3-入门指南-一-

404 阅读1小时+

Spark3 入门指南(一)

原文:Beginning Apache Spark 3

协议:CC BY-NC-SA 4.0

一、Apache Spark 简介

没有比现在更好的学习 Apache Spark 的时机了。由于其易用性、速度和灵活性,它已成为大数据堆栈中的关键组件之一。多年来,它已成为多种工作负载类型的统一引擎,如大数据处理、数据分析、数据科学和机器学习。许多行业的公司广泛采用这种可扩展的数据处理系统,包括脸书、微软、网飞和 LinkedIn。此外,它在每个主要版本中都得到了稳步的改进。

Apache Spark 的最新版本是 3.0,于 2020 年 6 月发布,标志着 Spark 作为开源项目的十周年纪念。这个版本包括对 Spark 许多方面的增强。值得注意的增强是创新的实时性能优化技术,以加速 Spark 应用程序,并帮助减少开发人员调整 Spark 应用程序所需的时间和精力。

本章提供了 Spark 的高级概述,包括核心概念、架构和 Apache Spark 堆栈中的各种组件。

概观

Spark 是一个通用的分布式数据处理引擎,旨在提高速度、易用性和灵活性。这三个属性的结合使得 Spark 如此受欢迎,并在行业中被广泛采用。

Apache Spark 网站声称,它运行某些数据处理作业的速度比 Hadoop MapReduce 快 100 倍。事实上,在 2014 年,Spark 赢得了 Daytona GraySort 大赛,这是一个行业基准,看看一个系统能以多快的速度对 100TB 的数据(1 万亿条记录)进行排序。Databricks 提交的材料声称,Spark 可以使用比 Hadoop MapReduce 创下的世界纪录少 10 倍的资源,将 100 TB 的数据排序快 3 倍。

自从 Spark 项目开始以来,易用性一直是 Spark 创造者的主要关注点之一。它提供了 80 多个高级的、通常需要的数据处理操作符,使开发人员、数据科学家和分析人员可以轻松地使用它们来构建各种有趣的数据应用程序。此外,这些运算符有多种语言版本:Scala、Java、Python 和 r。软件工程师、数据科学家和数据分析师可以挑选自己喜欢的语言,用 Spark 解决大规模数据处理问题。

在灵活性方面,Spark 提供了一个统一的数据处理堆栈,可以解决多种类型的数据处理工作负载,包括批处理应用程序、交互式查询、需要多次迭代的机器学习算法以及实时流应用程序,以近乎实时地提取可操作的见解。在 Spark 出现之前,每种类型的工作负载都需要不同的解决方案和技术。现在,公司只需利用 Spark 来满足其所有数据处理需求,它可以显著降低运营成本和资源。

大数据生态系统由许多技术组成,包括 Hadoop 分布式文件系统 (HDFS),这是一个分布式存储引擎和集群管理系统,可以有效地管理一个机器集群和不同的文件格式,以二进制和列格式存储大量数据。Spark 与大数据生态系统集成良好。这是 Spark 采用率快速增长的另一个原因。

Spark 的另一个很酷的地方是它是开源的。因此,任何人都可以下载源代码来检查代码,弄清楚某个特性是如何实现的,并扩展其功能。在某些情况下,它可以极大地帮助减少调试问题的时间。

历史

Spark 始于 2009 年加州大学伯克利分校 AMPLab 的一个研究项目。当时,该项目的研究人员观察到 Hadoop MapReduce 框架在处理交互式和迭代数据处理用例方面的低效率,因此他们想出了通过引入内存存储和有效处理故障恢复的方法来克服这些低效率的方法。一旦这个研究项目被证明是优于 MapReduce 的可行解决方案。它于 2010 年开源,并于 2013 年成为 Apache 顶级项目。

参与这个研究项目的许多研究人员成立了一家名为 Databricks 的公司,他们在 2013 年筹集了超过 4300 万美元。Databricks 是 Spark 背后的主要商业管家。2015 年,IBM 宣布了一项重大投资,旨在建立一个 Spark 技术中心,通过与开源社区密切合作来推进 Apache Spark,并将 Spark 打造为公司分析和商业平台的核心。

关于 Spark 的两篇热门研究论文分别是《Spark:带工作集的集群计算》( http://people.csail.mit.edu/matei/papers/2010/hotcloud_spark.pdf )和《弹性分布式数据集:内存集群计算的容错抽象》( http://people.csail.mit.edu/matei/papers/2012/nsdi_spark.pdf )。这些论文在学术会议上广受好评,为任何想要学习和了解 Spark 的人提供了良好的基础。

从一开始,Spark 开源项目就是一个非常活跃的项目和社区。贡献者的数量增加了 1000 多个,有超过 20 万个 Apache Spark meetups。Apache Spark 贡献者的数量已经超过了广泛流行的 Apache Hadoop 的贡献者数量。

Spark 的创造者为他们的项目选择了 Scala 编程语言,因为它结合了 Scala 的简洁性和静态类型。现在,Spark 被认为是用 Scala 编写的最大的应用程序之一,它的流行无疑帮助 Scala 成为主流编程语言。

Spark 核心概念和架构

在深入 Spark 的细节之前,对核心概念和各种核心组件有一个高层次的理解是很重要的。本节包括以下内容。

  • Spark 簇

  • 资源管理系统

  • Spark 应用

  • Spark 驱动器

  • Spark 执行者

Spark 集群和资源管理系统

Spark 本质上是一个分布式系统,旨在高效快速地处理大量数据。这种分布式系统通常部署在一组机器上,称为 Spark 集群。一个集群可以小到几台机器,也可以大到几千台机器。根据 https://spark.apache.org/faq.html 的 Spark FAQ,世界上最大的 Spark 集群有 8000 多台机器。

公司依靠像 Apache YARN 或 Apache Meso 这样的资源管理系统来高效、智能地管理一组机器。典型资源管理系统中的两个主要组件是集群管理器和工作器。主设备知道从设备的位置、内存大小以及每个从设备拥有的 CPU 内核数量。集群管理器的主要职责之一是通过向工作人员分配工作来协调工作。每个工作者提供资源(内存、CPU 等。)分配给集群管理器,并执行分配的工作。这类工作的一个例子是启动一个特定的流程并监控其运行状况。Spark 旨在轻松地与这些系统进行互操作。近年来,大多数采用大数据技术的公司都有一个 YARN 集群来运行 MapReduce 作业或其他数据处理框架,如 Apache Pig 或 Apache Hive。

完全采用 Spark 的创业公司可以使用现成的 Spark 集群管理器来管理一组专门使用 Spark 执行数据处理的机器。

Spark 应用

Spark 应用程序由两部分组成。一个是用 Spark APIs 表达的数据处理逻辑,一个是驱动。数据处理逻辑可以简单到只有几行代码来执行解决特定数据问题的几个数据处理操作,也可以复杂到训练一个复杂的机器学习模型,需要多次迭代并运行数小时才能完成。Spark 驱动程序实际上是 Spark 应用程序的中央协调器,它与集群管理器进行交互,以确定哪些机器运行数据处理逻辑。对于每一台机器,驱动程序请求集群管理器启动一个被称为执行器的进程。

Spark 驱动程序的另一项非常重要的工作是代表应用程序管理 Spark 任务并将其分配给每个执行器。如果数据处理逻辑要求 Spark 驱动器收集计算结果以呈现给用户,则它与每个 Spark 执行器协调以收集计算结果,并在将它们呈现给用户之前将它们合并在一起。Spark 驱动程序通过一个名为SparkSession的组件执行任务。

Spark 驱动器和执行器

每个 Spark 执行器都是一个 JVM 进程,专用于特定的 Spark 应用程序。Spark 执行器的生命周期是 Spark 应用程序的持续时间,可能是几分钟或几天。有一个有意识的设计决策是不在不同的多个 Spark 应用程序之间共享 Spark 执行器。这样做的好处是将每个应用程序相互隔离。不过,如果不将数据写入像 HDFS 这样的外部存储系统,在不同的应用程序之间共享数据并不容易。

简而言之,Spark 采用了主/从架构,其中驱动程序是主设备,执行程序是从设备。这些组件中的每一个都作为一个独立的进程在 Spark 集群上运行。Spark 应用程序由一个驱动程序和一个或多个执行程序组成。作为从角色,Spark 执行器执行被告知的任务,即以任务的形式执行数据处理逻辑。每个任务都在独立的 CPU 内核上执行。这就是 Spark 并行处理数据以提高速度的方式。此外,当应用程序逻辑要求时,每个 Spark 执行器负责在内存和/或磁盘上缓存一部分数据。

启动 Spark 应用程序时,您可以指定应用程序需要的执行器数量,以及每个执行器应该拥有的内存量和 CPU 内核数量。

图 1-1 显示了 Spark 应用程序和集群管理器之间的交互。

img/419951_2_En_1_Fig2_HTML.jpg

图 1-2

由一个驱动程序和三个执行器组成的 Spark 集群

img/419951_2_En_1_Fig1_HTML.jpg

图 1-1

Spark 应用程序和集群管理器之间的交互

Spark 统一堆栈

与其前身不同,Spark 提供了一个统一的数据处理引擎,称为 Spark stack。像其他设计良好的系统一样,该堆栈建立在一个名为 Spark Core 的强大基础之上,它提供了管理和运行分布式应用程序所需的所有功能,如调度、协调和处理容错。此外,它为数据处理提供了一个强大的通用编程抽象,称为弹性分布式数据集 (RDDs)。在这个坚实的基础之上是一个库集合,其中每个库都是为特定的数据处理工作负载而设计的。Spark SQL 擅长交互式数据处理。Spark 流是实时数据处理。Spark GraphX 用于图形处理。Spark MLlib 是机器学习用的。Spark R 使用 R shell 运行机器学习任务。

这一统一引擎为构建下一代大数据应用程序带来了多项重要优势。首先,应用程序的开发和部署更简单,因为它们使用一组统一的 API,并在单个引擎上运行。第二,结合不同类型的数据处理(批处理、流等。)的效率要高得多,因为 Spark 可以在相同的数据上运行这些不同的 API 集,而无需将中间数据写出到存储中。

最后,最令人兴奋的好处是,Spark 使全新的应用程序成为可能,因为它易于组合不同的数据处理类型集;例如,对实时数据流的机器学习预测的结果运行交互式查询。一个每个人都能联想到的类比是智能手机,由强大的相机、手机和 GPS 设备组成。通过结合这些组件的功能,智能手机可以实现像 Waze 这样的创新应用,Waze 是一种交通和导航应用。

img/419951_2_En_1_Fig3_HTML.jpg

图 1-3

Spark 统一堆栈

Spark 核心

Spark 核心是 Spark 分布式数据处理引擎的基石。它由 RDD、分布式计算基础设施和编程抽象组成。

分布式计算基础设施负责在集群中的许多机器之间分配、协调和调度计算任务。这使得能够在大型机器集群上高效、快速地执行大量数据的并行数据处理。分布式计算基础设施的另外两个重要职责是处理计算任务失败和跨机器移动数据的有效方式,称为数据混洗。高级 Spark 用户应该熟悉 Spark 分布式计算基础设施,以便有效地设计高性能 Spark 应用程序。

RDD 密钥编程抽象是每个 Spark 用户都应该学习并有效使用各种提供的 API 的东西。RDD 是跨集群划分的可并行操作的容错对象集合。本质上,它为 Spark 应用程序开发人员提供了一组 API,使他们能够轻松高效地执行大规模数据处理,而无需担心数据驻留在集群上的什么位置以及机器故障。RDD API 面向多种编程语言,包括 Scala、Java 和 Python。它们允许用户传递本地函数在集群上运行,这是非常强大和独特的。rdd 将在后面的章节中详细介绍。

Spark stack 中的其余组件被设计为运行在 Spark Core 之上。因此,Spark 内核在 Spark 版本之间所做的任何改进或优化都会自动提供给其他组件。

Spark SQL

Spark SQL 是构建在 Spark Core 之上的一个模块,它是为大规模结构化数据处理而设计的。自从它带来了新水平的灵活性、易用性和性能以来,它的受欢迎程度一直在飙升。

结构化查询语言(SQL)已经成为数据处理的通用语言,因为它易于用户表达他们的意图。执行引擎然后执行智能优化。Spark SQL 将它带到了 Pb 级的数据处理领域。Spark 用户现在可以发出 SQL 查询来执行数据处理,或者使用通过 DataFrame API 公开的高级抽象。数据帧实际上是组织成指定列的数据的分布式集合。这不是一个新的想法。它的灵感来自于 R 和 Python 中的数据帧。考虑数据帧的一个更简单的方法是,它在概念上相当于关系数据库中的一个表。

在幕后,Spark SQL Catalyst optimizer 执行许多分析数据库引擎中常见的优化。

提升 Spark 灵活性的另一个 Spark SQL 特性是能够从各种结构化格式和存储系统读取和写入数据,例如 JavaScript 对象符号(JSON)、逗号分隔值(CSV)、Parquet 或 ORC 文件、关系数据库、Hive 等。

根据 2021 年的 Spark 调查,Spark SQL 是增长最快的组件。这是有意义的,因为 Spark SQL 使“大数据”工程师之外的更广泛的受众能够利用分布式数据处理的能力,即数据分析师或任何熟悉 SQL 的人。

Spark SQL 的座右铭是编写更少的代码,读取更少的数据,而优化器会完成最艰巨的工作。

Spark 结构化流

有人说,“动态数据的价值等于或大于历史数据。”在高度竞争的行业中,处理数据的能力已经成为许多公司的竞争优势。Spark 结构化流模块能够以高吞吐量和容错的方式处理来自各种数据源的实时流数据。数据可以从 Kafka、Flume、Kinesis、Twitter、HDFS 或 TCP socket 等来源获取。

Spark 处理流数据的主要抽象是离散化流(d stream),它通过将输入数据分成小批(基于时间间隔)来实现增量流处理模型,这些小批可以定期结合当前处理状态来产生新的结果。

流处理有时涉及到连接静态数据,Spark 使这变得非常容易。换句话说,由于统一的 Spark 堆栈,在 Spark 中可以很容易地将批处理和交互式查询与流处理结合起来。

Spark 版引入了一个新的可伸缩和容错的流处理引擎,称为结构化流。该引擎进一步简化了流处理应用程序开发人员的生活,它将流计算视为静态数据上的批处理计算。这个新的引擎自动递增地和连续地执行流处理逻辑,并在新的流数据到达时产生结果。结构化流媒体引擎的另一个独特功能是保证端到端的一次性支持,这使得“大数据”工程师在将数据保存到关系数据库或 NoSQL 数据库等存储系统方面比以前容易得多。

随着这个新引擎的成熟,它支持一类新的易于开发和维护的流处理应用程序。

根据 Databricks 首席架构师 Reynold Xin 的说法,执行流分析的最简单方法是不必对流进行推理。

Spark MLlib(消歧义)

MLlib 是 Spark 的机器学习库。它提供了 50 多种常见的机器学习算法和抽象,用于管理和简化模型构建任务,如特征化、构建管道、评估和调整模型以及模型的持久性,以帮助将模型从开发转移到生产。

从 Spark 2.0 版本开始,MLlib APIs 基于数据帧,以利用 Spark SQL 引擎中 Catalyst 和钨组件提供的用户友好性和许多优化。

机器学习算法是迭代的,这意味着它们会经过多次迭代,直到实现预期目标。Spark 使得实现这些算法变得极其容易,并且可以通过一个机器集群以可扩展的方式运行它们。常用的机器学习算法,如分类、回归、聚类和协同过滤,可供数据科学家和工程师使用。

图计算

图形处理对由顶点和连接它们的边组成的数据结构进行操作。图数据结构通常用于表示现实生活中的互联实体网络,包括 LinkedIn 上的职业社交网络、互联网上的互联网页网络等等。Spark GraphX 是一个库,它通过提供一个带有附加到每个顶点和边的属性的有向多图的抽象来实现图形并行计算。GraphX 包含了一组常见的图形处理算法,包括页面排名、连接组件、最短路径等。

Spark

SparkR 是一个 R 包,它提供了使用 Apache Spark 的轻量级前端。r 是一种流行的统计编程语言,支持数据处理和机器学习任务。然而,R 并不是为处理无法在单台机器上运行的大型数据集而设计的。SparkR 利用 Spark 的分布式计算引擎,使用熟悉的 R shell 和许多数据科学家喜爱的流行 API 来实现大规模数据分析。

Apache Spark 3.0

3.0 版本对 Spark stack 中的大多数组件进行了新的特性和增强。然而,大约 60%的增强是针对 Spark SQL 和 Spark 核心组件的。查询性能优化是 Spark 3.0 的主题之一,所以大部分的关注和开发都在 Spark SQL 组件中。根据 Databricks 完成的 TPC-DS 30 TB 基准测试,Spark 3.0 大约比 Spark 2.4 快两倍。本节重点介绍一些与性能优化相关的显著特性。

自适应查询执行框架

顾名思义,查询执行框架在运行时根据关于数据大小、分区数量等的最新统计数据来调整执行计划。因此,Spark 可以动态切换连接策略,自动优化偏斜连接,并调整分区数量。所有这些智能优化都提高了 Spark 应用程序的查询性能。

动态分区修剪(DPP)

DPP 背后的主要思想很简单,就是避免读取不必要的数据。它是专门为使用星型模式中的事实表和维度表的连接来查询数据的用例而设计的。通过减少事实表中需要根据给定的过滤条件与维度表连接的行数,它可以显著提高连接性能。基于 TPC-DS 基准测试,这种优化技术可以将 60%的查询的性能提高 2 到 18 倍。

加速器感知调度程序

越来越多的 Spark 用户正在利用 Spark 处理大数据和机器学习工作负载。后一类工作负载往往需要 GPU 来加速机器学习模型训练过程。这种增强使 Spark 用户能够为他们涉及机器学习的复杂工作负载描述和请求 GPU 资源。

Apache Spark 应用程序

Spark 是一个多功能、快速、可伸缩的数据处理引擎。从一开始,它就被设计成一个通用引擎,并且已经证明它可以用来解决许多用例。因此,各个行业的许多公司都在使用 Spark 来解决许多现实生活中的用例。下面是使用 Spark 开发的一些应用程序。

  • 客户智能应用

  • 数据仓库解决方案

  • 实时流解决方案

  • 推荐引擎

  • 日志处理

  • 面向用户的服务

  • 欺诈检测

Spark 示例应用

在大数据处理领域,典型的示例应用程序是字数统计应用程序。这一传统始于 MapReduce 框架的引入。从那以后,每一本与大数据处理技术相关的书都必须遵循这个不成文的传统,纳入这个典范的例子。word count 示例应用程序中的问题空间对每个人来说都很容易理解,因为它所做的只是计算某个特定的单词在每组文档中出现的次数,无论是一本书的一章还是来自 Internet 的数百 TB 的网页。

清单 1-1 是 Scala 语言中 Spark 的一个字数统计示例应用。

val textFiles = sc.textFile("hdfs://<folder>")
val words = textFiles.flatMap(line => line.split(" "))
val wordTuples = words.map(word => (word, 1))
val wordCounts = wordTuples.reduceByKey(_ + _)
wordCounts.saveAsTextFile("hdfs://<outoupt folder>")

Listing 1-1The Word Count Spark Example Application Written in Scala Language

在这五行代码背后发生了很多事情。第一行负责读取指定文件夹下的文本文件。第二行遍历每个文件中的每一行,然后将每一行标记为一个单词数组,最后将每个数组展平为每行一个单词。第三行为每个单词加 1,以计算所有文档中的单词数。第四行执行每个单词计数的求和。最后,最后一行将结果保存在指定的文件夹中。希望这能让您对 Spark 执行数据处理的易用性有一个大致的了解。未来的章节将更详细地介绍每一行代码的作用。

Apache Spark 生态系统

在大数据领域,创新不会停滞不前。随着时间的推移,最佳实践和架构不断涌现。Spark 生态系统正在扩展和发展,以解决数据湖中的一些新兴需求,帮助数据科学家更高效地与大量数据进行交互,并加快机器学习开发生命周期。本节重点介绍了 Spark 生态系统中一些激动人心的最新创新。

DeltaLake

在这一点上,大多数公司都认识到了数据的价值,并制定了某种形式的策略来接收、存储、处理和提取数据中的见解。Delta Lake 的理念是利用分布式存储解决方案为各种数据消费者(如数据科学家、数据工程师和业务分析师)存储结构化和非结构化数据。为了确保 Delta Lake 中的数据可用,必须在数据目录、数据发现、数据质量、访问控制和数据一致性语义方面存在疏漏。数据一致性语义提出了许多挑战,公司已经发明了技巧或“创可贴”解决方案。

Delta Lake 是一个针对数据一致性语义的开源解决方案,它提供了一种开放的数据存储格式,具有事务保证、模式实现和演化支持。DeltaLake 将在后面进一步讨论。

树袋熊

多年来,数据科学家一直在使用 Python pandas 库在他们的机器学习相关任务中执行数据操作。熊猫库( https://pandas.pydata.org )是一个“基于 Python 编程语言构建的快速、强大、灵活且易于使用的开源数据分析和操作工具。”pandas 非常受欢迎,并且已经成为事实上的库,因为它强大而灵活的抽象称为数据操作的数据帧。然而,pandas 被设计成只能在一台机器上运行。要在 Python 中执行并行计算,可以探索一个名为 Dask ( https://docs.dask.org )的开源项目。

考拉通过在 Apache Spark 上实现 pandas DataFrame API,结合了两个世界的精华,强大而灵活的 DataFrame 抽象和 Spark 的分布式数据处理引擎。

这项创新使数据科学家能够利用他们的熊猫知识与比过去大得多的数据集进行交互。

考拉 1.0 版本于 2020 年 6 月发布,覆盖了熊猫 API 的 80%。考拉的目标是让数据科学项目能够利用大型数据集,而不是被它们阻挡。

MLflow

机器学习领域已经存在很长时间了。最近,由于算法的进步,访问大量有用数据集(如图像和大量文本)的便利性,以及教育资源的可用性,它变得更加触手可及。然而,将机器学习应用于商业问题已被证明是一个挑战,因为管理机器学习生命周期更像是一个软件工程问题。

MLflow 是一个开源项目。它是在 2018 年构想的,旨在提供一个平台来帮助管理机器学习生命周期。它由以下组件组成,以满足生命周期每个阶段的各种需求。

  • 跟踪记录和比较机器学习实验。

  • 项目提供了组织机器学习项目的一致格式,以轻松共享和复制机器学习模型。

  • Models 提供了一个标准化的格式来打包机器学习模型,一个一致的 API 来处理机器学习模型,例如加载和部署它们。

  • Registry 是一个模型存储,它托管机器学习模型并跟踪它们的血统、版本和部署状态转换。

摘要

  • Apache Spark 自诞生以来当然产生了许多 Spark。它在大数据领域创造了许多激动人心的事情和机会。更重要的是,它允许您创建许多新的和创新的大数据应用程序,以解决数据应用程序的各种数据处理问题。

  • Spark 的三个重要特性是易用性、速度和灵活性。

  • Spark 分布式计算基础设施采用主从架构。每个 Spark 应用程序由一个驱动程序和一个或多个执行程序组成,用于并行处理数据。并行性是在短时间内处理大量数据的关键因素。

  • Spark 提供了统一的可扩展和分布式数据处理引擎,可用于批处理、交互式和探索性数据处理、实时流处理、构建机器学习模型和预测以及图形处理。

  • Spark 应用程序可以用多种编程语言编写,包括 Scala、Java、Python 或 r。

二、使用 Apache Spark

当谈到使用 Spark 或构建 Spark 应用程序时,有许多选择。本章描述了三个常见的选项,包括使用 Spark shell、从命令行提交 Spark 应用程序以及使用名为 Databricks 的托管云平台。本章的最后一部分面向那些希望在本地机器上安装 Apache Spark 源代码的软件工程师,他们将研究 Spark 源代码并了解某些特性是如何实现的。

下载和安装

要学习或试验 Spark,将它本地安装在您的计算机上是很方便的。通过这种方式,您可以轻松地尝试某些功能,或者使用小型数据集测试您的数据处理逻辑。将 Spark 本地安装在您的笔记本电脑上,您可以在任何地方学习它,包括您舒适的客厅、海滩或墨西哥的酒吧。

Spark 是用 Scala 写的。它经过打包,可以在 Windows 和类似 UNIX 的系统(例如 Linux、macOS)上运行。要在本地运行 Spark,只需要在您的计算机上安装 Java。

建立一个多租户 Spark 生产集群需要更多的信息和资源,这超出了本书的范围。

下载 Spark

Apache Spark 网站的下载部分( http://spark.apache.org/downloads.html )有下载预打包 Spark 二进制文件的详细说明。在写这本书的时候,最新的版本是 3.1.1。包类型方面,选择 Hadoop 最新版本的。图 2-1 显示了下载 Spark 的各种选项。最简单的方法是下载预打包的二进制文件,因为它包含在您的计算机上运行 Spark 所必需的 JAR 文件。单击行项目 3 上的链接会触发二进制文件下载。有一种方法可以从源代码手动构建 Spark 二进制文件。本章稍后将介绍如何操作的说明。

img/419951_2_En_2_Fig1_HTML.jpg

图 2-1

Apache Spark 下载选项

安装 Spark

一旦文件成功下载到您的计算机上,下一步就是解压缩它。spark-3.1.1-bin-hadoop2.7.tgz文件位于 GZIP 压缩的 tar 存档文件中,因此您需要使用正确的工具来解压缩它。

对于 Linux 或 macOs 电脑,tar命令应该已经存在。所以运行下面的命令来解压缩下载的文件。

tar xvf spark-3.1.1-bin-hadoop2.7.tgz

对于 Windows 计算机,您可以使用 WinZip 或 7-zip 工具来解压缩下载的文件。

解压缩成功完成后,应该有一个名为 spark-3.1.1-bin-hadoop2.7 的目录。从这里开始,这个目录称为 spark 目录。

Note

如果下载了不同版本的 Spark,目录名会略有不同。

spark-3.1.1-bin-hadoop2.7目录下大概有十几个目录。表 2-1 描述了值得了解的内容。

表 2-1

spark-3.1.1-bin-hadoop2.7 中的子目录

|

名字

|

描述

| | --- | --- | | 容器 | 包含各种可执行文件,用于在 Scala 或 Python 中调用 Spark shell、提交 Spark 应用程序、运行 Spark 示例 | | 数据 | 包含各种 Spark 示例的小样本数据文件 | | 例子 | 包含所有 Spark 示例的源代码和二进制文件 | | 震动 | 包含运行 Spark 所需的必要二进制文件 | | 命令 | 包含管理 Spark 集群的可执行文件 |

下一步是通过调用 Spark shell 来测试安装。

Spark shell 类似于 Unix shell。它提供了一个交互式环境,可以轻松地学习 Spark 和分析数据。大多数 Spark 应用程序都是使用 Python 或 Scala 编程语言开发的。Spark shell 可用于这两种语言。如果你是一名数据科学家,Python 是你的最爱,你不会感到被冷落。下一节将展示如何使用 Spark Scala 和 Spark Python shell。

Note

Scala 是一种基于 Java JVM 的语言,因此很容易在 Scala 应用程序中利用现有的 Java 库。

Spark Scala 外壳

要启动 Spark Scala shell,在 Spark 目录中输入./bin/spark-shell命令。几秒钟后,您应该会看到类似于图 2-2 的东西。

img/419951_2_En_2_Fig2_HTML.jpg

图 2-2

Scala Spark 壳输出

要退出 Scala Spark shell,请键入:quit:q

Note

Java 版本 11 或更高版本是运行 Spark Scala shell 的首选。

Spark Python Shell

要启动 Spark Python shell,请在 Spark 目录中输入./bin/pyspark命令。几秒钟后,您应该会看到类似于图 2-3 的东西。

img/419951_2_En_2_Fig3_HTML.jpg

图 2-3

Python Spark shell 的输出

要退出 Python Spark shell,请输入ctrl-d

Note

Spark Python shell 需要 Python 3.7.x 或更高版本。

Spark Scala shell 和 Spark Python shell 分别是 Scala REPL 和 Python REPL 的扩展。REPL 是读取-评估-打印循环的首字母缩写。它是一个交互式计算机编程环境,接受用户输入,对其进行评估,并将结果返回给用户。一旦输入一行代码,REPL 会立即提供关于是否有语法错误的反馈。如果没有任何语法错误,它会对它们进行评估。如果有输出,它会显示在 shell 中。交互和即时反馈环境使开发人员能够通过绕过正常软件开发过程中的代码编译步骤来提高工作效率。

要学习 Spark,Spark shell 是一个非常方便的工具,可以随时随地在您的本地计算机上使用。除了您处理的数据文件需要驻留在您的计算机上之外,它没有任何外部依赖性。然而,如果你有一个互联网连接,有可能访问这些远程数据文件,但它会很慢。

本书的其余章节使用 Spark Scala shell。

享受 Spark Scala Shell 带来的乐趣

本节提供了关于 Scala Spark shell 的信息,以及一组有用的命令,这些命令在使用它进行探索性数据分析或交互式构建 Spark 应用程序时非常有效。

./bin/spark-shell命令有效地启动了 Spark 应用程序,并提供了一个环境,您可以在其中交互式地调用 Spark Scala APIs 来轻松地执行探索性数据处理。由于 Spark Scala shell 是 Scala REPL 的扩展,所以用它同时学习 Scala 和 Spark 是一个很好的方法。

有用的 Spark Scala Shell 命令和提示

一旦 Spark Scala shell 启动,它会将您置于一个交互式环境中,以便输入 shell 命令和 Scala 代码。本节涵盖了各种有用的命令和一些使用 shell 的技巧。

进入 Spark Shell 后,键入以下命令以获得可用命令的完整列表。

scala>  :help

该命令的输出如图 2-4 所示。

img/419951_2_En_2_Fig4_HTML.jpg

图 2-4

可用 shell 命令列表

有些命令比其他命令更常用,因为它们很有用。表 2-2 描述了常用的命令。

表 2-2

有用的 Spark Shell 命令

|

名字

|

描述

| | --- | --- | | :历史 | 该命令显示在之前的 Spark shell 会话和当前会话中输入的内容。这对于复制非常有用。 | | :加载 | 加载并执行所提供文件中的代码。当数据处理逻辑很长时,这尤其有用。跟踪文件中的逻辑要容易一些。 | | :重置 | 试用各种 Scala 或 Spark APIs 一段时间后,您可能会忘记各种变量的值。该命令将 shell 重置为干净状态,以便于推理。 | | :无声 | 这是为那些对查看 shell 中输入的每个 Scala 或 Spark APIs 的输出有些厌倦的高级用户准备的。要重新启用输出,只需再次键入:silent。 | | :退出 | 这是一个不言自明的命令,但是知道它很有用。通常,人们试图通过进入:退出来退出外壳,这是行不通的。 | | :类型 | 显示变量的类型。:类型 |

除了这些命令之外,还有一个有助于提高开发人员工作效率的特性是代码完成特性。与流行的集成开发环境(ide)如 Eclipse 或 IntelliJ 一样,代码完成特性帮助开发人员探索可能的选项并减少键入错误。

在 shell 中,键入spa,然后按 Tab 键。环境添加字符将“spa”转换为“spark”。此外,它还显示了 Spark 的可能匹配(见图 2-5 )。

img/419951_2_En_2_Fig5_HTML.jpg

图 2-5

spa 的选项卡完成输出

scala> spa <tab>

除了完成部分输入单词的名称,制表符结束还可以显示对象的可用成员变量和函数。

在 shell 中,键入spark,然后按 Tab 键。这将显示由spark变量代表的 Scala 对象的可用成员变量和函数列表(参见图 2-6 )。

img/419951_2_En_2_Fig6_HTML.jpg

图 2-6

名为“spark”的对象的可用成员变量和函数列表

:history命令显示先前输入的命令或代码行。这表明 Spark shell 保留了输入内容的记录。快速显示或回忆最近输入的内容的一种方法是按向上箭头键。一旦你向上滚动到你想要执行的行,只需按下回车键来执行它。

与 Scala 和 Spark 的基本交互

上一节介绍了导航 Spark shell 的基础知识;本节介绍了在 Spark shell 中使用 Scala 和 Spark 的一些基本方法。这些基础知识将在以后的章节中非常有帮助,因为您会更深入地研究 Spark DataFrame 和 Spark SQL 等主题。

与 Scala 的基本交互

让我们从 Spark Scala shell 中的 Scala 开始,它为学习 Scala 提供了一个成熟的环境。把 Spark Scala shell 想象成一个空体的 Scala 应用程序,这就是你的用武之地。您可以用 Scala 函数和应用程序逻辑来填充这个空身体。本节打算在 Spark shell 中演示几个简单的 Scala 示例。Scala 是一种迷人的编程语言,强大、简洁、优雅。请参考 Scala 相关书籍,了解更多关于这种编程语言的知识。

学习任何编程语言的典型例子是“Hello World”例子,它需要打印出一条消息。让我们开始吧。在 Spark Scala shell 中输入以下代码行;输出应该如图 2-7 所示。

img/419951_2_En_2_Fig7_HTML.jpg

图 2-7

Hello World 示例命令的输出

scala> println("Hello from Spark Scala shell")

下一个示例定义了一个年龄数组,并在 Spark shell 中打印出这些元素值。此外,这个例子说明了上一节中提到的代码完成特性。

要定义一个年龄数组并将其赋给一个不可变的变量,请在 Spark shell 中输入以下内容。图 2-8 显示了评估输出。

img/419951_2_En_2_Fig8_HTML.jpg

图 2-8

定义年龄数组的输出

scala> val ages = Array(20, 50, 35, 41)

现在你可以在下面一行代码中引用ages变量。让我们假设您不能准确地记住Array类中的一个函数名来迭代数组中的元素,但是您知道它以“fo”开头。您可以输入以下内容并点击选项卡,看看 Spark shell 可以提供什么帮助。

scala> ages.fo

按 Tab 键后,Spark shell 显示如图 2-9 所示。

img/419951_2_En_2_Fig9_HTML.jpg

图 2-9

代码完成的输出

啊哈!您需要foreach函数来遍历数组中的元素。让我们用它来打印年龄。

scala> ages.foreach(println)

图 2-10 显示了预期的输出。

img/419951_2_En_2_Fig10_HTML.jpg

图 2-10

打印年龄的输出

对于 Scala 新手来说,前面的代码语句可能看起来有点晦涩;但是,你可以直观地猜测它是做什么的。当foreach函数遍历“ages”数组中的每个元素时,它将该元素传递给println函数以将值打印到控制台。这种风格在接下来的章节中会经常用到。

本节最后一个例子定义了一个 Scala 函数来确定年龄是奇数还是偶数;然后用它来查找数组中的奇数年龄。

scala> def isOddAge(age:Int) : Boolean = {
  (age % 2) == 1
}

如果您来自 Java 编程背景,这个函数签名可能看起来很奇怪,但要破译它是做什么的并不太难。请注意,该函数不使用关键字return来返回其主体中表达式的值。在 Scala 中,不需要添加return关键字。函数体中最后一条语句的输出返回给调用者(如果该函数被定义为返回值)。图 2-11 显示了 Spark 壳的输出。

img/419951_2_En_2_Fig11_HTML.jpg

图 2-11

如果有语法错误,Spark shell 将返回函数签名

为了计算出ages数组中的奇数年龄,让我们利用Array类中的filter函数。

scala> ages.filter(age => isOddAge(age)).foreach(println)

这行代码进行过滤,然后遍历结果,打印出奇数年龄。在 Scala 中,使用函数链使代码简洁是一种常见的做法。图 2-12 显示了 Spark 壳的输出。

img/419951_2_En_2_Fig12_HTML.jpg

图 2-12

过滤和打印出的输出只是奇数的年龄

现在让我们在前面定义的 Scala 变量和函数上尝试一下:type shell 命令。一旦您使用 Spark shell 一段时间,并且忘记了某个变量的数据类型或某个函数的返回类型,这个命令就会派上用场。图 2-13 显示了:type命令的示例。

img/419951_2_En_2_Fig13_HTML.jpg

图 2-13

输出:类型命令

学习 Spark,不一定要掌握 Scala 编程语言。然而,你必须熟悉 Scala 的基础知识并熟练使用。在 https://github.com/deanwampler/JustEnoughScalaForSpark ,有一个很好的资源可以学习刚刚够学习 Spark 的 Scala。该资源在各种 Spark 相关会议上展示。

Spark UI 和与 Spark 的基本交互

在上一节中,我提到了 Spark shell 是一个 Scala 应用程序。这只是部分正确。Spark shell 是用 Scala 编写的 Spark 应用程序。当 Spark shell 启动时,会初始化并设置一些东西供您使用,包括 Spark UI 和一些重要的变量。

Spark UI

如果你回头仔细检查图 2-2 或图 2-3 中的 Spark 壳输出,你会看到一条类似下面的线。(对于您的 Spark shell,URL 可能会有所不同。)

SparkContext Web UI 在 http://<ip>:4040 可用。

如果您将浏览器指向 Spark shell 中的 URL,它会显示如图 2-14 所示的内容。

img/419951_2_En_2_Fig14_HTML.jpg

图 2-14

Spark UI

Spark UI 是一个 web 应用程序,旨在帮助监控和调试 Spark 应用程序。它包含 Spark 应用程序的详细运行时信息和各种资源消耗。运行时包括各种度量,这些度量对于诊断 Spark 应用程序中的性能问题非常有帮助。需要注意的一点是,Spark UI 仅在 Spark 应用程序运行时可用。

Spark UI 顶部的导航栏包含到各种选项卡的链接,包括作业、阶段、存储、环境、执行器和 SQL。我将简要介绍环境和执行者选项卡,并在后面的章节中描述其余的选项卡。

Environment 选项卡包含 Spark 应用程序运行环境的静态信息。这包括运行时信息、spark 属性、系统属性和类路径条目。表 2-3 描述了这些区域。

表 2-3

环境选项卡中的部分

|

名字

|

描述

| | --- | --- | | 运行时信息 | 包含 Spark 所依赖的各种组件的位置和版本,包括 Java 和 Scala。 | | Spark 特性 | 该区域包含在 Spark 应用程序中配置的基本和高级属性。基本属性包括应用程序的基本信息,如应用程序 id、名称等。高级属性旨在打开或关闭某些 Spark 功能,或者以最适合特定应用程序的特定方式调整它们。有关可配置属性的完整列表,请参见位于 https://spark.apache.org/docs/latest/configuration.html 的参考资料。 | | 资源配置文件 | 关于 Spark 集群中 CPU 数量和内存量的信息。 | | Hadoop 属性 | 各种 Hadoop 和 Hadoop 文件系统属性。 | | 系统属性 | 这些属性主要是 OS 和 Java 级别的,而不是 Spark 特有的。 | | 类路径条目 | 包含 Spark 应用程序中使用的类路径和 jar 文件的列表。 |

Executors 选项卡包含支持 Spark 应用程序的每个执行器的摘要和细分信息。这些信息包括特定资源的容量,以及每个执行器使用了多少资源。资源包括内存、磁盘和 CPU。Summary 部分提供了 Spark 应用程序中所有执行器的资源消耗的鸟瞰图。图 2-15 显示了更多细节。

img/419951_2_En_2_Fig15_HTML.png

图 2-15

仅使用一个执行器的 Spark 应用程序的 Executor 选项卡

您将在后面的章节中再次讨论 Spark UI。

与 Spark 的基本交互

一旦一个 Spark shell 成功启动,一个名为spark的重要变量就被初始化并准备好使用。变量spark代表了一个SparkSession类的实例。让我们使用:type命令来验证这一点。

scala>:type spark


Spark 壳在图 2-16 中显示其类型。

img/419951_2_En_2_Fig16_HTML.jpg

图 2-16

显示“Spark”变量的类型

Spark 2.0 中引入了SparkSession类,以提供与底层 Spark 功能交互的单一入口点。这个类有读取文本和二进制格式的非结构化和结构化数据的 API,比如 JSON、CSV、Parquet、ORC 等等。此外,SparkSession组件提供了检索和设置 Spark 配置的工具。

让我们开始与 Spark shell 中的spark变量交互,打印出一些有用的信息,比如版本和现有配置。在 Spark shell 中,键入以下代码来打印 Spark 版本。图 2-17 显示输出。

img/419951_2_En_2_Fig17_HTML.jpg

图 2-17

Spark 版本输出

scala> spark.version

再正式一点,可以使用上一节介绍的println函数打印出如图 2-18 所示的 Spark 版本和输出。

img/419951_2_En_2_Fig18_HTML.jpg

图 2-18

使用 println 函数显示 Spark 版本

scala> println("Spark version: " + spark.version)

要查看 Spark shell 中的默认配置,您可以访问sparkconf变量。下面是显示默认配置的代码,输出如图 2-19 所示。

img/419951_2_En_2_Fig19_HTML.jpg

图 2-19

Spark shell 应用中的默认配置

scala> spark.conf.getAll.foreach(println)

要查看您可以从spark变量访问的可用对象的完整集合,您可以利用 Spark shell 代码完成特性。

scala> spark.<tab>

图 2-20 显示了该命令的结果。

img/419951_2_En_2_Fig20_HTML.jpg

图 2-20

可以从 spark 变量访问的变量的完整列表

接下来的章节有更多使用spark与 Spark 底层功能交互的例子。

协作笔记本简介

协作笔记本是由 Databricks 提供的商业产品,data bricks 是名为 Apache Spark 的开源项目的最初创建者。根据产品文档,Collaborative Notebooks 是为数据工程师、数据科学家和数据分析师设计的,用于执行数据分析和构建支持多种语言、内置数据可视化和自动数据版本化的机器学习模型。它还提供 Spark on demand 计算基础架构,并可以按照特定的计划执行生产数据管道的作业。它围绕 Apache Spark 构建,为全球客户提供四个主要价值主张。

  • 完全管理的 Spark 集群

  • 用于探索和可视化的交互式工作空间

  • 生产流水线调度程序

  • 为您最喜爱的基于 Spark 的应用提供支持的平台

协作笔记本产品有两个版本,完整平台版和社区版。商业版是一个付费产品,提供高级功能,如创建多个集群、用户管理和作业调度。社区版是免费的,非常适合开发人员、数据科学家、数据工程师以及任何想要学习 Apache Spark 或尝试 Databricks 的人。

以下部分涵盖了协作笔记本社区版的基本功能。它为学习 Spark、执行数据分析或构建 Spark 应用程序提供了一个简单直观的环境。本节不是一个全面的指南。为此,您可以参考 Databricks 用户指南( https://docs.databricks.com/user-guide/index.html )。

要使用协作笔记本,您需要在 https://databricks.com/try-databricks 注册一个社区版的免费账户。这个注册过程简单快捷;几分钟之内就可以创建一个帐户。一旦在注册表单中提供并提交了必要的信息,您很快就会收到来自 Databricks 的电子邮件,确认您的电子邮件,它看起来有点像图 2-21 。

img/419951_2_En_2_Fig21_HTML.jpg

图 2-21

Databricks 电子邮件确认您的电子邮件地址

点击图 2-21 所示的网址链接,进入 Databricks 签到表,如图 2-22 所示。

img/419951_2_En_2_Fig22_HTML.jpg

图 2-22

数据块登录页面

使用电子邮件和密码成功登录后,您会看到如图 2-23 所示的 Databricks 欢迎页面。

img/419951_2_En_2_Fig23_HTML.jpg

图 2-23

数据块欢迎页面

随着时间的推移,欢迎页面可能会发生变化,因此它看起来并不完全像图 2-23 。请随意浏览教程或文档。

本节的目的是在 Databricks 中创建一个笔记本,以便您可以学习上一节中介绍的命令。以下是主要步骤。

  1. 创建一个集群。

  2. 创建一个文件夹。

  3. 创建一个笔记本。

创建一个集群

community edition (CE)最酷的特性之一是,它免费提供了一个 15 GB 内存的单节点 Spark 集群。在写这本书的时候,这个单节点集群托管在 AWS 云上。因为 ce 帐户是免费的,所以它提供了同时创建多个集群的能力。只要群集还在使用,它就会一直保持运行状态。如果闲置两个小时,它会自动关机。这意味着您不必主动关闭集群。

要创建集群,请单击页面左侧垂直导航栏中的集群图标。集群页面如图 2-24 所示。

img/419951_2_En_2_Fig24_HTML.jpg

图 2-24

没有活动集群的数据块集群页面

现在点击 Create Cluster 按钮,调出新的集群表单,如图 2-25 所示。

img/419951_2_En_2_Fig25_HTML.jpg

图 2-25

创建集群表单

该表单上唯一的必填字段是集群名称。表 2-4 描述了每个字段。

表 2-4

数据块新的集群表单字段

|

名字

|

描述

| | --- | --- | | 集群名称 | 用于标识集群的唯一名称。名称的每个单词之间可以有空格;比如《我的星火簇》。 | | 数据块运行时版本 | Databricks 支持许多版本的 Spark。出于学习目的,请选择最新版本,它会自动为您填充。每个版本都绑定到一个特定的 AWS 映像。 | | 情况 | 对于 CE 版,没有其他选择。 | | AWS–可用性区域 | 这允许您决定单节点集群运行在哪个 AWS 可用性区域。根据您所在的位置,选项可能会有所不同。 | | Spark–Spark 配置 | 这允许您指定用于启动 Spark 集群的任何特定于应用程序的配置。例子包括打开某些 Spark 特性的 JVM 配置。 |

输入集群名称后,单击创建集群按钮。创建一个单节点 Spark 集群可能需要 10 分钟。如果需要,尝试切换到不同的可用性区域,如果默认区域需要很长时间。一旦成功创建一个 Spark 簇,簇名旁边会出现一个绿点,如图 2-26 所示。

img/419951_2_En_2_Fig26_HTML.jpg

图 2-26

成功创建集群后

通过单击您的集群的名称或本页上的各种链接,您可以随意探索。如果您试图按照相同的步骤创建另一个 Spark 集群,它不允许您这样做。

要终止活动的 Spark 簇,请单击 Actions 列下的方块。

有关在数据块中创建和管理 Spark 集群的更多信息,请访问 https://docs.databricks.com/user-guide/clusters/index.html

让我们进入下一步,创建一个文件夹。

创建文件夹

在讨论如何创建文件夹之前,有必要花点时间描述一下 Databricks 中的工作空间概念。考虑 workspace 最简单的方法是将其视为计算机上的文件系统,这意味着可以利用其分层属性来组织各种笔记本。

要创建文件夹,请单击页面左侧垂直导航栏中的工作区图标。工作区列滑出,如图 2-27 所示。

img/419951_2_En_2_Fig27_HTML.jpg

图 2-27

工作区列

现在点击工作区栏右上方的向下箭头,弹出菜单出现(见图 2-28 )。

img/419951_2_En_2_Fig28_HTML.jpg

图 2-28

用于创建文件夹的菜单项

选择创建➤文件夹菜单项,弹出新文件夹名称对话框(见图 2-29 )。

img/419951_2_En_2_Fig29_HTML.jpg

图 2-29

“新建文件夹名称”对话框

现在,您可以输入一个文件夹名称(即第二章),并点击创建文件夹按钮以完成该过程。章 2 文件夹现在应该出现在工作区栏中,如图 2-30 所示。

img/419951_2_En_2_Fig30_HTML.jpg

图 2-30

第二章文件夹出现在工作区列中

在创建笔记本之前,值得一提的是,还有一种创建文件夹的替代方法。将鼠标指针放在工作区列中的任意位置,然后右键单击;出现相同的菜单选项。

有关工作区和创建文件夹的更多信息,请访问 https://docs.databricks.com/user-guide/workspace.html

创建笔记本

在章节 2 文件夹中创建一个 Scala 笔记本。首先,在工作区栏中选择章节 2 文件夹。章节 2 栏在工作区栏后滑出,如图 2-31 所示。

img/419951_2_En_2_Fig31_HTML.jpg

图 2-31

章节 2 栏出现在工作区栏的右侧

现在你既可以点击章节 2 栏右上角的向下箭头,也可以在章节 2 栏的任意位置点击鼠标右键,调出菜单,如图 2-32 所示。

img/419951_2_En_2_Fig32_HTML.jpg

图 2-32

创建笔记本菜单项

选择“笔记本”菜单项会弹出“创建笔记本”对话框。为您的笔记本命名,并确保为语言字段选择 Scala 选项。应该自动填充集群的值,因为 CE 版本一次只能有一个集群。该对话框看起来应该如图 2-33 所示。

img/419951_2_En_2_Fig33_HTML.jpg

图 2-33

选择了 Scala 语言选项的“创建笔记本”对话框

点击创建按钮后,一个全新的笔记本被创建,如图 2-34 所示。

img/419951_2_En_2_Fig34_HTML.jpg

图 2-34

新 Scala 笔记本

如果你从未使用过 IPython 笔记本,笔记本的概念一开始可能会显得有些陌生。然而,一旦你习惯了,你会发现它很直观,很有趣。

笔记本本质上是一个交互式计算环境(类似于 Spark shell,但是更好)。您可以执行 Spark 代码,使用 Markdown 或 HTML 标记语言用富文本记录您的代码,并使用各种类型的图表和图形可视化您的数据分析结果。

以下部分仅涵盖几个基本部分,以帮助您高效使用 Spark 笔记本。有关使用 Databricks 笔记本并与之交互的完整说明列表,请访问 https://docs.databricks.com/user-guide/notebooks/index.html

Spark Notebook 包含一个单元格集合,每个单元格包含一个要执行的代码块或用于文档目的的标记。

Note

使用 Spark Notebook 的一个好习惯是将数据处理逻辑分成多个逻辑组,这样每个逻辑组都驻留在一个或多个单元中。这类似于开发可维护软件应用程序的实践。

让我们把笔记本分成两部分。第一部分包含您在“Scala 的基本交互”一节中输入的代码片段。第二部分包含您在“与 Spark 的基本交互”一节中输入的代码片段。

让我们从添加一个 Markdown 语句开始,通过在第一个单元格中输入以下内容来记录笔记本的第一部分(参见图 2-35 )。

img/419951_2_En_2_Fig35_HTML.jpg

图 2-35

单元格包含节头标记语句

%md #### Basic Interactions with Scala

要执行标记语句,请确保鼠标光标在单元格 1 中,按住 Shift 键,然后按 Enter 键。这是在单元格中运行代码或标记语句的快捷方式。结果应该如图 2-36 所示。

img/419951_2_En_2_Fig36_HTML.jpg

图 2-36

执行标记语句的输出

请注意,Shift+Enter 组合键执行该单元格中的语句,并在其下方创建一个新单元格。现在让我们在第二个单元格中输入“Hello World”示例并执行该单元格。输出应该如图 2-37 所示。

img/419951_2_En_2_Fig37_HTML.jpg

图 2-37

执行 println 语句的输出

“与 Scala 的交互”部分剩余的三个代码语句被复制到笔记本中(见图 2-38 )。

img/419951_2_En_2_Fig38_HTML.jpg

图 2-38

“与 Scala 的交互”一节中剩余的代码语句

像 Spark Scala shell 一样,Scala Notebook 是一个成熟的 Scala 交互环境,在这里可以执行 Scala 代码。

现在让我们输入第二个标记语句来表示笔记本第二部分的开始,以及“与 Spark 的交互”部分中剩余的代码片段。图 2-39 显示了输出。

img/419951_2_En_2_Fig39_HTML.jpg

图 2-39

与 Spark 部分交互的代码片段的输出

%md #### Basic Interactions with Spark

使用 Spark 笔记本时,有一些重要注意事项需要了解。它提供了一个非常方便的自动保存功能。当您输入市场声明或代码片段时,笔记本的内容会自动保存。事实上,文件菜单项下的菜单项没有保存笔记本的选项。

有时需要在两个现有单元之间创建一个新单元。一种方法是将鼠标光标移动到它们之间的空间,然后单击出现的加号图标创建一个新的单元格。图 2-40 显示了加号图标的样子。

img/419951_2_En_2_Fig40_HTML.jpg

图 2-40

使用加号图标在两个现有单元格之间创建一个新单元格

有时,您需要与在远程办公室工作的同事或其他合作者分享您的笔记本电脑,以展示您出色的 Spark 知识或获得他们对您的数据分析的反馈。只需单击 Spark 笔记本顶部的 File 菜单项,然后选择 Publish 子菜单项。图 2-41 显示了它的样子。

img/419951_2_En_2_Fig41_HTML.jpg

图 2-41

笔记本发布菜单项

点击发布子菜单项,弹出确认对话框(见图 2-42 )。如果你坚持到底,笔记本发布对话框(见图 2-43 )提供了一个你可以发送给世界上任何人的 URL。通过该 URL,您的同事或协作者可以查看您的笔记本,或者将它导入他们的 Databricks 工作区。

img/419951_2_En_2_Fig43_HTML.jpg

图 2-43

笔记本发布的 URL

img/419951_2_En_2_Fig42_HTML.jpg

图 2-42

发布确认对话框

本节仅涵盖使用数据块的基本部分。许多其他高级功能使 Databricks 成为执行交互式数据分析或构建机器学习模型等高级数据解决方案的平台。

CE 提供了一个单节点 Spark 集群的免费帐户。通过 Databricks 产品学习 Spark 变得比以前容易多了。我强烈建议您在学习 Spark 的过程中尝试一下 Databricks。

设置 Spark 源代码

本节面向软件开发人员或任何有兴趣了解 Spark 在代码级如何工作的人。由于 Apache Spark 是一个开源项目,它的源代码是公开的,可以从 GitHub 下载,研究某些特性是如何实现的。Spark 代码是由这个星球上一些最聪明的 Scala 程序员用 Scala 编写的,所以学习 Spark 代码是提高一个人的 Scala 编程技能和知识的好方法。

有两种方法可以将 Apache Spark 源代码下载到您的计算机上。您可以从位于 http://spark.apache.org/downloads.html 的 Spark 下载页面下载它,这个页面之前用于下载 Spark 二进制文件。这一次,让我们选择源代码包类型,如图 2-44 。

img/419951_2_En_2_Fig44_HTML.jpg

图 2-44

Apache Spark 源代码下载选项

要完成源代码下载过程,请单击第 3 行的链接下载压缩的源代码文件。最后一步是将文件解压缩到您选择的目录中。

您还可以使用git clone命令从 GitHub 存储库中下载 Apache Spark 源代码。这需要在您的计算机上安装 git。Git 可以在 https://git-scm.com/downloads 下载。安装说明可从 https://git-scm.com/book/en/v2/Getting-Started-Installing-Git 获得。在您的计算机上正确安装 Git 后,发出以下命令在 GitHub 上克隆 Apache Spark git 存储库( https://github.com/apache/spark )。

git clone git://github.com/apache/spark.git

一旦 Apache Spark 源代码被下载到你的计算机上,进入 http://spark.apache.org/developer-tools.html 获取关于如何将它们导入到你喜欢的 IDE 中的信息。

摘要

  • 说到学习 Spark,有几个选择。您可以使用本地安装的 Spark,也可以使用协作笔记本社区版。这些工具让任何人学习 Spark 都变得简单方便。

  • Spark shell 是一个强大的交互式环境,用于学习 Spark APIs 和交互式分析数据。有两种类型的 Spark shell,Spark Scala shell 和 Spark Python shell。

  • Spark shell 提供了一组命令来帮助用户提高工作效率。

  • 协作笔记本是一个全面管理的平台,旨在简化构建和部署数据探索、数据管道和机器学习解决方案。交互式工作区提供了一种直观的方式来组织和管理笔记本。每个笔记本都包含标记语句和代码片段的组合。与他人共享笔记本只需点击几下鼠标。

  • 对于有兴趣了解 Spark 内部原理的软件开发人员来说,下载并研究 Apache Spark 源代码是满足这种好奇心的好方法。

三、Spark SQL:基础

随着 Spark 作为统一数据处理引擎的发展和成熟,在每个新版本中都有更多的特性,它的编程抽象也在发展。当 Spark 在 2012 年向世界推出时,弹性分布式数据集 (RDD)是最初的核心编程抽象。在 Spark 版中,引入了一种新的编程抽象,称为结构化 API。这是处理数据工程任务(如执行数据处理或构建数据管道)的新的首选方式。结构化 API 旨在通过易于使用、直观且富于表现力的 API 来提高开发人员的工作效率。新的编程抽象要求数据以结构化格式可用,数据计算逻辑需要遵循一定的结构。有了这两条信息,Spark 就可以执行必要的复杂优化来加速数据处理应用程序。

图 3-1 展示了 Spark SQL 组件是如何构建在可靠的 Spark 核心组件之上的。这种分层架构使其能够轻松利用 Spark 核心组件中引入的任何新改进。

img/419951_2_En_3_Fig1_HTML.jpg

图 3-1

Spark SQL 组件

本章介绍 Spark SQL 模块,它是为结构化数据处理而设计的。它提供了一个易于使用的抽象,用最少的代码来表达数据处理逻辑,并且在它的背后,它智能地执行必要的优化。

Spark SQL 模块由两个主要部分组成。第一个是称为 DataFrame 和 Dataset 的结构 API 的表示,它们定义了用于处理结构化数据的高级 API。DataFrame 概念的灵感来自 Python 熊猫 DataFrame。主要区别在于 Spark 中的 DataFrame 可以处理分布在多台机器上的大量数据。Spark SQL 模块的第二部分是 Catalyst optimizer,它负责所有在幕后工作的复杂机器,使您的生活更加轻松,并最终加快您的数据处理逻辑。Spark SQL 模块提供的一个很酷的功能是执行 SQL 查询来执行数据处理。有了这个功能,Spark 可以获得一个新的用户群,称为业务分析师,他们非常熟悉 SQL 语言,因为这是他们经常使用的主要工具之一。

区分结构化数据和非结构化数据的一个主要概念是模式,它以列名和相关数据类型的形式定义数据结构。模式概念是 Spark 结构化 API 不可或缺的一部分。

结构化数据通常以某种格式捕获。有些格式是基于文本的,有些是基于二进制的。文本数据的常见格式是 CSV、XML 和 JSON,二进制数据的常见格式是 Avro、Parquet 和 ORC。现成的 Spark SQL 模块使得从这些格式中读取数据和向其中写入数据变得非常容易。这种多功能性的一个意想不到的结果是 Spark 可以用作数据格式转换工具。

在进入结构化 API 之前,让我们讨论一下最初的编程抽象,以便更好地理解新抽象背后的动机。

了解 RDD

要真正理解 Spark 是如何工作的,你必须理解 RDD 的本质。它为构建结构化 API 提供了坚实的基础和抽象。简而言之,一个 RDD 代表一个容错的元素集合,这些元素被划分到一个集群中可以并行操作的节点上。它由以下特征组成。

  • 一组对父 rdd 的依赖关系

  • 一组分区,即构成整个数据集的区块

  • 计算数据集中所有行的函数

  • 关于分区方案的元数据(可选)

  • 数据在群集上的驻留位置(可选)

Spark runtime 使用这五条信息来调度和执行使用 RDD 运算表达的数据处理逻辑。

前三条信息组成了血统信息,Spark 将它用于两个目的。第一个是确定 rdd 的执行顺序,第二个是故障恢复。

依赖项集合实质上是 RDD 的输入数据。需要此信息来重现故障场景中的 RDD,因此它提供了弹性特征。

该组分区使 Spark 能够并行执行计算逻辑,以加快计算时间。

Spark 需要产生 RDD 输出的最后一部分是计算功能,它是由 Spark 用户提供的。compute 函数被发送到集群中的每个执行器,以针对每个分区中的每一行执行。

RDD 抽象既简单又灵活。这种灵活性有一个缺点,即 Spark 无法洞察用户的意图。它不知道计算逻辑是在执行数据过滤、连接还是聚合。因此,Spark 不能执行任何优化,例如执行谓词下推以减少从输入源读取的数据量,推荐更有效的连接类型以加快计算速度,或者修剪输出不再需要的列。

DataFrame API 简介

数据帧是组织成行的不可变的分布式数据集合。每一个都由一组列组成,每一列都有一个名称和一个关联的类型。换句话说,这种分布式数据集合具有由模式定义的结构。如果您熟悉关系数据库管理系统(RDBMS)中的表概念,您会意识到数据帧本质上是等价的。一个通用的Row对象代表数据帧中的每一行。与 RDD API 不同,DataFrame APIs 提供了一组特定于域的操作,这些操作是相关的并且具有丰富的语义。在接下来的章节中,您将了解更多关于这些 API 的内容。像 RDD API 一样,DataFrame APIs 分为两种类型:转换和动作。评估语义在 RDD 是相同的。转换被延迟评估,而动作被急切地评估。

可以通过从许多结构化数据源读取数据以及从 Hive 或其他数据库中的表读取数据来创建数据帧。此外,Spark SQL 模块通过提供关于 RDD 中数据的模式信息,提供了将 RDD 轻松转换为数据帧的 API。DataFrame API 有 Scala、Java、Python 和 r 版本。

创建数据帧

有许多方法可以创建数据帧;其中一个共同点是隐式或显式地提供一个模式。

从 RDD 创建数据帧

让我们从从 RDD 创建数据帧开始。清单 3-1 首先创建一个包含两列整数的 RDD。然后它调用toDF隐式函数,该函数使用指定的列名将 RDD 转换为数据帧。列类型是从 RDD 中的数据值推断出来的。清单 3-2 显示了数据帧中两个常用的函数,printSchema,showprintSchema函数将列名及其相关类型打印到控制台。该函数以表格格式打印出数据帧中的数据。默认情况下,它显示 20 行。要更改显示的默认行数,可以将一个数字传递给show函数。清单 3-3 是一个指定要显示的行数的例子。

kvDF.show(5)
+----+------+
| key| value|
+----+------+
|   1|    59|
|   2|    60|
|   3|    66|
|   4|   280|
|   5|    40|
+----+------+

Listing 3-3Call show Function to Display 5 Rows in Tabular Format

kvDF.printSchema
|-- key: integer (nullable = false)
|-- value: integer (nullable = false)

kvDF.show
+----+-------+
| key|  value|
+----+-------+
|   1|     58|
|   2|     18|
|   3|    237|
|   4|     32|
|   5|     80|
|   6|    210|
|   7|    567|
|   8|    360|
|   9|    288|
|  10|    260|
+----+-------+

Listing 3-2Print Schema and Show the Data of a DataFrame

import scala.util.Random
val rdd = spark.sparkContext.parallelize(1 to 10).map(x => (x, Random.nextInt(100)* x))
val kvDF = rdd.toDF("key","value")

Listing 3-1Creating DataFrame from an RDD of Numbers

Note

值列中的实际数字可能看起来不同,因为它们是通过调用Random.nextInt()函数随机生成的。

创建数据帧的另一种方法是指定 RDD 和模式,这可以通过编程方式创建。清单 3-4 首先使用一个行对象数组创建一个 RDD,其中每个行对象包含三列。它以编程方式创建一个模式,最后将 RDD 和模式提供给createDataFrame函数以转换成 DataFrame。清单 3-5 显示了模式和数据帧peopleDF中的数据。

peopleDF.printSchema
 |-- id: long (nullable = true)
 |-- name: string (nullable = true)
 |-- age: long (nullable = true)

peopleDF.show
+----+-------------+----+
| id |        name | age|
+----+-------------+----+
|   1|     John Doe|  30|
|   2|    Mary Jane|  25|
+----+-------------+----+

Listing 3-5Display Schema of peopleDF and Its Data

import org.apache.spark.sql.Row
import org.apache.spark.sql.types._

val peopleRDD = spark.sparkContext.parallelize(Array(Row(1L, "John Doe",  30L),Row(2L, "Mary Jane", 25L)))

val schema = StructType(Array(
        StructField("id", LongType, true),
        StructField("name", StringType, true),
        StructField("age", LongType, true)
))

val peopleDF = spark.createDataFrame(peopleRDD, schema)

Listing 3-4Create a DataFrame from a RDD with a Schema Created Programmatically

编程创建模式的能力使 Spark 应用程序能够根据一些外部配置灵活地调整模式。

每个StructField对象都有三条信息:名称、类型、值是否可以为空。

DataFrame 中的每个列类型都映射到一个内部 Spark 类型,它可以是简单的标量类型,也可以是复杂的类型。表 3-1 按照先标量类型后复杂类型的顺序引用 Spark 中可用的 Scala 类型。

表 3-1

Spark Scala 类型参考

|

数据类型

|

Scala 类型

| | --- | --- | | 布尔类型 | 布尔代数学体系的 | | 字节类型 | 字节 | | 排序方式 | 短的 | | 整合类型 | (同 Internationalorganizations)国际组织 | | LongType(长型) | 长的 | | 浮动型 | 浮动 | | DoubleType(双精度型) | 两倍 | | 十进制 | Java . math . bigdecline | | StringType | 线 | | 二元类型 | 数组[字节] | | TimestampType | java.sql.Timestamp | | datatype(日期类型) | java.sql.Date | | ArrayType | scala.collection.Seq | | 字体渲染 | scala.collection.Map | | 结构类型 | org.apache.spark.sql.Row |

从一系列数字创建数据帧

Spark 2.0 为主要使用数据帧和数据集 API 的 Spark 应用程序引入了新的入口点。这个新的入口点由SparkSession类表示,它有一个叫做range的方便函数,您可以使用它轻松地创建一个包含一列的数据集,该列的名称为id,类型为LongType。这个函数有一些变化,可以使用额外的参数来指定结束和步骤。清单 3-6 提供了使用该函数创建数据帧的例子。

val df1 = spark.range(5).toDF("num").show
+-----+
|  num|
+-----+
|    0|
|    1|
|    2|
|    3|
|    4|
+-----+

spark.range(5,10).toDF("num").show
+-----+
|  num|
+-----+
|    5|
|    6|
|    7|
|    8|
|    9|
+-----+

spark.range(5,15,2).toDF("num").show
+------+
|   num|
+------+
|     5|
|     7|
|     9|
|    11|
|    13|
+------+

Listing 3-6Examples Using SparkSession.range Function to Create a DataFrame

range函数的最后一个版本有三个参数。第一个代表起始值,第二个代表结束值(不含),最后一个代表步长。注意range函数只能创建一个列数据帧。你对如何创建两列数据帧有什么想法吗?

创建多列 DataFrame 的一个选项是使用 Spark 的隐式方法,它转换 Scala Seq 集合中的元组集合。列表 3-7 是 Spark 的toDF隐式的一个例子。

val movies = Seq(("Damon, Matt", "The Bourne Ultimatum", 2007L),
                 ("Damon, Matt", "Good Will Hunting", 1997L))

val moviesDF = movies.toDF("actor", "title", "year")

moviesDF.printSchema
|-- actor: string (nullable = true)
|-- title: string (nullable = true)
|-- year: long (nullable = false)

moviesDF.show
+-----------+--------------------+------+
|      actor|               title|  year|
+-----------+--------------------+------+
|Damon, Matt|The Bourne Ultimatum|  2007|
|Damon, Matt|   Good Will Hunting|  1997|
+-----------+--------------------+------+

Listing 3-7Converting a Collection Tuples to a DataFrame Using Spark’s toDF Implicit

这些创建数据帧的有趣方法使得学习和使用数据帧 API 变得容易,而不需要从一些外部文件加载数据。然而,当您开始对大型数据集执行严肃的数据分析时,必须知道如何从外部数据源加载数据,这将在接下来讨论。

从数据源创建数据帧

Spark SQL 支持一组内置数据源,其中每个数据源都映射到一种数据格式。Spark SQL 模块中的数据源层被设计为可扩展的,因此自定义数据源可以很容易地集成到 DataFrame APIs 中。Spark 社区编写了数百个自定义数据源,实现起来并不太难。

Spark 中用于读写数据的两个主要类分别是DataFrameReaderDataFrameWriter。本节介绍了如何使用DataFrameReader类中的 API,以及从特定数据源读取数据时的各种可用选项。

DataFrameReader类的实例和SparkSession类的read变量一样可用。您可以从 Spark shell 或 Spark 应用程序中引用它,如清单 3-8 所示。

spark.read

Listing 3-8Using read Variable from SparkSession

清单 3-9 中描述了与 DataFrameReader 交互的常见模式。

spark.read.format(...).option("key", value").schema(...).load()

Listing 3-9Common Pattern for Interacting with DataFrameReader

表 3-2 描述了读取数据时使用的三条主要信息:格式、选项和模式。关于这三条信息的更多内容将在本章后面讨论。

表 3-2

DataFrameReader 上的主要信息

|

名字

|

可选择的

|

评论

| | --- | --- | --- | | 格式 | 不 | 它可以是内置数据源之一,也可以是自定义格式。对于内置格式,可以使用短名称(json、parquet、jdbc、orc、csv、text)。对于自定义数据源,需要提供完全限定的名称。参见清单 3-10 中的示例。 | | 选择权 | 是 | DataFrameReader 对每种数据源格式都有一组默认选项。您可以通过提供一个值作为option函数来覆盖这些默认值。 | | 计划 | 是 | 一些数据源在数据文件中嵌入了模式,尤其是 Parquet 和 ORC。在这些情况下,会自动推断出模式。对于其他情况,您可能需要提供一个模式。 |

spark.read.json("<path>")
spark.read.format("json")

spark.read.parquet("<path>")
spark.read.format("parquet")

spark.read.jdbc
spark.read.format("jdbc")

spark.read.orc("<path>")
spark.read.format("orc")

spark.read.csv("<path>")
spark.read.format("csv")

spark.read.text("<path>")
spark.read.format("text")

// custom data source – fully qualified package name
spark.read.format("org.example.mysource")

Listing 3-10Specifying Data Source Format

表 3-3 描述了 Spark 的六个内置数据源,并为每个数据源提供了注释。

表 3-3

Spark 的内置数据源

|

名字

|

数据格式

|

评论

| | --- | --- | --- | | 文本文件 | 文本 | 没有结构。 | | 战斗支援车 | 文本 | 逗号分隔的值。可以指定另一个分隔符。可以从标题中引用列名。 | | 数据 | 文本 | 流行的半结构化格式。列名和数据类型是自动推断的 | | 镶木地板 | 二进制的 | (默认格式)Hadoop 社区中流行的二进制格式。 | | 妖魔 | 二进制的 | Hadoop 社区中另一种流行的二进制格式。 | | 数据库编程 | 二进制的 | 读写 RDBMS 的通用格式。 |

通过读取文本文件创建数据帧

文本文件包含非结构化数据。在读入 Spark 时,每一行都成为数据帧中的一行。在 www.gutenberg.org 有很多纯文本格式的免费书籍可供下载。对于纯文本文件,解析单词的一种常见方法是用空格分隔符分隔每行。这类似于典型的字数统计示例的工作方式。清单 3-11 是一个自述文本文件的例子。

val textFile = spark.read.text("README.md")

textFile.printSchema
|-- value: string (nullable = true)

// show 5 lines and don't truncate
textFile.show(5, false)
+-------------------------------------------------------------------------+
|value                                                                    |
+-------------------------------------------------------------------------+
|# Apache Spark                                                           |
|                                                                         |
|Spark is a fast and general cluster computing system for Big Data. It provides |
|high-level APIs in Scala, Java, Python, and R, and an optimized engine that    |
|supports general computation graphs for data analysis. It also supports a      |
+-------------------------------------------------------------------------+

Listing 3-11Read README.md File as a Text File from Spark Shell

如果文本文件包含可以用来解析每行中的列的分隔符,那么最好使用 CSV 格式来读取它,这将在下一节中介绍。

通过读取 CSV 文件创建数据帧

一种流行的文本文件格式是 CSV,它代表逗号分隔的值。像 Microsoft Excel 这样的流行工具可以轻松地导入和导出 CSV 格式的数据。Spark 中的 CSV 解析器非常灵活,可以使用用户提供的分隔符解析文本文件。逗号分隔符恰好是默认分隔符。这意味着您可以使用 CSV 格式读取制表符分隔值文本文件或其他带有任意分隔符的文本文件。

有些 CSV 文件有文件头,有些没有。由于列值可能包含逗号,因此使用特殊字符对其进行转义是一种常见且良好的做法。表 3-4 描述了处理 CSV 格式时常用的选项。有关选项的完整列表,请参见 https://github.com/apache/sparkCSVOptions类。

表 3-4

CSV 常见选项

|

钥匙

|

价值

|

默认

|

描述

| | --- | --- | --- | --- | | 九月 | 单字符 | , | 用作每列分隔符的单个字符值。 | | 页眉 | 真,假 | 错误的 | 如果该值为 true,则意味着文件中的第一行代表列名。 | | 逃跑 | 任何字符 | \ | 用于转义列值中字符的字符与 sep 相同。 | | 推断模式 | 真,假 | 错误的 | Spark 是否应该尝试根据列值推断列类型。 |

headerinferSchema选项指定为 true 不需要您指定模式。否则,您需要手动或编程定义一个模式,并将其传递给schema函数。如果inferSchema选项为 false,并且没有提供模式,Spark 假定所有列的数据类型都是字符串类型。

您用作示例的数据文件在data/chapter4文件夹中称为movies.csv。该文件包含每一列的标题:actor, title, year.列表 3-12 提供了一些读取 CSV 文件的例子。

val movies = spark.read.option("header","true").csv("<path>/book/chapter4/data/movies/movies.csv")

movies.printSchema
 |-- actor: string (nullable = true)
 |-- title: string (nullable = true)
 |-- year: string (nullable = true)

// now try to infer the schema
val movies2 = spark.read.option("header","true").option("inferSchema","true")
                          .csv("<path>/book/chapter4/data/movies/movies.csv")

movies2.printSchema
 |-- actor: string (nullable = true)
 |-- title: string (nullable = true)
 |-- year: integer (nullable = true)

// now try to manually provide a schema
import org.apache.spark.sql.types._
val movieSchema = StructType(Array(StructField("actor_name", StringType, true),
                                              StructField("movie_title", StringType, true),
                                              StructField("produced_year", LongType, true)))
val movies3 = spark.read.option("header","true").schema(movieSchema)
                                .csv("<path>/book/chapter4/data/movies/movies.csv")

movies3.printSchema
 |-- actor_name: string (nullable = true)
 |-- movie_title: string (nullable = true)
 |-- produced_year: long (nullable = true)

movies3.show(5)

+-----------------+--------------+--------------+
|       actor_name|   movie_title| produced_year|
+-----------------+--------------+--------------+
|McClure, Marc (I)| Freaky Friday|          2003|
|McClure, Marc (I)|  Coach Carter|          2005|
|McClure, Marc (I)|   Superman II|          1980|
|McClure, Marc (I)|     Apollo 13|          1995|
|McClure, Marc (I)|      Superman|          1978|
+-----------------+--------------+--------------+

Listing 3-12Read CSV Files with Various Options

第一个示例读取文件movies.csv,并将第一行指定为标题。Spark 可以识别列名。但是,由于inferSchema选项没有设置为 true,所以所有列的类型都是string。第二个例子添加了inferSchema选项,Spark 能够识别列类型。第三个例子提供了一个列名不同于标题中的列名的模式,因此 Spark 使用所提供的列名。

现在让我们试着用不同的分隔符,而不是逗号来读入一个文本文件。在这种情况下,您为 Spark 使用的 sep 选项指定一个值。清单 3-13 显示了在data/chapter4文件夹中一个名为movies.tsv的文件。

val movies4 = spark.read.option("header","true").option("sep", "\t")
                                        .schema(movieSchema).csv("<path>/book/chapter4/data/movies/movies.tsv")

movies.printSchema
|-- actor_name: string (nullable = true)
|-- movie_title: string (nullable = true)
|-- produced_year: long (nullable = true)

Listing 3-13Read a TSV File with CSV Format

如您所见,处理包含逗号分隔值和其他分隔值的文本文件非常容易。

通过读取 JSON 文件创建数据帧

JSON 是 JavaScript 社区中非常有名的格式。它被认为是半结构化格式,因为每个对象(也称为行)都有一个结构,每个列都有一个名称。在 web 应用程序开发领域,JSON 被广泛用作后端服务器和浏览器端之间传输数据的数据格式。JSON 的优势之一是它提供了一种灵活的格式,可以对任何用例建模,并且它可以支持嵌套结构。JSON 有一个与冗长相关的缺点。数据文件的每一行中都有重复的列名(假设数据文件有一百万行)。

Spark 使得读取 JSON 文件中的数据变得很容易。但是,有一点你需要注意。JSON 对象可以在一行中表示,也可以跨多行表示,这是您需要让 Spark 知道的。假设 JSON 数据文件只包含列名,不包含数据类型,Spark 如何提出模式呢?Spark 尽力通过解析一组样本记录来推断模式。要采样的记录数量由samplingRatio选项决定,其默认值为 1.0。因此,加载一个非常大的 JSON 文件是相当昂贵的。在这种情况下,您可以降低samplingRatio值来加快数据加载过程。表 3-5 描述了 JSON 格式的常用选项列表。

表 3-5

JSON 常见选项

|

钥匙

|

价值

|

默认

|

描述

| | --- | --- | --- | --- | | 允许注释 | 真,假 | 错误的 | 忽略 JSON 文件中的注释 | | 多线 | 真,假 | 错误的 | 将整个文件视为一个跨越多行的大型 JSON 对象 | | 抽样比率 | Zero point three | One | 为推断架构而读取的采样大小 |

清单 3-14 展示了两个读取 JSON 文件的例子。第一个只是读取一个 JSON 文件,而不覆盖任何选项值。注意 Spark 会根据 JSON 文件中的信息自动检测列名和数据类型。第二个示例指定了一个模式。

val movies5 = spark.read.json("<path>/book/chapter4/data/movies/movies.json")

movies.printSchema
 |-- actor_name: string (nullable = true)
 |-- movie_title: string (nullable = true)
 |-- produced_year: long (nullable = true)

// specify a schema to override the Spark's inferring schema.
// producted_year is specified as integer type
import org.apache.spark.sql.types._
val movieSchema2 = StructType(Array(StructField("actor_name", StringType, true),
                                              StructField("movie_title", StringType, true),
                                              StructField("produced_year", IntegerType, true)))

val movies6 = spark.read.option("inferSchema","true").schema(movieSchema2)
                                        .json("<path>/book/chapter4/data/movies/movies.json")

movies6.printSchema
 |-- actor_name: string (nullable = true)
 |-- movie_title: string (nullable = true)
 |-- produced_year: integer (nullable = true)

Listing 3-14Various Example of Reading a JSON File

当模式中指定的列数据类型与 JSON 文件中的值不匹配时会发生什么?默认情况下,当 Spark 遇到损坏的记录或遇到解析错误时,它会将该行中所有列的值设置为 null。您可以告诉 Spark 快速失败,而不是获得空值。清单 3-15 通过将mode选项指定为failFast来告诉 Spark 的解析逻辑快速失败。

// set data type for actor_name as BooleanType
import org.apache.spark.sql.types._
val badMovieSchema = StructType(Array(StructField("actor_name", BooleanType, true),
                                               StructField("movie_title", StringType, true),
                                               StructField("produced_year", IntegerType, true)))

val movies7 = spark.read.schema(badMovieSchema)
                                        .json("<path>/book/chapter4/data/movies/movies.json")
movies7.printSchema
 |-- actor_name: boolean (nullable = true)
 |-- movie_title: string (nullable = true)
 |-- produced_year: integer (nullable = true)

movies7.show(5)
+----------+-----------+-------------+
|actor_name|movie_title|produced_year|
+----------+-----------+-------------+
|      null|       null|         null|
|      null|       null|         null|
|      null|       null|         null|
|      null|       null|         null|
|      null|       null|         null|
+----------+-----------+-------------+

// tell Spark to fail fast when facing a parsing error
val movies8 = spark.read.option("mode","failFast").schema(badMovieSchema)
                                        .json("<path>/book/chapter4/data/movies/movies.json")

movies8.printSchema
 |-- actor_name: boolean (nullable = true)
 |-- movie_title: string (nullable = true)
 |-- produced_year: integer (nullable = true)

// Spark will throw a RuntimeException when executing an action
movies8.show(5)
ERROR Executor: Exception in task 0.0 in stage 3.0 (TID 3)
java.lang.RuntimeException

: Failed to parse a value for data type BooleanType (current token: VALUE_STRING).


Listing 3-15Parsing Error and How to Tell Spark to Fail Fast

通过读取拼花文件创建数据帧

Parquet 是 Hadoop 生态系统中最流行的开源列存储格式之一。它是在 Twitter 上创建的。它的流行是由于它的自描述数据格式,并且它通过利用压缩以高度紧凑的结构存储数据。列存储格式旨在很好地处理数据分析工作负载,在数据分析过程中只使用一小部分列。Parquet 将每一列的数据存储在一个单独的文件中;因此,数据分析中不需要的列不必读入。在支持具有嵌套结构的复杂数据类型时,它非常灵活。像 CSV 和 JSON 这样的文本文件格式对于小文件来说是很好的,它们是人类可读的。对于处理大型数据集来说,Parquet 是一种更好的文件格式,可以降低存储成本并加快读取速度。如果你偷看一下chapter4/data/movies文件夹中的movies.parquet文件,你会看到它的大小大约是movies.csv的六分之一。

Spark 与 Parquet 文件格式配合得非常好,事实上,Parquet 是 Spark 中读写数据的默认文件格式。清单 3-16 显示了一个读取拼花文件的例子。注意,您不需要提供一个模式,也不需要让 Spark 推断模式。Spark 可以从 Parquet 文件中检索模式。

Spark 在从 Parquet 读取数据时做的一个很酷的优化是按列批次解压缩和解码,这大大加快了读取速度。

// Parquet is the default format, so we don't need to specify the format when reading
val movies9 = spark.read.load("<path>/book/chapter4/data/movies/movies.parquet")
movies9.printSchema
 |-- actor_name: string (nullable = true)
 |-- movie_title: string (nullable = true)
 |-- produced_year: long (nullable = true)

// If we want to more explicit, we can specify the path to the parqet function
val movies10 = spark.read.parquet("<path>/book/chapter4/data/movies/movies.parquet")
movies10.printSchema
 |-- actor_name: string (nullable = true)
 |-- movie_title: string (nullable = true)
 |-- produced_year: long (nullable = true)

Listing 3-16Reading a Parquet File

in Spark

通过读取 ORC 文件创建数据帧

优化行列(ORC)是 Hadoop 生态系统中另一种流行的开源自描述列存储格式。它是由 Cloudera 创建的,作为大规模加速 Hive 计划的一部分。在效率和速度方面,它与 Parquet 非常相似,是为分析工作负载而设计的。使用 ORC 文件就像使用拼花文件一样简单。清单 3-17 显示了一个从 ORC 文件中读取数据来创建数据帧的例子。

val movies11 = spark.read.orc("<path>/book/chapter4/data/movies/movies.orc")
movies11.printSchema
 |-- actor_name: string (nullable = true)
 |-- movie_title: string (nullable = true)
 |-- produced_year: long (nullable = true)

movies11.show(5)
+--------------------------+-------------------+--------------+
|                actor_name|        movie_title| produced_year|
+--------------------------+-------------------+--------------+
|         McClure, Marc (I)|       Coach Carter|          2005|
|         McClure, Marc (I)|        Superman II|          1980|
|         McClure, Marc (I)|          Apollo 13|          1995|
|         McClure, Marc (I)|           Superman|          1978|
|         McClure, Marc (I)| Back to the Future|          1985|
+--------------------------+-------------------+--------------+

Listing 3-17Reading ORC File in Spark

从 JDBC 创建数据帧

JDBC 是一个标准的应用程序 API,用于从关系数据库管理系统读取数据和向关系数据库管理系统写入数据。Spark 支持 JDBC 数据源,这意味着您可以使用 Spark 从任何现有的 RDBMSs(如 MySQL、PostgreSQL、Oracle、SQLite 等)读取数据和向其中写入数据。使用 JDBC 数据源时,需要提供一些重要的信息:RDBMS 的 JDBC 驱动程序、连接 URL、认证信息和表名。

为了让 Spark 连接到 RDBMS,它必须能够在运行时访问 JDBC 驱动程序 JAR 文件。因此,需要将 JDBC 驱动程序的位置添加到 Spark 类路径中。清单 3-18 展示了如何从 Spark Shell 连接到 MySQL。

 ./bin/spark-shell ../jdbc/mysql-connector-java-5.1.45/mysql-connector-java-5.1.45-bin.jar  --jars ../jdbc/mysql-connector-java-5.1.45/mysql-connector-java-5.1.45-bin.jar

Listing 3-18Specifying a JDBC Driver When Starting the Spark Shell

一旦 Spark shell 成功启动,您可以通过使用java.sql.DriverManager快速验证 Spark 是否可以连接到您的 RDBMS,如清单 3-19 所示。这个例子试图测试一个到 MySQL 的连接。如果你的 RDBMS 不是 MySQL,URL 格式会有一点不同,所以请查阅你正在使用的 JDBC 驱动程序的文档。

import java.sql.DriverManager
val connectionURL = "jdbc:mysql://localhost:3306/<table>?user=<username>&password=<password>"
val connection = DriverManager.getConnection(connectionURL)
connection.isClosed()
connection close()

Listing 3-19Testing Connection to MySQL in Spark Shell

如果您没有得到任何关于连接的异常,Spark shell 可以成功地连接到您的 RDBMS。

表 3-6 描述了使用 JDBC 驱动程序时需要指定的主要选项。有关选项的完整列表,请咨询 https://spark.apache.org/docs/latest/sql-programming-guide.html#jdbc-to-other-databases

表 3-6

JDBC 数据源的主要选项

|

钥匙

|

描述

| | --- | --- | | 全球资源定位器(Uniform Resource Locator) | Spark 要连接到的 JDBC URL。它至少应该包含主机、端口和数据库名称。对于 MySQL,它可能看起来像 JDBC:MySQL://localhost:3306/saki la。 | | 成问题的 | Spark 读取或写入数据的数据库表的名称。 | | 驾驶员 | Spark 实例化以连接到前面的 URL 的 JDBC 驱动程序的类名。请查阅您正在使用的 JDBC 驱动程序文档。对于 MySQL 连接器/J 驱动,类名为com.mysql.jdbc.Driver。 |

清单 3-20 展示了一个从 MySQL 服务器中的 Sakila 数据库的 film 表中读取数据的例子。

val mysqlURL= "jdbc:mysql://localhost:3306/sakila"
val filmDF = spark.read.format("jdbc").option("driver", "com.mysql.jdbc.Driver")
                                                        .option("url", mysqlURL)
                                                        .option("dbtable", "film")
                                                        .option("user", "<username>")
                                                        .option("password","<pasword>")
                                                        .load()

filmDF.printSchema
 |-- film_id: integer (nullable = false)
 |-- title: string (nullable = false)
 |-- description: string (nullable = true)
 |-- release_year: date (nullable = true)
 |-- language_id: integer (nullable = false)
 |-- original_language_id: integer (nullable = true)
 |-- rental_duration: integer (nullable = false)
 |-- rental_rate: decimal(4,2) (nullable = false)
 |-- length: integer (nullable = true)
 |-- replacement_cost: decimal(5,2) (nullable = false)
 |-- rating: string (nullable = true)
 |-- special_features: string (nullable = true)
 |-- last_update: timestamp (nullable = false)

filmDF.select("film_id","title").show(5)

+-------+---------------------+
|film_id|                title|
+-------+---------------------+
|      1|     ACADEMY DINOSAUR|
|      2|       ACE GOLDFINGER|
|      3|     ADAPTATION HOLES|
|      4|     AFFAIR PREJUDICE|
|      5|          AFRICAN EGG|
+-------+---------------------+

Listing 3-20Reading Data from a Table in MySQL Server

当使用 JDBC 数据源时,Spark 将过滤条件尽可能向下推到 RDBMS。通过这样做,大部分数据在 RDBMS 级别被过滤掉,因此这加快了数据过滤逻辑,并大大减少了 Spark 需要读取的数据量。这种优化被称为谓词下推,当 Spark 知道数据源可以支持过滤功能时,它经常这样做。Parquet 是另一个具有这种能力的数据源。第四章中的“Catalyst Optimizer”部分提供了一个示例。

使用结构化操作

现在您已经知道如何创建数据帧,下一步是学习如何使用结构化操作来操作或转换它们。与 RDD 运算不同,结构化运算被设计为更加关系化,这意味着这些运算反映了您可以使用 SQL 执行的表达式类型,如投影、过滤、转换、连接等。与 RDD 操作类似,结构化操作也分为两类:转换和操作。结构化转换和动作的语义与 rdd 中的相同。换句话说,结构化转换被延迟评估,而结构化动作被急切地评估。

结构化操作有时被描述为分布式数据操作的领域特定语言(DSL)。DSL 是一种专门用于特定应用领域的计算机语言。在这种情况下,应用程序域是分布式数据操作。如果你曾经使用过 SQL,那么学习结构化操作是很容易的。

表 3-7 描述了常用的数据帧结构化转换。提醒一下,数据帧是不可变的,它的转换操作总是返回一个新的数据帧。

表 3-7

常用的数据帧结构化转换

|

操作

|

描述

| | --- | --- | | 挑选 | 从数据帧中的现有列集中选择一个或多个列。select 的一个更专业的术语是投影。在投影过程中,可以变换和操纵柱。 | | 选择表达式 | 类似于 select,但在转换每一列时提供了强大的 SQL 表达式。 | | 过滤器在哪里 | filterwhere都有相同的语义。where与 SQL 中的 where 条件更加相关和相似。它们都用于根据给定的布尔条件过滤行。 | | 明显的删除重复项 | 从数据帧中删除重复的行 | | 分类排序依据 | 按提供的列对数据帧进行排序 | | 限制 | 通过取前“n”行返回一个新的数据帧。 | | 联盟 | 合并两个数据帧中的行,并将其作为新的数据帧返回。 | | 带栏 | 用于在数据帧中添加列或替换现有列 | | 带列名 | 重命名现有列。如果给定的列名在模式中不存在,那么它就是一个空操作。 | | 滴 | 从 DataFrame 中删除一列或多列。如果模式不包含给定的列名,则该操作不执行任何操作 | | 样品 | 根据给定的分数、可选的种子值和可选的替换选项,随机选择一组行。 | | 随机拆分 | 基于给定的权重将数据帧分割成一个或多个数据帧。在机器学习过程中,将主数据集分为训练数据集和测试数据集。 | | 加入 | 连接两个数据帧。Spark 支持许多类型的连接。更多信息将在下一章介绍。 | | 群组依据 | 按一列或多列对数据帧进行分组。常见的模式是在 groupBy 之后执行聚合。更多信息将在下一章介绍。 |

使用列

表 3-7 中的大多数数据帧结构化操作需要你指定一个或多个列。对于某些应用程序,列是在字符串中指定的;对于其他的,列需要被指定为Column类的实例。质疑为什么有两种选择,什么时候用什么,是完全公平的。要回答这些问题,您需要理解Column类提供的功能。在较高层次上,Column 类的功能可以分为以下几类。

  • 像加法、乘法等数学运算

  • 列值或文字之间的逻辑比较,如等于、大于和小于

  • 字符串模式匹配,如以开头,以结尾,等等。

关于Column类中可用函数的完整列表,请参考位于 https://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Column 的 Scala 文档。

了解了Column类提供的功能后,您可以得出结论,无论何时需要指定列表达式,都有必要将列指定为Column类的实例,而不是字符串。接下来的例子说明了这一点。

引用一个专栏有不同的方式,这在 Spark 用户社区中造成了混乱。一个常见的问题是何时使用哪一个,答案是——视情况而定。表 3-8 描述了可用的功能选项。

表 3-8

引用列的方式

|

功能

|

例子

|

描述

| | --- | --- | --- | | "" | "columnName" | 将列作为字符串类型引用。 | | col | col(``columnName``) | col函数返回Column类的一个实例。 | | column | column(``columnName``) | 类似于col,这个函数返回一个Column类的实例。 | | $ | $``columnName | 一种仅在 Scala 中构造Column类的语法糖方式。 | | (tick) | 'columnName | 一种利用 Scala 符号文字特性在 Scala 中构造Column类的语法糖方式。 |

colcolumn函数是同义的,在 Scala 和 Python Spark APIs 中都有。如果你经常在 Spark Scala 和 Python APIs 之间切换,那么使用col函数是有意义的,这样你的代码就有了一致性。如果您主要或专门使用 Spark Scala APIs,那么我建议您使用'(撇号),因为只需要键入一个字符。DataFrame 类有自己的col函数,在执行连接时,该函数可以区分两个或更多 data frame 中同名的列。清单 3-21 提供了引用一个列的不同方法的例子。

import org.apache.spark.sql.functions._

val kvDF = Seq((1,2),(2,3)).toDF("key","value")

// to display column names in a DataFrame, we can call the columns function
kvDF.columns
Array[String] = Array(key, value)

kvDF.select("key")
kvDF.select(col("key"))
kvDF.select(column("key"))
kvDF.select($"key")
kvDF.select('key)

// using the col function of DataFrame
kvDF.select(kvDF.col("key"))

kvDF.select('key, 'key > 1).show
+---+----------+
|key| (key > 1)|
+---+----------+
|  1|     false|
|  2|      true|
+---+----------+

Listing 3-21Different Ways of Referring to Columns

这个例子演示了一个列表达式,因此需要指定一个列作为Column类的实例。如果列被指定为字符串,则会导致类型不匹配错误。在各种数据帧结构操作的例子中可以找到更多的列表达式的例子。

使用结构化转换

本节提供了表 3-7 中列出的结构化转换的使用示例。为了保持一致,所有示例都一致使用'(撇号)来指代数据帧中的列。为了减少冗余,大多数例子都引用通过读取拼花文件创建的movies数据帧(参见清单 3-22 )。

val movies = spark.read.parquet("<path>/chapter4/data/movies/movies.parquet")

Listing 3-22Creating the movies DataFame from a Parquet File

选择(列)

这种转换通常执行投影,从数据帧中选择所有列或列的子集。在选择过程中,每个列都可以通过列表达式进行转换。这种转换有两种变体。一个将列作为字符串,另一个将列作为Column类。这种转换不允许您在使用这两种变体之一时混合使用列类型。清单 3-23 是这两种变化的一个例子。

movies.select("movie_title","produced_year").show(5)
+------------------------+--------------+
|             movie_title| produced_year|
+------------------------+--------------+
|            Coach Carter|          2005|
|             Superman II|          1980|
|               Apollo 13|          1995|
|                Superman|          1978|
|      Back to the Future|          1985|
+------------------------+--------------+

// using a column expression to transform year to decade
movies.select('movie_title,('produced_year - ('produced_year % 10)).as("produced_decade")).show(5)

+------------------------+----------------+
|             movie_title| produced_decade|
+------------------------+----------------+
|            Coach Carter|            2000|
|             Superman II|            1980|
|               Apollo 13|            1990|
|                Superman|            1970|
|      Back to the Future|            1980|
+------------------------+----------------+

Listing 3-23Two Variations of Select Transformation

第二个例子需要两个列表达式:取模和减法。两者都是通过Column类中的模(%)和减法(-)函数实现的(参见 Scala 文档)。默认情况下,Spark 使用列表达式作为结果列的名称。为了提高可读性,as函数将其重命名为一个更易于阅读的列名。作为一个敏锐的读者,您可能会发现可以向 DataFrame 添加一列或多列的 select 转换。

selectExpr(表示式)

此转换是 select 转换的变体。一个很大的区别是它接受一个或多个 SQL 表达式,而不是列。然而,两者本质上都在执行相同的投射任务。SQL 表达式是强大而灵活的构造,允许您自然地表达列转换逻辑,就像您思考的方式一样。您可以用字符串格式表示 SQL 表达式,Spark 将它们解析成一个逻辑树,以正确的顺序对它们求值。

如果您想创建一个包含电影数据帧中所有列的新数据帧,并引入一个新的列来表示电影制作的年代,请执行清单 3-24 中所示的操作。

movies.selectExpr("*","(produced_year - (produced_year % 10)) as decade").show(5)
+-----------------+--------------------+-------------------+----------+
|       actor_name|         movie_title|      produced_year|    decade|
+-----------------+--------------------+-------------------+----------+
|McClure, Marc (I)|        Coach Carter|               2005|      2000|
|McClure, Marc (I)|         Superman II|               1980|      1980|
|McClure, Marc (I)|           Apollo 13|               1995|      1990|
|McClure, Marc (I)|            Superman|               1978|      1970|
|McClure, Marc (I)|  Back to the Future|               1985|      1980|
+-----------------+--------------------+-------------------+----------+

Listing 3-24Adding the Decade Column to Movies DataFrame using SQL Expression

SQL 表达式和内置函数的结合使得执行数据分析变得容易,否则需要多个步骤。清单 3-25 展示了在一条语句中确定电影数据集中唯一电影标题和唯一演员的数量是多么容易。count函数对整个数据帧执行聚合。

movies.selectExpr("count(distinct(movie_title)) as movies","count(distinct(actor_name)) as actors").show
+---------+--------+
|   movies| actors |
+---------+--------+
|     1409|   6527 |
+---------+--------+

Listing 3-25Using SQL Expression and Built-in Functions

填充符(条件),其中(条件)

这种转变很简单。它过滤掉不满足给定条件的行,换句话说,当条件评估为 false 时。看待筛选转换行为的另一种方式是,它只返回满足指定条件的行。给定的条件可以是简单的,也可以是复杂的。使用这种转换需要知道如何利用Column类中的一些逻辑比较函数,比如等于、小于、大于和不等于。filterwhere转换的行为是一样的,所以选择一个你觉得最舒服的。后者只是比前者关系更密切一点。清单 3-26 展示了一些过滤的例子。

movies.filter('produced_year < 2000)
movies.where('produced_year > 2000)

movies.filter('produced_year >= 2000)
movies.where('produced_year >= 2000)

// equality comparison require 3 equal signs
movies.filter('produced_year === 2000).show(5)
+-------------------+---------------------------+--------------+
|         actor_name|                movie_title| produced_year|
+-------------------+---------------------------+--------------+
|  Cooper, Chris (I)|         Me, Myself & Irene|          2000|
|  Cooper, Chris (I)|                The Patriot|          2000|
|    Jolie, Angelina|       Gone in Sixty Sec...|          2000|
|     Yip, Françoise|             Romeo Must Die|          2000|
|     Danner, Blythe|           Meet the Parents|          2000|
+-------------------+---------------------------+--------------+

// inequality comparison uses an interesting looking operator =!=
movies.select("movie_title","produced_year").filter('produced_year =!= 2000).show(5)
+-------------------+--------------+
|        movie_title| produced_year|
+-------------------+--------------+
|       Coach Carter|          2005|
|        Superman II|          1980|
|          Apollo 13|          1995|
|           Superman|          1978|
| Back to the Future|          1985|
+-------------------+--------------+

// to combine one or more comparison

expressions, we will use either the OR and AND expression operator
movies.filter('produced_year >= 2000 && length('movie_title) < 5).show(5)
+----------------+------------+--------------+
|      actor_name| movie_title| produced_year|
+----------------+------------+--------------+
| Jolie, Angelina|        Salt|          2010|
|  Cueto, Esteban|         xXx|          2002|
|   Butters, Mike|         Saw|          2004|
|  Franko, Victor|          21|          2008|
|   Ogbonna, Chuk|        Salt|          2010|
+----------------+------------+--------------+

// the other way of accomplishing the result is by calling the filter function two times
movies.filter('produced_year >= 2000).filter(length('movie_title) < 5).show(5)

Listing 3-26Filter Rows with Logical Comparison Functions in Column Class

不同,删除重复项

这两种转换具有相同的行为。但是,dropDuplicates允许您控制应该在重复数据删除逻辑中使用哪些列。如果未指定,重复数据删除逻辑将使用DataFrame中的所有列。清单 3-27 展示了计算电影数据集中有多少部电影的不同方法。

movies.select("movie_title").distinct.selectExpr("count(movie_title) as movies").show
movies.dropDuplicates("movie_title").selectExpr("count(movie_title) as movies").show

+--------+
|  movies|
+--------+
|    1409|
+--------+

Listing 3-27Using distinct and dropDuplicates to Achieve the Same Goal

就性能而言,这两种方法没有区别,因为 Spark 将它们转换为相同的逻辑计划。

排序(列),排序依据(列)

两种转换具有相同的语义。orderBy转换比另一个转换更有关系。默认情况下,排序是升序,很容易将其更改为降序。当指定多个列时,每个列可能有不同的顺序。清单 3-28 有一些例子。

val movieTitles = movies.dropDuplicates("movie_title")
                                       .selectExpr("movie_title", "length(movie_title) as title_length", , "produced_year")

movieTitles.sort('title_length).show(5)
+-----------+-------------+--------------+
|movie_title| title_length| produced_year|
+-----------+-------------+--------------+
|         RV|            2|          2006|
|         12|            2|          2007|
|         Up|            2|          2009|
|         X2|            2|          2003|
|         21|            2|          2008|
+-----------+-------------+--------------+

// sorting in descending order
movieTitles.orderBy('title_length.desc).show(5)
+---------------------+-------------+--------------+
|          movie_title| title_length| produced_year|
+---------------------+-------------+--------------+
| Borat: Cultural L...|           83|          2006|
| The Chronicles of...|           62|          2005|
| Hannah Montana & ...|           57|          2008|
| The Chronicles of...|           56|          2010|
| Istoriya pro Rich...|           56|          1997|
+---------------------+-------------+--------------+

// sorting by two columns in different orders
movieTitles.orderBy('title_length.desc, 'produced_year).show(5)
+---------------------+-------------+--------------+
|          movie_title| title_length| produced_year|
+---------------------+-------------+--------------+
| Borat: Cultural L...|           83|          2006|
| The Chronicles of...|           62|          2005|
| Hannah Montana & ...|           57|          2008|
| Istoriya pro Rich...|           56|          1997|
| The Chronicles of...|           56|          2010|
+---------------------+-------------+--------------+

Listing 3-28Sorting the DataFrame in Ascending and Descending Order

请注意,最后两部电影的片名长度相同,但它们的年份是按照正确的升序排列的。

极限值

该转换通过获取前 n 行返回一个新的数据帧。这种转换通常在排序完成后使用,以便根据排序顺序找出顶部的 n 或底部的 n 行。清单 3-20 展示了一个使用limit转换来查找名字最长的前十名演员的例子。

// first create a DataFrame with their name and associated length
val actorNameDF = movies.select("actor_name").distinct.selectExpr("*", "length(actor_name) as length")

// order names by length and retrieve the top 10
actorNameDF.orderBy('length.desc).limit(10).show
+--------------------------------+-------+
|              actor_name        | length|
+--------------------------------+-------+
|    Driscoll, Timothy 'TJ' James|     28|
|    Badalamenti II, Peter Donald|     28|
|    Shepard, Maridean Mansfield |     27|
|    Martino, Nicholas Alexander |     27|
|    Marshall-Fricker, Charlotte |     27|
|    Phillips, Christopher (III) |     27|
|    Pahlavi, Shah Mohammad Reza |     27|
|    Juan, The Bishop Don Magic  |     26|
|    Van de Kamp Buchanan, Ryan  |     26|
|     Lough Haggquist, Catherine |     26|
+--------------------------------+-------+

Listing 3-29Using the limit Transformation to Figure Top Ten Actors with the Longest Name

联盟(奥赛达费布雷省)

你知道了数据帧是不可变的。如果需要向现有的数据帧中添加更多的行,那么union转换对于这个目的以及合并两个数据帧中的行是有用的。这种转换要求两个数据帧具有相同的模式,这意味着两个列名及其顺序必须完全匹配。假设数据帧中的一部电影缺少一个演员,而您想要修复这个问题。清单 3-30 展示了如何使用联合转换来实现这一点。

// the movie we want to add missing actor is "12"
val shortNameMovieDF = movies.where('movie_title === "12")
shortNameMovieDF.show
+---------------------+------------+---------------+
|           actor_name| movie_title| produced_year |
+---------------------+------------+---------------+
|     Efremov, Mikhail|          12|           2007|
|      Stoyanov, Yuriy|          12|           2007|
|      Gazarov, Sergey|          12|           2007|
| Verzhbitskiy, Viktor|          12|           2007|
+---------------------+------------+---------------+

// create a DataFrame with one row
import org.apache.spark.sql.Row
val forgottenActor = Seq(Row("Brychta, Edita", "12", 2007L))
val forgottenActorRDD = spark.sparkContext.parallelize(forgottenActor)
val forgottenActorDF = spark.createDataFrame(forgottenActorRDD, shortNameMovieDF.schema)

// now adding the missing action

val completeShortNameMovieDF = shortNameMovieDF.union(forgottenActorDF)
completeShortNameMovieDF.union(forgottenActorDF).show
+----------------------+------------+---------------+
|            actor_name| movie_title|  produced_year|
+----------------------+------------+---------------+
|      Efremov, Mikhail|          12|           2007|
|       Stoyanov, Yuriy|          12|           2007|
|       Gazarov, Sergey|          12|           2007|
|  Verzhbitskiy, Viktor|          12|           2007|
|        Brychta, Edita|          12|           2007|
+----------------------+------------+---------------+

Listing 3-30Add a Missing Actor to the movies DataFrame

withColumn(colName, column)

这种转换向数据帧添加了一个新列。它需要两个输入参数;一个列名和一个列表达式形式的值。您可以通过使用selectExpr转换来完成几乎相同的目标。但是,如果给定的列名与某个现有列名匹配,则该列将被给定的列表达式替换。清单 3-31 提供了添加新列以及替换现有列的例子。

// adding a new column based on a certain column expression
movies.withColumn("decade", ('produced_year - 'produced_year % 10)).show(5)
+------------------+------------------------+--------------+-----------+
|        actor_name|             movie_title| produced_year|     decade|
+------------------+------------------------+--------------+-----------+
| McClure, Marc (I)|            Coach Carter|          2005|       2000|
| McClure, Marc (I)|             Superman II|          1980|       1980|
| McClure, Marc (I)|               Apollo 13|          1995|       1990|
| McClure, Marc (I)|                Superman|          1978|       1970|
| McClure, Marc (I)|      Back to the Future|          1985|       1980|
+------------------+------------------------+--------------+-----------+

// now replace the produced_year with new values
movies.withColumn("produced_year", ('produced_year - 'produced_year % 10)).show(5)
+------------------+-------------------+--------------+
|        actor_name|        movie_title| produced_year|
+------------------+-------------------+--------------+
| McClure, Marc (I)|       Coach Carter|          2000|
| McClure, Marc (I)|        Superman II|          1980|
| McClure, Marc (I)|          Apollo 13|          1990|
| McClure, Marc (I)|           Superman|          1970|
| McClure, Marc (I)| Back to the Future|          1980|
+------------------+-------------------+--------------+

Listing 3-31Add as Well Replacing a Column Using withColumn Transformation

with column renamed(existing colname,newColName)

这种转换严格地说是重命名数据帧中现有的列名。有理由问为什么 Spark 会提供这种转变。事实证明,这种转换在以下情况下很有用。

  • 将一个晦涩的列名重命名为更人性化的名称。神秘的列名可能来自您无法控制的现有模式,例如,当您公司的合作伙伴在一个拼花文件中生成您需要的列时。

  • 在连接两个碰巧有一个或多个相同列名的数据帧之前。这种转换可以重命名两个数据帧之一中的一个或多个列,因此在连接后可以很容易地引用它们。

注意,如果提供的existingColName在模式中不存在,Spark 不会抛出错误,它也不会做任何事情。清单 3-32 将movies DataFrame 中的一些列名重命名为简称。顺便说一下,这也可以通过使用选择或selectExpr转换来实现。我把这个留给你做练习。

movies.withColumnRenamed("actor_name", "actor")
           .withColumnRenamed("movie_title", "title")
           .withColumnRenamed("produced_year", "year").show(5)
+------------------+--------------------+------+
|             actor|               title|  year|
+------------------+--------------------+------+
| McClure, Marc (I)|        Coach Carter|  2005|
| McClure, Marc (I)|         Superman II|  1980|
| McClure, Marc (I)|           Apollo 13|  1995|
| McClure, Marc (I)|            Superman|  1978|
| McClure, Marc (I)|  Back to the Future|  1985|
+------------------+--------------------+------+

Listing 3-32Using withColumnRenamed Transformation to Rename Some of the Column Names

drop(列名称 1,列名称 2)

这种转换只是从数据帧中删除指定的列。您可以指定一个或多个要删除的列名,但是只删除模式中存在的列名,而忽略不存在的列名。您可以使用select转换,通过投影出您想要保留的列来删除列。但是,如果一个 DataFrame 有 100 列,而您想删除几列,那么这个转换比select转换更方便使用。清单 3-33 提供了删除列的示例。

movies.drop("actor_name", "me").printSchema
 |-- movie_title: string (nullable = true)
 |-- produced_year: long (nullable = true)

Listing 3-33Drop Two Columns, One Exists and the Other One Doesn’t

如您所见,第二列"me"在模式中不存在,drop 转换简单地忽略了它。

样本(分数),样本(分数,种子),样本(分数,种子,替换)

该转换从数据帧中返回一组随机选择的行。返回的行数大约等于指定的分数,表示一个百分比,该值必须介于 0 和 1 之间。种子植入随机数生成器,该生成器生成一个要包含在结果中的行号。如果未指定种子,则使用随机生成的值。withReplacement选项决定是否将随机选择的行放回选择池中。换句话说,当withReplacement为真时,特定的选定行有可能被选择不止一次。那么,什么时候需要使用这种转换呢?当原始数据集很大,并且需要将其缩减到较小的尺寸以便快速迭代数据分析逻辑时,这是非常有用的。清单 3-34 提供了使用sample转换的例子。

// sample with no replacement and a ratio
movies.sample(false, 0.0003).show(3)
+--------------------+----------------------+--------------+
|          actor_name|           movie_title| produced_year|
+--------------------+----------------------+--------------+
|     Lewis, Clea (I)|  Ice Age: The Melt...|          2006|
|      Lohan, Lindsay|   Herbie Fully Loaded|          2005|
|Tagawa, Cary-Hiro...|       Licence to Kill|          1989|
+--------------------+----------------------+--------------+

// sample with replacement, a ratio and a seed
movies.sample(true, 0.0003, 123456).show(3)
+---------------------+-----------------+--------------+
|           actor_name|      movie_title| produced_year|
+---------------------+-----------------+--------------+
| Panzarella, Russ (V)|   Public Enemies|          2009|
|         Reed, Tanoai|        Daredevil|          2003|
|         Moyo, Masasa|     Spider-Man 3|          2007|
+---------------------+-----------------+--------------+

Listing 3-34Different ways of Using the sample Transformation

如你所见,返回的电影是相当随机的。

随机拆分(重量)

这种转换通常在准备数据以训练机器学习模型的过程中使用。与前面的转换不同,这个转换返回一个或多个数据帧。它返回的数据帧数量基于您指定的权重数量。如果这组权重的总和不等于 1,则它们会被相应地归一化为总和为 1。清单 3-35 提供了一个将电影数据帧分割成三个小帧的例子。

// the weights need to be an Array
val smallerMovieDFs = movies.randomSplit(Array(0.6, 0.3, 0.1))

// let's see if the counts are added up to the count of movies DataFrame
movies.count
Long = 31393

smallerMovieDFs(0).count
Long = 18881

smallerMovieDFs(0).count + smallerMovieDFs(1).count + smallerMovieDFs(2).count
Long = 31393

Listing 3-35Use randomSplit to Split movies DataFrame into Three Parts

处理缺失或错误的数据

实际上,您经常使用的数据并不像您希望的那样干净。可能是因为数据在发展,因此有些列有值,有些没有。在数据操作逻辑的开始处理这类问题是很重要的,以防止任何不愉快的意外,导致长时间运行的数据处理作业停止工作。

Spark 社区认识到处理缺失数据的需要是生活中的现实。因此,Spark 提供了一个名为DataFrameNaFunctions的专用类来帮助处理这个不方便的问题。DataFrameNaFunctions的一个实例可以作为DataFrame类中的an成员变量。有三种常见的处理缺失或错误数据的方法。第一种方法是删除一列或多列中缺少值的行。第二种方法是用用户提供的值来填充那些缺失的值。第三种方法是用你知道如何处理的东西替换坏数据。

让我们从删除丢失数据的行开始。您可以告诉 Spark 删除任何一列或只有特定列有缺失数据的行。清单 3-36 显示了删除丢失数据的行的几种不同方式。

// first create a DataFrame with missing values in one or more columns
import org.apache.spark.sql.Row

val badMovies = Seq(Row(null, null, null),
                    Row(null, null, 2018L),
                    Row("John Doe", "Awesome Movie", null),
                    Row(null, "Awesome Movie", 2018L),
                    Row("Mary Jane", null, 2018L))
val badMoviesRDD = spark.sparkContext.parallelize(badMovies)
val badMoviesDF = spark.createDataFrame(badMoviesRDD, movies.schema)
badMoviesDF.show
+-----------+-----------------+--------------+
| actor_name|      movie_title| produced_year|
+-----------+-----------------+--------------+
|       null|             null|          null|
|       null|             null|          2018|
|   John Doe|    Awesome Movie|          null|
|       null|    Awesome Movie|          2018|
|  Mary Jane|             null|          2018|
+-----------+-----------------+--------------+

// dropping rows that have missing data in any column
// both of the lines below achieve the same output
badMoviesDF.na.drop().show
badMoviesDF.na.drop("any").show
+----------+------------+--------------+
|actor_name| movie_title| produced_year|
+----------+------------+--------------+
+----------+------------+--------------+
// drop rows that have missing data in every single column
badMoviesDF.na.drop("all").show
+-----------+--------------+--------------+
| actor_name|   movie_title| produced_year|
+-----------+--------------+--------------+
|       null|          null|          2018|
|   John Doe| Awesome Movie|          null|
|       null| Awesome Movie|          2018|
|  Mary Jane|          null|          2018|
+-----------+--------------+--------------+

// drops rows that column actor_name has missing data
badMoviesDF.na.drop(Array("actor_name")).show
+------------+---------------+--------------+
|  actor_name|    movie_title| produced_year|
+------------+---------------+--------------+
|    John Doe|  Awesome Movie|          null|
|   Mary Jane|           null|          2018|
+------------+---------------+--------------+

Listing 3-36Dropping Rows with Missing Data

使用结构化操作

本节介绍结构化操作。它们与 RDD 动作具有相同的急切求值语义,因此它们触发导致特定动作的所有转换的计算。表 3-9 描述了结构化动作的列表。

表 3-9

常用的结构化操作

|

操作

|

描述

| | --- | --- | | show()``show(numRows)``show(truncate)``show(numRows, truncate) | 以表格格式显示行。如果未指定 numRows,则显示前 20 行。truncate 选项控制是否截断长度超过 20 个字符的字符串列。 | | head()``first()``head(n)``take(n) | 返回第一行。如果指定了 n,则返回前 n 行。first 是 first 的别名。take(n)是 first(n)的别名。 | | takeAsList(n) | 以 Java 列表的形式返回前 n 行。注意不要带太多行;否则,它可能会导致应用程序的驱动程序进程出现内存不足的错误。 | | collect``collectAsList | 以数组或 Java 列表的形式返回所有行。应用与采取列表操作中描述的相同的注意事项。 | | count | 返回数据帧中的行数。 | | describe | 计算数据帧中数值列和字符串列的常见统计信息。可用的统计数据有计数、平均值、标准差、最小值、最大值和任意近似百分位数。 |

其中大多数是不言自明的。show 动作已经在结构化转换部分的许多例子中使用过。

另一个有趣的动作叫做describe,接下来讨论。

描述(列名)

有时,对您正在处理的数据的基本统计有一个大致的了解是很有用的。此操作可以计算字符串和数字列的基本统计信息,如计数、平均值、标准差、最小值和最大值。您可以选择计算哪个或哪些字符串或数字列的统计数据。清单 3-37 就是一个例子。

movies.describe("produced_year").show
+-----------+-------------------------+
|    summary|            produced_year|
+-----------+-------------------------+
|      count|                    31392|
|       mean|       2002.7964449541284|
|     stddev|        6.377236851493877|
|        min|                     1961|
|        max|                     2012|
+-----------+-------------------------+

Listing 3-37Use describe Action to Show the Statistics of produced_year Column

数据集简介

在某一点上,关于数据帧和数据集 API 之间的区别有很多混淆。给定这些选项,可以问它们之间的区别是什么,每个选项的优点和缺点,以及何时使用哪个选项。认识到 Spark 用户社区中的这一巨大混乱,Spark 设计师决定在 Spark 2.0 版本中统一 DataFrame APIs 和 Dataset APIs,以减少用户学习和记忆的抽象。

从 Spark 2.0 版本开始,只有一个称为 Dataset 的高级抽象,它有两种风格:强类型 API 和非类型 API。术语DataFrame不会消失;相反,它被重新定义为 Dataset 中一般对象集合的别名。从代码的角度来看,DataFrame 本质上是Dataset[Row]的类型别名,其中Row是通用的非类型化 JVM 对象。数据集是强类型 JVM 对象的集合,由 Scala 中的case类或 Java 中的类表示。表 3-10 描述了 Spark 支持的每种编程语言中可用的数据集 API 风格。

表 3-10

数据集风格

|

语言

|

风味

| | --- | --- | | 斯卡拉 | 数据集[T]和数据帧 | | 爪哇 | 数据表 | | 计算机编程语言 | 数据帧 | | 稀有 | 数据帧 |

Python 和 R 语言没有编译时类型安全;因此,仅支持非类型化数据集 API(也称为 DataFrame)。

把数据集当成 DataFrame 的弟弟。它的独特属性包括类型安全和面向对象。数据集是强类型、不可变的数据集合。像数据帧一样,数据被映射到一个定义的模式。但是,数据帧和数据集之间有一些重要的区别。

  • 数据集中的每一行都由一个用户定义的对象表示,因此您可以将单个列作为该对象的成员变量来引用。这为您提供了编译类型的安全性。

  • 数据集有名为encoders的帮助器,它们是智能和高效的编码实用程序,可以将每个用户定义的对象中的数据转换为紧凑的二进制格式。当数据集缓存在内存中时,这意味着内存使用的减少,当 Spark 需要在混洗过程中通过网络传输时,这意味着字节数的减少。

就限制而言,数据集 API 仅在 Scala 和 Java 等强类型语言中可用。将行对象转换为特定于域的对象会产生转换成本,当数据集有数百万行时,这种成本可能是一个因素。此时,您应该会想到一个关于何时使用数据帧 API 和数据集 API 的问题。数据集 API 适用于需要定期运行并由数据工程师团队编写和维护的生产作业。对于大多数交互式和探索性分析用例,使用 DataFrame APIs 就足够了。

Note

Scala 语言中的 case 类就像 Java 语言中的 JavaBean 类;但是,它有一些内置的有趣属性。case 类的实例是不可变的,因此它通常用于建模特定于领域的对象。此外,很容易推断出 case 类实例的内部状态,因为它们是不可变的。toString 和 equals 方法是自动生成的,以便更容易打印出 case 类的内容并在 case 类实例之间进行比较。Scala case 类与 Scala 模式匹配特性配合得很好。

创建数据集

在创建数据集之前,需要定义一个特定于域的对象来表示每一行。有几种方法可以创建数据集。第一种方法是使用 DataFrame 类的as(符号)函数将 DataFrame 转换为 Dataset。第二种方法是使用SparkSession.createDataset()函数从对象集合中创建数据集。第三种方法是使用toDS隐式转换工具。清单 3-38 提供了创建数据集的不同示例。

// define Movie case class
case class Movie(actor_name:String, movie_title:String, produced_year:Long)


// convert DataFrame to strongly typed Dataset
val moviesDS = movies.as[Movie]

// create a Dataset using SparkSession.createDataset() and the toDS implicit function
val localMovies = Seq(Movie("John Doe", "Awesome Movie", 2018L),
                                    Movie("Mary Jane", "Awesome Movie", 2018L))

val localMoviesDS1 = spark.createDataset(localMovies)
val localMoviesDS2 = localMovies.toDS()
localMoviesDS1.show
+------------+---------------+-------------+
|  actor_name|    movie_title|produced_year|
+------------+---------------+-------------+
|    John Doe|  Awesome Movie|         2018|
|   Mary Jane|  Awesome Movie|         2018|
+------------+---------------+-------------+

Listing 3-38Different Ways of Creating Datasets

在创建数据集的不同方法中,第一种方法是最受欢迎的。当使用 Scala case 类将 DataFrame 转换为 Dataset 时,Spark 会执行验证,以确保 Scala case 类中的成员变量名与 DataFrame 模式中的列名相匹配。如果有不匹配,Spark 会让您知道。

使用数据集

现在您已经有了一个数据集,您可以使用转换和操作来操作它。在本章的前面,数据帧中的列使用了这些选项之一。对于数据集,每一行都用强类型对象表示;因此,您可以只使用成员变量名来引用列,这为您提供了类型安全和编译时验证。如果名字中有拼写错误,编译器会在开发阶段立即标记出来。清单 3-39 是操作数据集的例子。

// filter movies that were produced in 2010 using
moviesDS.filter(movie => movie.produced_year == 2010).show(5)
+---------------------+---------------------+-------------+
|           actor_name|          movie_title|produced_year|
+---------------------+---------------------+-------------+
|    Cooper, Chris (I)|             The Town|         2010|
|      Jolie, Angelina|                 Salt|         2010|
|      Jolie, Angelina|          The Tourist|         2010|
|       Danner, Blythe|       Little Fockers|         2010|
|   Byrne, Michael (I)| Harry Potter and ...|         2010|
+---------------------+---------------------+-------------+

// displaying the title of the first movie in the moviesDS
moviesDS.first.movie_title
String = Coach Carter

// try with misspelling the movie_title and get compilation error
moviesDS.first.movie_tile
error: value movie_tile is not a member of Movie

// perform projection using map transformation
val titleYearDS = moviesDS.map(m => ( m.movie_title, m.produced_year))
titleYearDS.printSchema
 |-- _1: string (nullable = true)
 |-- _2: long (nullable = false)

// demonstrating a type-safe transformation that fails at compile time, performing subtraction on a column with string type

// a problem is not detected for DataFrame until runtime
movies.select('movie_title - 'movie_title)
// a problem is detected at compile time
moviesDS.map(m => m.movie_title - m.movie_title)
error: value - is not a member of String

// take action returns rows as Movie objects to the driver
moviesDS.take(5)
Array[Movie] = Array(Movie(McClure, Marc (I),Coach Carter,2005), Movie(McClure, Marc (I),Superman II,1980), Movie(McClure, Marc (I),Apollo 13,1995))

Listing 3-39Manipulating a Dataset in a Type-Safe Manner

对于那些经常使用 Scala 编程语言的人来说,使用数据集强类型 API 感觉很自然,给你的印象是数据集中的那些对象驻留在本地。

当您使用数据集强类型 API 时,Spark 隐式地将每个Row实例转换为您提供的特定于域的对象。这种转换在性能方面有一些代价;然而,它提供了更多的灵活性。

帮助决定何时在 DataFrame 上使用 Dataset 的一个通用准则是,希望在编译时具有更高程度的类型安全,这对于由多个数据工程师开发和维护的复杂 ETL Spark 作业来说非常重要。

在 Spark SQL 中使用 SQL

在大数据时代,SQL 被描述为大数据分析的通用语言。Spark 中最酷的特性之一是能够使用 SQL 执行大规模的分布式数据操作。精通 SQL 的数据分析师现在可以使用 Spark 对大型数据集执行数据分析。需要记住的重要一点是,Spark 中的 SQL 是为在线分析处理(OLAP)用例设计的,而不是为在线事务处理(OLTP)用例设计的。换句话说,它不适用于低延迟用例。

SQL 随着时间的推移不断发展和改进。Spark 实现了 ANSI SQL:2003 修订版的一个子集,大多数流行的 RDBMS 服务器都支持它。符合这一修订版意味着 Spark SQL 数据处理引擎可以使用广泛使用的行业标准决策支持基准 TPC-DS 进行基准测试。

2016 年末,脸书开始将其最大的一些 Hive 工作负载迁移到 Spark,以利用 Spark SQL 引擎的强大功能(参见 https://code.facebook.com/posts/1671373793181703/apache-spark-scale-a-60-tb-production-use-case/ ))。

Note

结构化查询语言(SQL)是一种特定于领域的语言,它对以表格格式组织的结构化数据进行数据分析和操作。SQL 中的概念基于关系代数;然而,这是一种容易学习的语言。SQL 与 Scala 或 Python 等其他编程语言之间的一个关键区别是,SQL 是一种声明式编程语言,这意味着您可以表达您想要对数据做什么,并让 SQL 执行引擎找出如何执行数据操作以及必要的优化以加快执行时间。如果你是 SQL 新手,在这个网站的 www.datacamp.com/courses/intro-to-sql-for-data-science 有一个免费的课程。

在 Spark 中运行 SQL

Spark 为在 Spark 中运行 SQL 提供了一些不同的选项。

  • Spark SQL CLI(。/bin/spark-sql)

  • JDBC/ODBC 服务器

  • Spark 应用程序中的编程

前两个选项集成了 Apache Hive 以利用其 megastore,这是一个包含关于各种系统和用户定义的表的元数据和模式信息的存储库。本节只讨论最后一个选项。

数据帧和数据集本质上类似于数据库中的表。在发出 SQL 查询来操作它们之前,您需要将它们注册为临时视图。每个视图都有一个名称,在select子句中用作表名。Spark 为视图提供了两个层次的范围。一个是在 Spark 会议级别。当在这一级注册数据帧时,只有在同一会话中发出的查询才能引用该数据帧。当相关的 Spark 会话关闭时,会话范围的级别消失。第二个范围级别是全局的,这意味着这些视图对于所有 Spark 会话中的 SQL 语句都是可用的。所有注册的视图都保存在 Spark 元数据目录中,可以通过SparkSession访问。清单 3-40 是注册视图并使用 Spark 目录检查视图元数据的一个例子。

// display tables in the catalog, expecting an empty list
spark.catalog.listTables.show
+-------+------------+---------------+------------+------------+
|   name|    database|    description|   tableType| isTemporary|
+-------+------------+---------------+------------+------------+
+-------+------------+---------------+------------+------------+

// now register movies DataFrame as a temporary view
movies.createOrReplaceTempView("movies")

// should see the movies view in the catalog
spark.catalog.listTables.show
+-------+---------+------------+-----------+--------------+
|   name| database| description|  tableType|   isTemporary|
+-------+---------+------------+-----------+--------------+
| movies|     null|        null|  TEMPORARY|          true|
+-------+---------+------------+-----------+--------------+

// show the list of columns of movies view in catalog
spark.catalog.listColumns("movies").show
+--------------+------------+---------+---------+------------+------------+
|          name| description| dataType| nullable| isPartition|    isBucket|
+--------------+------------+---------+---------+------------+------------+
|    actor_name|        null|   string|     true|       false|       false|
|   movie_title|        null|   string|     true|       false|       false|
| produced_year|        null|   bigint|     true|       false|       false|
+--------------+------------+---------+---------+------------+------------+

// register movies as global temporary view called movies_g
movies.createOrReplaceGlobalTempView("movies_g")

Listing 3-40Register the movies DataFrame as a Temporary View and Inspecting Metadata Catalog

清单 3-40 给出了几个视图供您选择。发布 SQL 查询的编程方式是使用SparkSession类的sql函数。在 SQL 语句中,您可以访问所有 SQL 表达式和内置函数。SparkSession.sql函数执行给定的 SQL 查询;它返回一个数据帧。发布 SQL 语句和使用数据帧转换和动作的能力为您在 Spark 中选择如何执行分布式数据处理提供了很大的灵活性。

清单 3-41 提供了发出简单和复杂 SQL 语句的例子。

// simple example of executing a SQL statement without a registered view
val infoDF = spark.sql("select current_date() as today , 1 + 100 as value")
infoDF.show
+----------+--------+
|     today|   value|
+----------+--------+
|2017-12-27|     101|
+----------+--------+

// select from a view
spark.sql("select * from movies where actor_name like '%Jolie%' and produced_year > 2009").show
+---------------+----------------+--------------+
|     actor_name|     movie_title| produced_year|
+---------------+----------------+--------------+
|Jolie, Angelina|            Salt|          2010|
|Jolie, Angelina| Kung Fu Panda 2|          2011|
|Jolie, Angelina|     The Tourist|          2010|
+---------------+----------------+--------------+

// mixing SQL statement and DataFrame transformation
spark.sql("select actor_name, count(*) as count from movies group by actor_name")
         .where('count > 30)
         .orderBy('count.desc)
         .show
+----------------------+--------+
|            actor_name|   count|
+----------------------+--------+
|      Tatasciore, Fred|      38|
|         Welker, Frank|      38|
|    Jackson, Samuel L.|      32|
|         Harnell, Jess|      31|
+----------------------+--------+

// using a subquery to figure out the number movies produced each year.
// leverage """ to format multi-line SQL statement

spark.sql("""select produced_year, count(*) as count
                   from (select distinct movie_title, produced_year from movies)
                   group by produced_year""")
         .orderBy('count.desc).show(5)

+------------------+--------+
|     produced_year|   count|
+------------------+--------+
|              2006|      86|
|              2004|      86|
|              2011|      86|
|              2005|      85|
|              2008|      82|
+------------------+--------+

// select from a global view requires prefixing the view name with key word 'global_temp'
spark.sql("select count(*) from global_temp.movies_g").show
+--------+
|   count|
+--------+
|   31393|
+--------+

Listing 3-41Executing SQL Statements in Spark

不是通过DataFrameReader类读取数据文件并将新创建的数据帧注册为临时视图,而是有一种简单方便的方法对数据文件发出 SQL 查询。清单 3-42 就是一个例子。

spark.sql("SELECT * FROM parquet.`<path>/chapter4/data/movies/movies.parquet`").show(5)

Listing 3-42Issue SQL Query Against a Data File

将数据写出到存储系统

至此,您已经知道如何使用DataFrameReader从各种文件格式或数据库服务器中读取数据,并且知道如何使用 SQL 或结构化 API 的转换和操作来操作数据。有时,您需要将数据帧中数据处理逻辑的结果写入外部存储系统(例如,本地文件系统、HDFS 或亚马逊 S3)。在一个典型的 ETL 数据处理作业中,结果很可能被写到一些持久存储系统中。

在 Spark SQL 中,DataFrameWriter类负责将数据帧中的数据写出到外部存储系统的逻辑和复杂性。作为 DataFrame 类中的write变量,DataFrameWriter类的一个实例可供您使用。与DataFrameWriter的交互方式与DataFrameReader的交互方式类似。您可以从 Spark shell 或 Spark 应用程序中引用它,如清单 3-43 所示。

movies.write

Listing 3-43Using write Variable from DataFrame Class

清单 3-44 描述了与DataFrameWriter交互的常见模式。

movies.write.format(...).mode(...).option(...).partitionBy(...).bucketBy(...).sortBy(...).save(path)

Listing 3-44Common Interacting Pattern with DataFrameWriter

DataFrameReader类似,默认格式是拼花;因此,如果所需的输出格式是拼花,则没有必要指定格式。partitionBybucketBy,sortBy函数控制基于文件的数据源中输出文件的目录结构。基于读取模式构建目录布局可以显著减少分析所需读取的数据量。在本章的后面你会学到更多。save函数的输入是一个目录名,而不是文件名。

the DataFrameWriter class is the save mode, which controls how Spark handles the situation when the specified output location中的一个重要选项存在。表 3-11 列出了各种支持的保存模式。

表 3-11

保存模式

|

方式

|

描述

| | --- | --- | | 附加 | 这将把数据帧数据追加到指定目标位置已经存在的文件列表中。 | | 写得过多 | 这将使用数据帧中的数据完全覆盖指定目标位置上已经存在的任何数据文件。 | | 错误错误如果存在系统默认值 | 这是默认模式。如果指定的目标位置存在,DataFrameWriter 将引发错误。 | | 忽视 | 如果指定的目标位置存在,那么什么也不做。换句话说,不要在 DataFrame 中写出数据。 |

清单 3-45 展示了一些使用各种格式和模式组合的例子

// write data out in CVS format, but using a '#' as delimiter
movies.write.format("csv").option("sep", "#").save("/tmp/output/csv")

// write data out using overwrite save mode
movies.write.format("csv").mode("overwrite").option("sep", "#").save("/tmp/output/csv")

Listing 3-45Using DataFrameWriter to Write Out Data to File-based Sources

写出到输出目录的文件数量对应于数据帧的分区数量。清单 3-46 展示了如何找出一个数据帧的分区数量。

movies.rdd.getNumPartitions
Int = 1

Listing 3-46Display the Number of DataFrame Partitions

当数据帧中的行数不大时,需要有一个输出文件,以便于共享。实现这个目标的一个小技巧是将数据帧中的分区数量减少到一个,然后将其写出。清单 3-47 展示了一个如何做到这一点的例子。

val singlePartitionDF = movies.coalesce(1)

Listing 3-47Reduce the Number of Partitions in a DataFrame to 1

使用分区和分桶写出数据的想法是从 Apache Hive 用户社区借鉴来的。根据经验,按列分区应该具有较低的基数。在movies数据帧中,produced_year列是按列分区的良好候选。假设您想要写出由produced_year列分区的movies数据帧。DataFrameWriter 将所有具有相同produced_year的电影写入单个目录。输出文件夹中的目录数量对应于movies数据帧中的年数。清单 3-48 是使用partitionBy函数的一个例子。

movies.write.partitionBy("produced_year").save("/tmp/output/movies ")

// the /tmp/output/movies directory will contain the following subdirectories
produced_year=1961 to produced_year=2012

Listing 3-48Write the movies DataFrame Using Partition By produced_year Column

partitionBy选项生成的目录名看起来很奇怪,因为每个目录名都由分区列名和相关值组成。这两条信息在数据读取时用于根据数据访问模式选择要读取的目录,因此最终读取的数据比其他情况少得多。

三者:数据帧、数据集和 SQL

现在您知道了在 Spark SQL 模块中有三种不同的操作结构化数据的方法。表 3-12 显示了每个选项在语法和分析谱中的位置。

表 3-12

语法和分析错误谱

|   |

结构化查询语言

|

数据帧

|

资料组

| | --- | --- | --- | --- | | 系统错误 | 运行时间 | 编译时间 | 编译时间 | | 分析错误 | 运行时间 | 运行时间 | 编译时间 |

越早发现错误,您的工作效率就越高,数据处理应用程序就越稳定。

数据帧持久性

数据帧可以在内存中持久化/缓存,就像使用 rdd 一样。DataFrame 类中提供了相同的常见持久性 API(persist 和 unpersist)。然而,缓存数据帧时有一个很大的区别。因为 Spark SQL 知道数据帧中的数据模式,所以它可以以列格式组织数据,并应用任何适用的压缩来最小化空间使用。最终结果是,当两者由相同的数据文件支持时,在内存中存储数据帧比存储 RDD 需要更少的空间。表 3-5 中描述的所有不同存储选项都适用于数据帧的保存。清单 3-49 演示了用一个人类可读的名字来持久化一个数据帧,这个名字在 Spark UI 中很容易识别。

val numDF = spark.range(1000).toDF("id")
// register as a view
numDF.createOrReplaceTempView("num_df")
// use Spark catalog to cache the numDF using name "num_df"
spark.catalog.cacheTable("num_df")
// force the persistence to happen by taking the count action
numDF.count

Listing 3-49Persisting a DataFrame with a Human Readable Name

接下来,将浏览器指向 Spark UI(运行 Spark shell 时为http://localhost:4040),然后单击 Storage 选项卡。图 3-2 显示了一个例子。

img/419951_2_En_3_Fig2_HTML.png

图 3-2

存储选项卡

摘要

在本章中,您学习了以下内容。

  • Spark SQL 模块为结构化分布式数据操作提供了一个新的强大的抽象。结构化数据有一个已定义的模式,由列名和列数据类型组成。

  • Spark SQL 中的主要编程抽象是数据集,它有两种风格的 API:强类型 API 和非类型 API。对于强类型 API,每一行都由一个域指定的对象表示。对于非类型化的 API,范围行由一个行对象表示。DataFrame 现在只是 Dataset[Row]的别名。强类型 API 为您提供静态类型和编译时检查;因此,它们只在强类型语言中可用,比如 Scala 或 Java。

  • Spark SQL 支持从各种流行的数据源读取不同格式的数据。DataFrameReader类负责通过从这些数据源中读取数据来创建数据帧。

  • 像 RDD 一样,数据集有两种类型的结构化操作。它们是转变和行动。前者评价慵懒,后者评价热切。

  • Spark SQL 使得使用 SQL 对大型集合执行数据处理变得非常容易。这为数据分析师和非程序员打开了大门。

  • 从数据集或数据帧中写出数据是通过一个名为DataFrameWriter的类来完成的。

Spark SQL Exercises

以下练习基于chapter3/data/movies目录下的movies.tsvmovie-ratings.tsv文件。这些文件中的列分隔符是一个制表符,所以确保使用它来分隔每一行。

movies.tsv文件中的每一行代表一部电影中的一个演员。如果一部电影中有十个演员,那么这部电影就有行。

  1. 计算每年生产的电影数量。输出应该有两列:year 和 count。输出应按计数降序排列。

  2. 计算每个演员参演的电影数量。输出应该有两列:actor、count。输出应按计数降序排列。

  3. 计算每年收视率最高的电影,并包括该电影中的所有演员。输出应该每年只有一部电影,它应该包含四列:年份,电影名称,评级,一个分号分隔的演员姓名列表。这个问题需要在movies.tsvmovie-ratings.tsv文件之间进行连接。解决这个问题有两种方法。首先是计算出每年收视率最高的电影,然后加入演员名单。第二个是先执行 join,然后算出每年收视率最高的电影和演员名单。每种方法的结果都不同。你认为这是为什么?

  4. 确定哪一对演员合作得最多。合作被定义为出现在同一部电影中。输出应该有三列:actor1、actor2 和 count。输出应该按照计数降序排序。这个问题的解决方案需要进行自连接。