Spark.NET 入门指南(一)
一、了解 Apache Spark
Apache Spark 是一个数据分析平台,它使大数据变得可访问,并将大规模数据处理带入每个开发人员的生活。使用 Apache Spark,读取本地机器上的单个 CSV 文件就像读取数据湖中的一百万个 CSV 文件一样容易。
一个例子
让我们看一个例子。清单 1-1 (C#)和 1-2(F #版本)中的代码从一组 CSV 文件中读取,并计算有多少记录匹配特定条件。该代码读取特定路径中的所有 CSV 文件,因此我们读取的文件数量实际上是无限的。
尽管本章中的示例是功能完整的示例,但是它们需要一个可以工作的 Apache Spark 实例,无论是在本地还是在集群上。我们在第二章介绍了 Apache Spark 的设置和运行。NET for Apache Spark 第三章。
open Microsoft.Spark.Sql
[<EntryPoint>]
let main argv =
let path = argv.[0]
let spark = SparkSession.Builder().GetOrCreate()
spark.Read().Option("header", "true").Csv(path)
|> fun dataframe -> dataframe.Filter(Functions.Col("name").EqualTo("Ed Elliott")).Count()
|> printfn "There are %d row(s)"
0
Listing 1-2Counting how many rows match a filter in one or a million CSV files in F#
using System;
using System.Linq;
using Microsoft.Spark.Sql;
using static Microsoft.Spark.Sql.Functions;
namespace Introduction_CSharp
{
class Program
{
static void Main(string[] args)
{
var path = args.FirstOrDefault();
var spark = SparkSession
.Builder()
.GetOrCreate();
var dataFrame = spark.Read().Option("header", "true").Csv(path);
var count = dataFrame.Filter(Col("name") == "Ed Elliott").Count();
Console.WriteLine($"There are {count} row(s)");
}
}
}
Listing 1-1Counting how many rows match a filter in one or a million CSV files in C#
执行这两个程序中的任何一个都会显示与过滤器匹配的行数:
» dotnet run --project ./Listing0-1 "/Users/ed/sample-data/1.csv"
There are 1 row(s)
» dotnet run --project ./Listing0-2 "/Users/ed/sample-data/1.csv"
There are 1 row(s)
如果我们对单个文件使用这种方法,那就没问题,代码看起来非常高效,但是当相同的代码可以在包含许多节点和数 Pb 数据的集群中高效运行时,您就可以看到 Apache Spark 有多么强大了。
核心使用案例
Apache Spark 在大数据处理领域是独一无二的,因为它允许数据处理、分析以及机器学习。通常,您可以使用 Apache Spark:
-
将数据转换为 ETL 或 ELT 数据管道的一部分
-
分析从一个小文件到数百万个文件中数 Pb 数据的数据集
-
创建机器学习(ML)应用程序来实现人工智能
转变您的数据
Apache Spark 可以读取和写入 Java 虚拟机支持的任何文件格式或数据库,这意味着我们可以从 JDBC 连接读取和写入文件。Apache Spark 开箱即用,能够读取各种文件格式,比如 CSV 或 Parquet。但是,您总是可以引用其他 JAR 文件来添加对其他文件类型的支持,例如,crealytics 的“spark-excel”插件( https://github.com/crealytics/spark-excel )允许您在 Apache Spark 中读写 XLSX 文件。
为了展示 Apache Spark 在处理时的强大功能,并展示它是如何从头开始构建性能的,我参与了一个项目,在这个项目中,我们将读取一个巨大的 parquet 文件,其中包含一个流行的国际网站的所有 Adobe 点击流数据。在我们的例子中,数据是一个包含用户在网站上所有行为的文件;对于一个经常访问的网站,该文件可能有数 GB,包含一系列事件,包括无效数据。我的团队的任务是高效地读取数百万行的整个文件,并检索一个特定操作的最小子集。在 Apache Spark 之前,我们可能会将整个文件放入数据库,然后过滤掉我们不想要的行,或者使用像微软的 SSIS 这样的工具,它会读入整个文件。当我们在 Apache Spark 中实现它时,我们为我们想要的特定行类型编写了一个过滤器。Apache Spark 从文件中读取数据,并使用谓词下推将过滤器传递给读取 parquet 文件的驱动程序,因此,在最早的时候,无效的行就被过滤掉了。这个项目向我们展示了 Apache Spark 所展示的性能水平和易用性,这是我们的团队以前从未见过的。
清单 1-3 (C#)和 1-4 (F#)中的代码将演示如何从数据源读取数据,将数据过滤到您需要的行,并展示如何将数据写出到一个新文件中,这对于 Apache Spark 来说再简单不过了。
open Microsoft.Spark.Sql
open System
[<EntryPoint>]
let main argv =
let writeResults (x:DataFrame) =
x.Write().Mode("overwrite").Parquet("output.parquet")
printfn "Wrote: %u rows" (x.Count())
let spark = SparkSession.Builder().GetOrCreate()
spark.Read().Parquet("1.parquet")
|> fun p -> p.Filter(Functions.Col("Event_Type").EqualTo(Functions.Lit(999)))
|> fun filtered -> writeResults filtered
0 // return an integer exit code
» dotnet run --project ./ Listing0-4
Wrote: 10 rows
Listing 1-4Reading, filtering, and writing data back out again in F#
using System;
using Microsoft.Spark.Sql;
namespace TransformingData_CSharp
{
class Program
{
static void Main(string[] args)
{
var spark = SparkSession
.Builder()
.GetOrCreate();
var filtered = spark.Read().Parquet("1.parquet")
.Filter(Functions.Col("event_type") == Functions.Lit(999));
filtered.Write().Mode("overwrite").Parquet("output.parquet");
Console.WriteLine($"Wrote: {filtered.Count()} rows");
}
}
}
» dotnet run --project ./ Listing0-3
Wrote: 10 rows
Listing 1-3Reading, filtering, and writing data back out again in C#
分析你的数据
Apache Spark 包含了您期望从数据库中获得的数据分析能力,如聚合、窗口和 SQL 函数,您可以使用公共 API 如data.GroupBy(Col("Name")).Count()来访问这些功能。有趣的是,您也可以编写 Spark SQL,这意味着您可以使用 SQL 查询来访问您的数据。Spark SQL 使 Apache Spark 面向更广泛的受众,包括开发人员以及分析师和数据科学家。无需学习 Scala、Python、Java、R 以及现在的 C#或 F#就能使用 Apache Spark 的强大功能是一个引人注目的特性。
清单 1-5 和 1-6 显示了另一个例子,我们生成三个数据集,将这些数据集联合在一起,然后聚合并显示结果。NET,然后在清单 1-7 中,我们演示了相同的结果,但是没有使用。NET 代码,我们将一个 SQL 查询传递给 Apache Spark,并执行该查询来创建一个我们可以使用的结果集;请注意,有一些 Apache Spark 环境,如 Databricks 笔记本,我们可以只编写 SQL 而不编写任何应用程序代码。
open Microsoft.Spark.Sql
open System
[<EntryPoint>]
let main argv =
let spark = SparkSession.Builder().GetOrCreate()
spark.Range(100L).WithColumn("Name", Functions.Lit("Ed"))
|> fun d -> d.Union(spark.Range(100L).WithColumn("Name", Functions.Lit("Bert")))
|> fun d -> d.Union(spark.Range(100L).WithColumn("Name", Functions.Lit("Lillian")))
|> fun d -> d.GroupBy(Functions.Col("Name")).Count()
|> fun d -> d.Show()
0
Listing 1-6Create three datasets, union, aggregate, and count in F#
using System;
using Microsoft.Spark.Sql;
using static Microsoft.Spark.Sql.Functions;
namespace TransformingData_CSharp
{
class Program
{
static void Main(string[] args)
{
var spark = SparkSession
.Builder()
.GetOrCreate();
var data = spark.Range(100).WithColumn("Name", Lit("Ed"))
.Union(spark.Range(100).WithColumn("Name", Lit("Bert")))
.Union(spark.Range(100).WithColumn("Name", Lit("Lillian")));
var counts = data.GroupBy(Col("Name")).Count();
counts.Show();
}
}
}
Listing 1-5Create three datasets, union, aggregate, and count in C#
最后,在清单 1-7 中,我们将使用 Spark SQL 来实现相同的结果。
using System;
using Microsoft.Spark.Sql;
namespace TransformingData_SQL
{
class Program
{
static void Main(string[] args)
{
var spark = SparkSession
.Builder()
.GetOrCreate();
var data = spark.Sql(@"
WITH users
AS (
SELECT ID, 'Ed' as Name FROM Range(100)
UNION ALL
SELECT ID, 'Bert' as Name FROM Range(100)
UNION ALL
SELECT ID, 'Lillian' as Name FROM Range(100)
) SELECT Name, COUNT(*) FROM users GROUP BY Name
");
data.Show();
}
}
}
Listing 1-7Create three datasets, union, aggregate, and count in Spark SQL
Apache Spark 在所有三个实例中执行的代码是相同的,并产生以下输出:
» dotnet run --project ./Listing0-7
+-------+--------+
| Name|count(1)|
+-------+--------+
| Bert| 100|
|Lillian| 100|
| Ed| 100|
+-------+--------+
机器学习
Apache Spark 的最后一个核心用例是编写机器学习(ML)应用。今天,有很多不同的环境可以编写 ML 应用程序,比如 Scikit-Learn、TensorFlow 和 PyTorch。然而,在 ML 应用程序中使用 Apache Spark 的好处是,如果您已经使用 Apache Spark 处理了数据,那么您将获得相同的熟悉的 API,更重要的是,您可以重用现有的基础设施。
要了解在 Apache Spark 中使用 ML API 可以做什么,请参见 https://spark.apache.org/docs/latest/ml-guide.html 。
。Apache Spark 的 NET
Apache Spark 是用 Scala 编写的,运行在 Java 虚拟机(JVM)上,但是有大量开发人员的主要语言是 C#,其次是 F#。那个。NET for Apache Spark 项目旨在将 Apache Spark 的全部功能引入到。NET 开发人员。微软将该项目作为开源项目启动,在开放环境中开发,并接受拉式请求、问题和功能请求。
那个。NET for Apache Spark project 在。NET CLI 代码和 JVM。工作方式是有一个 Java 类,用 Scala 写的;名为DotnetRunner的 Java 类创建一个 TCP 套接字,然后DotnetRunner运行一个 dotnet 程序,你的程序创建一个SparkSession。SparkSession与 TCP 套接字建立连接,将请求转发给 JVM 并返回响应。你可以想到。NET for Apache Spark 库作为。NET 代码和 JVM。
微软团队做出了一个重要的早期决策,这影响了我们如何从. NET 使用 Apache Spark。Apache Spark 最初是从所谓的 RDD API 开始的。RDD API 允许用户访问 Apache Spark 使用的底层数据结构。当 Apache Spark 版发布时,它包含了一个新的 DataFrame API。DataFrame API 有几个额外的好处,比如一个新的"catalyst"优化器,这意味着使用 DataFrame API 比原来的 RDD API 更有效。让 Apache Spark 优化查询,而不是尝试使用 RDD API 自己优化调用,也简单得多。DataFrame API 为 Python 和 R 以及现在的. NET 带来了同等的性能。以前的 RDD API 对于 Scala 或 Java 代码要比 Python 快得多。使用新的 DataFrame API,在大多数情况下,Python 或 R 代码与 Scala 和 Java 代码一样快。
微软团队决定只为新的 DataFrame API 提供支持,这意味着现在不可能使用来自。NET for Apache Spark。老实说,我不认为这是一个重要的问题,它当然不是采用。NET for Apache Spark。这种只支持后来的 API 的情况一直延续到 ML 库,这里有两个用于 ML 的 API,MLLib 和 ML。Apache Spark 团队不赞成 MLLib,而支持 ML 库,所以在。NET for Apache Spark,我们也只实现了 ML 版本的 API。
特征奇偶校验
那个。NET for Apache Spark project 于 2019 年 4 月首次向公众发布,包含了 Apache Spark 中也提供的许多核心功能。然而,有相当多的功能缺失,甚至从 DataFrame API 中也是如此,这忽略了可能不会实现的 API,如 RDD API。自最初发布以来,微软团队和外部贡献者已经增加了功能的数量。与此同时,Apache Spark 团队也发布了更多的功能,所以在某些方面,Microsoft project 正在追赶 Apache 团队,所以目前并不是所有的功能都可以在。NET 项目。在过去的一年多时间里,差距一直在缩小,我完全预计在未来一年左右的时间里,差距会越来越小,功能对等将在某个时候存在。
如果您试图使用。NET for Apache Spark project,并且缺少一些功能,这对您来说是一个障碍,您可以选择几个选项来实现缺少的功能,我在附录 b 中对此进行了介绍。
摘要
Apache Spark 是一个引人注目的数据处理项目,它使得查询大型分布式数据集变得非常简单。。NET for Apache Spark 将这种能力带到了。NET 开发人员,就我而言,对使用 C#和 F#创建 ETL、ELT、ML 和各种数据处理应用程序的可能性感到兴奋。
二、配置 Spark
为了开发 Apache Spark 应用程序的. NET,我们需要在开发机器上安装 Apache Spark,然后进行配置。NET for Apache Spark,以便我们的应用程序正确执行。当我们在生产中运行 Apache Spark 应用程序时,我们将使用一个集群,或者类似于 YARN 集群,或者使用一个完全托管的环境,比如 Databricks。当我们开发应用程序时,我们在本地使用相同版本的 Apache Spark,就像我们在许多机器的集群上运行时一样。在我们的开发机器上拥有相同的版本意味着当我们开发和测试代码时,我们可以确信在生产中运行的代码是相同的。
在这一章中,我们将介绍正确运行所需的各种组件;Apache Spark 是一个 Java 应用程序,所以我们需要安装和配置正确的 Java 版本,然后下载和配置 Apache Spark。只有当我们有正确版本的 Java 和 Apache Spark 运行时,我们才能够用 C#或 F#编写一个在 Apache Spark 上执行的. NET 应用程序。
选择您的软件版本
在本节中,我们将首先帮助您选择应该使用哪个版本的 Apache Spark 和哪个版本的 Java。尽管这看起来应该是一个简单的选择,但是有一些特定的要求,并且正确地做到这一点对于顺利开始是至关重要的。
选择 Apache Spark 的版本
在这一节中,我们将看看如何选择 Apache Spark 的版本。Apache Spark 是一个积极开发的开源项目,新版本经常出现,有时甚至一个月出现多次。然而。NET for Apache Spark project 并不支持每个版本,要么是因为它不支持它,要么是因为开发团队还没有添加。
当我们运行 Apache Spark 应用程序的. NET 时,我们需要理解我们需要。NET 代码,它运行在特定版本的。NET 框架或者。NET 核心。那个。NET for Apache Spark 代码与有限的 Apache Spark 版本兼容,根据您拥有的 Apache Spark 版本,您可能需要 Java 8 或 Java 11。
要帮助选择您需要的组件版本,请访问的主页。NET 为 Apache Spark 项目, https://github.com/dotnet/spark ,还有一个板块“支持的 Apache Spark”;水流。NET for Apache Spark 版本"v1.0.0"支持这些版本的 Apache Spark:
-
2.3.*
-
2.4.0
-
2.4.1
-
2.4.3
-
2.4.4
-
2.4.5
-
3.0.0
注意不支持 2.4.2,Apache Spark 的 3.0.0 在。NET for Apache Spark v1.0.0 于 2020 年 10 月发布。在可能的情况下,你应该尽可能把这两个项目的最高版本作为目标,今天,也就是 2020 年 11 月,我将开始一个新的项目。NET for Apache Spark 1 . 0 . 0 版和 Apache Spark 3.0 版。不幸的是,我们在这里写的任何具体建议都会很快过时。在撰写本文和回顾本章之间,建议从使用。NET for Apache Spark 版本 v0.12.1 和 v1.0.0。
一旦您选择了要使用的 Apache Spark 代码版本,请访问该版本的发行说明,如 https://spark.apache.org/docs/3.0.0/ 或 https://spark.apache.org/docs/3.0.0/ 。发行说明包括支持哪个版本的 Java VM 的详细信息。如果您尝试在一个不受支持的 JVM 版本上运行,那么您的应用程序将会失败,所以在这里您确实需要小心。
当您下载 Apache Spark 时,您有几个选项。您可以下载源代码并自己编译,我们不在这里讨论,但是您可以从 https://spark.apache.org/docs/latest/building-spark.html 获得如何从源代码编译的说明。您也可以选择使用预构建的 Hadoop 或不使用 Hadoop 进行下载,但是您需要提供自己的 Hadoop 安装。通常,对于开发机器,我会下载带有预构建 Hadoop 的 Apache Spark 版本,因此您不必维护 Hadoop 的实例。但是在某些情况下,您可能希望使用不带 Hadoop 的版本,并单独安装 Hadoop。一个例子是,如果你从你的开发实例中,想要读写文件到 Azure 数据湖存储,那么你将需要一个单独的 Hadoop 实现作为预构建的实现。在撰写本文时,预构建的 Hadoop 实现不支持 Azure 数据湖协议。在实践中,您可能会发现,当您在集群上运行时,您针对本地文件进行开发,并且只需要读写像 Azure Data Lake 存储这样的东西。
总结一下:
-
为 Apache Spark 版本选择. NET 理想情况下,获得最新版本。
-
选择 Apache Spark 版本——支持的最新版本。NET for Apache Spark。
-
选择 Java VM 版本–选择您选择的 Apache Spark 支持的最新版本。
-
选择带或不带 Hadoop 的下载–除非您有使用独立 Hadoop 的特定要求,否则请使用预构建 Hadoop 的 Apache Spark 版本。
在下一节中,我们将探讨如何选择 Java 的版本,这并不总是像第一次出现时那样简单。
选择 Java 版本
Sun Microsystems 最初开发了 Java,任何人都可以免费使用。2019 年 4 月,甲骨文改变了 Java 的许可方式,因此从 2019 年 4 月起,甲骨文 8 版以上的 Java 版本不再免费用于生产或非个人使用。更令人困惑的是,Oracle 还发布了一个名为 OpenJDK 的 Java 版本,它没有这些限制;许多人选择使用 OpenJDK 版本的 Java,而不是 Oracle 版本的 Java。要了解有关这些许可变更的更多信息,请参见 www.oracle.com/java/technologies/javase/jdk-faqs.html 。
谈到 Java 版本,两种不同的方案指向同一个逻辑版本;Java 1.8 和 Java 8,虽然看起来很不一样,但都是同一个版本。
与 Oracle OpenJDK 相比,您从 Oracle JDK 获得的是支持,这是许多组织的基本要求。这篇堆栈溢出帖子的前两个答案描述了这个问题,并帮助指导您选择使用哪种风格的 Java:https://stackoverflow.com/questions/52431764/difference-between-openjdk-and-adoptium-adoptopenjdk。
配置 Apache Spark 和。macOS 上 Apache Spark 的. NET
在这一节中,我们将介绍如何获得 Apache Spark 运行开发机器的本地实例;一旦我们有了支持的版本的工作安装。NET,我们可以创建我们的第一个。NET 应用程序在下一章。在后面的小节中,我们将看看如何在 Windows 和 Linux 上配置 Apache Spark。
配置已安装的 Java
在安装 Java 之前,有必要检查您是否已经安装并配置了正确的 Java 版本,或者您已经安装了正确的版本,但是配置了单独的版本。要查看 macOS 上有哪些版本的 Java,运行/usr/libexec/java_home -V。在这种情况下,输出显示了 Java 8 和 13 JDKs:
» /usr/libexec/java_home -V
Matching Java Virtual Machines (2):
13.0.1, x86_64: "OpenJDK 13.0.1" /Library/Java/JavaVirtualMachines/openjdk-13.0.1.jdk/Contents/Home
1.8.0_232, x86_64: "AdoptOpenJDK 8" /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home
在我的例子中,我需要 Java 8,这样我就可以运行 Apache Spark“3 . 0 . 0”,所以我检查两个 Java 实例的哪个版本被设置为默认版本:
» java -version
openjdk version "1.8.0_232"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_232-b09)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.232-b09, mixed mode)
如果选择了不正确的版本,那么我需要使用工具"/usr/libexec/java_home"更新我的 JAVA_HOME 环境变量,并向该工具传递我想要使用的 JAVA 版本:
» export JAVA_HOME=$(/usr/libexec/java_home -v 13)
» java -version
openjdk version "13.0.1" 2019-10-15
OpenJDK Runtime Environment (build 13.0.1+9)
OpenJDK 64-Bit Server VM (build 13.0.1+9, mixed mode, sharing)
或者
» export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
» java -version
openjdk version "1.8.0_232"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_232-b09)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.232-b09, mixed mode)
安装 Java
如果您没有 Apache Spark 可以使用的 Java 版本,我们将需要下载一个 JDK 版本。在本节中,我们将下载并安装 AdoptOpenJDK 8 JDK,因此我们转到发布页面( https://adoptopenjdk.net/releases.html )并选择
-
open JDK 8(lt)
-
热点
-
苹果
-
JDK。包装
这些选项下载一个 pkg 文件,然后您可以使用 GUI 或命令行installer -pkg jdk.pkg安装该文件。如果您下载了这个包,这个包将安装并配置 Java。如果您启动一个新的终端并运行java -version,检查现在显示的是正确的版本;如果它没有显示正确的版本,请按照上一节中的步骤操作。
下载和配置 Apache Spark
现在您已经有了正确的 Java 工作版本,可以下载 Apache Spark 了。进入首页( https://spark.apache.org/downloads.html ,选择你想要的版本和包类型,然后下载;我下载并复制我的到我的主目录,然后运行
» tar -xvf spark-3.0.0-bin-hadoop2.7.tgz
该命令将文件提取到~/ spark-3.0.0-bin-hadoop2.7/,因此下一步是设置几个环境变量:
-
飞奔回家
-
小路
我们将SPARK_HOME设置为我们刚刚提取的目录,我将更新我的。zshrc 的新文件夹,并更新我的变量。zshrc 要包括“$SPARK_HOME/bin”;确保在更新路径之前设置了$SPARK_HOME。
试验
要验证 Apache Spark 的安装是否正常,运行spark-shell,这是一个运行命令的 REPL。如果您运行spark-shell,您应该看到显示的 spark 标志和一个命令提示符,您可以在这里运行 spark 命令。
» spark-shell
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Spark context Web UI available at http://localhost:4040
Spark context available as 'sc' (master = local[*], app id = local-1595401509136).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 3.0.0
/_/
Using Scala version 2.12.10 (OpenJDK 64-Bit Server VM, Java 1.8.0_262)
Type in expressions to have them evaluated.
Type :help for more information.
scala>
如果您得到scala>提示符,那么这是一切正常的好迹象,但是让我们看看是否可以运行 spark 命令来做一些本地处理:
scala> spark.sql("select * from range(10)").withColumn("ID2", col("ID")).show
+---+---+
| id|ID2|
+---+---+
| 0| 0|
| 1| 1|
| 2| 2|
| 3| 3|
| 4| 4|
| 5| 5|
| 6| 6|
| 7| 7|
| 8| 8|
| 9| 9|
+---+---+
我们在这里做的是通过运行一些 spark SQL 调用range函数来创建一个DataFrame。range函数根据我们的要求创建尽可能多的行,然后我们使用 withColumn 创建第二列,其值与第一列中的值相同。最后,我们使用show来显示DataFrame的内容。
退出 REPL 就像退出维姆;使用:q。
覆盖默认配置
要配置 Apache Spark,我们可以使用$ Spark _ HOME/conf;中的配置文件。这些是一组控制 Apache Spark 如何工作的文本文件。当您第一次下载 Apache Spark 时,只有模板配置文件,没有实际的配置文件:
~/spark-3.0.0-bin-hadoop2.7/conf » ls
docker.properties.template metrics.properties.template spark-env.sh.template
fairscheduler.xml.template slaves.template
log4j.properties.template spark-defaults.conf.template
如果我们想要配置任何配置文件,我们应该首先通过删除将模板文件复制到实际文件中。模板"从文件名的末尾开始:
» cp ./spark-defaults.conf.template ./spark-defaults.conf
» cp ./log4j.properties.template ./log4j.properties
然后,您可以使用您最喜欢的编辑器打开配置文件并编辑它们。对于 Apache Spark 的开发实例,我会将控制台日志记录的 log4j 部分改为只显示错误;显示警告会产生很多我们通常可以忽略的输出。如果您更改默认文件内容,如下所示:
# Set everything to be logged to the console
log4j.rootCategory=INFO, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
到
# Set only errors to be logged to the console
log4j.rootCategory=ERROR, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
这通常是一个好的开始。在 spark-defaults.conf 文件中,配置 Apache Spark 可以在您的开发机器上使用多少内存通常是一个好主意;例如,我的机器有 16GB 的 RAM,所以我将 Apache Spark 配置为使用 6GB 的 executor 内存。我还广泛使用了 Databricks 中的 Delta Lake 库,因此通过将它添加到我的默认配置中,我每次都可以访问它,并且不需要记住使用附加库启动 Apache Spark:
spark.executor.memory=6g
spark.jars.packages=io.delta:delta-core_2.12:0.7.0
一旦我更改了配置文件,重新运行 spark-shell 来验证更改是否有效总是值得的。这一次,当我运行 spark-shell 时,我可以看到日志记录变得不那么详细了,并且我的 delta 包已经被加载了:
» ./spark-shell
Ivy Default Cache set to: /Users/ed/.ivy2/cache
The jars for the packages stored in: /Users/ed/.ivy2/jars
:: loading settings :: url = jar:file:/Users/ed/spark-3.0.0-bin-without-hadoop/jars/ivy-2.4.0.jar!/org/apache/ivy/core/settings/ivysettings.xml
io.delta#delta-core_2.12 added as a dependency
:: resolving dependencies :: org.apache.spark#spark-submit-parent-4987d518-30f9-4696-ac0e-1b20ed99f224;1.0
confs: [default]
found io.delta#delta-core_2.12;0.7.0 in central
found org.antlr#antlr4;4.7 in central
found org.antlr#antlr4-runtime;4.7 in local-m2-cache
found org.antlr#antlr-runtime;3.5.2 in central
found org.antlr#ST4;4.0.8 in central
found org.abego.treelayout#org.abego.treelayout.core;1.0.3 in spark-list
found org.glassfish#javax.json;1.0.4 in central
found com.ibm.icu#icu4j;58.2 in central
:: resolution report :: resolve 210ms :: artifacts dl 8ms
:: modules in use:
com.ibm.icu#icu4j;58.2 from central in [default]
io.delta#delta-core_2.12;0.7.0 from central in [default]
org.abego.treelayout#org.abego.treelayout.core;1.0.3 from spark-list in [default]
org.antlr#ST4;4.0.8 from central in [default]
org.antlr#antlr-runtime;3.5.2 from central in [default]
org.antlr#antlr4;4.7 from central in [default]
org.antlr#antlr4-runtime;4.7 from local-m2-cache in [default]
org.glassfish#javax.json;1.0.4 from central in [default]
---------------------------------------------------------------------
| | modules || artifacts |
| conf | number| search|dwnlded|evicted|| number|dwnlded|
---------------------------------------------------------------------
| default | 8 | 0 | 0 | 0 || 8 | 0 |
---------------------------------------------------------------------
:: retrieving :: org.apache.spark#spark-submit-parent-4987d518-30f9-4696-ac0e-1b20ed99f224
confs: [default]
0 artifacts copied, 8 already retrieved (0kB/7ms)
如果您能够运行 Apache Spark REPL,并且能够运行 Spark 命令,那么您很可能能够运行您的。当我们在下一章创建我们的第一个应用程序时。
配置 Apache Spark 和。NET for Apache Spark on Windows
在这一节中,我们将介绍如何获得 Apache Spark 运行开发机器的本地实例;一旦我们有了支持的版本的工作安装。NET,我们可以创建我们的第一个。NET 应用程序在下一章。
配置已安装的 Java
在安装 Java 之前,有必要检查您是否已经安装并配置了正确的 Java 版本,或者您已经安装了正确的版本,但是配置了单独的版本。要想知道你的 Windows 机器上有哪些版本的 Java,看看你的program files目录中有哪些java.exe文件。在这种情况下,输出显示了 Java 8 和 11 JDKs:
C:\>cd %ProgramFiles%
C:\Program Files>attrib java.exe /s
A C:\Program Files\AdoptOpenJDK\jdk-11.0.8.10-hotspot\bin\java.exe
A C:\Program Files\AdoptOpenJDK\jdk-8.0.262.10-hotspot\bin\java.exe
A C:\Program Files\AdoptOpenJDK\jdk-8.0.262.10-hotspot\jre\bin\java.exe
您可以选择是为整个系统设置正确的 Java 版本,还是在每次需要运行应用程序时设置 Java 版本。关于这个选择没有硬性的规则,但是我通常在运行时设置 Java 的版本,除非我 100%使用单一版本的 Java VM。
要在运行时设置 Java 的版本,需要设置两个环境变量:JAVA_HOME 和 PATH。JAVA_HOME 指向 JAVA 版本的根文件夹,PATH 包含 java.exe 所在文件夹的路径:
C:\Program Files>SET JAVA_HOME=C:\Program Files\AdoptOpenJDK\jdk-8.0.262.10-hotspot
C:\Program Files>SET PATH=%JAVA_HOME%\bin;%PATH%
然后,您必须使用命令提示符来启动使用 Java VM 的应用程序,您可以在命令提示符中设置环境变量。
如果你更喜欢为你的整个系统设置版本,如果你总是使用相同的版本会更简单,你应该进入 Windows 设置并搜索“编辑系统环境变量”,使用控制面板小程序为JAVA_HOME创建一个指向你的 java 目录的“系统变量”,并将包含 java.exe 的 java bin 文件夹的路径添加到PATH环境变量的开头。
当您在控制台会话中或使用 Windows 设置对话框在系统范围内配置了环境变量(这需要重新启动)后,您可以通过运行java -version来测试您是否配置了正确的 Java 版本:
C:\Program Files>java -version
openjdk version "1.8.0_262"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_262-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.262-b10, mixed mode).
如果您决定在每次运行应用程序时都配置JAVA_HOME和PATH,那么创建一个为您完成工作的批处理文件是很有用的,因此您只需在运行应用程序之前调用它。
安装 Java
如果您没有 Apache Spark 可以使用的 Java 版本,我们将需要下载一个 JDK 版本。在本节中,我们将下载并安装 AdoptOpenJDK 8 JDK,因此我们转到发布页面( https://adoptopenjdk.net/releases.html )并选择
-
open JDK 8(lt)
-
热点
-
Windows 操作系统
-
JDK .msi 文件
这些选项下载一个 MSI 文件,然后您可以安装该文件。如果您下载了这个包,这个包将安装并配置 Java。如果您启动一个新的终端并运行java -version,检查现在显示的是正确的版本;如果它没有显示正确的版本,请按照上一节中的步骤操作。
下载和配置 Apache Spark
现在你已经有了正确的 Java 工作版本,可以从主页( https://spark.apache.org/downloads.html )下载 Apache Spark,选择你想要的版本和包类型,然后下载。
当 Apache Spark 完成下载后,因为该文件是一个 gzipped tar 归档文件,所以您将无法提取它,除非您使用类似 WinZip 或 7-Zip 的文件。我们将首先使用 7-Zip 来解压缩 tar 归档文件:
>"c:\Program Files\7-Zip\7z.exe" x spark-3.0.0-bin-hadoop2.7.gz
7-Zip 将创建一个名为 spark-3.0.0-bin-hadoop2.7 的文件,这是一个 tar 归档文件;然后我们可以再次使用 7-Zip 来获得实际的文件
>"c:\Program Files\7-Zip\7z.exe" x spark-3.0.0-bin-hadoop2.7 -ospark
我们最终得到一个名为 spark 的目录,其中包含我们想要的实际文件夹。如果您切换到实际的 spark 目录并运行dir,您应该会看到这些文件和文件夹:
>dir
Volume in drive C is Windows
Volume Serial Number is C024-E5C2
Directory of C:\Users\ed\Downloads\spark\spark-3.0.0-bin-hadoop2.7
05/30/2020 12:02 AM <DIR> .
05/30/2020 12:02 AM <DIR> ..
05/30/2020 12:02 AM <DIR> bin
05/30/2020 12:02 AM <DIR> conf
05/30/2020 12:02 AM <DIR> data
05/30/2020 12:02 AM <DIR> examples
05/30/2020 12:02 AM <DIR> jars
05/30/2020 12:02 AM <DIR> kubernetes
05/30/2020 12:02 AM 21,371 LICENSE
05/30/2020 12:02 AM <DIR> licenses
05/30/2020 12:02 AM 42,919 NOTICE
05/30/2020 12:02 AM <DIR> python
05/30/2020 12:02 AM <DIR> R
05/30/2020 12:02 AM 3,756 README.md
05/30/2020 12:02 AM 187 RELEASE
05/30/2020 12:02 AM <DIR> sbin
05/30/2020 12:02 AM <DIR> yarn
下一步是将 spark 文件夹移动到您想要保存它的位置。你可以把代码放在你选择的任何地方;然而,我倾向于创建一个 c:\spark 文件夹,并将每个版本复制到该文件夹中。在这种情况下,对于 Apache Spark 3.0.0,这意味着我的 Spark 文件夹将是 c:\ Spark \ spark-3.0.0-bin-hadoop2.7。与安装和配置 Java 一样,我们还需要创建一个名为SPARK_HOME的环境变量,并更新PATH以包含 Spark-3 . 0 . 0-bin-Hadoop 2.7 目录中 bin 文件夹的路径。如果您一次只使用一个版本的 Apache Spark,那么您可以通过使用 Windows 设置并搜索“编辑系统环境变量”来更改系统环境变量,并使用控制面板小程序为SPARK_HOME创建一个“系统变量”,并编辑PATH的环境变量以包含我们之前提取的 bin 文件夹。
如果您确实想创建一个 c:\spark 文件夹,那么这是一个保存批处理脚本的好地方,用来配置您希望使用的 Apache Spark 的每个版本;对于每个版本的 Apache Spark,在 Windows 上,我创建了一个名为c:\spark\version.cmd的文件,因此,在本例中为c:\spark\3-0-0.cmd,并包含 Java 和 Apache Spark 环境变量:
SET JAVA_HOME=C:\Program Files\AdoptOpenJDK\jdk-8.0.262.10-hotspot
SET SPARK_HOME=c:\spark\spark-3.0.0-bin-hadoop2.7
SET PATH=%JAVA_HOME%\bin;%SPARK_HOME%\bin;%PATH%
连接器
当您在 Windows 上运行时,预构建的 Hadoop 版本(允许读写文件)缺少一些核心功能。要读取或写入文件,我们需要一个额外的 exe 调用 winutils。您可以从 Hadoop 源代码手动编译它,也可以下载一个预构建的。如果你想从 Hadoop 源代码构建,那么请看这里的说明: https://github.com/steveloughran/winutils 。如果您决定下载一个预建的,那么访问页面( https://github.com/cdarlint/winutils )并为您的 Hadoop 版本选择一个 winutils 版本。在前面的例子中,我们下载了 Apache Spark 3 . 0 . 0 版和 Hadoop 版的spark-3.0.0-bin-hadoop2.7.gz,;目前,GitHub 上 winutils repo 中 Hadoop 2.7 的最新版本是 2.7.7。如果你进入那个文件夹,你可以下载 winutils.exe。Apache Spark 不需要任何其他文件。配置 Apache Spark 实例最简单的方法是将您的 winutils.exe 文件复制到您的%SPARK_HOME%\bin目录,并设置一个名为HADOOP_HOME的附加环境变量,如果您只打算使用 Apache Spark 的单一版本,则使用系统环境变量,或者将它包含在您的特定版本批处理脚本中:
SET JAVA_HOME=C:\Program Files\AdoptOpenJDK\jdk-8.0.262.10-hotspot
SET SPARK_HOME=c:\spark\spark-3.0.0-bin-hadoop2.7
SET HADOOP_HOME=%SPARK_HOME%
SET PATH=%JAVA_HOME%\bin;%SPARK_HOME%\bin;%PATH%
试验
要验证 Apache Spark 的安装是否正常,运行spark-shell,这是一个运行命令的 REPL。如果您运行spark-shell,您应该看到显示的 spark 标志和一个命令提示符,您可以在这里运行 spark 命令。我第一次运行 Java 和 Apache Spark 时,还必须允许访问 Windows 防火墙。
» spark-shell
Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Spark context Web UI available at http://localhost:4040
Spark context available as 'sc' (master = local[*], app id = local-1595570221598).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 3.0.0
/_/
Using Scala version 2.12.10 (OpenJDK 64-Bit Server VM, Java 1.8.0_262)
Type in expressions to have them evaluated.
Type :help for more information
scala>
如果您得到scala>提示符,那么这是一切正常的好迹象,但是让我们看看是否可以运行 spark 命令来做一些本地处理:
scala> spark.sql("select * from range(10)").withColumn("ID2", col("ID")).show
+---+---+
| id|ID2|
+---+---+
| 0| 0|
| 1| 1|
| 2| 2|
| 3| 3|
| 4| 4|
| 5| 5|
| 6| 6|
| 7| 7|
| 8| 8|
| 9| 9|
+---+---+
我们在这里做的是通过运行一些 spark SQL 调用range函数来创建一个DataFrame。range函数根据我们的要求创建尽可能多的行,然后我们使用 withColumn 创建第二列,其值与第一列中的值相同。最后,我们使用show来显示DataFrame的内容。
退出 REPL 就像退出维姆;使用:q。
在 Windows 上退出 REPL 时,默认情况下,日志记录级别配置为显示可以忽略的错误。如果您看到诸如“ERROR ShutdownHookManager:Exception while deleting Spark temp dir”这样的错误,我们将在下一节中配置日志记录来隐藏这个良性错误。
覆盖默认配置
要配置 Apache Spark,我们可以使用位于%SPARK_HOME%\conf文件夹中的配置文件。这些是一组控制 Apache Spark 如何工作的文本文件。当您第一次下载 Apache Spark 时,只有模板配置文件,没有实际的配置文件:
C:\spark\spark-3.0.0-bin-hadoop2.7\conf>dir
Volume in drive C is Windows
Volume Serial Number is C024-E5C2
Directory of C:\spark\spark-3.0.0-bin-hadoop2.7\conf
05/30/2020 12:02 AM <DIR> .
05/30/2020 12:02 AM <DIR> ..
05/30/2020 12:02 AM 996 docker.properties.template
05/30/2020 12:02 AM 1,105 fairscheduler.xml.template
05/30/2020 12:02 AM 2,025 log4j.properties.template
05/30/2020 12:02 AM 7,801 metrics.properties.template
05/30/2020 12:02 AM 865 slaves.template
05/30/2020 12:02 AM 1,292 spark-defaults.conf.template
05/30/2020 12:02 AM 4,221 spark-env.sh.template
如果我们想要配置任何配置文件,我们应该首先将模板文件复制到实际文件中,我们需要 spark-defaults.conf 和 log4j.properties:
C:\spark\spark-3.0.0-bin-hadoop2.7\conf>copy spark-defaults.conf.template spark-defaults.conf
1 file(s) copied.
C:\spark\spark-3.0.0-bin-hadoop2.7\conf>copy log4j.properties.template log4j.properties
1 file(s) copied.
然后,您可以使用您最喜欢的编辑器打开配置文件并编辑它们。对于 Apache Spark 的开发实例,我会将控制台日志记录的 log4j 部分改为只显示错误;显示警告会产生很多我们通常可以忽略的输出。如果您更改默认文件内容,如下所示:
# Set everything to be logged to the console
log4j.rootCategory=INFO, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
到
# Set only errors to be logged to the console
log4j.rootCategory=ERROR, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
我们还需要确保这两行出现在 log4j.properties 文件中;否则,我们每次退出 Apache Spark 时都会看到一个错误,这不会导致任何副作用,只会引起混乱和怀疑:
log4j.logger.org.apache.spark.util.ShutdownHookManager=OFF
log4j.logger.org.apache.spark.SparkEnv=ERROR
这通常是一个好的开始。在 spark-defaults.conf 文件中,配置 Apache Spark 可以在您的开发机器上使用多少内存通常是一个好主意;例如,我的机器有 16GB 的 RAM,所以我将 Apache Spark 配置为使用 6GB 的 executor 内存。我还广泛使用了 Databricks 中的 Delta Lake 库,因此通过将它添加到我的默认配置中,我每次都可以访问它,并且不需要记住使用附加库启动 Apache Spark:
spark.executor.memory=6g
spark.jars.packages=io.delta:delta-core_2.12:0.7.0
一旦我更改了配置文件,重新运行 spark-shell 来验证更改是否有效总是值得的。这一次,当我运行 spark-shell 时,我可以看到日志记录变得不那么详细了,并且我的 delta 包已经被加载了:
C:\spark\spark-3.0.0-bin-hadoop2.7\conf>spark-shell
Ivy Default Cache set to: C:\Users\ed\.ivy2\cache
The jars for the packages stored in: C:\Users\ed\.ivy2\jars
:: loading settings :: url = jar:file:/C:/spark/spark-3.0.0-bin-hadoop2.7/jars/ivy-2.4.0.jar!/org/apache/ivy/core/settings/ivysettings.xml
io.delta#delta-core_2.12 added as a dependency
:: resolving dependencies :: org.apache.spark#spark-submit-parent-01c3fcde-c454-4d1c-a62a-4a6a55bc3838;1.0
confs: [default]
found io.delta#delta-core_2.12;0.7.0 in central
found org.antlr#antlr4;4.7 in central
found org.antlr#antlr4-runtime;4.7 in central
found org.antlr#antlr-runtime;3.5.2 in central
found org.antlr#ST4;4.0.8 in central
found org.abego.treelayout#org.abego.treelayout.core;1.0.3 in central
found org.glassfish#javax.json;1.0.4 in central
found com.ibm.icu#icu4j;58.2 in central
:: resolution report :: resolve 1390ms :: artifacts dl 32ms
:: modules in use:
com.ibm.icu#icu4j;58.2 from central in [default]
io.delta#delta-core_2.12;0.7.0 from central in [default]
org.abego.treelayout#org.abego.treelayout.core;1.0.3 from central in [default]
org.antlr#ST4;4.0.8 from central in [default]
org.antlr#antlr-runtime;3.5.2 from central in [default]
org.antlr#antlr4;4.7 from central in [default]
org.antlr#antlr4-runtime;4.7 from central in [default]
org.glassfish#javax.json;1.0.4 from central in [default]
-------------------------------------------------------------------
| | modules || artifacts |
| conf | number| search|dwnlded|evicted|| number|dwnlded|
-------------------------------------------------------------------
| default | 8 | 0 | 0 | 0 || 8 | 0 |
-------------------------------------------------------------------
:: retrieving :: org.apache.spark#spark-submit-parent-01c3fcde-c454-4d1c-a62a-4a6a55bc3838
confs: [default]
0 artifacts copied, 8 already retrieved (0kB/31ms)
20/07/24 06:10:26 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Spark context Web UI available at http:// win10.jfdetya2p5vexax3rmqk4rrt4a.ax.internal.cloudapp.net:4040
Spark context available as 'sc' (master = local[*], app id = local-1595571047482).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 3.0.0
/_/
Using Scala version 2.12.10 (OpenJDK 64-Bit Server VM, Java 1.8.0_262)
Type in expressions to have them evaluated.
Type :help for more information.
scala>
如果您能够运行 Apache Spark REPL,并且能够运行 Spark 命令,那么您很可能能够运行您的。当我们在下一章创建我们的第一个应用程序时。
配置 Apache Spark 和。NET for Apache Spark on Linux(Ubuntu)
在这一节中,我们将介绍如何获得 Apache Spark 运行开发机器的本地实例;一旦我们有了支持的版本的工作安装。NET,我们可以创建我们的第一个。NET 应用程序在下一章。
配置已安装的 Java
在安装 Java 之前,有必要检查您是否已经安装并配置了正确的 Java 版本,或者您已经安装了正确的版本,但是配置了单独的版本。要查看 Ubuntu 上有哪些 Java 版本,请使用update-alternatives工具:
$ sudo update-alternatives --config java
There are 2 choices for the alternative java (providing /usr/bin/java).
Selection Path Priority Status
------------------------------------------------------------
* 0 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 1111 auto mode
1 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 1111 manual mode
2 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java 1081 manual mode
update-alternatives显示我们已经安装了 Java 11 和 Java 8。但是,Java 11 是默认的,它不支持当前最新版本的。NET for Apache Spark,需要 Java 8。
您可以选择通过传入 Java 8 运行时的选择号来更改缺省值,也可以在运行 Apache Spark 应用程序之前创建一个脚本,该脚本总是配置正确的 Java 版本:
#!/bin/bash
export JAVA_HOME=$(update-java-alternatives --list java-1.8.0-openjdk-amd64 | awk '{print $3}')
export PATH=$JAVA_HOME/bin:$PATH
在我们创建了这个 shell 文件之后,我们可以对它进行源代码处理,然后运行 java -version 来检查脚本是否成功运行:
$ source ./spark.sh
$ java -version
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (build 1.8.0_252-8u252-b09-1~18.04-b09)
OpenJDK 64-Bit Server VM (build 25.252-b09, mixed mode)
在这种情况下,我们现在看到我们在终端会话中设置的 Java 是 Java 8,这是正确的。
安装 Java
如果您没有 Apache Spark 可以使用的 Java 版本,我们将需要下载一个 JDK 版本。在本节中,我们将使用 OpenJDK 的版本 8;要使用apt-get进行安装,请运行sudo apt-get install openjdk-8-jdk。
如果您启动一个新的终端并运行java -version,检查现在显示的是正确的版本;如果它没有显示正确的版本,请按照上一节中的步骤操作。
下载和配置 Apache Spark
现在您已经有了正确的 Java 工作版本,可以下载 Apache Spark 了。进入首页( https://spark.apache.org/downloads.html ,选择你想要的版本和包类型,然后下载;我下载并复制我的到我的主目录,然后运行
$ tar -xvf spark-3.0.0-bin-hadoop2.7.tgz
该命令将文件提取到~/ spark-3.0.0-bin-hadoop2.7/,因此下一步是设置几个环境变量:
-
飞奔回家
-
小路
我们将SPARK_HOME设置为我们刚刚提取的目录,我将更新我的。bashrc 并更新我们的变量。巴沙尔要包括"$SPARK_HOME/bin;确保在更新路径之前设置了$SPARK_HOME。
测试安装
要验证 Apache Spark 的安装是否正常,运行spark-shell,这是一个运行命令的 REPL。如果您运行spark-shell,您应该看到显示的 spark 标志和一个命令提示符,您可以在这里运行 spark 命令。
$ spark-shell
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Spark context Web UI available at http://localhost:4040
Spark context available as 'sc' (master = local[*], app id = local-1595401509136).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 3.0.0
/_/
Using Scala version 2.12.10 (OpenJDK 64-Bit Server VM, Java 1.8.0_262)
Type in expressions to have them evaluated.
Type :help for more information.
scala>
如果您得到scala>提示符,那么这是一切正常的好迹象,但是让我们看看是否可以运行 spark 命令来做一些本地处理:
scala> spark.sql("select * from range(10)").withColumn("ID2", col("ID")).show
+---+---+
| id|ID2|
+---+---+
| 0| 0|
| 1| 1|
| 2| 2|
| 3| 3|
| 4| 4|
| 5| 5|
| 6| 6|
| 7| 7|
| 8| 8|
| 9| 9|
+---+---+
我们在这里做的是通过运行一些 spark SQL 调用range函数来创建一个DataFrame。range函数根据我们的要求创建尽可能多的行,然后我们使用 withColumn 创建第二列,其值与第一列中的值相同。最后,我们使用show来显示DataFrame的内容。
退出 REPL 就像退出维姆;使用:q。
覆盖默认配置
要配置 Apache Spark,我们可以使用$SPARK_HOME/conf中的配置文件;这些是一组控制 Apache Spark 如何工作的文本文件。当您第一次下载 Apache Spark 时,只有模板配置文件,没有实际的配置文件:
~/spark-3.0.0-bin-hadoop2.7/conf » ls
docker.properties.template metrics.properties.template spark-env.sh.template
fairscheduler.xml.template slaves.template
log4j.properties.template spark-defaults.conf.template
如果我们想要配置任何配置文件,我们应该首先通过删除将模板文件复制到实际文件中。模板"从文件名的末尾开始:
$ cp ./spark-defaults.conf.template ./spark-defaults.conf
$ cp ./log4j.properties.template ./log4j.properties
然后,您可以使用您最喜欢的编辑器打开配置文件并编辑它们。对于 Apache Spark 的开发实例,我会将控制台日志记录的 log4j 部分改为只显示错误;显示警告会产生很多我们通常可以忽略的输出。如果您更改默认文件内容,如下所示:
# Set everything to be logged to the console
log4j.rootCategory=INFO, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
到
# Set only errors to be logged to the console
log4j.rootCategory=ERROR, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
这通常是一个好的开始。在 spark-defaults.conf 文件中,配置 Apache Spark 可以在您的开发机器上使用多少内存通常是一个好主意;例如,我的机器有 16GB 的 RAM,所以我将 Apache Spark 配置为使用 6GB 的 executor 内存。我还广泛使用了 Databricks 中的 Delta Lake 库,因此通过将它添加到我的默认配置中,我每次都可以访问它,并且不需要记住使用附加库启动 Apache Spark:
spark.executor.memory=6g
spark.jars.packages=io.delta:delta-core_2.12:0.7.0
一旦我更改了配置文件,重新运行 spark-shell 来验证更改是否有效总是值得的。这一次,当我运行 spark-shell 时,我可以看到日志记录变得不那么详细了,并且我的 delta 包已经被加载了:
$ ./spark-shell
Ivy Default Cache set to: /Users/ed/.ivy2/cache
The jars for the packages stored in: /Users/ed/.ivy2/jars
:: loading settings :: url = jar:file:/Users/ed/spark-3.0.0-bin-without-hadoop/jars/ivy-2.4.0.jar!/org/apache/ivy/core/settings/ivysettings.xml
io.delta#delta-core_2.12 added as a dependency
:: resolving dependencies :: org.apache.spark#spark-submit-parent-4987d518-30f9-4696-ac0e-1b20ed99f224;1.0
confs: [default]
found io.delta#delta-core_2.12;0.7.0 in central
found org.antlr#antlr4;4.7 in central
found org.antlr#antlr4-runtime;4.7 in local-m2-cache
found org.antlr#antlr-runtime;3.5.2 in central
found org.antlr#ST4;4.0.8 in central
found org.abego.treelayout#org.abego.treelayout.core;1.0.3 in spark-list
found org.glassfish#javax.json;1.0.4 in central
found com.ibm.icu#icu4j;58.2 in central
:: resolution report :: resolve 210ms :: artifacts dl 8ms
:: modules in use:
com.ibm.icu#icu4j;58.2 from central in [default]
io.delta#delta-core_2.12;0.7.0 from central in [default]
org.abego.treelayout#org.abego.treelayout.core;1.0.3 from spark-list in [default]
org.antlr#ST4;4.0.8 from central in [default]
org.antlr#antlr-runtime;3.5.2 from central in [default]
org.antlr#antlr4;4.7 from central in [default]
org.antlr#antlr4-runtime;4.7 from local-m2-cache in [default]
org.glassfish#javax.json;1.0.4 from central in [default]
---------------------------------------------------------------------
| | modules || artifacts |
| conf | number| search|dwnlded|evicted|| number|dwnlded|
---------------------------------------------------------------------
| default | 8 | 0 | 0 | 0 || 8 | 0 |
---------------------------------------------------------------------
:: retrieving :: org.apache.spark#spark-submit-parent-4987d518-30f9-4696-ac0e-1b20ed99f224
confs: [default]
0 artifacts copied, 8 already retrieved (0kB/7ms)
点网络工作者配置
在简介中,我们介绍了的体系结构。NET for Apache Spark,这就是。NET 代码使用 TCP 套接字连接到 JVM 代码,因此。NET 代码可以调用 JVM 中的代码。有一个例外,那就是用户定义函数或 UDF。UDF 通过在另一个单独的特定于微软的进程中运行来工作,JVM 连接到该进程并从中来回发送消息。如果我们使用 UDF,那么我们还需要从。NET 并配置一个环境变量来指向这个可执行文件。我们将在第四章中介绍更多关于 UDF 的内容,你可能永远都不需要使用 UDF。
来配置。NET for Apache Spark,所以 UDF 工作,你首先需要访问项目的 releases 页面( https://github.com/dotnet/spark/releases ),选择你要使用的版本和操作系统,下载并解压到某个地方,然后创建DOTNET_WORKER_DIR环境变量指向与微软的目录。Spark。工人可执行文件。
常见错误疑难解答
在这一节中,我们将讨论运行 Apache Spark 时出现的常见错误。
不支持的类文件主要版本
当运行 Apache Spark 命令时,您会看到一条消息“Java . lang . illegalargumentexception:Unsupported class file major version 58”,末尾的数字是不相关的。该消息意味着 Java 类版本与当前运行时不兼容。这通常是由于在不正确的 Java 版本上运行 Apache Spark 造成的。Apache Spark 不验证它运行在哪个版本的 Java 上。Apache Spark 试图运行,如果版本错误,就会失败。使用“java -version"检查您配置了哪个版本的 Java,并且在 Apache Spark 输出中,它应该打印出它认为自己使用的是哪个版本的 Java。除非您运行 Apache Spark,并且它输出了适用于您的 Apache Spark 版本的正确 Java 版本,否则您的程序总是会失败。
退出 Spark 时出现异常
当在 Windows 上退出 Apache Spark 应用程序时,您会看到“错误关闭 HookManager:删除 Spark 临时目录时出现异常”。这是一个可以安全忽略的错误,如果您用这两行更新% SPARK _ HOME % \ conf \ log4j . properties,那么错误消息将被隐藏:
log4j.logger.org.apache.spark.util.ShutdownHookManager=OFF
log4j.logger.org.apache.spark.SparkEnv=ERROR
无法运行 Spark 外壳
如果你在 Windows 上,你试图运行spark-shell,但是你得到一个错误“系统找不到指定的路径”,这可能是由JAVA_HOME环境变量末尾的分号或者其他一些关于JAVA_HOME的错误引起的。为了确保您的JAVA_HOME变量是正确的,尝试dir "%JAVA_HOME%\bin\java.exe",如果这没有显示 java.exe 存在,运行echo %JAVA_HOME%\bin\java.exe并确保整个路径指向一个 java.exe。
如果 Apache Spark 出现另一个错误,那么 Stack Overflow 是一个提问的好地方,或者如果您联系 Apache Spark 邮件列表,有一个很好的社区可以提供帮助。要查看所有可用的社区帮助,请参见 https://spark.apache.org/community.html 。
摘要
要让 Apache Spark 在您的开发机器上运行,需要安装和配置正确的 Java 版本,现在是 Java 8。当。NET for Apache Spark project 支持 Apache Spark 3.0,它将是 Java 11 及其未来版本,很可能支持更高版本的 Java。
整个过程是安装和配置 Java,然后下载和配置 Apache Spark。如果你用的是 Windows,那么你也应该下载 winutils.exe。如果你希望使用用户定义的函数,那么你还需要微软的。承载您的 UDF 代码的. NET 辅助进程。
如果在本章结束时,您可以运行 spark-shell,并且您使用的是正确的 Java 版本,那么您的状态很好,应该会很兴奋地编写您的第一个。NET for Apache Spark 在下一章。
三、Spark.NET 编程
在这一章中,我们将编写我们的第一个。NET 的 Apache Spark 应用程序,我们可以执行,甚至在我们最喜欢的调试。NET IDE。我们将讨论在我们的项目中我们需要做什么,然后我们需要做什么,以便我们可以使用 Apache Spark 执行程序。
一旦我们运行了第一个程序,我们将继续研究如何从现有的 PySpark 示例进行转换,并强调一些从 Scala 转换到. NET 时需要记住的 Scala 特性。从 Python 和 Scala 进行转换的原因是,它们是 Apache Spark 最常用的语言,并将在相当长的一段时间内,可能是永远。了解如何阅读 Python 和 Scala 示例并将示例转换成。NET 会让你更有效率,我坚信在。NET for Apache Spark 对于任何实现都是至关重要的。
第一个程序
在我们的第一个程序中,我们将使用 Apache Spark 来创建一个DataFrame以及这个DataFrame中的数据。我们将创建一个SparkSession,它类似于 Apache Spark 应用程序的网关,我们通过一个SparkSession让 Apache Spark 执行查询。一旦我们有了一个SparkSession,我们将使用它来创建一些数据,然后进行一些处理并保存输出。最后,我们将看看数据的物理位置,以及如何将数据从 JVM 拉入我们的。NET 程序,包括任何潜在的性能问题。
我将使用 dotnet 的命令行版本来创建一个新的控制台应用程序,但是请随意使用 IDE 创建一个 dotnet 控制台项目。
» dotnet new console -output HelloSpark --language "C#"
Getting ready...
The template "Console Application" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on HelloSpark/HelloSpark.csproj...
Determining projects to restore...
Restored /Users/ed/git/scratch/HelloSpark/HelloSpark.csproj (in 121 ms).
Restore succeeded.
要创建一个 F#控制台应用程序,我们可以使用
» dotnet new console -output HelloSpark --language "F#"
微软。Spark NuGet 包
一旦我们有了一个控制台应用程序,我们需要添加微软。Spark NuGet 包( www.nuget.org/packages/Microsoft.Spark/ )。
» cd HelloSpark
» dotnet add package Microsoft.Spark
Determining projects to restore...
Writing /var/folders/yw/9n3l8f4x2856pxvys69_lh580000gp/T/tmpgLiEtb.tmp
info : Adding PackageReference for package 'Microsoft.Spark' into project '/Users/ed/git/scratch/HelloSpark/HelloSpark.csproj'.
info : Restoring packages for /Users/ed/git/scratch/HelloSpark/HelloSpark.csproj...
info : GET https://api.nuget.org/v3-flatcontainer/microsoft.spark/index.json
info : OK https://api.nuget.org/v3-flatcontainer/microsoft.spark/index.json 112ms
info : Package 'Microsoft.Spark' is compatible with all the specified frameworks in project '/Users/ed/git/scratch/HelloSpark/HelloSpark.csproj'.
info : PackageReference for package 'Microsoft.Spark' version '0.12.1' added to file '/Users/ed/git/scratch/HelloSpark/HelloSpark.csproj'.
info : Committing restore...
info : Generating MSBuild file /Users/ed/git/scratch/HelloSpark/obj/HelloSpark.csproj.nuget.g.targets.
info : Writing assets file to disk. Path: /Users/ed/git/scratch/HelloSpark/obj/project.assets.json
log : Restored /Users/ed/git/scratch/HelloSpark/HelloSpark.csproj (in 905 ms).
这些命令已经在 macOS 上运行;尽管 Windows 和 Linux 上的输出略有不同,但命令是相同的。
如果你做到了这一步,那么你就会有一个包含微软的项目。Spark NuGet 包,如果您按照第一章中的步骤操作,那么您也将拥有一个本地 Apache Spark 实例,我们现在可以使用它了。
Spark 会议
是我们用来调用 Apache Spark 并让它执行我们的处理的类。每个 Java 虚拟机(JVM)只能有一个SparkSession,并且有一个特定的模式来获取我们的SparkSession,这涉及到使用SparkSession.Builder()和调用GetOrCreate,这将获得一个现有的会话或者创建一个新的会话。清单 3-1 和 3-2 展示了如何使用GetOrCreate创建一个SparkSession。
let spark = SparkSession
.Builder()
.AppName("DemoApp")
.Config("some-option", "value")
.Config("some-other-option", "value")
.GetOrCreate()
Listing 3-2Getting a reference to a SparkSession in F#
var spark = SparkSession
.Builder()
.AppName("DemoApp")
.Config("some-option", "value")
.Config("some-other-option", "value")
.GetOrCreate();
Listing 3-1Getting a reference to a SparkSession in C#
在这些代码清单中,我们可以看到SparkSession有一个名为Builder()的静态方法,它返回一个Builder。Builder让我们设置各种配置设置,以及应用程序名称。我们可以设置很多配置设置,要查看全套选项,请访问 http://spark.apache.org/docs/latest/configuration.html#available-properties 。
在这两个代码清单中,我使用变量名spark创建了SparkSession。我建议你也使用与在某些环境中使用 Apache Spark 相同的名字,比如 PySpark 或 Scala REPL,或者在 Databricks 笔记本中,变量 Spark 是预先定义的,并指向活动的SparkSession。如果您保持这种命名策略,那么在不同环境之间移植代码会更容易。
当我们使用 Apache Spark 时,我们需要理解代码和数据驻留在哪里。如果我创建一个变量并存储字符串“Hello Apache Spark”,那么变量和数据将存在于。NET 应用;阿帕奇 Spark 将无法看到它。相反,如果我使用 Apache Spark 读取一个文件,Apache Spark 将可以使用该数据,但是我们不能在我们的。NET 应用程序。我们要么需要从 Apache Spark 的某个地方写出数据,然后从我们的。NET 应用程序,否则我们需要将数据从 Apache Spark 返回到我们的。NET 应用程序。收集回数据会将数据代理回. NET 中。如果这是几行数据,那就没问题,但如果是万亿字节的数据,那么这可能会导致性能问题。
我们现在来看看我们的第一个完整的示例程序。在 Apache Spark 中,我们对创建和修改DataFrames感兴趣,然后我们可以聚集和保存一些数据,或者再次写回文件或数据库。在下一个例子中,列表 3-3 ,我们将创建一个 DataFrame,将输出保存到一个 CSV 文件,然后将结果“收集”回。这样我们就可以在. NET 中迭代每一行并对其进行操作。
using System;
using Microsoft.Spark.Sql;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var spark = SparkSession
.Builder()
.AppName("DemoApp")
.GetOrCreate();
var dataFrame = spark.Sql("select id, rand() as random_number from range(1000)");
dataFrame
.Write()
.Format("csv")
.Option("header", true)
.Option("sep", "|")
.Mode("overwrite")
.Save(args[1]);
foreach (var row in dataFrame.Collect())
{
if (row[0] as int? % 2 == 0)
{
Console.WriteLine($"row: {row[0]}");
}
}
}
}
}
Listing 3-3Our first full .NET for Apache Spark application in C#
如果我们将它分解,在清单 3-4 中,我们从SparkSession开始,通过使用SparkSession.Builder()来创建它,它又是从SparkSession获得的。
var spark = SparkSession
.Builder()
.AppName("DemoApp")
.GetOrCreate();
Listing 3-4Creating a SparkSession in C#
当我们创建 SparkSession 时,我们可以选择传入一个 AppName。这在我们对共享 Apache Spark 实例进行故障排除时非常有用,因为 AppName 显示在 SparkUI 中,我们将在第十章中详细介绍。
然后我们在清单 3-5 中展示了我们可以使用spark.Sql创建一个DataFrame并传入一个 SQL 查询。在这些演示中,我使用range来创建行;在实际项目中,您更可能通过读入数据来创建数据帧。
var dataFrame = spark.Sql("select id, rand() as random_number from range(1000)");
Listing 3-5Using SQL to create 1000 rows in C#
这里,我们将两个函数传递给spark.Sql。我们传递rand(),它为每一行创建一个随机数,传递range,它创建行,在本例中,行的 ID 列在 0 到 999 之间。在清单 3-6 中,我们展示了如何将数据帧写入磁盘。这里需要注意的是,我们特别将“csv”作为格式传入。然而,默认的 CSV 选项很少是理想的,所以我们可以覆盖默认值,强制写入列标题,并使用选项“header”和“sep”来更改默认分隔符;有关 CSV 格式选项的完整列表,请参见 DataWriter API 文档中的csv方法: https://spark.apache.org/docs/latest/api/java/org/apache/spark/sql/DataFrameWriter.html 。
dataFrame
.Write()
.Format("csv")
.Option("header", true)
.Option("sep", "|")
.Mode("overwrite")
.Save(args[1]);
Listing 3-6Writing the DataFrame to disk using C#
此时,数据仅在 JVM 端,我们可以要求 Apache Spark 写入数据或对数据执行任何我们想要的操作,但我们无法在. NET 中看到数据。我们可以看到有一个DataFrame并计算行数,但要将数据带回。NET,我们需要调用Collect()。在清单 3-7 中,我们展示了Collect,它返回一个IEnumerable<Row>,让我们访问每一行和每一行上的每一列。
foreach (var row in dataFrame.Collect())
{
if (row[0] as int? % 2 == 0)
{
Console.WriteLine($"row: {row[0]}");
}
}
Listing 3-7Writing the DataFrame to disk using C#
现在我们有了完整的 C#程序;我们还将在清单 3-8 中展示用 F#实现的同一个程序。
[<EntryPoint>]
let main argv =
let spark = SparkSession
.Builder()
.AppName("DemoApp")
.GetOrCreate()
let dataFrame = spark.Sql("select id, rand() as random_number from range(1000)")
dataFrame
.Write()
.Format("csv")
.Option("header", true)
.Option("sep", "|")
.Mode("overwrite")
.Save(argv.[1]);
dataFrame.Collect()
|> Seq.map(fun row -> row.Get(0) :?> int)
|> Seq.filter(fun id -> id % 2 = 0)
|> Seq.iter(fun i -> printfn "row: %d" i)
0
Listing 3-8Our first full .NET for Apache Spark application in F#
F#版本与 C#版本非常相似,直到我们想要将数据收集回。NET 中,我们映射IEnumerable<Row>来检索我们想要的特定列,然后过滤和迭代行。
在下一节中,我们将介绍如何在本地 Apache Spark 实例上运行我们的程序,以及如何调试。NET 代码在我们最喜欢的 IDE。
执行程序
在第一章中,我们讨论了写作所涉及的内容。NET 代码最终调用 Java 虚拟机(JVM)内部的 Java 类和方法。总而言之,我们需要做的是启动 Apache Spark 的一个会话,并使用。NET 的 Apache Spark 项目启动了我们的。NET 进程并代理。NET 代码和 JVM 代码。在这一节中,我们将介绍如何运行我们的应用程序,以及如何在我们最喜欢的 IDE 中调试代码。
在命令行上执行
为了执行我们的程序,我们需要运行spark-submit,它启动 Java 虚拟机并初始化 Apache Spark 的一个实例。我们向spark-submit传递一个参数,它告诉 Apache Spark 加载微软附带的 JAR 文件。Spark NuGet 包并运行org.apache.spark.deploy.dotnet.DotnetRunner类。DotnetRunner类启动一个监听端口,然后运行我们的应用程序,当我们的应用程序使用SparkSession.Builder()连接到 Apache Spark 时,DotnetRunner然后接受连接并在两个进程之间传递请求和响应。
因此,完整的命令行是 spark-submit 脚本,我们想要实例化的类名,到 Microsoft。Spark JAR 文件,执行 *our 的命令。NET 程序,*和我们程序需要的任何参数。我们在表 3-1 中展示了这些论点。
表 3-1
运行我们的。NET for Apache Spark 应用程序
|争吵
|
笔记
|
例子
| | --- | --- | --- | | Spark-提交 | 启动 Apache Spark | Spark-提交 | | -班级 | DotnetRunner 的完整类名 | org . Apache . spark . deploy . dot net . dotnetrunner | | JAR 文件的路径 | JAR 在 NuGet 包中,并在构建时复制到 bin 文件夹中我认为最好是显式地传递完整路径,因为 Apache Sparks 工作目录可能不是您的工作目录在 Mac/Linux 上,这将区分大小写,而在 Windows 上,如果路径中有空格,您将需要使用引号将路径括起来,因此尽可能避免空格 | /Users/user/git/hello park/bin/Debug/netcoreapp 3.1/Microsoft-spark-2.4 . x-0 . 12 . 1 . jar | | 我们的计划和任何争论 | | dotnet 运行-项目/项目/路径/tmp/CSV-输出 |
如果您下载本书的示例代码并提取文件,构建解决方案,然后您会发现 JAR 文件已经被复制到 bin 目录。
这个 JAR 特定于 Apache Spark 的每个版本,以及 Apache Spark 和 Microsoft 的版本。Spark 库编码在文件名中。例如,JAR 文件“microsoft-Spark-2.4.x-0 . 12 . 1 . JAR”适用于 Apache Spark 版本“2.4 . x”并且适用于 Microsoft。Spark 库版本“0.12.1”。
如果不确定自己用的是哪个版本的 Apache Spark,可以运行“spark-shell --version”,屏幕上会显示版本。例如,如果在您构建了解决方案之后,您的 JAR 文件路径是"c:\code\dotnet-spark\ch03\Chapter03\Listing3-3\bin\Debug\netcoreapp3.1\ microsoft-spark-2.4.x-0.12.1.jar"并且清单 3-3 需要一个单独的参数,该参数是将 csv 文件写入的路径,那么您的命令行应该是
spark-submit --class org.apache.spark.deploy.dotnet.DotnetRunner "c:\code\dotnet-spark\ch03\Chapter03\Listing3-3\bin\Debug\netcoreapp3.1\ microsoft-spark-2.4.x-0.12.1.jar" dotnet run "c:\code\dotnet-spark\ch03\Chapter03\Listing3-3" "c:\code\dotnet-spark\ch03\Chapter03\output-csv"
当我们运行时,我们应该在输出中看到以下文本:
[2020-08-13T09:34:00.5421480Z] [Machine] [Info] [ConfigurationService] Using port 49596 for connection.
[2020-08-13T09:34:00.5623630Z] [Machine] [Info] [JvmBridge] JvMBridge port is 49596
line: 0
line: 2
line: 4
line: 6
此外,如果我们查看传入的输出目录,应该会看到一组 CSV 文件。经常让 Apache Spark 的新用户感到困惑的是,尽管您传入了一个 CSV 文件的路径,但还是创建了一个目录,在该目录中,您会得到 12 个 CSV 文件。有 12 个文件,因为 Apache Spark 在它的执行器之间分割它的处理,并且 12 个不同的执行器写了这些文件。将一个大文件拆分成多个小文件的好处是,当 Apache Spark 读回这些文件供以后处理时,可以独立读取每个文件,这有助于轻松地并行加载。当您在生产环境中运行时,您可以看出读取几个单独的文件与读取压缩文件的性能差异,由于压缩的性质,这意味着一次只有一个执行器可以读取文件。
在写出DataFrame之前,可以让 Apache Spark 在DataFrame上使用Coalesce或Repartition写入单个文件或任意数量的文件,但是这样做可能会导致以后的性能问题。
调试。IDE 中的. NET 代码
运行我们的程序是很棒的,但是有时我们想调试我们的代码。这意味着使用调试器,如 Visual Studio、Visual Studio 代码或 JetBrains Rider。当我们在调试器中运行时,我们仍然需要考虑这样一个事实,即我们需要在一个 JVM 上启动 Apache Spark 实例,它是一个独立于我们的进程的进程。NET 应用程序。
为了调试我们的应用程序,我们可以使用 debug 参数,我们可以将它传递给 Scala DotnetRunner类。代替运行我们的程序,DotnetRunner将创建监听端口并等待传入的连接。使用 debug 命令允许我们单独启动应用程序的调试会话,并连接到 Apache Spark 实例。
命令行类似于我们使用的最后一个。不同之处在于,我们将传递单词“debug ”,而不是运行我们的。NET 程序:
spark-submit --class org.apache.spark.deploy.dotnet.DotnetRunner "c:\code\dotnet-spark\ch03\Chapter03\Listing3-3\bin\Debug\netcoreapp3.1\ microsoft-spark-2.4.x-0.12.1.jar" debug
当我们运行该命令时,应该会看到以下输出:
************************************************************
* .NET Backend running debug mode. Press enter to exit *
*************************************************************
然后我们可以在我们最喜欢的调试器中启动我们的应用程序,当我们调用SparkSession时。Builder(),我们的应用程序连接到已经运行的 Apache Spark 实例。
当我们像这样运行时,我们可以像在. NET 应用程序中一样设置断点并检查局部变量。重要的是要记住,虽然我们看到了像SparkSession和DataFrame这样的对象,但这些并不是 JVM 中存在的实际的SparkSession或DataFrame,而是对真实对象的引用,所以我们看不到它们内部的数据,除非我们使用Collect()来传递数据。在图 3-1 中,我们看到了 JetBrains Rider IDE,它设置了一个断点并显示关于局部变量的信息。
图 3-1
调试我们的。NET for Apache Spark 应用程序
使用像 JetBrains Rider 或 Visual Studio 这样的调试器,我们可以单步调试代码,设置断点,并检查局部变量。图 3-2 显示了变量的内容。对 JVM 对象的引用的对象如dataFrame或spark是引用,我们看不到任何有用的属性。变量,比如row,它们已经被收集回。在这种情况下,有 1000 行,所以从 JVM 代理不会占用太多内存或花费太多,但是如果有几十亿行,那么这可能是一个问题。
图 3-2
探索局部变量
令人兴奋的是,尽管dataFrame变量是对 JVM 对象的引用,我们看不到任何实际的属性,但我们仍然可以计算表达式。在图 3-3 中,我使用了 Rider 中的评估窗口对dataFrame.Count()进行了评估,结果显示出来。在 Visual Studio 中,您可以使用即时窗口或监视窗口来做类似的事情。
图 3-3
检查局部变量
Debug 命令的进一步使用
通常,当应用程序退出时,Apache Spark 实例和 Java 虚拟机也会退出。当您运行 debug 命令时,Apache Spark 实例保持活动状态,因此您可以启动应用程序的一个新实例,并连接到 Apache Spark 的前一个实例。在一些情况下,保持 Apache Spark 的运行实例是有用的。首先,当运行单元和集成测试时,如果您需要一个 Apache Spark 实例,您可以启动一个单独的实例并在所有测试中重用它,这样可以将启动 Apache Spark 的开销保持在最低水平。第二种情况是您正在开发一个应用程序,如果您有一个调试实例在后台运行,那么您可以快速测试更改,而不必每次都启动和停止 Apache Spark 实例。
当调试实例启动时,它监听一个特定的端口,即 5567,您可以通过使用环境变量DOTNETBACKEND_PORT更改默认端口来启动和使用多个调试实例。如果您更改调试实例侦听的端口,那么您还需要。NET 进程。
将现有应用程序转换为。网
在这一节中,我们将了解如何读取为 Apache Spark 编写的 PySpark 和 Scala 代码,以及如何将它们转换成。NET 代码。有一些事情需要理解,即使我们不知道如何用 Python 或 Scala 编程,我们也应该能够阅读代码并了解它是如何工作的,因为 Apache Spark 的大多数示例、范例和现有应用程序都是用 Python 或 Scala 编写的;因此,能够理解这些并将它们转化为。网络至关重要。
Apache Spark 发行版包括一个示例目录,其中包含一组 Java、Python、Scala 和 r 中的示例。
即使您已经精通 Python 和/或 Scala,我也鼓励您浏览这些示例,因为在介绍这些语言的同时,我们开始展示如何在 Apache Spark 中进行基本处理。
将 PySpark 示例转换为。网
在 Apache Spark 分发示例目录中,我们将转换“src/main/python/sql/basic.py”示例。本书中使用的文件也包含在示例代码中,如清单 3-9 所示。代码的 C#版本在包含的项目中,名为“python tosharp”,F#版本名为“python tosharp”。
在清单 3-9 中,我们看到了 Python 中所谓的 docstring,这是 Python 记录代码的方式,类似于我们经常添加到 C#方法和类中的 XML 注释。
"""
A simple example demonstrating basic Spark SQL features.
Run with:
./bin/spark-submit examples/src/main/python/sql/basic.py
"""
Listing 3-9Python docstring or documentation
Python 是非常层次化的,因为您可以在模块级、文件级、类级或函数级导入其他模块,无论导入发生在哪里,都可以在下面的级别进行导入,所以当您需要查看导入了什么时,您必须从函数开始,并向上检查更多的导入。在清单 3-10 中,我们可以看到四个导入,除了最后一个导入“from pyspark.sql.types import *”,它导入“pyspark.sql.types”下的所有内容,每个语句都从一个模块中导入一个函数或类。进口的另一种写法是“import pyspark.sql”或“from pyspark.sql import SparkSession, Row”。
from __future__ import print_function
# $example on:init_session$
from pyspark.sql import SparkSession
# $example off:init_session$
# $example on:schema_inferring$
from pyspark.sql import Row
# $example off:schema_inferring$
# $example on:programmatic_schema$
# Import data types
from pyspark.sql.types import *
# $example off:programmatic_schema$
Listing 3-10Python imports
在清单 3-11 ,C#和 3-12,F#中,我们显示而不是导入 pyspark。XXX,我们改参考微软。Spark.XXX,所以 pyspark.sql.SparkSession 变成了 Microsoft . spark . SQL . spark session。
open Microsoft.Spark.Sql
open Microsoft.Spark.Sql.Types
Listing 3-12Python imports converted to F#
using Microsoft.Spark.Sql;
using Microsoft.Spark.Sql.Types;
Listing 3-11Python imports converted to C#
在清单 3-13 中,我们看到我们定义的第一个函数。在 Python 中,程序的结构是用空格来定义的,所以当我们看到一个“def name():”的时候,所有缩进的东西,至少是一个层次,在文件的更下面是同一个函数的一部分。这个函数称为“basic_df_example ”,包含一个名为“spark”的参数。该函数做的第一件事是使用“spark.read.json”读入一个 JSON 文件。JSON 文件的内容被保存到一个DataFrame,然后使用show()显示出来。
def basic_df_example(spark):
df = spark.read.json("examples/src/main/resources/people.json")
# Displays the content of the DataFrame to stdout
df.show()
Listing 3-13Defining a function in Python and reading in a JSON file as a DataFrame
在清单 3-14 ,C#中,我们将创建一个调用。网络版。在 C#中,我们需要指定“spark”参数的类型,它将是SparkSession。我们将在文件的后面看到 spark 的定义。在清单 3-15 ,F#中,我们将创建一个同样读取 JSON 文件并显示内容的函数。
let BasicDfExample (spark:SparkSession) =
let dataFrame = spark.Read().Json("examples/src/main/resources/people.json")
dataFrame.Show()
Listing 3-15Defining a function in F# and reading in a JSON file as a DataFrame
static void BasicDfExample(SparkSession spark)
{
var dataFrame = spark.Read().Json("examples/src/main/resources/people.json");
dataFrame.Show();
}
Listing 3-14Defining a method in C# and reading in a JSON file as a DataFrame
然后,Python 代码打印清单 3-16 中数据帧的模式,这是一种直观查看文件读取是否正确的简便方法。
df.printSchema()
Listing 3-16Printing the schema of a DataFrame
在清单 3-17 和 3-18 中,我们需要将printSchema转换成。净当量,也就是PrintSchema。当您从 Python 切换到。NET 中,改变方法的大小写是最常见的事情。
dataFrame.PrintSchema()
Listing 3-18Printing the schema of a DataFrame in F#
dataFrame.PrintSchema()
Listing 3-17Printing the schema of a DataFrame in C#
在清单 3-19 中,Python 代码通过名称选择一列,然后打印出结果DataFrame。在DataFrame上调用select会创建一个DataFrame的新实例,因此任何操作都不会影响代码的其他部分。
df.select("name").show()
Listing 3-19Selecting a single column and displaying the contents
在清单 3-20 和 3-21 中,我们展示了 C#和 F#版本。
dataFrame.Select("name").Show()
Listing 3-21Selecting a single column and displaying the resulting DataFrame in F#
dataFrame.Select("name").Show();
Listing 3-20Selecting a single column and displaying the resulting DataFrame in C#
在清单 3-22 中,代码选择了两列,而不是使用列名,正如我们在清单 3-19 中看到的。我们在这里看到的是用列名索引的DataFrame,得到的是Column对象。Column对象很重要,因为它定义了列名和源DataFrame。考虑一下,如果你有两个数据帧,它们都包括列“ID”,如果你连接这两个数据帧并做一个Select("ID"),你会得到哪一列?这是不可能的,这将导致 Apache Spark 失败。在这种情况下,您可以通过从DataFrame中检索Column引用而不是使用字符串名称来精确地指定您想要的 ID 列。我们看到的第二件事是年龄列是一个计算,理解这个计算被传递到 Apache Spark 是很重要的,所以 Apache Spark 运行时运行这个计算,而不是数据被传递到 Python 进程和运行这个计算的 Python。对于大型数据集,这对性能非常重要。
df.select(df['name'], df['age'] + 1).show()
Listing 3-22Selecting two columns and performing a calculation on the age column
在清单 3-23 ,在 C#和 3-24,F#中,我们可以做同样的事情,但是应该注意,当我们调用dataFrame["age"] +1时,它是dataFrame["age"].Plus(1)的语法糖,这样写可能会更清楚。
dataFrame.Select(dataFrame.["name"], (dataFrame.["age"] + 1)).Show()
dataFrame.Select(dataFrame.["name"], (dataFrame.["age"].Plus(1))).Show()
Listing 3-24Selecting multiple columns and performing a calculation in F# including the more explicit “.Plus” call
dataFrame.Select(dataFrame["age"], dataFrame["age"] + 1).Show();
dataFrame.Select(dataFrame["age"], dataFrame["age"].Plus(1)).Show();
Listing 3-23Selecting multiple columns and performing a calculation in C# including the more explicit “.Plus” call
在清单 3-25 中,代码过滤了一个DataFrame,再次使用语法糖来隐藏对Column对象的Gt函数的调用。
df.filter(df['age'] > 21).show()
Listing 3-25Filtering a DataFrame using the Gt function on the Column object
列表 3-26 和 3-27 显示了。NET 版本的过滤器,同样,理解实际的计算发生在 Apache Spark 内部的 JVM 端,而不是运行在。NET 代码。
dataFrame.Filter(dataFrame["age"].Gt(21)).Show();
Listing 3-27Filtering a DataFrame using the Gt function on the Column object, in F#
dataFrame.Filter(dataFrame["age"].Gt(21)).Show();
Listing 3-26Filtering a DataFrame using the Gt function on the Column object, in C#
在清单 3-28 中,我们可以通过按年龄分组并计算每个年龄有多少行来看到数据帧上的聚集;C#和 F#版本在清单 3-29 和 3-30 中。
dataFrame.GroupBy(dataFrame.["age"]).Count().Show()
Listing 3-30Aggregating DataFrames in F#
dataFrame.GroupBy(dataFrame["age"]).Count().Show();
Listing 3-29Aggregating DataFrames in C#
df.groupBy("age").count().show()
Listing 3-28Aggregations in Apache Spark
在清单 3-31 中,我们看到DataFrame被转换成 Apache Hive 视图,这允许使用 SQL 语句查询DataFrame。SparkSession类有Sql()方法,它允许我们运行 SQL 语句。然而,在那个 SQL 上下文中没有办法看到DataFrame、中的数据,除非我们获取DataFrame并使其对 SQL 上下文可用,我们使用一个临时视图来做到这一点。临时视图将和SparkSession一样长,所以当会话结束时,临时视图也将结束。
df.createOrReplaceTempView("people")
sqlDF = spark.sql("SELECT * FROM people")
sqlDF.show()
Listing 3-31Making a DataFrame accessible by the SQL context
清单 3-32 展示了如何在 C#中创建视图,清单 3-33 在 F#中
dataFrame.CreateOrReplaceTempView("people")
let sqlDataFrame = spark.Sql("SELECT * FROM people")
Listing 3-33Making a DataFrame accessible by the SQL context in F#
dataFrame.CreateOrReplaceTempView("people");
var sqlDataFrame = spark.Sql("SELECT * FROM people");
Listing 3-32Making a DataFrame accessible by the SQL context in C#
CreateOrReplaceTempView功能有一些变化。您可以使用CreateTempView,它不会覆盖现有的视图,或者您可以使用CreateGlobalTempView或CreateOrReplaceGlobalTempView。即使在您的会话停止之后,全局临时视图对正在运行的 Apache Spark 实例中的其他SparkSessions也是可用的,但是当您的会话结束时,临时视图会被销毁。在清单 3-34 中,我们看到创建了一个全局临时视图,顺便说一下,这意味着当我们想在 SQL 上下文中使用它时,我们需要在视图名称前加上前缀“global_temp”。
df.createGlobalTempView("people")
spark.sql("SELECT * FROM global_temp.people").show()
spark.newSession().sql("SELECT * FROM global_temp.people").show()
Listing 3-34Making a DataFrame accessible to other SparkSession’s SQL context
在清单 3-35 中,我们将看到如何在 C#中使一个数据帧可被其他 SparkSession 访问。
dataFrame.CreateGlobalTempView("people");
spark.Sql("SELECT * FROM global_temp.people").Show();
spark.NewSession().Sql("SELECT * FROM global_temp.people").Show();
Listing 3-35Making a DataFrame accessible to other SparkSession’s SQL context in C#
在清单 3-36 中,我们将看到如何在 F#中使一个数据帧可被其他 SparkSession 访问。
dataFrame.CreateGlobalTempView("people")
spark.Sql("SELECT * FROM global_temp.people").Show()
spark.NewSession().Sql("SELECT * FROM global_temp.people").Show()
Listing 3-36Making a DataFrame accessible to other SparkSession’s SQL context F#
我们将在本节中看到的最后一点 Python 代码是清单 3-37 中 Python 脚本文件的标准主定义。
if __name__ == "__main__":
# $example on:init_session$
spark = SparkSession \
.builder \
.appName("Python Spark SQL basic example") \
.config("spark.some.config.option", "some-value") \
.getOrCreate()
# $example off:init_session$
basic_df_example(spark)
Listing 3-37Python script entry point
这在 Python 脚本中是标准的。这意味着,如果您有意运行脚本,那么这个if语句中的代码就会运行。但是,如果您运行一个单独的文件来导入这个 Python 文件,那么代码将不会被执行。
在清单 3-37 中,我们还看到SparkSession builder被用于getOrCreate一个SparkSession,它被传递给basic_df_example函数。清单 3-38 和 3-39 显示了 C#和 F#的等效物。
let spark = SparkSession.Builder().GetOrCreate()
BasicDfExample spark
Listing 3-39F# entry point
static void Main(string[] args)
{
var spark = SparkSession.Builder().GetOrCreate();
BasicDfExample(spark);
}
Listing 3-38C# entry point
将 Scala 示例转换为。网
当我们看一个 Scala Apache Spark 应用程序时,我们看到两种类型的代码;首先,我们看到处理命令行参数的 Scala 代码,其次,我们看到调用 Apache Spark 类和方法的 Scala 代码。许多对 Apache Spark 的调用在 Python 或 Scala 中都是相同的,在。除了不同的命名标准。清单 3-40 显示了一个有效的 Python 或一个有效的 Scala 语句。
df.printSchema()
Listing 3-40Valid Python or valid Scala
不像我们对 PySpark 版本的代码那样一行一行地走,Scala 中有一些你经常在 Apache Spark 示例中遇到的东西,理解它们会很有帮助。
使用$引用列
在 Scala 中,有一个获取对Column的引用的捷径。如果你正在查询一个DataFrame,并且你需要提供一个列,你可以写$"ColumnName ",所以如果你看到类似清单 3-41 ,那么你可以看到 Scala 可以使用这个快捷方式,而不是稍长的版本 PySpark 或。NET 需求,如清单 3-42 所示。
dataFrame.Select(dataFrame["ColumnName"])
Listing 3-42.NET has no shortcut to reference a column
dataFrame.Select($"ColumnName")
Listing 3-41Scala shortcut to reference a column
资料组
Scala 有一个 PySpark 或中没有的特性。NET,它是静态类型的数据集。静态类型化数据集允许您从数据源读取数据,并且每一行都根据已知的类进行验证,因此当您运行时,您知道每一行都符合正确的类型。对于 Scala 来说,这是一个非常有吸引力的用例,但是我们现在还没有在. NET 中使用它。
当您看到一个DataFrame被读入,然后使用.as[Type]转换成一个Dataset时,要注意代码可能通过属性名引用列,而不是传入列名;如需这方面的示例,请参见清单 3-43 ,其中过滤器使用 lambda 函数引用年龄作为标准的类属性。
case class Person(
name: String,
age: Long
)
val people = dataFrame.as[Person]
people.filter(p => p.age > 10)
Listing 3-43Scala referencing a column as a Dataset
如果你在一个例子中看到数据集被使用,那么你将需要使用我们在. NET 中的Column引用。
摘要
在这一章中,我们写了我们的第一个。NET for Apache Spark 应用程序,在命令行上运行程序,并调试应用程序。我们还研究了如何将 PySpark 示例代码转换成。NET 示例代码,并强调了阅读 Scala Apache Spark 代码时我们需要记住的几个“陷阱”。希望您已经能够跟随并让您的应用程序在您的开发机器上运行。
在下一章中,我们将了解什么是 Apache Spark 中的用户自定义函数,我们如何在哪里使用它们,以及如何调试它们,因为它们在我们已经拥有的多个进程的复杂性上增加了一层复杂性。
四、用户定义的函数
当我们执行 Apache Spark 代码时。NET 中,我们在 Java 虚拟机中调用方法和类,Apache Spark 根据我们的需求读取、写入、聚集和转换我们的数据。这是完全可能的,也很常见。NET 应用程序永远看不到实际的数据,JVM 处理所有的数据修改。如果 Apache Spark 拥有完成处理所需的所有类和方法,这没什么问题。然而,当我们需要做一些 Apache Spark 不支持的事情时,我们该怎么办呢?答案是用户定义函数(UDF)和用户定义聚合函数(UDAFs)。UDF 和 UDAFs 允许我们将数据放回。然后运行我们在. NET 中能想到的任何处理。
一个例子
稍后我们将访问用户定义的聚合函数;UDAFs 处理聚合数据,而不是对数据帧的每一行进行操作。如果您想在不使用本机 Apache Spark 代码的情况下实现自己的 Group By、Sum 或 Count,那么您应该编写一个 UDAF。
在清单 4-1 中,我们可以看到我们在哪里。NET 代码对每一行进行操作,并返回一个新值,我们传入一列,并返回一个新列。
let main argv =
let spark = SparkSession.Builder().GetOrCreate()
let udfIntToString = Microsoft.Spark.Sql.Functions.Udf<int, string>(fun (id) -> "The id is " + id.ToString())
let dataFrame = spark.Sql("SELECT ID from range(1000)")
dataFrame.Select(udfIntToString.Invoke(dataFrame.["ID"])).Show()
0
Listing 4-2Calling a .NET UDF from Apache Spark in F#
var spark = SparkSession.Builder().GetOrCreate();
Func<Column, Column> udfIntToString = Udf<int, string>(id => IntToStr(id));
var dataFrame = spark.Sql("SELECT ID from range(1000)");
dataFrame.Select(udfIntToString(dataFrame["ID"])).Show();
string IntToStr(int id)
{
return $"The id is {id}";
}
Listing 4-1Calling a .NET UDF from Apache Spark in C#
清单 4-1 和 4-2 的输出如下:
+-------------------------------------+
|System.String <Main>b__0_0(Int32)(ID)|
+-------------------------------------+
| The id is 0|
| The id is 1|
| The id is 2|
| The id is 3|
| The id is 4|
| The id is 5|
| The id is 6|
| The id is 7|
| The id is 8|
| The id is 9|
| The id is 10|
| The id is 11|
| The id is 12|
| The id is 13|
| The id is 14|
| The id is 15|
| The id is 16|
| The id is 17|
| The id is 18|
| The id is 19|
+-------------------------------------+
only showing top 20 rows
体系结构
的方式。NET UDFs 的工作方式类似于。NET 驱动程序的工作原理是,Java DotNetRunner 类启动。NET 代码,并打开一个网络套接字,用于代理请求的来回传递。Apache Spark 创建了 UDF 进程,它碰巧认为这是一个 Python 进程。Apache Spark 等待创建一个套接字,然后 Apache Spark 向下发送数据,并期待一些数据作为响应。Apache Spark 拥有这一功能已经有一段时间了,并且是用于 Python 和 R 支持的相同过程。如果你还记得。因为 Apache Spark JVM 进程启动了。NET 进程,我们现在有了 JVM。NET 进程,现在是第二个。NET 辅助进程,用于接收 UDF 的数据并调用。NET 代码。
这里需要理解一些复杂性,因为。NET 辅助进程加载。NET 应用程序作为库,并使用反射来查找要作为 UDF 执行的代码。这意味着尽管我们有我们的。NET 进程中,任何 UDF 工作都是在一个单独的进程中执行的,因此 UDF 之外的任何初始化可能都不会发生。任何共享状态都将丢失,因此请继续在 UDF 中处理您需要的内容。在清单 4-3 中,我们将查看 UDF 以及任何共享状态是如何丢失的。在清单 4-4 中,我们看到在 F#中我们如何需要显式地将一个变量标记为可变的,这在 F#中通常是不推荐的,即使这样共享的状态也会丢失;具有讽刺意味的是,用 F#编写 UDF 的最简单的方法是遵循没有共享状态的指导。
let mutable SharedState = 100
[<EntryPoint>]
let main argv =
let spark = SparkSession.Builder().GetOrCreate();
let dataFrame = spark.Sql("SELECT ID FROM range(1000)")
SharedState = 991923
let addUdf = Microsoft.Spark.Sql. DataFrameFunctions.VectorUdf<Int64DataFrameColumn, Int64DataFrameColumn>(fun (column) -> column.Add(SharedState));
dataFrame.Select(dataFrame.["ID"], addUdf.Invoke(dataFrame.["ID"])).Show();
0
Listing 4-4Explicitly creating a mutable shared variable in F#, the altered, changed state is not available in the UDF process
private static int AddAmount = 100;
private static Int64DataFrameColumn Add100(Int64DataFrameColumn id)
{
return id.Add(AddAmount);
}
static void Main(string[] args)
{
var spark = SparkSession.Builder().GetOrCreate();
var d = spark.Sql("SELECT ID FROM range(1000)");
AddAmount = 991923;
var addUdf = VectorUdf<Int64DataFrameColumn, Int64DataFrameColumn>((id) => Add100(id));
d.Select(d["ID"], addUdf(d["ID"])).Show();
}
Listing 4-3Shared state is lost when running UDFs in C#
这里的输出显示即使总的来说。NET 进程中,我们将变量 AddAmount 设置为 991923,新列使用 AddAmount 初始化为 100 的值。
» spark-submit --class org.apache.spark.deploy.dotnet.DotnetRunner ./microsoft-spark-2.4.x-0.12.1.jar dotnet ./Listing4-3.dll
+---+-----------------------------+
| ID|Int32 <Main>b__0_1(Int32)(ID)|
+---+-----------------------------+
| 0| 100|
| 1| 101|
| 2| 102|
| 3| 103|
| 4| 104|
| 5| 105|
| 6| 106|
| 7| 107|
| 8| 108|
| 9| 109|
| 10| 110|
| 11| 111|
| 12| 112|
| 13| 113|
| 14| 114|
| 15| 115|
| 16| 116|
| 17| 117|
| 18| 118|
| 19| 119|
+---+-----------------------------+
only showing top 20 rows
关于路径有几个重要的部分。第一个是工作进程需要从 https://github.com/dotnet/spark/releases 下载并放在一个目录中。然后应该有一个名为"DOTNET_WORKER_PROCESS"的环境变量,它指向目录。在这个目录中,应该有一个名为Microsoft.Spark.Worker的可执行文件。
当我们运行我们的。NET 应用程序,然后我们可以使用"dotnet run --project"并传递项目 csproj/fsproj 文件或包含项目文件的目录,应用程序将启动。对于 UDF 代码,有一个汇编加载器,这意味着你要么需要调用"dotnet run Listing4-3.dll"和 dll 的完整路径,要么与 dll 在同一个目录中。为 UDF 找到正确的路径可能很棘手,但是如果您有一个包含已编译应用程序的目录,并且您从那里运行该应用程序,那么 UDF 程序集加载器应该会找到正确的代码。如果汇编加载程序在运行 UDF 时找不到要加载的 dll,您将看到一条带有System.IO.FileNotFoundException exception的错误消息。
如果你没有微软。Spark.Worker 应用程序或DOTNET_WORKER_PROCESS配置为 Apache Spark 可以找到进程并执行程序,那么您将得到错误,UDF 将不会运行。
表演
Apache Spark 针对性能进行了优化。它使用的文件格式(如 Parquet)针对性能进行了优化,通常使用列数据格式来帮助高效地处理大型数据,跳过当前进程不需要的列。在进程间传递数据会增加性能开销,这是不可避免的。Apache Spark 中最初的 UDF 支持使用了所谓的 Python Pickling,这是一种序列化和反序列化通过连接发送的数据的方法。Python 酸洗是一种非常昂贵的发送数据的方式,并且在行级别上工作,因此如果您有一个 UDF 从包含数百列的数据集中读取一列,那么每一列都会被酸洗并通过连接发送。将数据保存在 JVM 端的应用程序和将数据保存到另一个进程的应用程序之间的性能差异非常显著。
Apache Arrow 的创建是为了使在进程间共享数据更加有效。与 Pickling 不同,Apache Arrow 是一种列格式,因此只有 UDF 使用的列在进程间传输。
如果你不是用 Scala 或 Java 编写,出于性能原因,一般建议总是避免 UDF,但是如果你需要使用它们,并且你关心性能,那么你有一些选择。人们过去常用的一种变通方法是用 Scala 或 Java 编写 UDF 并注册它们,但使用 PySpark 调用 UDF。清单 4-5 展示了如何在 C#中做到这一点,清单 4-6 展示了如何在 F#中做到这一点。注意,我们需要一个 Java 类,它位于已经添加到 Apache Spark 实例的“classpath”中的 JAR 文件中,以便执行下面的两个示例。
let spark = SparkSession.Builder().GetOrCreate()
spark.Udf().RegisterJava("java_function
", "com.company.ClassName")
let dataFrame = spark.Sql("SELECT ID, java_function(ID) as java_function_output FROM range(1000)")
dataFrame.Select(Microsoft.Spark.Sql.Functions.CallUDF("java_udf", dataFrame.["ID"])).Show();
Listing 4-6Registering a Java UDF and calling that from Spark SQL and from the DataFrame API in F#
var spark = SparkSession.Builder().GetOrCreate();
spark.Udf().RegisterJava("java_function", "com.company.ClassName");
var dataFrame = spark.Sql("SELECT ID, java_function(ID) as java_function_output FROM range(1000)");
dataFrame.Select(CallUDF("java_udf", dataFrame["ID"])).Show();
Listing 4-5Registering a Java UDF and calling that from Spark SQL and from the DataFrame API in C#
酸洗
如果我们满足于不必担心我们的性能。NET 的 Apache Spark 应用程序,我们必须使用 UDF 来实现我们的目标,我们可以使用 pickling 来调用 UDF。清单 4-7 和 4-8 分别显示了如何在 C#和 F#中使用酸洗。我们使用本地类型定义一个函数并直接调用它。尽管在代码中没有使用旧式的酸洗来明确调用这一点,但我们需要意识到,任何数据都将被酸洗,并且对于大型数据集来说可能会很慢。
let spark = SparkSession.Builder().GetOrCreate()
let dataFrame = spark.Sql("SELECT ID FROM range(1000)")
let add100 = Functions.Udf<System.Nullable<int>, int>(fun input -> if input.HasValue then input.Value + 100 else 100 )
dataFrame.Select(add100.Invoke(dataFrame.["ID"])).Show()
0
Listing 4-8A pickling UDF in F#
static void Main(string[] args)
{
var spark = SparkSession.Builder().GetOrCreate();
var dataFrame = spark.Sql("SELECT ID FROM range(1000)");
var add100 = Udf<int?, int>((input) => input + 100 ?? 100);
dataFrame.Select(add100(dataFrame["ID"])).Show();
}
Listing 4-7A pickling UDF in C#
阿帕契箭
为了提高 UDF 的性能,Apache Spark 实现了对使用 Apache Arrow 在进程间共享数据的支持。这意味着不再需要旧式的酸洗,当我们将一个列传递给 UDF 时,只有这一个列在两个过程之间传递。使用 Apache Arrow 的 UDF 有几种不同的名称,您可能会看到它们被称为矢量化 UDF 或 Pandas UDFs。来创建它们。NET for Apache Spark,我们需要使用 Apache 中定义的列类型,如Int64DataFrameColumn或StringDataFrameColumn,而不是创建一个接受本机类型的函数。Arrow 获取 Microsoft.Spark 引用的包。
清单 4-9 和 4-10 展示了如何创建一个VectorUDF,它非常类似于酸洗 UDF,除了我们需要定义类型VectorUDF的功能。
open Microsoft.Data.Analysis
open Microsoft.Spark.Sql
open System
[<EntryPoint>]
let main argv =
let spark = SparkSession.Builder().GetOrCreate();
let dataFrame = spark.Sql("SELECT ID FROM range(1000)");
let add100 = DataFrameFunctions.VectorUdf<Int64DataFrameColumn, Int64DataFrameColumn, Int64DataFrameColumn>(fun first second -> first.Add(second))
dataFrame.Select(add100.Invoke(dataFrame.["ID"], dataFrame.["ID"])).Show()
0
Listing 4-10Using the DataFrameFunctions to create a VectorUDF in F#
using Microsoft.Data.Analysis;
using Microsoft.Spark.Sql;
using static Microsoft.Spark.Sql.DataFrameFunctions;
namespace Listing4_9
{
class Program
{
static void Main(string[] args)
{
var spark = SparkSession.Builder().GetOrCreate();
var dataFrame = spark.Sql("SELECT ID FROM range(1000)");
var add100 = VectorUdf<Int64DataFrameColumn, Int64DataFrameColumn, Int64DataFrameColumn>((first, second) => first.Add(second));
dataFrame.Select(add100(dataFrame["ID"], dataFrame["ID"])).Show();
}
}
}
Listing 4-9Using the DataFrameFunctions to create a VectorUDF in C#
用户定义的聚合函数(UDAFs)
UDAF 类似于矢量化 UDF,因为它们也使用 Apache Arrow 格式,但是 udaf 不是在单个操作中接收整个列并对其进行操作,而是对分组数据集进行操作,每个分组集都被发送到 UDAF 进行处理。结果是每个组都有一个我们定义的输出。
如果我们看表 4-1 ,我们有一些数据,我们将在名称列上分组。
表 4-1
抽样资料
|名字
|
购买
|
费用
| | --- | --- | --- | | 爱德华 | 三明治 | 2.95 | | 爱德华 | 筹码 | 3.45 | | 撒拉 | 三明治 | $8.95 |
我们这里有两个自然组,一个是 Ed,一个是 Sarah。例如,如果我们在 name 列上创建一个 UDAF 组,并传入 Name 和 Cost 列,那么我们的 UDAF 将被一个名为RecordBatch的对象调用,将有两个RecordBatch's,看起来像表 4-2 和 4-3 。
表 4-3
第二批记录
|名字
|
费用
| | --- | --- | | 撒拉 | 8.95 |
表 4-2
第一批记录
|名字
|
费用
| | --- | --- | | 爱德华 | 1.99 | | 爱德华 | $3.45 |
这使我们能够检查传入的所有列,并创建我们的聚合。在清单 4-11 中,我们看看如何在 C#中使用 UDAF 来处理这些批处理。在我们看过 C#中的例子后,我们将在清单 4-17 中浏览 F#中的例子。
static void Main(string[] args)
{
var spark = SparkSession.Builder().GetOrCreate();
var dataFrame = spark.Sql(
"SELECT 'Ed' as Name, 'Sandwich' as Purchase, 4.95 as Cost UNION ALL SELECT 'Sarah', 'Drink', 2.95 UNION ALL SELECT 'Ed', 'Chips', 1.99 UNION ALL SELECT 'Ed', 'Drink', 3.45 UNION ALL SELECT 'Sarah', 'Sandwich', 8.95");
dataFrame = dataFrame.WithColumn("Cost", dataFrame["Cost"].Cast("Float"));
dataFrame.Show();
var allowableExpenses = dataFrame.GroupBy("Name").Apply(new StructType(new[]
{
new StructField("Name", new StringType()),new StructField("TotalCostOfAllowableExpenses", new FloatType())
}), TotalCostOfAllowableExpenses
);
allowableExpenses.PrintSchema();
allowableExpenses.Show();
}
private static RecordBatch TotalCostOfAllowableExpenses(RecordBatch records)
{
var purchaseColumn = records.Column("Purchase") as StringArray;
var costColumn = records.Column("Cost") as FloatArray;
float totalCost = 0F;
for (int i = 0; i < purchaseColumn.Length; i++)
{
var cost = costColumn.GetValue(i);
var purchase = purchaseColumn.GetString(i);
if(purchase != "Drink" && cost.HasValue)
totalCost += cost.Value;
}
int returnLength = records.Length > 0 ? 1 : 0;
return new RecordBatch(
new Schema.Builder()
.Field( f => f.Name("Name").DataType(ArrowStringType.Default))
.Field( f => f.Name("TotalCostOfAllowableExpenses").DataType(Apache.Arrow.Types.FloatType.Default))
.Build(),
new IArrowArray[]
{
records.Column("Name"),
new FloatArray.Builder().Append(totalCost).Build()
}, returnLength);
}
Listing 4-11How to process batches in a UDAF
如果我们将它分解,在清单 4-12 中,我们创建了一个数据帧;这通常通过读入一些数据来完成。然后,我显式地将 Cost 列转换为 float。当我们使用 Apache Arrow 格式时,每个数据类型都必须完全正确。如果有任何错误,数据将不会被正确地序列化和反序列化,Apache Spark 将会崩溃,因此确保您知道每一列是什么数据类型将会对您有所帮助。
var dataFrame = spark.Sql(
"SELECT 'Ed' as Name, 'Sandwich' as Purchase, 4.95 as Cost UNION ALL SELECT 'Sarah', 'Drink', 2.95 UNION ALL SELECT 'Ed', 'Chips', 1.99 UNION ALL SELECT 'Ed', 'Drink', 3.45 UNION ALL SELECT 'Sarah', 'Sandwich', 8.95");
dataFrame = dataFrame.WithColumn("Cost", dataFrame["Cost"].Cast("Float"));
Listing 4-12Create a DataFrame and cast the cost column to float
在清单 4-13 中,我们看到如何通过调用现有DataFrame上的GroupBy函数来创建一个新的DataFrame。在我们的 GroupBy 调用中,我们还定义了将从 UDAF 接收的数据帧的结构。当我第一次开始查看 UDAFs 时,我的印象是这是传递给 UDAF 的数据帧的模式,但事实上这是 UDAF 将返回的模式。
var allowableExpenses = dataFrame.GroupBy("Name").Apply(new StructType(new[]
{
new StructField("Name", new StringType()),new StructField("TotalCostOfAllowableExpenses", new FloatType())
}), TotalCostOfAllowableExpenses
);
Listing 4-13Calling GroupBy on our existing DataFrame
在清单 4-14 中,我们可以看到如何在我们的 UDAF 中接收记录批,并且我们可以检索我们需要的任何列。
var purchaseColumn = records.Column("Purchase") as StringArray;
var costColumn = records.Column("Cost") as FloatArray;
Listing 4-14Retrieving rows from the RecordBatch
当我们检索列时,我们得到的是数组,我们可以迭代到例子中;请记住,我们收到的行是针对每个唯一组的。
在清单 4-15 中,我们有自己的定制处理,在这一点上,您可以做任何对您的应用程序有意义的处理。在这个例子中,我们遍历所有的行,并对任何不是“饮料”的购买成本求和。
float totalCost = 0F;
for (int i = 0; i < purchaseColumn.Length; i++)
{
var cost = costColumn.GetValue(i);
var purchase = purchaseColumn.GetString(i);
if(purchase != "Drink" && cost.HasValue)
totalCost += cost.Value;
}
Listing 4-15Processing RecordBatch’s to include our custom logic
需要记住的关键一点是,我们根本不必关心 name 列。分组全部由 Apache Spark 处理,所以我们可以保持一个运行总数。这意味着为每个组计算不同的值很简单,但反过来,这意味着我们无法在不同的组之间共享状态。
在清单 4-16 中,我们返回 RecordBatch 的数据。在这种情况下,我们返回允许项目的名称和总成本。您可以在这里返回您喜欢的任何内容,但是您将不能在每个组中返回一行以上的内容。
int returnLength = records.Length > 0 ? 1 : 0;
return new RecordBatch(
new Schema.Builder()
.Field( f => f.Name("Name").DataType(ArrowStringType.Default))
.Field( f => f.Name("TotalCostOfAllowableExpenses").DataType(Apache.Arrow.Types.FloatType.Default))
.Build(),
new IArrowArray[]
{
records.Column("Name"),
new FloatArray.Builder().Append(totalCost).Build()
}, returnLength);
}
Listing 4-16Returning data to Apache Spark from the UDAF
我们返回的数据是 Apache Arrow 格式的,因此,我们需要使用Schema.Builder来创建模式和字段,然后将数据作为一个IArrowArray对象的数组传入。在这个例子中,对于数据,我们精确地按照传递给我们的方式传递 name 列,但是对于 cost 列,我们使用Builder创建一个新的FloatArray,并追加总成本。遵循这种模式意味着对于组中的每个项目,我们会收到许多行,但只返回一行,即聚合数据。
这一开始可能会很混乱,但关键是对于 UDAF,您每次接收每个组的一组记录,并且您为正在处理的组中的所有条目返回一个记录。
在清单 4-17 中,我们有相同的例子,但是在 F#中。
open Apache.Arrow
open Apache.Arrow.Types
open Microsoft.Spark.Sql
open Microsoft.Spark.Sql.Types
let totalCostOfAllowableItems(records: RecordBatch): RecordBatch =
let nameColumn = records.Column "Name" :?> StringArray
let purchaseColumn = records.Column "Purchase" :?> StringArray
let costColumn = records.Column "Cost" :?> FloatArray
let shouldInclude (purchase) = purchase <> "Drink"
let count() =
let mutable costs : float32 array = Array.zeroCreate purchaseColumn.Length
for index in 0 .. purchaseColumn.Length - 1 do
costs.SetValue((if shouldInclude (purchaseColumn.GetString(index)) then costColumn.GetValue(index).Value else float32(0)), index)
costs |> Array.sum
let returnLength = if records.Length > 0 then 1 else 0
let schema = Schema.Builder()
.Field(
Field("Name", StringType.Default, true))
.Field(
Field("TotalCostOfAllowableExpenses", FloatType.Default, true)
)
.Build()
let data: IArrowArray[] = [|
nameColumn
(FloatArray.Builder()).Append(count()).Build()
|]
new RecordBatch(schema, data, returnLength)
[<EntryPoint>]
let main argv =
let spark = SparkSession.Builder().GetOrCreate();
let dataFrame = spark.Sql("SELECT 'Ed' as Name, 'Sandwich' as Purchase, 4.95 as Cost UNION ALL SELECT 'Sarah', 'Drink', 2.95 UNION ALL SELECT 'Ed', 'Chips', 1.99 UNION ALL SELECT 'Ed', 'Drink', 3.45 UNION ALL SELECT 'Sarah', 'Sandwich', 8.95")
let dataFrameWithCost = dataFrame.WithColumn("Cost", dataFrame.["Cost"].Cast("Float"))
dataFrameWithCost.Show()
let structType = StructType ([|
StructField("Name", StringType())
StructField("TotalCostOfAllowablePurchases", FloatType())
|])
let categorized = dataFrameWithCost.GroupBy("Name").Apply(structType, totalCostOfAllowableItems)
categorized.PrintSchema();
categorized.Show();
0
Listing 4-17User-Defined Aggregate Function in F#
调试用户定义的函数
因为我们有 Apache Spark 启动一个或多个独立进程来处理用户定义函数的概念,这意味着很难在 Visual Studio 中进行调试。那个。NET for Apache Spark 项目包括微软。被触发时调用。NET Debugger.Launch()方法,该方法暂停进程并显示附加调试器的提示,您可以在这里选择 Visual Studio 实例。不幸的是,尽管我找不到任何文档来证实这一点,但我无法让Debugger.Launch()方法在 macOS 或 Linux 上做任何事情,所以除非你在 Windows 上,否则你可能会发现不可能在调试器中调试 UDF 或 UDAFs。相反,您需要退回到创建日志文件和将详细信息写到磁盘这样的事情。使用Console.WriteLine()甚至没有任何用处,因为输出被 Apache Spark 吞噬了,没有显示出来。
要启用Debugger.Launch(),您可以将它添加到您的 UDF 或 UDAF 代码中,这将触发 UI 来允许您选择调试器,或者您可以将环境变量“DOTNET_WORKER_DEBUG”设置为 1。当工作进程启动时,如果环境变量存在并被设置为 1,那么工作进程会为您调用Debugger.Launch()。
摘要
仅仅关于用户定义的函数和用户定义的集合函数,就有很多东西需要理解。关键的要点是,在可能的情况下,我们应该完全避免函数的用户代码。如果我们可以完全不用代理任何数据就完成我们的处理,那么这将是最快和最简单的。如果我们关心性能,但是需要我们的自定义代码,那么我们应该避免旧的酸洗 UDF,并确保我们使用 VectorUDF 类。