第一部分 Spark介绍
第二部分 Spark的使用基础
第三部分 Spark工具箱
第四部分 使用不同的数据类型
第五部分 高级分析和机器学习
第六部分 MLlib应用
第七部分 图分析
第八部分 深度学习
在前面的章节,关于Spark的结构化APIs,我们介绍了Spark 的核心概念,比如 transformations 和 actions。 这些简单概念模块是Spark巨大的工具和函数库生态系统的基础。
Spark有简单的原始语言组成, 底层的APIs 和 结构化APIs ,及包含在Spark中的一系列 标准库 。
开发者用这些工具处理各种不用的任务。从图像处理、机器学习到流处理和集成运算 有大量的函数库和资料库。本章会对Spark所提供的内容进行快速的介绍。本章的每一部分都 本处的其他章节加以详细的阐述。本章只是简单的展示什么是可能的。
本章包含:
spark-submit 提交的生产应用程序;
DataSets:结构化且类型安全的APIs;
Structured Streaming;
机器学习和高级分析;
Spark的低级别APIs;
Spark R;
Spark的 包生态;
全书会深度介绍这些主题,本章的目的仅仅是做一个快速浏览。
应用程序
Spark简化了大数据程序的推断和开发。
Spark还可以 通过 包含在其核心代码中的 叫做“sprak-submit”的工具,来将交互式探索 转化为 应用程序。spark-submit只做一件事情,就是允许你将你的应用提交到 当前被管理的集群中运行。当你提交了这些,应用会一直运行直到结束或 报错。 你可以通过Spark支持的集群管理器(包括Standalone、Mesos、YARN)来做这些。
在做这些的过程中,你有一些可以用来调整或控制的选项 来制定应用程序拥有的资源、应该如何运行及特定应用的参数。
你可以使用任意Spark支持的语言来编写这些应用程序,并提交运行这些应用。
Datasets:类型安全的结构化APIs
这一部分介绍的是 一种 为Java和Scala提供的Spark结构化APIs 的类型安全版本,Datasets。这种API不支持Python和R,因为他们是 动态类型语言, 但这种API是用Java和Scala编写大型应用的一种强大工具。
回忆DataFrames,是一种 类型为Row的 分布式对象结合,可以保存多种类型的列表数据。
Datasets允许用户 指定DataFrames内部的记录 为一个Java类 ,并以一个类对象 集来操纵,类似与一个Java ArrayList 或 Scala Seq。Datasets提供的APIs是类型安全的,意思就是 你不能 以一个不同于 一开始所指定的类 来查看Datasets中的 对象。 这使得Datasets 在多个软件工程师 必须通过定义好的接口进行交互来 编写大型应用时非常具有吸引力。
Datasets类 由对象的类型 参数化:在Java中为Dataset
一个了不起的事情 是 我们可以只在我们需要或想要的时候 使用Datasets。例如,在下面的例子中,会定义自用的对象,并通过任意的 map 和 filter函数来操作。一旦执行了操作,Spark会自动地将其 转化回 DataFrame,我们可以进一步使用Spark中包含的各种函数 对其进行操作。 这使得Datasets很容易降低到 较低级别,在需要时执行类型安全的代码,并升高级别使用SQL 获得更快的分析能力。我们会在本书后面的部分对其进行更广泛的讲解,这里只给出一个简单的例子 来展示 我们如何既使用类型安全函数 有使用 DataFrame 类SQL表达式 来快速写出业务逻辑。
%scala
// A Scala case class (similar to a struct) that will automatically
// be mapped into a structured data table in Spark
case class Flight(DEST_COUNTRY_NAME: String, ORIGIN_COUNTRY_NAME: String, count: BigInt)
val flightsDF = spark.read.parquet("/mnt/defg/flight-data/parquet/2010-summary.parquet/")
val flights = flightsDF.as[Flight]
另外一个好处是,你可以对Datasets调用 collect或take,我们会 从Datasets中收集适当类型的对象,而不是DataFrame Rows。这使得很容易 在不更改代码的情况下 以分布式和本地方式 获得 类型安全 并安全地执行操作。
%scala
flights
.filter(flight_row => flight_row.ORIGIN_COUNTRY_NAME != "Canada")
.take(5)
Structrued Streaming
Structured Streaming 是一个用户流处理的高级别APIs,在Spark2.2开始提供这一产品。Structured Streaming允许你使用Spark的结构化APIs 在批处理模式下指向相同的操作,并以流的方式运行。
这可以减少延迟并允许增量处理。 Structured Streaming 最好的一点是其允许你 快速获得流系统的输出值 而几乎不需要更改代码。还可以简化推理,因为你可以编写批处理任务 来作为原型,然后你可以将其转化为流处理任务。 所有这些工作的 运行方式 都是 增量处理数据。
我们来通过一个例子来 说明使用Structured Streaming是多么容易。 我们使用一组零售数据。一个文件代表一天的数据。
我们将数据设置为这种格式,来模拟由不同流程以一致和规律的方式产生的数据。将是这些零售数据想象成由零售商店产出并传送来给我们的Structured Streaming工作读取。
数据样例:
InvoiceNo,StockCode,Description,Quantity,InvoiceDate,UnitPrice,CustomerID,Country
536365,85123A,WHITE HANGING HEART T-LIGHT HOLDER,6,2010-12-01 08:26:00,2.55,17850.0,United Kingdom
536365,71053,WHITE METAL LANTERN,6,2010-12-01 08:26:00,3.39,17850.0,United Kingdom
536365,84406B,CREAM CUPID HEARTS COAT HANGER,8,2010-12-01 08:26:00,2.75,17850.0,United Kingdom
我们首先将数据当作静态数据来分析,并传建一个DataFrame。我们同样从静态数据中创建一个Schema。 在流中还有很多 模式推理的方法。
%scala
val staticDataFrame = spark.read.format("csv")
.option("header", "true")
.option("inferSchema", "true")
.load("/mnt/defg/retail-data/by-day/*.csv")
staticDataFrame.createOrReplaceTempView("retail_data")
val staticSchema = staticDataFrame.schema
%python
staticDataFrame = spark.read.format("csv")\
.option("header", "true")\
.option("inferSchema", "true")\
.load("/mnt/defg/retail-data/by-day/*.csv")
staticDataFrame.createOrReplaceTempView("retail_data")
staticSchema = staticDataFrame.schema
因为我们通过时间序列数据来进行分析,所有很有必要说一说 如何来对数据进行分类和聚合。在本例中我们会查看 给定顾客(通过CustormId来区分)进行最大消费的时间。比如,我们会增加一个总消费列 来看看顾客在哪些日子花的最多。
窗口函数中会包含 所有 单日数据的聚合。这只是一个在我们数据时间序列列上的一个窗口。这是操作日期和时间戳的有效工具,因为我们可以指定更人性化的需求(通过间隔)
,Spark会为我们将它们聚合起来。
%scala
import org.apache.spark.sql.functions.{window, column, desc, col}
staticDataFrame
.selectExpr(
"CustomerId",
"(UnitPrice * Quantity) as total_cost",
"InvoiceDate")
.groupBy(
col("CustomerId"), window(col("InvoiceDate"), "1 day"))
.sum("total_cost")
.show(5)
%python
from pyspark.sql.functions import window, column, desc, col
staticDataFrame\
.selectExpr(
"CustomerId",
"(UnitPrice * Quantity) as total_cost" ,
"InvoiceDate" )\
.groupBy(
col("CustomerId"), window(col("InvoiceDate"), "1 day"))\
.sum("total_cost")\
.show(5)
输出数据为:
+----------+--------------------+------------------+
|CustomerId| window | sum(total_cost) |
+----------+--------------------+------------------+
| 17450.0 |[2011-09-20 00:00...| 71601.44 |
| null |[2011-11-14 00:00...| 55316.08 |
| null |[2011-11-07 00:00...| 42939.17 |
| null |[2011-03-29 00:00...| 33521.39999999998|
| null |[2011-12-08 00:00...|31975.590000000007|
+----------+--------------------+------------------+
空值代表 对一些事务 没有CustomerId。
这是一个静态的DataFrame版本,现在让我们看看Streaming的demo,看看它是如何工作的。你会注意到实际上只有非常小的变换。
最大的变化 是我们使用readStream而不是read。此外,你会注意到maxFilesPerTriqqer选项,其指定我们一次读取的文件数量。这是为了使我们的演示更加“流化”,在生产场景中将省略这一点。
你可能在本地模式下运行这些,那么设置shuffle分区的数量是很好的实践,这样更适合本地模式。 设置指定在一个shuffle后所创建的分区的数量,默认是200个,但因为本机上不会有太多的执行器,所以将其设为5是合适的。我们在前面的章节进行了同样的操作。
val streamingDataFrame = spark.readStream
.schema(staticSchema)
.option("maxFilesPerTrigger", 1)
.format("csv")
.option("header", "true")
.load("d/mnt/defg/retail-data/by-day/*.csv")
%python
streamingDataFrame = spark.readStream\
.schema(staticSchema)\
.option("maxFilesPerTrigger", 1)\
.format("csv")\
.option("header", "true")\
.load("/mnt/defg/retail-data/by-day/*.csv")
可以看到DataFrame是一个流。
streamingDataFrame.isStreaming // returns true
让我们执行和之前的DataFrame操作相同的的业务逻辑,在整个进程中进行求和。
%scala
val purchaseByCustomerPerHour = streamingDataFrame
.selectExpr(
"CustomerId",
"(UnitPrice * Quantity) as total_cost",
"InvoiceDate")
.groupBy(
$"CustomerId", window($"InvoiceDate", "1 day"))
.sum("total_cost")
%python
purchaseByCustomerPerHour = streamingDataFrame\
.selectExpr(
"CustomerId",
"(UnitPrice * Quantity) as total_cost" ,
"InvoiceDate" )\
.groupBy(
col("CustomerId"), window(col("InvoiceDate"), "1 day"))\
.sum("total_cost")
这仍是一个lazy操作,我们需要执行一个Streaming action来开始 数据流的执行。
在启动流之前,我们会设置一个小优化,使其能更好的在单机上运`行,就是设置shuffle后输出的分区数量。
spark.conf.set("spark.sql.shuffle.partitions", "5")
Streaming actions 和常见的 static action有一点区别,因为我们想在某处填充数据 而不是调用count之类的东西(这些东西在流中没有意义)。我们将使用的action会输出到一个内存表,我们将在每各 trigger之后更新该表。在本例中,每个 trigger是基于一个文件(在读设置中配置的maxFilesPerTrigger)的。Spark会更改内存表中的数据,所以我们总能得到上面聚合中所指定的最大值。
%scala
purchaseByCustomerPerHour.writeStream
.format("memory") // memory = store in-memory table
.queryName("customer_purchases") // counts = name of the in-memory table
.outputMode("complete") // complete = all the counts should be in the table
.start()
%python
purchaseByCustomerPerHour.writeStream\
.format("memory")\
.queryName("customer_purchases")\
.outputMode("complete")\
.start()
一旦启动了流,如果我们将结果写入结果数据槽,我们就可以对流执行查询操作来调试结果集的样子,
%scala
spark.sql("""
SELECT *
FROM customer_purchases
ORDER BY 'sum(total_cost)' DESC
""")
.show(5)
%python
spark.sql("""
SELECT *
FROM customer_purchases
ORDER BY 'sum(total_cost)' DESC
""")\
.show(5)
你会注意到随着我们读取更多的数据,表的结构发生了变化。对每个文件,结果可能会根据数据改变,也可能不会。自然地,因为我们将顾客分组,我们希望看到随着时间推移,顾客购买量会增加。另一个可以使用的配置是将结果 写在控制台。
purchaseByCustomerPerHour.writeStream
.format("console")
.queryName("customer_purchases_2")
.outputMode("complete")
.start()
这些streaming方法在生产环境下不会使用,这里只是为了用来方便展示Structured Streaming的能力。
注意到窗口函数是如何基于事件时间构建的,而不是Spark处理数据的时间。这是Structured Streaming所解决的Spark Streaming 的缺点之一。
机器学习和高级分析
Spark另一个受欢迎的方面是它 运用内置MLlib机器学习库 执行大规模 机器学习任务的能力。
MLlib允许预处理、整理(munging)、训练模型,并可根据大规模数据 进行预测。
Spark为执行各种机器学习任务提供了一套复杂的机器学习API,包括 分类、回归、聚类、深度学习等。
staticDataFrame.printSchema()
root
1-- InvoiceNo: string (nullable = true)
1-- StockCode: string (nullable = true)
1-- Description: string (nullable = true)
1-- Quantity: integer (nullable = true)
1-- InvoiceDate: timestamp (nullable = true)
1-- UnitPrice: double (nullable = true)
1-- CustomerID: double (nullable = true)
1-- Country: string (nullable = true)
MLlib中的机器学习算法 要求数据用数值表示。 staticDataFrame的数据包含多种不同的格式。因此我们需要将数据转化为 数值表示。 在此情况下,我们使用几个DataFrame转换 对时间数据进行处理。
%scala
import org.apache.spark.sql.functions.date_format
val preppedDataFrame = staticDataFrame
.na.fill(0) //na返回DataFrameNaFunctions类型,用来对DataFrame中值为null或NaN的列做处理
.withColumn("day_of_week", date_format($"InvoiceDate", "EEEE"))
.coalesce(5)
%python
from pyspark.sql.functions import date_format, col
preppedDataFrame = staticDataFrame\
.na.fill(0)\
.withColumn("day_of_week", date_format(col("InvoiceDate"), "EEEE"))\
.coalesce(5)
现在将数据分为训练集 和 测试集。 这里我们通过某次购买发生的数据 来手动分割。
但我们也可以利用MLlib的转换APIs,通过训练验证分割 或 交叉验证 来创建 训练 和测试集。这部分内容会在本书的第6部分进行介绍。
%scala
val trainDataFrame = preppedDataFrame
.where("InvoiceDate < '2011-07-01'")
val testDataFrame = preppedDataFrame
.where("InvoiceDate >= '2011-07-01'")
%python
trainDataFrame = preppedDataFrame\
.where("InvoiceDate < '2011-07-01'")
testDataFrame = preppedDataFrame\
.where("InvoiceDate >= '2011-07-01'")
现在已经准备好了数据。因为这是一组时间序列数据,我们将通过数据集中的一个任意时间来进行分割。这可能不是对训练 和 测试集的最优分割,但出于本例的意图和目的 已经足够。数据被粗略地 分为两半。
目前使用的这些转换 是DataFrame转化,Spark的MLlib也提供了一些转换 允许我们自动化 一些一般的转换。 比如StringIndexer。
%scala
import org.apache.spark.ml.feature.StringIndexer
val indexer = new StringIndexer()
.setInputCol("day_of_week")
.setOutputCol("day_of_week_index")
%python
from pyspark.ml.feature import StringIndexer
indexer = StringIndexer()\
.setInputCol("day_of_week")\
.setOutputCol("day_of_week_index")
这会 将 day_of_weeks转换为相应的数值。比如,Spark可能用6代表周六,1代表周一。然而在这种编号方案下,会隐式地声明 周六 大于 周一。这显然是不对的。
因此我们需要用 OneHotEncoder 来将每个值 转化为自己的列。这些布尔标记 标识 星期的某一天 是否是相关的某一天。
%scala
import org.apache.spark.ml.feature.OneHotEncoder
val encoder = new OneHotEncoder()
.setInputCol("day_of_week_index")
.setOutputCol("day_of_week_encoded")
%python
from pyspark.ml.feature import OneHotEncoder
encoder = OneHotEncoder()\
.setInputCol("day_of_week_index")\
.setOutputCol("day_of_week_encoded")
每一个都会生成一组列,我们可以将其“组装assemble”成一个向量。
Spark中所有的机器学习算法都要求输入一个 Vector 类型,其必须是一个数值集。
%scala
import org.apache.spark.ml.feature.VectorAssembler
val vectorAssembler = new VectorAssembler()
.setInputCols(Array("UnitPrice", "Quantity", "day_of_week_encoded"))
.setOutputCol("features")
%python
from pyspark.ml.feature import VectorAssembler
vectorAssembler = VectorAssembler()\
.setInputCols(["UnitPrice", "Quantity", "day_of_week_encoded"])\
.setOutputCol("features")
我们可以看到 这里有三个特征,价格、数量、星期几。 现在我们会将其设置为一个管道,任意我们需要转换的 后来的数据 都会经过完全相同的过程。
%scala
import org.apache.spark.ml.Pipeline
val transformationPipeline = new Pipeline()
.setStages(Array(indexer, encoder, vectorAssembler))
%python
from pyspark.ml import Pipeline
transformationPipeline = Pipeline()\
.setStages([indexer, encoder, vectorAssembler])
现在对训练的准备是一个两步的过程。
1、我们首先需要 fit 转换器 to 数据集。 我们对此进行了深入的讨论,但基本上 我们的 StringIndexer需要知道 索引有多少个唯一值。 一旦确定,编码是很容易的,但Spark必须查看索引的列中的所有不同值 为了后续存储这些值。
%scala
val fittedPipeline = transformationPipeline.fit(trainDataFrame)
%python
fittedPipeline = transformationPipeline.fit(trainDataFrame)
2、一旦我们拟合了训练数据,我们可以使用 定制的pipeline管道,用它以一致且可重复的方式 来转换所有的数据。
%scala
val transformedTraining = fittedPipeline.transform(trainDataFrame)
%python
transformedTraining = fittedPipeline.transform(trainDataFrame)
值得提及的是 我们本可以在 pipeline中包含模型训练。这里没有这样做 是为了演示 一个缓冲数据的范例。 接下来,我们会对模型进行一些超参数调整,因为我们不想重复 完全相同的 转换,我们会利用一个 会在本书第四部分讨论的 优化——caching。这会 将 中间转换数据集 的一个副本存入内存,以允许我们反复使用,相比于运行完整的pipeline 耗费很低。
transformedTraining.cache()
现在我们有了一个训练集,是时候训练模型了。 首先我们 import 我们想要使用的相关模型 并将其实例化。
%scala
import org.apache.spark.ml.clustering.KMeans
val kmeans = new KMeans()
.setK(20)
.setSeed(1L)
%python
from pyspark.ml.clustering import KMeans
kmeans = KMeans()\
.setK(20)\
.setSeed(1L)
在Spark中,训练机器学习模型 是一个 两阶段的过程。
首先实例化一个未训练的模型,然后训练它。
在MLlib的DataFrame API中,每种算法总是有两种类型。对未训练版本遵循 Algorithm的命名模式,对训练版本遵循AlgorithmModel。
在本例中, 就是 KMeans 和KMeansModel。
MLlib的DataFrame API 的预测器 大致地共享相同的接口,我们在前面看到 预处理转换 StringInderxer。这并不奇怪,因为它使培训整个管道(包括模型)变得简单。在我们的例子中我们想一步一步地执行,所以我们选择不去使用这些功能。
%scala
val kmModel = kmeans.fit(transformedTraining)
%python
kmModel = kmeans.fit(transformedTraining)
我们可以看到这是的结果成本,是非常高的,这很可能是因为我们不需要缩放数据或转换。
kmModel.computeCost(transformedTraining)
%scala
val transformedTest = fittedPipeline.transform(testDataFrame)
%python
transformedTest = fittedPipeline.transform(testDataFrame)
kmModel.computeCost(transformedTest)
自然地我们可以继续改进模型,多层预处理 就像超参数调整 来保证 得到一个好的模型。
低级APIs
Spark 包含许多低级别的 基本体,来允许 通过RDDs 进行任意的Java和Python对象操作。
事实上,Spark中的所有东西 都是建立在RDDs之上的。 正如我们会在下一章节介绍的,DataFrame操作 是基于RDDs的,并且为方便的 和 非常有效的分布式执行,向下编译为这些低级别工具 。
有些事情你可以用RDDs来做,特别是当你读或操作原始数据时,但大多数情况下你应该坚持使用 Structured APIs。RDDs比DatFrame级别低,因为他们是向终端用户显示 物理执行特征的(比如分区)。
可以使用RDDs来做的一件事是 并行处理parallelize你存储在驱动机器的内存中的原始数据。对这种情况,我们并行化一些简单的数字,然后创建一个DataFrame。然后我们可以将其转换为一个DataFrame,以便与其他DataFrame一起使用。
%scala
spark.sparkContext.parallelize(Seq(1, 2, 3)).toDF()
%python
from pyspark.sql import Row
spark.sparkContext.parallelize([Row(1), Row(2), Row(3)]).toDF()
RDDs在Scala和Python中都是可用的。然而,它们不是等价的。
这不同于DataFrame API(Scala和Python的执行特性是相同的),由于一些底层的实现细节。
我们另外介绍lower level APIs ,包括RDDs。
对于终端用户,你不需要执行任务过多使用RDDs,除非你要维护旧的Spark代码。基本上没有什么情况 需要你使用RDDs而不是 structured APIs,除了操作一些非常原始的未处理和非结构数据。
实际上,使用高级APIs将就是使你的工作远离低级APIs的实现细节。
SparkR
SparkR是用于在Spark上运行R的工具。其遵循所有Spark的其他语言 遵循的 原则。
使用SparkR,我们只需要简单地将其引入到我们的环境中,并运行我们的代码。与Python API非常类似,只是其遵循R的语法。在大多数情况下,几乎所有在Python上可行的东西 都可在SparkR中使用。
%r
library(SparkR)
sparkDF <- read.df("/mnt/defg/flight-data/csv/2015-summary.csv",
source = "csv", header="true", inferSchema = "true")
take(sparkDF, 5)
%r
collect(orderBy(sparkDF, "count"), 20)
R 用户 还可以利用其他R 函数库。
Spark生态和各种库
Spakr 最好的一部分就是其 社区创建的 包和工具 的生态系统。
其中一些工具 因为成熟和广泛的运用 甚至 加入了 核心Spark项目。
最大的Spark包索引 可以在 https://spark-packages.org/找到,任何用户都可以发布到这个包仓库。同样还有各种 其他的项目和包 可以在网上找到,例如在GitHub上。