大数据技术之_19_Spark学习_02_Spark Core 应用解析+ RDD 概念 + RDD 编程 + 键值对 RDD + 数据读取与保存主要方式 + RDD 编程进阶 + Spark Core 实例练习
第1章 RDD 概念1.1 RDD 为什么会产生1.2 RDD 概述1.2.1 什么是 RDD1.2.2 RDD 的属性1.3 RDD 弹性1.4 RDD 特点1.4.1 分区1.4.2 只读1.4.3 依赖1.4.4 缓存1.4.5 CheckPoint第2章 RDD 编程2.1 RDD 编程模型2.2 RDD 创建2.2.1 由一个已经存在的 Scala 集合创建,即集合并行化(测试用)2.2.2 由外部存储系统的数据集创建(开发用)2.3 RDD 编程2.3.1 Transformation(转换)2.3.2 Action(行动)2.3.3 数值 RDD 的统计操作2.3.4 向 RDD 操作传递函数注意事项2.3.5 在不同 RDD 类型间转换2.4 RDD 持久化2.4.1 RDD 的缓存2.4.2 RDD 缓存方式2.5 RDD 检查点机制2.5.1 checkpoint 写流程2.5.2 checkpoint 读流程2.6 RDD 的依赖关系2.6.1 窄依赖(Narrow Dependency)2.6.2 宽依赖(Wide Dependency)2.6.3 Lineage2.7 DAG 的生成2.8 RDD 相关概念关系2.9 Spark Core 实例练习第3章 键值对 RDD3.1 键值对 RDD 的转化操作3.1.1 转化操作列表3.1.2 聚合操作3.1.3 数据分组3.1.4 连接3.1.5 数据排序3.2 键值对 RDD 的行动操作3.3 键值对 RDD 的数据分区3.3.1 获取 RDD 的分区方式3.3.2 Hash 分区方式3.3.3 Range 分区方式3.3.4 自定义分区方式(重点)3.3.5 分区 Shuffle 优化3.3.6 基于分区进行操作3.3.7 从分区中获益的操作第4章 数据读取与保存主要方式4.1 文本文件输入输出4.2 JSON 文件输入输出4.3 CSV 文件输入输出4.4 SequenceFile 文件输入输出4.5 对象文件输入输出4.6 Hadoop 输入输出格式4.7 文件系统的输入输出4.8 数据库的输入输出第5章 RDD 编程进阶5.1 累加器5.2 自定义累加器5.3 广播变量第6章 Spark Core 实例练习6.1 计算独立 IP 数6.2 统计每个视频独立 IP 数6.3 统计一天中每个小时间的流量附录
第1章 RDD 概念
1.1 RDD 为什么会产生
RDD:Resilient Distributed Dataset 弹性分布式数据集
RDD 是 Spark 的基石,是实现 Spark 数据处理的核心抽象。那么 RDD 为什么会产生呢?
Hadoop 的 MapReduce 是一种基于数据集的工作模式,面向数据,这种工作模式一般是从存储上加载数据集,然后操作数据集,最后写入物理存储设备。数据更多面临的是一次性处理。
MR 的这种方式对数据领域两种常见的操作不是很高效。第一种是迭代式的算法
。比如机器学习中 ALS、凸优化梯度下降等。这些都需要基于数据集或者数据集的衍生数据反复查询反复操作。MR 这种模式不太合适,即使多 MR 串行处理,性能和时间也是一个问题。数据的共享依赖于磁盘。另外一种是交互式数据挖掘
,MR 显然不擅长。
MR 中的迭代:
Spark中的迭代:
我们需要一个效率非常快,且能够支持迭代计算和有效数据共享的模型,Spark 应运而生。RDD 是基于工作集的工作模式,更多的是面向工作流。
但是无论是 MR 还是 RDD 都应该具有类似位置感知、容错和负载均衡等特性。
1.2 RDD 概述
1.2.1 什么是 RDD
RDD(Resilient Distributed Dataset)叫做分布式数据集
,是 Spark 中最基本的数据抽象,它代表一个不可变
、可分区(分片)
、里面的元素可并行计算的集合(弹性)
。在 Spark 中,对数据的所有操作不外乎创建 RDD、转化已有 RDD 以及调用 RDD 操作进行求值。每个 RDD 都被分为多个分区,这些分区运行在集群中的不同节点上。RDD 可以包含 Python、Java、Scala 中任意类型的对象,甚至可以包含用户自定义的对象。RDD 具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD 允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。
RDD 支持两种操作:转化操作和行动操作。RDD 的转化操作是返回一个新的 RDD 的操作,比如 map() 和 filter(),而行动操作则是向驱动器程序返回结果或把结果写入外部系统的操作。比如 count() 和 first()。
Spark 采用惰性计算
模式,RDD 只有第一次在一个行动操作
中用到时,才会真正计算。Spark 可以优化整个计算过程。默认情况下,Spark 的 RDD 会在你每次对它们进行行动操作时重新计算。如果想在多个行动操作中重用同一个 RDD,可以使用 RDD.persist() 让 Spark 把这个 RDD 缓存下来。
1.2.2 RDD 的属性
1) 一组分片(Partition),即
数据集的基本组成单位
。对于 RDD 来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建 RDD 时指定 RDD 的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的 CPU Core 的数目。
2) 一个计算每个分区的函数。Spark 中 RDD 的计算是以分片为单位的
,每个 RDD 都会实现 compute 函数以达到这个目的。compute 函数会对迭代器进行复合,不需要保存每次计算的结果。
3) RDD 之间的依赖关系。RDD 的每次转换都会生成一个新的 RDD,所以 RDD 之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark 可以通过这个依赖关系重新计算丢失的分区数据,而不是对 RDD 的所有分区进行重新计算。
4) 一个 Partitioner,即 RDD 的分片函数。当 前Spark 中实现了两种类型的分片函数,一个是基于哈希的 HashPartitioner,另外一个是基于范围的 RangePartitioner。只有对于于 key-value 的 RDD,才会有 Partitioner,非 key-value 的 RDD 的 Parititioner 的值是 None。Partitioner 函数不但决定了 RDD 本身的分片数量,也决定了Parent RDD Shuffle 输出时的分片数量。
5) 一个列表,存储存取每个 Partition 的优先位置(preferred location)。对于一个 HDFS 文件来说,这个列表保存的就是每个 Partition 所在的块的位置。按照“移动数据不如移动计算”的理念,Spark 在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
RDD 是一个应用层面的逻辑概念。一个 RDD 多个分片。RDD 就是一个元数据记录集,记录了 RDD 内存所有的关系数据。
1.3 RDD 弹性
1) 自动进行内存和磁盘数据存储的切换
Spark 优先把数据放到内存中,如果内存放不下,就会放到磁盘里面,程序进行自动的存储切换。
2) 基于血统的高效容错机制
在 RDD 进行转换和动作的时候,会形成 RDD 的 Lineage 依赖链,当某一个 RDD 失效的时候,可以通过重新计算上游的 RDD 来重新生成丢失的 RDD 数据。
3) Task 如果失败会自动进行特定次数的重试
RDD 的计算任务如果运行失败,会自动进行任务的重新计算,默认次数是 4 次。
4) Stage 如果失败会自动进行特定次数的重试
如果 Job 的某个 Stage 阶段计算失败,框架也会自动进行任务的重新计算,默认次数也是 4 次。
5) Checkpoint 和 Persist 可主动或被动触发
RDD 可以通过 Persist 持久化将 RDD 缓存到内存或者磁盘,当再次用到该 RDD 时直接读取就行。也可以将 RDD 进行检查点,检查点会将数据存储在 HDFS 中,该 RDD 的所有父 RDD 依赖都会被移除。
6) 数据调度弹性
Spark 把这个 JOB 执行模型抽象为通用的有向无环图 DAG,可以将多 Stage 的任务串联或并行执行,调度引擎自动处理 Stage 的失败以及 Task 的失败。
7) 数据分片的高度弹性
可以根据业务的特征,动态调整数据分片的个数,提升整体的应用执行效率。
RDD 全称叫做弹性分布式数据集(Resilient Distributed Datasets),它是一种分布式的内存抽象,表示一个只读的记录分区的集合,它只能通过其他 RDD 转换而创建,为此,RDD 支持丰富的转换操作(如 map, join, filter, groupby 等),通过这种转换操作,新的 RDD 则包含了如何从其他 RDDs 衍生所必需的信息,所以说 RDDs 之间是有依赖关系的。基于 RDDs 之间的依赖,RDDs 会形成一个有向无环图 DAG,该 DAG 描述了整个流式计算的流程,实际执行的时候,RDD 是通过血缘关系(Lineage)一气呵成的,即使出现数据分区丢失,也可以通过血缘关系重建分区,总结起来,基于 RDD 的流式计算任务可描述为:从稳定的物理存储(如分布式文件系统)中加载记录,记录被传入由一组确定性操作构成的 DAG,然后写回稳定存储。另外 RDD 还可以将数据集缓存到内存中,使得在多个操作之间可以重用数据集,基于这个特点可以很方便地构建迭代型应用(图计算、机器学习等)或者交互式数据分析应用。可以说 Spark 最初也就是实现 RDD 的一个分布式系统,后面通过不断发展壮大成为现在较为完善的大数据生态系统,简单来讲,Spark-RDD 的关系类似于 Hadoop-MapReduce 关系。
1.4 RDD 特点
RDD 表示只读的分区的数据集,对 RDD 进行改动,只能通过 RDD 的转换操作,由一个 RDD 得到一个新的 RDD,新的 RDD 包含了从其他 RDD 衍生所必需的信息。RDDs 之间存在依赖,RDD 的执行是按照血缘关系延时计算的。如果血缘关系较长,可以通过持久化 RDD 来切断血缘关系。
1.4.1 分区
RDD 逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个 compute 函数得到每个分区的数据。如果 RDD 是通过已有的文件系统构建,则 compute 函数是读取指定文件系统中的数据,如果 RDD 是通过其他 RDD 转换而来,则 compute 函数是执行转换逻辑将其他 RDD 的数据进行转换。
1.4.2 只读
如下图所示,RDD 是只读的,要想改变 RDD 中的数据,只能在现有的 RDD 基础上创建新的 RDD。
由一个 RDD 转换到另一个 RDD,可以通过丰富的操作算子实现,不再像 MapReduce 那样只能写 map 和 reduce 了,如下图所示。
RDD 的操作算子包括两类,一类叫做 transformations,它是用来将 RDD 进行转化,构建 RDD 的血缘关系;另一类叫做 actions,它是用来触发 RDD 的计算,得到 RDD 的相关计算结果或者将 RDD 保存的文件系统中。下图是 RDD 所支持的操作算子列表。
1.4.3 依赖
RDDs 通过操作算子进行转换,转换得到的新 RDD 包含了从其他 RDDs 衍生所必需的信息,RDDs 之间维护着这种血缘关系,也称之为依赖。如下图所示,依赖包括两种,一种是窄依赖,RDDs 之间分区是一一对应的,另一种是宽依赖,下游 RDD 的每个分区与上游 RDD (也称之为父 RDD)的每个分区都有关,是多对多的关系。
通过 RDDs 之间的这种依赖关系,一个任务流可以描述为 DAG (有向无环图),如下图所示,在实际执行过程中宽依赖对应于 Shuffle (图中的 reduceByKey 和 join),窄依赖中的所有转换操作可以通过类似于管道的方式一气呵成执行(图中 map 和 union 可以一起执行)。
1.4.4 缓存
如果在应用程序中多次使用同一个 RDD,可以将该 RDD 缓存起来,该 RDD 只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该 RDD 的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。如下图所示,RDD-1 经过一系列的转换后得到 RDD-n 并保存到 hdfs,RDD-1 在这一过程中会有个中间结果,如果将其缓存到内存,那么在随后的 RDD-1 转换到 RDD-m 这一过程中,就不会计算其之前的 RDD-0 了。
1.4.5 CheckPoint
虽然 RDD 的血缘关系天然地可以实现容错,当 RDD 的某个分区数据失败或丢失,可以通过血缘关系重建。但是对于长时间迭代型应用来说,随着迭代的进行,RDDs 之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。为此,RDD 支持 checkpoint 将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为 checkpoint 后的 RDD 不需要知道它的父 RDDs 了,它可以从 checkpoint 处拿到数据。
给定一个 RDD 我们至少可以知道如下几点信息:
1、分区数以及分区方式;
2、由父 RDDs 衍生而来的相关依赖信息;
3、计算每个分区的数据,计算步骤为:
1)如果被缓存,则从缓存中取的分区的数据;
2)如果被 checkpoint,则从 checkpoint 处恢复数据;
3)根据血缘关系计算分区的数据。
第2章 RDD 编程
2.1 RDD 编程模型
在 Spark 中,RDD 被表示为对象,通过对象上的方法调用来对 RDD 进行转换。经过一系列的 transformations 定义 RDD 之后,就可以调用 actions 触发 RDD 的计算,action 可以是向应用程序返回结果(count, collect 等),或者是向存储系统保存数据(saveAsTextFile 等)。在 Spark 中,只有遇到 action,才会执行 RDD 的计算(即延迟计算),这样在运行时可以通过管道的方式传输多个转换。
要使用 Spark,开发者需要编写一个 Driver 程序,它被提交到集群以调度运行 Worker,如下图所示。Driver 中定义了一个或多个 RDD,并调用 RDD 上的 action,Worker 则执行 RDD 分区计算任务。
Driver 和 Worker 内部示意图:
2.2 RDD 创建
在 Spark 中创建 RDD 的创建方式大概可以分为三种:从集合中创建 RDD;从外部存储创建 RDD;从其他 RDD 创建。
2.2.1 由一个已经存在的 Scala 集合创建,即集合并行化(测试用)
scala> val rdd1 = sc.parallelize(Array(1, 2, 3, 4, 5, 6, 7, 8))
Step1、而从集合中创建 RDD,Spark 主要提供了两种函数:parallelize 和 makeRDD。我们可以先看看这两个函数的声明:
def parallelize[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T]
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T]
def makeRDD[T: ClassTag](
seq: Seq[(T, Seq[String])]): RDD[T]
Step2、我们可以从上面看出 makeRDD 有两种实现,而且第一个 makeRDD 函数接收的参数和 parallelize 完全一致。其实第一种 makeRDD 函数实现是依赖了 parallelize 函数的实现,来看看 Spark 中是怎么实现这个 makeRDD 函数的:
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
parallelize(seq, numSlices)
}
Step3、第一个 makeRDD 的实现可以自己指定分区的数量,它的默认分区取决于 CPU 的核心总数,示例代码如下:
scala> val rdd2 = sc.makeRDD(Array(1, 2, 3, 4, 5, 6))
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at makeRDD at <console>:24
scala> rdd2.partitions.size
res1: Int = 4 本博主的 hadoop102 虚拟机 CPU 配置的是 4 核
追踪
默认分区的底层源码如下:
def parallelize[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
assertNotStopped()
new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
}
def defaultParallelism: Int = {
assertNotStopped()
taskScheduler.defaultParallelism
}
def defaultParallelism(): Int
override def defaultParallelism(): Int = backend.defaultParallelism()
override def defaultParallelism(): Int = {
conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
}
Step4、我们可以看出,这个 makeRDD 函数完全和 parallelize 函数一致。但是我们得看看第二种 makeRDD 函数的具体实现了,它接收的参数类型是 Seq[(T, Seq[String])],Spark 文档的说明是:
Distribute a local Scala collection to form an RDD, with one or more location preferences (hostnames of Spark nodes) for each object. Create a new partition for each collection item.
分发本地 Scala 集合以形成 RDD,每个对象具有一个或多个位置首选项(Spark 节点的主机名)。 为每个集合项创建一个新分区。
原来,这个函数还为数据提供了位置信息,来看看我们怎么使用:
scala> val guigu1= sc.parallelize(List(1, 2, 3))
guigu1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[10] at parallelize at <console>:21
scala> val guigu2 = sc.makeRDD(List(1, 2, 3))
guigu2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[11] at makeRDD at <console>:21
scala> val seq = List((1, List("slave01")),| (2, List("slave02")))
seq: List[(Int, List[String])] = List((1, List(slave01)),
(2,List(slave02)))
scala> val guigu3 = sc.makeRDD(seq)
guigu3: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[12] at makeRDD at <console>:23
scala> guigu3.preferredLocations(guigu3.partitions(1))
res26: Seq[String] = List(slave02)
scala> guigu3.preferredLocations(guigu3.partitions(0))
res27: Seq[String] = List(slave01)
scala> guigu1.preferredLocations(guigu1.partitions(0))
res28: Seq[String] = List()
我们可以看到,makeRDD 函数有两种实现,第一种实现其实完全和 parallelize 一致;而第二种实现可以为数据提供位置信息,而除此之外的实现和 parallelize 函数也是一致的,如下:
def parallelize[T: ClassTag](seq: Seq[T], numSlices: Int = defaultParallelism): RDD[T]
def makeRDD[T: ClassTag](seq: Seq[(T, Seq[String])]): RDD[T]
都是返回 ParallelCollectionRDD,而且这个 makeRDD 的实现不可以自己指定分区的数量,而是固定为 seq 参数的 size 大小。
扩展知识:
RDD 的运行规划图
2.2.2 由外部存储系统的数据集创建(开发用)
包括本地的文件系统,还有所有 Hadoop 支持的数据集,比如 HDFS、Cassandra、HBase 等。
scala> val atguigu = sc.textFile("hdfs://hadoop102:9000/RELEASE")
atguigu: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/RELEASE MapPartitionsRDD[4] at textFile at <console>:24
2.3 RDD 编程
RDD 一般分为数值 RDD 和键值对 RDD,本章不进行具体区分,先统一来看,下一章会对键值对 RDD 做专门说明。
2.3.1 Transformation(转换)
RDD 中的所有转换都是延迟加载的
,也就是说,它们并不会直接计算结果。相反的,它们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当发生一个要求返回结果给 Driver
的动作时,这些转换才会真正运行。这种设计让 Spark 更加有效率地运行。
常用的 Transformation 如下:
1、map(func)
返回一个新的 RDD,该 RDD 由每一个输入元素经过 func 函数转换后组成。
源码:
def map[U: ClassTag](f: T => U): RDD[U]
示例代码:
scala> val mapSource = sc.parallelize(1 to 10)
mapSource: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[3] at parallelize at <console>:24
scala> mapSource.collect()
res: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> val map = mapSource.map(_ * 2)
map: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[4] at map at <console>:26
scala> map.collect 或者 map.collect() 括号可以省略
res4: Array[Int] = Array(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
2、filter(func)
返回一个新的 RDD,该 RDD 由经过 func 函数计算后返回值为 true 的输入元素组成。
源码:
def filter(f: T => Boolean): RDD[T]
示例代码:
scala> val filterSource = sc.makeRDD(1 to 10, 10)
filterSource: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[5] at makeRDD at <console>:24
scala> filterSource.collect
res5: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> filterSource.filter(_ % 2 == 0).collect
res6: Array[Int] = Array(2, 4, 6, 8, 10)
3、 flatMap(func)
类似于 map,但是每一个输入元素可以被映射为 0 或多个输出元素(所以 func 应该返回一个序列,而不是单一元素)。
源码:
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]
示例代码:
scala> val flatMapSource = sc.makeRDD(Array("a b c", "d e f", "h i j"))
flatMapSource: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[2] at makeRDD at <console>:24
scala> flatMapSource.map(_.split(" ")).collect
res1: Array[Array[String]] = Array(Array(a, b, c), Array(d, e, f), Array(h, i, j))
scala> flatMapSource.map(_.split(" "))
res2: org.apache.spark.rdd.RDD[Array[String]] = MapPartitionsRDD[4] at map at <console>:27
scala> flatMapSource.flatMap(_.split(" ")).collect
res3: Array[String] = Array(a, b, c, d, e, f, h, i, j)
scala> flatMapSource.flatMap(_.split(" "))
res4: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[6] at flatMap at <console>:27
# flatMap(_.split(" ")) 等价于 flatMap(Array(_))
scala> flatMapSource.flatMap(Array(_)).collect
res5: Array[String] = Array(a b c, d e f, h i j)
scala> flatMapSource.collect
res6: Array[String] = Array(a b c, d e f, h i j)
4、mapPartitions(func)
类似于 map,但独立地在 RDD 的每一个分片上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是 Iterator[T] => Iterator[U]。
假设有 N 个元素,有 M 个分区,那么 map 的函数的将被调用 N 次,而 mapPartitions 被调用 M 次,一个函数一次处理所有分区。mapPartitions 的执行效率要比 map 高。
源码:
def mapPartitions[U: ClassTag](
f: Iterator[T] => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U]
示例代码:
scala> val mapPartSource = sc.makeRDD(1 to 10, 5)
mapPartSource: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[8] at makeRDD at <console>:24
scala> mapPartSource.collect
res7: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> mapPartSource.mapPartitions(_.map(_ + "a"))
res8: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[9] at mapPartitions at <console>:27
scala> mapPartSource.mapPartitions(_.map(_ + "a")).collect
res9: Array[String] = Array(1a, 2a, 3a, 4a, 5a, 6a, 7a, 8a, 9a, 10a)
scala> mapPartSource.mapPartitions(items => items.map(x => x + "a")).collect
res10: Array[String] = Array(1a, 2a, 3a, 4a, 5a, 6a, 7a, 8a, 9a, 10a)
--------------------以下为扩展练习代码--------------------
scala> mapPartSource.partitions.size
res11: Int = 5
scala> mapPartSource.collect
res15: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> mapPartSource.mapPartitions(x => Iterator(x.mkString("|"))).collect mkString() 该函数把集合元素转化为字符串,可能还会添加分隔符、前缀、后缀。
res16: Array[String] = Array(1|2, 3|4, 5|6, 7|8, 9|10)
scala> val mapPartSource2 = sc.makeRDD(Array("a b c", "d e f", "h i j"), 5)
mapPartSource2: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[13] at makeRDD at <console>:24
scala> mapPartSource2.mapPartitions(x => Iterator(x.mkString("|"))).collect
res17: Array[String] = Array("", a b c, "", d e f, h i j)
scala> val mapPartSource3 = sc.makeRDD(Array("a b c", "d e f", "h i j", "k l m", "o p q"), 2)
mapPartSource3: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[15] at makeRDD at <console>:24
scala> mapPartSource3.mapPartitions(x => Iterator(x.mkString("|"))).collect
res18: Array[String] = Array(a b c|d e f, h i j|k l m|o p q)
5、mapPartitionsWithIndex(func)
类似于 mapPartitions,但 func 带有一个整数参数表示分片的索引值,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是 (Int, Interator[T]) => Iterator[U]。
源码:
private[spark] def mapPartitionsWithIndexInternal[U: ClassTag](
f: (Int, Iterator[T]) => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U]
示例代码:
scala> val source = sc.makeRDD(1 to 10)
source: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at makeRDD at <console>:24
scala> source.partitions.size
res0: Int = 4
scala> source.mapPartitionsWithIndex((index, x) => Iterator(index + ":" + x.mkString("|"))).collect
res3: Array[String] = Array(0:1|2, 1:3|4|5, 2:6|7, 3:8|9|10)
6、sample(withReplacement, fraction, seed)
以指定的随机种子随机抽样出数量为 fraction 的数据,withReplacement 表示是抽出的数据是否放回,true 为有放回的抽样,false 为无放回的抽样,seed 用于指定随机数生成器种子。
例如:从 RDD 中随机且有放回的抽出 50% 的数据,随机种子值为 3(即可能以1 2 3的其中一个起始值)。主要用于观察大数据集的分布情况。
源码:
def sample(
withReplacement: Boolean,
fraction: Double,
seed: Long = Utils.random.nextLong): RDD[T]
示例代码:
scala> val rdd = sc.parallelize(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[8] at parallelize at <console>:24
scala> rdd.collect()
res11: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> var sample1 = rdd.sample(true, 0.4, 2).collect
sample1: Array[Int] = Array(1, 2, 2, 7, 7, 8, 9) 为什么抽样出7个数据呢?
scala> var sample2 = rdd.sample(false, 0.2, 3).collect
sample2: Array[Int] = Array(1, 9)
7、takeSample
和 sample 的区别是:takeSample 返回的是最终的结果集合。
8、union(otherDataset)
对源 RDD 和参数 RDD 求并集
后返回一个新的 RDD。
源码:
def union(other: RDD[T]): RDD[T]
示例代码:
scala> val rdd1 = sc.parallelize(1 to 5)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[13] at parallelize at <console>:24
scala> val rdd2 = sc.parallelize(5 to 10)
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[14] at parallelize at <console>:24
scala> val rdd3 = rdd1.union(rdd2).collect
rdd3: Array[Int] = Array(1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10)
9、intersection(otherDataset)
对源 RDD 和参数 RDD 求交集
后返回一个新的 RDD。
源码:
def intersection(other: RDD[T]): RDD[T]
示例代码:
scala> val rdd1 = sc.parallelize(1 to 7)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[16] at parallelize at <console>:24
scala> val rdd2 = sc.parallelize(5 to 10)
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[17] at parallelize at <console>:24
scala> val rdd3 = rdd1.intersection(rdd2).collect
rdd3: Array[Int] = Array(5, 6, 7)
10、distinct([numTasks]))
对原 RDD 进行去重后返回一个新的 RDD。默认情况下,只有 8 个并行任务来操作,但是可以传入一个可选的 numTasks 参数改变它。
源码:
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
def distinct(): RDD[T]
示例代码:
scala> val distinctRdd = sc.parallelize(List(1, 2, 1, 5, 2, 9, 6, 1))
distinctRdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[24] at parallelize at <console>:24
scala> distinctRdd.distinct().collect
res12: Array[Int] = Array(1, 9, 5, 6, 2)
scala> distinctRdd.distinct(2).collect
res13: Array[Int] = Array(6, 2, 1, 9, 5)
11、partitionBy
对 KV 结构 RDD 进行重新分区,如果原有的 partionRDD 和现有的 partionRDD 是一致的话就不进行分区,否则会生成 shuffleRDD。
源码:
def partitionBy(partitioner: Partitioner): RDD[(K, V)]
示例代码:
scala> val rdd = sc.parallelize(Array((1,"aaa"), (2,"bbb"), (3,"ccc"), (4,"ddd")), 2)
rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[31] at parallelize at <console>:24
scala> rdd.partitions.size
res14: Int = 2
scala> var rdd2 = rdd.partitionBy(new org.apache.spark.HashPartitioner(3))
rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ShuffledRDD[32] at partitionBy at <console>:26
scala> rdd2.partitions.size
res15: Int = 3
12、reduceByKey(func, [numTasks])
在一个 (K,V) 的 RDD 上调用,返回一个 (K,V) 的 RDD,使用指定的 reduce 函数,将相同 key 的值聚合到一起,reduce 任务的个数可以通过第二个可选的参数来设置。
源码:
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)]
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
def reduceByKey(func: (V, V) => V): RDD[(K, V)]
示例代码:
scala> val rdd = sc.parallelize(List(("female",1), ("male",5), ("female",5), ("male",2)))
rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[33] at parallelize at <console>:24
scala> val reduce = rdd.reduceByKey((x, y) => x + y)
reduce: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[34] at reduceByKey at <console>:26
scala> reduce.collect()
res16: Array[(String, Int)] = Array((female,6), (male,7))
--------------------以下为扩展练习代码--------------------
scala> val rdd = sc.makeRDD(1 to 15).union(sc.makeRDD(7 to 20))
rdd: org.apache.spark.rdd.RDD[Int] = UnionRDD[2] at union at <console>:24
scala> rdd.map(x => (x, 1))
res0: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[3] at map at <console>:27
scala> rdd.map((_, 1))
res2: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[4] at map at <console>:27
scala> res0.collect
res3: Array[(Int, Int)] = Array((1,1), (2,1), (3,1), (4,1), (5,1), (6,1), (7,1), (8,1), (9,1), (10,1), (11,1), (12,1), (13,1), (14,1), (15,1), (7,1), (8,1), (9,1), (10,1), (11,1), (12,1), (13,1), (14,1), (15,1), (16,1), (17,1), (18,1), (19,1), (20,1))
scala> res2.collect
res4: Array[(Int, Int)] = Array((1,1), (2,1), (3,1), (4,1), (5,1), (6,1), (7,1), (8,1), (9,1), (10,1), (11,1), (12,1), (13,1), (14,1), (15,1), (7,1), (8,1), (9,1), (10,1), (11,1), (12,1), (13,1), (14,1), (15,1), (16,1), (17,1), (18,1), (19,1), (20,1))
scala> val rdd = sc.makeRDD(1 to 15).union(sc.makeRDD(7 to 20)).map((_, 1))
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[8] at map at <console>:24
scala> rdd.reduceByKey(_ + _).collect
res5: Array[(Int, Int)] = Array((16,1), (8,2), (1,1), (17,1), (9,2), (18,1), (10,2), (2,1), (19,1), (11,2), (3,1), (4,1), (20,1), (12,2), (13,2), (5,1), (14,2), (6,1), (15,2), (7,2))
scala> rdd.reduceByKey((x, y) => (x + y)).collect
res6: Array[(Int, Int)] = Array((16,1), (8,2), (1,1), (17,1), (9,2), (18,1), (10,2), (2,1), (19,1), (11,2), (3,1), (4,1), (20,1), (12,2), (13,2), (5,1), (14,2), (6,1), (15,2), (7,2))
13、groupByKey
groupByKey 也是对每个 key 进行操作,但只生成一个 sequence。
源码:
def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]
示例代码:
scala> rdd.reduceByKey(_ + _).collect
res5: Array[(Int, Int)] = Array((16,1), (8,2), (1,1), (17,1), (9,2), (18,1), (10,2), (2,1), (19,1), (11,2), (3,1), (4,1), (20,1), (12,2), (13,2), (5,1), (14,2), (6,1), (15,2), (7,2))
scala> rdd.groupByKey
res7: org.apache.spark.rdd.RDD[(Int, Iterable[Int])] = ShuffledRDD[11] at groupByKey at <console>:27
scala> rdd.groupByKey().collect
res8: Array[(Int, Iterable[Int])] = Array((16,CompactBuffer(1)), (8,CompactBuffer(1, 1)), (1,CompactBuffer(1)), (17,CompactBuffer(1)), (9,CompactBuffer(1, 1)), (18,CompactBuffer(1)), (10,CompactBuffer(1, 1)), (2,CompactBuffer(1)), (19,CompactBuffer(1)), (11,CompactBuffer(1, 1)), (3,CompactBuffer(1)), (4,CompactBuffer(1)), (20,CompactBuffer(1)), (12,CompactBuffer(1, 1)), (13,CompactBuffer(1, 1)), (5,CompactBuffer(1)), (14,CompactBuffer(1, 1)), (6,CompactBuffer(1)), (15,CompactBuffer(1, 1)), (7,CompactBuffer(1, 1)))
scala> rdd.groupByKey().map(item => (item._1, item._2.sum)).collect 方式一:仅使用 map
res10: Array[(Int, Int)] = Array((16,1), (8,2), (1,1), (17,1), (9,2), (18,1), (10,2), (2,1), (19,1), (11,2), (3,1), (4,1), (20,1), (12,2), (13,2), (5,1), (14,2), (6,1), (15,2), (7,2))
scala> rdd.groupByKey().map(item => (item._1, item._2.reduceLeft(_ + _))).collect 方式二:使用 map + reduceLeft
res11: Array[(Int, Int)] = Array((16,1), (8,2), (1,1), (17,1), (9,2), (18,1), (10,2), (2,1), (19,1), (11,2), (3,1), (4,1), (20,1), (12,2), (13,2), (5,1), (14,2), (6,1), (15,2), (7,2))
scala> rdd.groupByKey().map{case (x, y) => (x, y.sum)}.collect 方式三:使用模式匹配
res13: Array[(Int, Int)] = Array((16,1), (8,2), (1,1), (17,1), (9,2), (18,1), (10,2), (2,1), (19,1), (11,2), (3,1), (4,1), (20,1), (12,2), (13,2), (5,1), (14,2), (6,1), (15,2), (7,2))
reduceByKey 和 groupByKey 的区别:
14、combineByKey 此函数博大精深,海纳百川
def combineByKey[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C)
对相同的 K,把 V 合并成一个集合。
createCombiner: combineByKey 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey() 会使用一个叫作 createCombiner() 的函数来创建那个键对应的累加器的初始值。
mergeValue: 如果这是一个在处理当前分区之前已经遇到的键,它会使用 mergeValue() 方法将该键的累加器对应的当前值与这个新的值进行合并。
mergeCombiners: 由于每个分区都是独立处理的,因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器,就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并。
示例代码:
scala> val rdd = sc.makeRDD(Array(("a",50),("a",70),("b",60),("a",60),("b",80),("c",90),("b",90),("c",60),("c",80)),3)
rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[21] at makeRDD at <console>:24
scala> rdd.combineByKey((_, 1), (c:(Int,Int), v) => (c._1 + v, c._2 + 1), (c1:(Int,Int), c2:(Int,Int)) => (c1._1 + c2._1, c1._2 + c2._2))
res16: org.apache.spark.rdd.RDD[(String, (Int, Int))] = ShuffledRDD[22] at combineByKey at <console>:27
scala> res16.collect
res17: Array[(String, (Int, Int))] = Array((c,(230,3)), (a,(180,3)), (b,(230,3)))
scala> res17.map(v => (v._1, v._2._1 / v._2._2)) 方式一:仅使用 map
res18: Array[(String, Int)] = Array((c,76), (a,60), (b,76))
scala> res17.map{case (stu, (sum, count)) => (stu, sum / count)} 方式二:使用模式匹配
res26: Array[(String, Int)] = Array((c,76), (a,60), (b,76))
scala> case class Score(name: String, score: Int) 方式三:使用对象(比如样例类),将数据转换为对象(样例类),再将对象转换成 KV 类型的数据(转换时使用对象的属性)
defined class Score
scala> val rdd = sc.makeRDD(Array(Score("a",50),Score("a",70),Score("b",60),Score("a",60),Score("b",80),Score("c",90),Score("b",90),Score("c",60),Score("c",80)),3)rdd: org.apache.spark.rdd.RDD[Score] = ParallelCollectionRDD[23] at makeRDD at <console>:26
scala> rdd.map(sco => (sco.name, sco.score))
res27: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[24] at map at <console>:29
scala> res27.combineByKey((_,1), (c:(Int,Int), v) => (c._1 + v, c._2 + 1), (c1:(Int,Int), c2:(Int,Int)) => (c1._1 + c2._1, c1._2 + c2._2))
res28: org.apache.spark.rdd.RDD[(String, (Int, Int))] = ShuffledRDD[25] at combineByKey at <console>:31
scala> res28.collect
res29: Array[(String, (Int, Int))] = Array((c,(230,3)), (a,(180,3)), (b,(230,3)))
图解如下:
15、aggregateByKey
def aggregateByKey[U: ClassTag](zeroValue: U, partitioner: Partitioner)(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)]
是 combineBykey 的简化操作,zeroValue 类似于 createCombiner, seqOp 类似于 mergeValue, combOp 类似于 mergeCombiner。
在 kv 对的 RDD 中,按 key 将 value 进行分组合并,合并时,将初始值和每个 value 作为 seq 函数的参数,进行对应的计算,返回的结果作为一个新的 kv 对,然后再将结果按照 key 进行合并,最后将每个分组的 value 传递给 combine 函数进行计算(先将前两个 value 进行计算,将返回结果和下一个 value 传给 combine 函数,以此类推),将 key 与计算结果作为一个新的 kv 对输出。seqOp 函数用于在每一个分区中用初始值逐步迭代 value,combOp 函数用于合并每个分区中的结果。
示例代码:
scala> val rdd1 = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),1)
rdd1: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[0] at parallelize at <console>:24
scala> rdd1.aggregateByKey(0)(math.max(_,_),_+_).collect
res0: Array[(Int, Int)] = Array((1,4), (3,8), (2,3))
scala> val rdd2 = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)
rdd2: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[2] at parallelize at <console>:24
scala> rdd2.aggregateByKey(0)(math.max(_,_),_+_).collect
res1: Array[(Int, Int)] = Array((3,8), (1,7), (2,3))
--------------------以下为扩展练习代码--------------------
scala> val rdd3 = sc.makeRDD(Array(("a",50),("a",70),("b",60),("a",60),("b",80),("c",90),("b",90),("c",60),("c",80)),3)
rdd3: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[4] at makeRDD at <console>:24
scala> rdd3.aggregateByKey((0,0))((c:(Int,Int), v) => (c._1 + v, c._2 + 1), (c1:(Int,Int), c2:(Int,Int)) => (c1._1 + c2._1, c1._2 + c2._2)).collect
res3: Array[(String, (Int, Int))] = Array((c,(230,3)), (a,(180,3)), (b,(230,3)))
例如:
如果分一个分区,以 key 为 1 的分区为例,0 先和 3 比较得 3,3 再和 2 比较得 3,3 再和 4 比较得 4,所以整个 key 为 1 的组最终结果为(1,4),同理,key 为 2 的最终结果为(2,3),key 为 3 的最终结果为 (3,8)。
如果分三个分区,前两个 kv对 在一个分区,中间两个 kv对 在一个分区,最后两个 kv对 在一个分区,第一个分区的最终结果为 (1,3),第二个分区的最终结果为 (1,4) 和 (2,3),最后一个分区的最终结果为 (3,8),combine 后为 (1,7), (2,3), (3,8)。
图解如下:
16、foldByKey
def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)] 是 aggregateByKey 的简化操作,seqop 和 combop 相同。注意:V 的类型不能改变。
示例代码:
scala> val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[6] at parallelize at <console>:24
scala> val fold = rdd.foldByKey(0)(_+_)
fold: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[7] at foldByKey at <console>:26
scala> fold.collect()
res4: Array[(Int, Int)] = Array((3,14), (1,9), (2,3))
图解如下:
17、sortByKey([ascending], [numTasks])
在一个 (K,V) 的 RDD 上调用,K 必须实现 Ordered 接口,返回一个按照 key 进行排序的 (K,V) 的 RDD。
源码:
def sortByKey(
ascending: Boolean = true,
numPartitions: Int = self.partitions.length): RDD[(K, V)]
示例代码:
scala> val rdd = sc.parallelize(Array((3,"aa"),(6,"cc"),(2,"bb"),(1,"dd")))
rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[8] at parallelize at <console>:24
scala> rdd.sortByKey(true).collect() 升序
res5: Array[(Int, String)] = Array((1,dd), (2,bb), (3,aa), (6,cc))
scala> rdd.sortByKey(false).collect() 降序
res6: Array[(Int, String)] = Array((6,cc), (3,aa), (2,bb), (1,dd))
18、sortBy(func, [ascending], [numTasks])
与 sortByKey 类似,但是更灵活,可以用 func 先对数据进行处理,按照处理后的数据比较结果排序。
源码:
def sortBy[K](
f: (T) => K,
ascending: Boolean = true,
numPartitions: Int = this.partitions.length)
(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
示例代码:
scala> val rdd = sc.makeRDD(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[37] at makeRDD at <console>:24
scala> rdd.partitions.size
res12: Int = 4
scala> rdd.sortBy(_%4).collect
res13: Array[Int] = Array(4, 8, 5, 9, 1, 2, 6, 10, 3, 7)
scala> rdd.map(x => (x % 4, x)).sortByKey().map(x => x._2).collect
res14: Array[Int] = Array(4, 8, 5, 9, 1, 2, 6, 10, 3, 7)
19、join(otherDataset, [numTasks])
在类型为 (K,V) 和 (K,W) 的 RDD 上调用,返回一个相同 key 对应的所有元素对在一起的 (K,(V,W))
的 RDD。即和另外的 RDD 进行 JOIN。
源码:
def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))] 和另外的 RDD 进行 JOIN。
示例代码:
scala> val rdd1 = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c")))
rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[48] at parallelize at <console>:24
scala> val rdd2 = sc.parallelize(Array((1,4),(2,5),(3,6)))
rdd2: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[49] at parallelize at <console>:24
scala> rdd1.join(rdd2).collect()
res15: Array[(Int, (String, Int))] = Array((1,(a,4)), (2,(b,5)), (3,(c,6)))
leftOuterJoin、rightOuterJoin、fullOuterJoin 同理。
20、cogroup(otherDataset, [numTasks])
在类型为 (K,V) 和 (K,W) 的 RDD 上调用,返回一个 (K,(Iterable<V>,Iterable<W>))
类型的 RDD。
源码:
def cogroup[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (Iterable[V], Iterable[W]))] 类似于两个 RDD 分别做 groupByKey 然后再 全JOIN。
图解如下:
示例代码:
scala> val rdd1 = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c")))
rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[57] at parallelize at <console>:24
scala> val rdd2 = sc.parallelize(Array((1,4),(2,5),(5,6)))
rdd2: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[58] at parallelize at <console>:24
scala> rdd1.cogroup(rdd2).collect()
res17: Array[(Int, (Iterable[String], Iterable[Int]))] = Array((1,(CompactBuffer(a),CompactBuffer(4))), (5,(CompactBuffer(),CompactBuffer(6))), (2,(CompactBuffer(b),CompactBuffer(5))), (3,(CompactBuffer(c),CompactBuffer())))
scala> rdd1.groupByKey.join(rdd2.groupBy)
groupBy groupByKey
scala> rdd1.groupByKey.join(rdd2.groupByKey).collect
res18: Array[(Int, (Iterable[String], Iterable[Int]))] = Array((1,(CompactBuffer(a),CompactBuffer(4))), (2,(CompactBuffer(b),CompactBuffer(5))))
scala> rdd1.groupByKey.fullOuterJoin(rdd2.groupByKey).collect
res19: Array[(Int, (Option[Iterable[String]], Option[Iterable[Int]]))] = Array((1,(Some(CompactBuffer(a)),Some(CompactBuffer(4)))), (5,(None,Some(CompactBuffer(6)))), (2,(Some(CompactBuffer(b)),Some(CompactBuffer(5)))), (3,(Some(CompactBuffer(c)),None)))
21、cartesian(otherDataset)
笛卡尔积
def cartesian[U: ClassTag](other: RDD[U]): RDD[(T, U)] 笛卡尔积
示例代码:
scala> val rdd1 = sc.parallelize(1 to 3)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[74] at parallelize at <console>:24
scala> val rdd2 = sc.parallelize(2 to 5)
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[75] at parallelize at <console>:24
scala> rdd1.cartesian(rdd2).count
res21: Long = 12
scala> rdd1.cartesian(rdd2).collect()
res22: Array[(Int, Int)] = Array((1,2), (1,3), (1,4), (1,5), (2,2), (2,3), (2,4), (2,5), (3,2), (3,3), (3,4), (3,5))
22、pipe(command, [envVars])
对于每个分区,支持使用外部脚本比如 shell、perl 等处理分区内的数据。注意
:shell 脚本需要集群中的所有节点都能访问到。即脚本文件要分发到其他机器节点。
def pipe(command: String): RDD[String]
shell 脚本
pipe.sh
#!/bin/sh
echo "AA"
while read LINE; do
echo ">>>"${LINE}
done
示例代码:
scala> val rdd = sc.parallelize(List("hi","Hello","how","are","you"),1) 一个分区
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[78] at parallelize at <console>:24
scala> rdd.pipe("/home/atguigu/bin/test/pipe.sh")
res23: org.apache.spark.rdd.RDD[String] = PipedRDD[79] at pipe at <console>:27
scala> rdd.pipe("/home/atguigu/bin/test/pipe.sh").collect
res24: Array[String] = Array(AA, >>>hi, >>>Hello, >>>how, >>>are, >>>you)
scala> val rdd = sc.parallelize(List("hi","Hello","how","are","you"),3) 三个分区
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[82] at parallelize at <console>:24
scala> rdd.pipe("/home/atguigu/bin/test/pipe.sh").collect
res25: Array[String] = Array(AA, >>>hi, AA, >>>Hello, >>>how, AA, >>>are, >>>you)
23、coalesce(numPartitions)
缩减分区数,用于大数据集过滤后,提高小数据集的执行效率。
def coalesce(numPartitions: Int, shuffle: Boolean = false,partitionCoalescer: Option[PartitionCoalescer] = Option.empty)(implicit ord: Ordering[T] = null): RDD[T]
示例代码:
scala> val rdd = sc.parallelize(1 to 16,4)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[84] at parallelize at <console>:24
scala> rdd.partitions.size
res27: Int = 4
scala> val coalesceRDD = rdd.coalesce(3)
coalesceRDD: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[85] at coalesce at <console>:26
scala> coalesceRDD.partitions.size
res28: Int = 3
24、repartition(numPartitions)
根据分区数,重新通过网络随机洗牌所有数据。
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
25、repartitionAndSortWithinPartitions(partitioner)
repartitionAndSortWithinPartitions 函数是 repartition 函数的变种,与 repartition 函数不同的是,repartitionAndSortWithinPartitions 在给定的 partitioner 内部进行排序,性能比 repartition 要高。
26、glom
将每一个分区形成一个数组,形成新的 RDD 类型是 RDD[Array[T]]。
源码:
def glom(): RDD[Array[T]] 将每一个分区中的所有数据转换为一个 Array 数组,形成新的 RDD。
示例代码:
scala> val rdd = sc.parallelize(1 to 16,4)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[86] at parallelize at <console>:24
scala> rdd.glom().collect()
res29: Array[Array[Int]] = Array(Array(1, 2, 3, 4), Array(5, 6, 7, 8), Array(9, 10, 11, 12), Array(13, 14, 15, 16))
27、mapValues
针对于 (K,V) 形式的类型只对 V 进行操作。
源码:
def mapValues[U](f: V => U): RDD[(K, U)] 只对 KV 结构中 value 数据进行映射。value 可以改变类型。
示例代码:
scala> val rdd = sc.parallelize(Array((1,"a"),(1,"d"),(2,"b"),(3,"c")))
rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[88] at parallelize at <console>:24
scala> rdd.mapValues(_+"|||").collect()
res30: Array[(Int, String)] = Array((1,a|||), (1,d|||), (2,b|||), (3,c|||))
28、subtract
计算差的一种函数,去除两个 RDD 中相同的元素,不同的 RDD 将保留下来。举例说明:A - B = A - A 与 B 的交集
源码:
def subtract(other: RDD[T]): RDD[T] 求差集。
示例代码:
scala> val rdd1 = sc.parallelize(3 to 8)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[90] at parallelize at <console>:24
scala> val rdd2 = sc.parallelize(1 to 5)
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[91] at parallelize at <console>:24
scala> rdd1.subtract(rdd2).collect()
res31: Array[Int] = Array(8, 6, 7)
2.3.2 Action(行动)
常用的 Action 如下:
1、reduce(func)
通过 func 函数聚集 RDD 中的所有元素,这个功能必须是可交换且可并联的。
源码:
def reduce(f: (T, T) => T): T 归约某个 RDD
示例代码:
scala> val rdd = sc.makeRDD(1 to 10,2)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[97] at makeRDD at <console>:24
scala> rdd.reduce(_+_)
res33: Int = 55
2、collect()
将数据返回到 Driver,是以数组的形式返回数据集的所有元素(简单测试用,生产环境中不用)
3、count()
返回 RDD 的元素个数。
4、first()
返回 RDD 的第一个元素(类似于 take(1))。
5、take(n)
返回一个由数据集的前 n 个元素组成的数组。
6、takeSample(withReplacement, num, [seed])
返回一个数组,该数组由从数据集中随机采样的 num 个元素组成,可以选择是否用随机数替换不足的部分,seed 用于指定随机数生成器种子。
图解如下:
7、takeOrdered(n)
返回前几个的排序。
示例代码:
scala> var rdd = sc.makeRDD(Seq(10, 4, 2, 12, 3))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[103] at makeRDD at <console>:24
scala> rdd.top(2)
res50: Array[Int] = Array(12, 10)
scala> rdd.takeOrdered(2)
res51: Array[Int] = Array(2, 3)
8、aggregate
操作的是数值型数据,aggregate 函数将每个分区里面的元素通过 seqOp 和初始值进行聚合,然后用 combine 函数将每个分区的结果和初始值 (zeroValue) 进行 combine 操作。这个函数最终返回的类型不需要和 RDD 中元素类型一致。
def aggregate(zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)
图解如下:
示例代码:
scala> var rdd = sc.makeRDD(1 to 4,2)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[107] at makeRDD at <console>:24
scala> rdd.aggregate(1)((_*_),(_+_))
res53: Int = 15
9、fold(zeroValue)(func)
折叠操作,aggregate 的简化操作,seqop 和 combop一样。
10、saveAsTextFile(path) 以文本的方式保存到 HDFS 兼容的文件系统
将数据集的元素以 textfile 的形式保存到 HDFS 文件系统或者其他支持的文件系统,对于每个元素,Spark 将会调用 toString 方法,将它装换为文件中的文本。
示例代码:
scala> var rdd = sc.parallelize(1 to 10,2)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[108] at parallelize at <console>:24
scala> rdd.saveAsTextFile("hdfs://hadoop102:9000/textFile")
HDFS 上查看
[atguigu@hadoop102 hadoop-2.7.2]$ bin/hdfs dfs -cat hdfs://hadoop102:9000/textFile/par*
1
2
3
4
5
6
7
8
9
10
11、saveAsSequenceFile(path) 以 SequenceFile 形式来存文件
将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以是 HDFS 或者其他 Hadoop 支持的文件系统。
示例代码:
scala> var rdd = sc.parallelize(List((1,5),(1,6),(1,7),(2,8),(2,9),(3,10)),3)
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[110] at parallelize at <console>:24
scala> rdd.saveAsSequenceFile("hdfs://hadoop102:9000/seqFile")
HDFS 上查看
12、saveAsObjectFile(path) 以 ObjectFile 形式来存文件
用于将 RDD 中的元素序列化成对象,存储到文件中。
示例代码:
scala> var rdd = sc.parallelize(List((1,5),(1,6),(1,7),(2,8),(2,9),(3,10)))
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[112] at parallelize at <console>:24
scala> rdd.saveAsObjectFile("hdfs://hadoop102:9000/objFile")
scala> rdd.partitions.size
res57: Int = 4
HDFS 上查看
13、countByKey() 返回 Map 结构,获取每一个 key 的数量
针对 (K,V) 类型的 RDD,返回一个 (K,Int) 的 map,表示每一个 key 对应的元素个数。
示例代码:
scala> var rdd = sc.parallelize(List((1,5),(1,6),(1,7),(2,8),(2,9),(3,10)))
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[115] at parallelize at <console>:24
scala> rdd.countByKey()
res58: scala.collection.Map[Int,Long] = Map(1 -> 3, 2 -> 2, 3 -> 1)
14、foreach(func) 在数据集上的每一个元素运行 func 函数。
示例代码:
scala> var rdd = sc.makeRDD(1 to 10,2)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[119] at makeRDD at <console>:24
scala> var sum = sc.accumulator(0)
warning: there were two deprecation warnings; re-run with -deprecation for details
sum: org.apache.spark.Accumulator[Int] = 0
scala> rdd.foreach(sum+=_)
scala> sum.value
res61: Int = 55
scala> rdd.collect().foreach(println)
1
2
3
4
5
6
7
8
9
10
2.3.3 数值 RDD 的统计操作
Spark 对包含数值数据的 RDD 提供了一些描述性的统计操作。 Spark 的数值操作是通过流式算法实现的,允许以每次一个元素的方式构建出模型。这些统计数据都会在调用 stats() 时通过一次遍历数据计算出来,并以 StatsCounter 对象返回。
示例代码:
scala> var rdd = sc.makeRDD(1 to 100)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[122] at makeRDD at <console>:24
scala> rdd.count()
res64: Long = 100
scala> rdd.mean()
res65: Double = 50.5
scala> rdd.sum()
res66: Double = 5050.0
scala> rdd.max()
res67: Int = 100
scala> rdd.min()
res68: Int = 1
2.3.4 向 RDD 操作传递函数注意事项
Spark 的大部分转化操作和一部分行动操作,都需要依赖用户传递的函数来计算。在 Scala 中,我们可以把定义的内联函数、方法的引用或静态方法传递给 Spark,就像 Scala 的其他函数式 API 一样。我们还要考虑其他一些细节,比如所传递的函数及其引用的数据需要是可序列化的(实现了 Java 的 Serializable 接口)。 传递一个对象的方法或者字段时,会包含对整个对象的引用。
小结:传递函数的时候需要注意:如果你的 RDD 转换操作中的函数使用到了类的方法或者变量,那么你需要注意该类可能需要能够序列化。
示例代码:
class SearchFunctions(val query: String) extends java.io.Serializable{
def isMatch(s: String): Boolean = {
s.contains(query)
}
def getMatchesFunctionReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
// 问题:"isMatch"表示"this.isMatch",因此我们要传递整个"this"
rdd.filter(isMatch)
}
def getMatchesFieldReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
// 问题:"query"表示"this.query",因此我们要传递整个"this"
rdd.filter(x => x.contains(query))
}
def getMatchesNoReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
// 安全:只把我们需要的字段拿出来放入局部变量中
val query_ = this.query
rdd.filter(x => x.contains(query_))
}
}
如果在 Scala 中出现了 NotSerializableException,通常问题就在于我们传递了一个不可序列化的类中的函数或字段。
2.3.5 在不同 RDD 类型间转换
有些函数只能用于特定类型的 RDD,比如 mean() 和 variance() 只能用在数值 RDD 上,而 join() 只能用在键值对 RDD 上。在 Scala 和 Java 中,这些函数都没有定义在标准的 RDD 类中,所以要访问这些附加功能,必须要确保获得了正确的专用 RDD 类。
在 Scala 中,将 RDD 转为有特定函数的 RDD(比如在 RDD[Double] 上进行数值操作)是由隐式转换
来自动处理的。
2.4 RDD 持久化
2.4.1 RDD 的缓存
Spark 速度非常快的原因之一,就是在不同操作中可以在内存中持久化或缓存个数据集。当持久化某个 RDD 后,每一个节点都将把计算的分片结果保存在内存中,并在对此 RDD 或衍生出的 RDD 进行的其他动作中重用。这使得后续的动作变得更加迅速。
RDD 相关的持久化和缓存,是 Spark 最重要的特征之一
。可以说,缓存是 Spark 构建迭代式算法
和快速交互式查询
的关键。如果一个有持久化数据的节点发生故障,Spark 会在需要用到缓存的数据时重算丢失的数据分区。如果希望节点故障的情况不会拖累我们的执行速度,也可以把数据备份到多个节点上。
持久化也是懒执行的,持久化有两个操作:persist(StorageLevel),另外一个是 cache, cache 就相当于 MEMORY_ONLY 的 persist。
2.4.2 RDD 缓存方式
RDD 通过 persist 方法或 cache 方法可以将前面的计算结果缓存,默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空 间中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。
通过查看源码发现 cache 最终也是调用了 persist 方法,默认的存储级别都是仅在内存存储一份,Spark 的存储级别还有好多种,存储级别在 object StorageLevel 中定义的。在存储级别的末尾加上“_2”来把持久化数据存为两份。
缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD 的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于 RDD 的一系列转换,丢失的数据会被重算,由于 RDD 的各个 Partition 是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部 Partition。
注意
:使用 Tachyon 可以实现堆外缓存。
示例代码:
scala> val rdd = sc.makeRDD(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at makeRDD at <console>:24
scala> val nocache = rdd.map(_.toString+"["+System.currentTimeMillis+"]")
nocache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at map at <console>:26
scala> nocache.collect
res0: Array[String] = Array(1[1556167699748], 2[1556167699748], 3[1556167699546], 4[1556167699546], 5[1556167699546], 6[1556167699747], 7[1556167699747], 8[1556167699545], 9[1556167699545], 10[1556167699545])
scala> nocache.collect 时间戳变化
res1: Array[String] = Array(1[1556167711837], 2[1556167711837], 3[1556167711813], 4[1556167711813], 5[1556167711813], 6[1556167711841], 7[1556167711841], 8[1556167711814], 9[1556167711814], 10[1556167711814])
scala> val cache = rdd.map(_.toString+"["+System.currentTimeMillis+"]")
cache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[2] at map at <console>:26
scala> cache.cache 缓存一把
res2: cache.type = MapPartitionsRDD[2] at map at <console>:26
scala> cache.collect
res3: Array[String] = Array(1[1556167795046], 2[1556167795054], 3[1556167795023], 4[1556167795023], 5[1556167795023], 6[1556167795046], 7[1556167795046], 8[1556167795023], 9[1556167795025], 10[1556167795025])
scala> cache.collect 时间戳不变
res4: Array[String] = Array(1[1556167795046], 2[1556167795054], 3[1556167795023], 4[1556167795023], 5[1556167795023], 6[1556167795046], 7[1556167795046], 8[1556167795023], 9[1556167795025], 10[1556167795025])
2.5 RDD 检查点机制
Spark 中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将 RDD 写入 Disk 做检查点)是为了通过 lineage 做容错的辅助,lineage 过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的 RDD 开始重做 Lineage,就会减少开销。检查点通过将数据写入到 HDFS 文件系统实现了 RDD 的检查点功能。
cache 和 checkpoint 是有显著区别的,缓存把 RDD 计算出来然后放在内存中,但是 RDD 的依赖链(相当于数据库中的 redo 日志),也不能丢掉,当某个点某个 executor 宕了,上面 cache 的 RDD 就会丢掉, 需要通过依赖链重放计算出来,不同的是,checkpoint 是把 RDD 保存在 HDFS 中,是多副本可靠存储,所以依赖链就可以丢掉了,就斩断了依赖链, 是通过复制实现的高容错。
如果存在以下场景,则比较适合使用检查点机制:
1) DAG 中的 Lineage 过长,如果重算,则开销太大(如在 PageRank 中)。
2) 在宽依赖上做 checkpoint 获得的收益更大。
为当前 RDD 设置检查点。该函数将会创建一个二进制的文件,并存储到 checkpoint 目录中,该目录是用 SparkContext.setCheckpointDir() 设置的。在 checkpoint 的过程中,该 RDD 的所有依赖于父 RDD 中的信息将全部被移出。对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发。
示例代码:
scala> val data = sc.parallelize(1 to 100,5)
data: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[3] at parallelize at <console>:24
scala> sc.setCheckpointDir("hdfs://hadoop102:9000/checkpoint")
scala> data.count
res6: Long = 100
scala> data.checkpoint
scala> val ch1 = sc.parallelize(1 to 2)
ch1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[4] at parallelize at <console>:24
scala> val ch2 = ch1.map(_.toString+"["+System.currentTimeMillis+"]")
ch2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[5] at map at <console>:26
scala> val ch3 = ch1.map(_.toString+"["+System.currentTimeMillis+"]")
ch3: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[6] at map at <console>:26
scala> ch3.checkpoint
scala> ch2.collect
res9: Array[String] = Array(1[1556169758749], 2[1556169758749])
scala> ch2.collect
res10: Array[String] = Array(1[1556169761316], 2[1556169761316])
scala> ch3.collect
res11: Array[String] = Array(1[1556169778793], 2[1556169778790])
scala> ch3.collect
res12: Array[String] = Array(1[1556169780524], 2[1556169780525])
scala> ch3.collect
res13: Array[String] = Array(1[1556169780524], 2[1556169780525])
2.5.1 checkpoint 写流程
RDD checkpoint 过程中会经过几个状态:[ initialized → marked for checkpointing → checkpointing in progress → checkpointed ]
转换流程如下:
1) data.checkpoint 这个函数调用中设置的目录中所有依赖的 RDD 都会被删除,函数必须在 job 运行之前调用执行,强烈建议 RDD 缓存在内存中(又提到一次,千万要注意哟)!否则保存到文件的时候需要从头计算。初始化 RDD 的 checkpointData 变量为 ReliableRDDCheckpointData。这时候标记为 Initialized 状态。
2) 在所有 job action 的时候,runJob 方法中都会调用 rdd.doCheckpoint,这个会向前递归调用所有的依赖的 RDD,看看需不需要 checkpoint。需要 checkpoint,然后调用 checkpointData.get.checkpoint(),里面标记状态为 CheckpointingInProgress,里面调用具体实现类的 ReliableRDDCheckpointData 的 doCheckpoint 方法。
3) doCheckpoint -> writeRDDToCheckpointDirectory,注意这里会把 job 再运行一次,如果已经 cache 了,就可以直接使用缓存中的 RDD 了,就不需要重头计算一遍了(怎么又说了一遍),这时候直接把 RDD,输出到 hdfs 每个分区一个文件,会先写到一个临时文件,如果全部输出完,则进行 rename ,如果输出失败,就回滚 delete。
4) 标记状态为 Checkpointed,markCheckpointed 方法中清除所有的依赖,怎么清除依赖的呢?就是把 RDD 变量的强引用设置为 null,垃圾回收了,会触发 ContextCleaner 里面的监听,清除实际 BlockManager 缓存中的数据。
2.5.2 checkpoint 读流程
如果一个 RDD 我们已经 checkpoint 了,那么是什么时候用呢?checkpoint 将 RDD 持久化到 HDFS 或本地文件夹,如果不被手动 remove 掉,是一直存在的,也就是说可以被下一个 driver program 使用。比如 spark streaming 挂掉了, 重启后就可以使用之前 checkpoint 的数据进行 recover,当然在同一个 driver program 也可以使用。
我们讲下在同一个 driver program 中是怎么使用 checkpoint 数据的。如果一个 RDD 被checkpoint 了,如果这个 RDD 上有 action 操作时候,或者回溯的这个 RDD 的时候,这个 RDD 进行计算的时候,里面判断如果已经 checkpoint 过,对分区和依赖的处理都是使用的 RDD 内部的 checkpointRDD 变量。具体细节如下:
如果一个 RDD 被 checkpoint 了,那么这个 RDD 中对分区和依赖的处理都是使用的 RDD 内部的 checkpointRDD 变量,具体实现是 ReliableCheckpointRDD 类型。这个是在 checkpoint 写流程中创建的。依赖和获取分区方法中先判断是否已经 checkpoint,如果已经 checkpoint 了,就斩断依赖,使用 ReliableCheckpointRDD 来处理依赖和获取分区。
如果没有,才往前回溯依赖。依赖就是没有依赖,因为已经斩断了依赖,获取分区数据就是读取 checkpoint 到 hdfs 目录中不同分区保存下来的文件。
2.6 RDD 的依赖关系
RDD 和它依赖的父 RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。
2.6.1 窄依赖(Narrow Dependency)
窄依赖指的是每一个父 RDD 的 Partition 最多被子 RDD 的一个 Partition 使用。
总结:窄依赖我们形象的比喻为独生子女。
2.6.2 宽依赖(Wide Dependency)
宽依赖指的是多个子 RDD 的 Partition 会依赖同一个父 RDD 的 Partition,会引起 shuffle。
总结:宽依赖我们形象的比喻为超生子女。
2.6.3 Lineage
RDD 只支持粗粒度转换,即在大量记录上执行的单个操作。将创建 RDD 的一系列 Lineage(即血统)记录下来,以便恢复丢失的分区。RDD 的 Lineage 会记录 RDD 的元数据信息和转换行为,当该 RDD 的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。
2.7 DAG 的生成
DAG(Directed Acyclic Graph) 叫做有向无环图,原始的 RDD 通过一系列的转换就就形成了 DAG,根据 RDD 之间的依赖关系的不同将 DAG 划分成不同的 Stage,对于窄依赖,partition 的转换处理在 Stage 中完成计算。对于宽依赖,由于有 Shuffle 的存在,只能在 parent RDD 处理完成后,才能开始接下来的计算,因此宽依赖是划分 Stage(阶段) 的依据。
RDD 的任务切分
Application:一个能够打成 jar 包的 Spark 程序就是一个应用。里面应该有一个 SparkContext。
Job:一个应用中每一个 Action 操作所涉及到的所有转换叫一个 Job。
Stage:一个 Job 根据 RDD 之间的宽窄依赖关系划分为多个 Stage,Stage 之间是根据依赖关系来逐个执行的。
Task: 一个 Stage 运行的时候,RDD 的每一个分区都会被一个 Task 去处理,也可以认为是并行度
。
RDD 的运行规划图
写代码时我们都是从前往后写,但是划分 Stage 是从后往前划分。划分过程如下:
1、首先先把所有代码划分成为一个 Stage,然后该 Stage 入栈。
2、从最后的代码往前走,如果发现 RDD 之间的依赖关系是宽依赖,那么将宽依赖前面的所有代码划分为第二个 Stage,然后该 Stage 入栈。
3、根据 2 规则继续往前走,直到代码开头。
Spark 核心组件交互流程简图
2.8 RDD 相关概念关系
输入可能以多个文件的形式存储在 HDFS 上,每个 File 都包含了很多块,称为 Block。
当 Spark 读取这些文件作为输入时,会根据具体数据格式对应的 InputFormat 进行解析,一般是将若干个 Block 合并成一个输入分片,称为 InputSplit,注意 InputSplit 不能跨越文件。
随后将为这些输入分片生成具体的 Task。InputSplit 与 Task 是一一对应的关系。随后这些具体的 Task 每个都会被分配到集群上的某个节点的某个 Executor 去执行。
1) 每个节点可以起一个或多个Executor。
2) 每个 Executor 由若干 core 组成,每个 Executor 的每个 core 一次只能执行一个 Task。
3) 每个 Task 执行的结果就是生成了目标 RDD 的一个 partiton。
注意:这里的 core 是虚拟的 core 而不是机器的物理 CPU 核,可以理解为就是 Executor 的一个工作线程。
而 Task 被执行的并发度 = Executor 数目 * 每个 Executor 核数。
至于 partition 的数目:
1) 对于数据读入阶段,例如 sc.textFile,输入文件被划分为多少 InputSplit 就会需要多少初始 Task。
2) 在 Map 阶段 partition 数目保持不变。
3) 在 Reduce 阶段,RDD 的聚合会触发 shuffle 操作,聚合后的 RDD 的 partition 数目跟具体操作有关,例如 repartition 操作会聚合成指定分区数,还有一些算子是可配置的。RDD 在计算的时候,每个分区都会起一个 task,所以 RDD 的分区数目决定了总的的 Task 数目。
申请的计算节点(Executor)数目和每个计算节点核数,决定了你同一时刻可以并行执行的 Task。
比如你的 RDD 有 100 个分区,那么计算的时候就会生成 100 个 Task,你的资源配置为 10 个计算节点,每个两 2 个核,同一时刻可以并行的 Task 数目为 20,计算这个 RDD 就需要 5 个轮次。
如果计算资源不变,你有 101 个 Task 的话,就需要 6 个轮次,在最后一轮中,只有一个 Task 在执行,其余核都在空转。
如果资源不变,你的 RDD 只有 2 个分区,那么同一时刻只有 2 个 Task 运行,其余 18 个核空转,造成资源浪费。这就是在 spark 调优中,增大 RDD 分区数目,增大任务并行度的做法。
2.9 Spark Core 实例练习
格式: timestamp province city userid adid
某个时间点 某个省份 某个城市 某个用户 某个广告
用户 ID 范围: 0 - 99
省份、城市 ID 相同: 0 - 9
adid范围:0 - 19
需求:统计每一个省份点击 TOP3 的广告 ID
需求:统计每一个省份每一个小时点击 TOP3 广告的 ID
agent.log
1516609143867 6 7 64 16
1516609143869 9 4 75 18
1516609143869 1 7 87 12
1516609143869 2 8 92 9
1516609143869 6 7 84 24
1516609143869 1 8 95 5
1516609143869 8 1 90 29
1516609143869 3 3 36 16
1516609143869 3 3 54 22
1516609143869 7 6 33 5
......
......
示例代码:
Practice.scala
import java.text.SimpleDateFormat
import java.util.Date
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* timestamp province city userid adid
* 某个时间点 某个省份 某个城市 某个用户 某个广告
*
* 用户 ID 范围: 0 - 99
* 省份、城市 ID 相同: 0 - 9
* adid范围:0 - 19
*
* 需求1:统计每一个省份点击 TOP3 的广告 ID
* 需求2:统计每一个省份每一个小时点击 TOP3 广告的 ID
*/
case class Click(timestamp: Long, province: Int, city: Int, userid: Int, adid: Int)
object Practice {
def main(args: Array[String]): Unit = {
// 创建 SparkConf() 并设置 App 名称
val sparkConf = new SparkConf().setMaster("local[*]") setAppName ("practice")
// 创建 SparkContext,该对象是提交 Spark App 的入口
val sc = new SparkContext(sparkConf)
// 载入数据
// 1516609143867 6 7 64 16
val click: RDD[String] = sc.textFile("D:\learn\JetBrains\workspace_idea\spark\sparkcore_ad\src\main\resources\agent.log")
// 将字符串类型的数据转换为 Click 对象
val clickRDD: RDD[Click] = click.map { item =>
val param = item.split(" ")
Click(param(0).toLong, param(1).toInt, param(2).toInt, param(3).toInt, param(4).toInt)
}
clickRDD.cache()
// 需求1:统计每一个省份点击 TOP3 的广告 ID
// 统计每一个省份点击 TOP3 的广告 ID,先创建一个最小粒度
// pro-adid
val proAndAdid2CountRDD: RDD[(String, Int)] = clickRDD.map(click => (click.province + "_" + click.adid, 1)) // (6_16, 1) (6_12, 1) (6_19, 1) (6_16, 1) (6_12, 1) ...
val proAndAdid2CountsRDD: RDD[(String, Int)] = proAndAdid2CountRDD.reduceByKey(_ + _) // (6_16, 15) (6_12, 22) (6_19, 20) ....
// 再逐渐放大粒度
val pro2AdidCountsRDD = proAndAdid2CountsRDD.map { item =>
val param = item._1.split("_")
(param(0).toInt, (param(1).toInt, item._2)) // (6, (16,15))
}
val pro2AdidsRDD: RDD[(Int, Iterable[(Int, Int)])] = pro2AdidCountsRDD.groupByKey() // (6, (16,15),(12,22),(19,20),(2,13),...)
val result = pro2AdidsRDD.mapValues { item =>
item.toList.sortWith(_._2 > _._2).take(3) // (6, (12,22),(19,20),(16,15))
}
println("---")
println(result.collect())
val simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH")
// 需求2:统计每一个省份每一个小时点击 TOP3 广告的 ID
// pro-hour-adid
val proAndTimeAndAdid2CountRDD: RDD[(String, Int)] = clickRDD.map { click =>
(click.province + "_" + simpleDateFormat.format(new Date(click.timestamp)) + "_" + click.adid, 1) // (6_2019-4-25 10_16, 1)
}
val proAndTimeAndAdid2CountsRDD: RDD[(String, Int)] = proAndTimeAndAdid2CountRDD.reduceByKey(_ + _) // (6_2019-4-25 10_16, 15)
// 再逐渐放大粒度
val result2 = proAndTimeAndAdid2CountsRDD.map { item =>
val param = item._1.split("_")
(param(0) + "_" + param(1), (param(2).toInt, item._2)) // ((6_2019-4-25 10), (16, 15))
}.groupByKey().mapValues { item =>
item.toList.sortWith(_._2 > _._2).take(3) // ((6_2019-4-25 10), (16, 15))
}.map{ item =>
val param = item._1.split("_")
(param(0).toInt, (param(1), item._2)) // (6, (2019-4-25 10, (16, 15))
}.groupByKey()
println(result2.collect())
println("---")
sc.stop()
}
}
调试结果截图:
需求1:统计每一个省份点击 TOP3 的广告 ID
需求2:统计每一个省份每一个小时点击 TOP3 广告的 ID
第3章 键值对 RDD
键值对 RDD 是 Spark 中许多操作所需要的常见数据类型。本章做特别讲解。除了在基础 RDD 类中定义的操作之外,Spark 为包含键值对类型的 RDD 提供了一些专有的操作,在 PairRDDFunctions 专门进行了定义。这些 RDD 被称为 pair RDD。
有很多种方式创建 pair RDD,在输入输出章节会讲解。一般如果从一个普通的 RDD 转 为 pair RDD 时,可以调用 map() 函数来实现,传递的函数需要返回键值对。
3.1 键值对 RDD 的转化操作
3.1.1 转化操作列表
上一章进行了练习,这一章会重点讲解。
针对一个 pair RDD的转化操作
针对两个 pair RDD的转化操作
3.1.2 聚合操作
当数据集以键值对形式组织的时候,聚合具有相同键的元素进行一些统计是很常见的操作。之前讲解过基础 RDD 上的 fold()、combine()、reduce() 等行动操作,pair RDD 上则 有相应的针对键的转化操作。Spark 有一组类似的操作,可以组合具有相同键的值。这些操作返回 RDD,因此它们是转化操作而不是行动操作。
reduceByKey() 与 reduce() 相当类似,它们都接收一个函数,并使用该函数对值进行合并。 reduceByKey() 会为数据集中的每个键进行并行的归约操作
,每个归约操作会将键相同的值合并起来。因为数据集中可能有大量的键,所以 reduceByKey() 没有实现为向用户程序返回一个值的行动操作。实际上,它会返回一个由各键和对应键归约出来的结果值组成的新的 RDD。
foldByKey() 则与 fold() 相当类似,它们都使用一个与 RDD 和合并函数中的数据类型相同的零值作为初始值。与 fold() 一样,foldByKey() 操作所使用的合并函数对零值与另一个元素进行合并,结果仍为该元素。
求均值操作:版本一
input.mapValues(x => (x, 1)).reduceByKey((x, y) => (x._1 + y._1, x._2 + y._2)).map{ case (key, value) => (key, value._1 / value._2.toFloat) }
图解如下:
combineByKey() 是最为常用的基于键进行聚合的函数。大多数基于键聚合的函数都是用它实现的。和 aggregate() 一样,combineByKey() 可以让用户返回与输入数据的类型不同的返回值。
要理解 combineByKey(),要先理解它在处理数据时是如何处理每个元素的。由于 combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。
如果这是一个新的元素,combineByKey() 会使用一个叫作 createCombiner() 的函数来创建那个键对应的累加器的初始值。需要注意的是,这一过程会在每个分区中第一次出现各个键时发生,而不是在整个 RDD 中第一次出现一个键时发生。
如果这是一个在处理当前分区之前已经遇到的键,它会使用 mergeValue() 方法将该键的累加器对应的当前值与这个新的值进行合并。
由于每个分区都是独立处理的,因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器,就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并。
求均值:版本二
val result = input.combineByKey(
(v) => (v, 1),
(acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1),
(acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
).map{ case (key, value) => (key, value._1 / value._2.toFloat) }
result.collectAsMap().map(println(_))
3.1.3 数据分组
如果数据已经以预期的方式提取了键,groupByKey() 就会使用 RDD 中的键来对数据进行分组。对于一个由类型 K 的键和类型 V 的值组成的 RDD,所得到的结果 RDD 类型会是 [K, Iterable[V]]。
groupBy() 可以用于未成对的数据上,也可以根据除键相同以外的条件进行分组。它可以接收一个函数,对源 RDD 中的每个元素使用该函数,将返回结果作为键再进行分组。
多个 RDD 分组,可以使用 cogroup 函数,cogroup() 的函数对多个共享同一个键的 RDD 进行分组。对两个键的类型均为 K 而值的类型分别为 V 和 W 的 RDD 进行 cogroup() 时,得到的结果 RDD 类型为 [(K, (Iterable[V], Iterable[W]))]。如果其中的 一个 RDD 对于另一个 RDD 中存在的某个键没有对应的记录,那么对应的迭代器则为空。 cogroup() 提供了为多个 RDD 进行数据分组的方法。
3.1.4 连接
连接主要用于多个 pair RDD 的操作,连接方式多种多样:右外连接、左外连接、交叉连接以及内连接。
普通的 join 操作符表示内连接 2。只有在两个 pair RDD 中都存在的键才叫输出。当一个输入对应的某个键有多个值时,生成的 pair RDD 会包括来自两个输入 RDD 的每一组相对应的记录。
leftOuterJoin() 产生的 pair RDD 中,源 RDD 的每一个键都有对应的记录。每个键相应的值是由一个源 RDD 中的值与一个包含第二个 RDD 的值的 Option(在 Java 中为 Optional)对象组成的二元组。
rightOuterJoin() 几乎与 leftOuterJoin() 完全一样,只不过预期结果中的键必须出现在第二个 RDD 中,而二元组中的可缺失的部分则来自于源 RDD 而非第二个 RDD。
示例图解如下:
3.1.5 数据排序
sortByKey() 函数接收一个叫作 ascending 的参数,表示我们是否想要让结果按升序排序(默认值为 true,默认升序)。
3.2 键值对 RDD 的行动操作
3.3 键值对 RDD 的数据分区
Spark 目前支持 Hash 分区和 Range 分区,用户也可以自定义分区,Hash 分区为当前的默认分区,Spark 中分区器直接决定了 RDD 中分区的个数、RDD 中每条数据经过 Shuffle 过程属于哪个分区和 Reduce 的个数。
注意
:
(1) 只有 Key-Value 类型的 RDD 才有分区的,非 Key-Value 类型的 RDD 分区的值是 None。
(2) 每个 RDD 的分区 ID 范围:0~numPartitions-1,决定这个值是属于那个分区的。
3.3.1 获取 RDD 的分区方式
可以通过使用 RDD 的 partitioner 属性来获取 RDD 的分区方式。它会返回一个 scala.Option 对象, 通过 get 方法获取其中的值。
3.3.2 Hash 分区方式
HashPartitioner 分区的原理:对于给定的 key,计算其 hashCode,并除以分区的个数取余,如果余数小于 0,则用 余数+分区的个数,最后返回的值就是这个 key 所属的分区 ID。
示例代码:
scala> nopar.partitioner
res20: Option[org.apache.spark.Partitioner] = None
scala> val nopar = sc.parallelize(List((1,3),(1,2),(2,4),(2,3),(3,6),(3,8)),8)
nopar: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[10] at parallelize at <console>:24
scala>nopar.mapPartitionsWithIndex((index,iter)=>{ Iterator(index.toString+" : "+iter.mkString("|")) }).collect
res0: Array[String] = Array("0 : ", 1 : (1,3), 2 : (1,2), 3 : (2,4), "4 : ", 5 : (2,3), 6 : (3,6), 7 : (3,8))
scala> val hashpar = nopar.partitionBy(new org.apache.spark.HashPartitioner(7))
hashpar: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[12] at partitionBy at <console>:26
scala> hashpar.count
res18: Long = 6
scala> hashpar.partitioner
res21: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@7)
scala> hashpar.mapPartitions(iter => Iterator(iter.length)).collect()
res19: Array[Int] = Array(0, 3, 1, 2, 0, 0, 0)
3.3.3 Range 分区方式
HashPartitioner 分区弊端:可能导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有 RDD 的全部数据。
RangePartitioner 分区优势:尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大。
但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内
。
RangePartitioner 作用:将一定范围内的数映射到某一个分区内,在实现中,分界的算法尤为重要。用到了水塘抽样算法
。
3.3.4 自定义分区方式(重点)
要实现自定义的分区器,你需要继承 org.apache.spark.Partitioner 类并实现下面3个方法:
numPartitions: Int 返回创建出来的分区数。
getPartition(key: Any): Int 返回给定键的分区编号( 0 到 numPartitions-1 )。
equals() Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同。
假设我们需要将相同后缀的数据写入相同的文件,我们通过将相同后缀的数据分区到相同的分区并保存输出来实现。
示例代码1:
在 IDEA 中写代码
package com.atguigu.spark
import org.apache.spark.{Partitioner, SparkConf, SparkContext}
/**
* 自定义分区案例
*/
class CustomerPartitioner(numParts: Int) extends Partitioner {
// 覆盖分区数
override def numPartitions: Int = numParts
// 覆盖分区号获取函数
override def getPartition(key: Any): Int = {
val ckey: String = key.toString
ckey.substring(ckey.length - 1).toInt % numParts
}
}
object CustomerPartitionerDemo {
def main(args: Array[String]) {
val conf = new SparkConf().setMaster("local[*]")setAppName("partitioner")
val sc = new SparkContext(conf)
val data = sc.parallelize(List("aa.2", "bb.2", "cc.3", "dd.3", "ee.5"))
data.map((_, 1)).partitionBy(new CustomerPartitioner(5)).keys.saveAsTextFile("hdfs://hadoop102:9000/partitioner")
println("---")
sc.stop()
}
}
示例代码2:
在 Spark Shell 中写代码
scala> val data=sc.parallelize(List("aa.2","bb.2","cc.3","dd.3","ee.5").zipWithIndex,2) 函数 zipWithIndex 作用是快速构建 KV 结构的数据
data: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[7] at parallelize at <console>:27
scala> data.collect
res4: Array[(String, Int)] = Array((aa.2,0), (bb.2,1), (cc.3,2), (dd.3,3), (ee.5,4))
scala> data.mapPartitionsWithIndex((index,iter) => Iterator(index.toString +" : "+ iter.mkString("|"))).collect
res5: Array[String] = Array(0 : (aa.2,0)|(bb.2,1), 1 : (cc.3,2)|(dd.3,3)|(ee.5,4))
scala> :paste
// Entering paste mode (ctrl-D to finish)
class CustomerPartitioner(numParts: Int) extends org.apache.spark.Partitioner{
// 覆盖分区数
override def numPartitions: Int = numParts
// 覆盖分区号获取函数
override def getPartition(key: Any): Int = {
val ckey: String = key.toString
ckey.substring(ckey.length - 1).toInt % numParts
}
}
// Exiting paste mode, now interpreting.
defined class CustomerPartitioner
scala> data.partitionBy(new CustomerPartitioner(4)) 分4个区
res7: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[9] at partitionBy at <console>:31
scala> res7.mapPartitionsWithIndex((index,iter) => Iterator(index.toString +" : "+ iter.mkString("|"))).collect
res8: Array[String] = Array("0 : ", 1 : (ee.5,4), 2 : (aa.2,0)|(bb.2,1), 3 : (cc.3,2)|(dd.3,3))
使用自定义的 Partitioner 是很容易的:只要把它传给 partitionBy() 方法即可。Spark 中有许多依赖于数据混洗的方法,比如 join() 和 groupByKey(),它们也可以接收一个可选的 Partitioner 对象来控制输出数据的分区方式。
3.3.5 分区 Shuffle 优化
在分布式程序中,通信的代价是很大的,因此控制数据分布以获得最少的网络传输可以极大地提升整体性能。
Spark 中所有的键值对 RDD 都可以进行分区。系统会根据一个针对键的函数对元素进行分组。主要有哈希分区
和范围分区
,当然用户也可以自定义分区
函数。
通过分区可以有效提升程序性能。如下例子:
分析这样一个应用,它在内存中保存着一张很大的用户信息,也就是一个由 (UserID, UserInfo) 对组成的 RDD,其中 UserInfo 包含一个该用户所订阅的主题的列表。该应用会周期性地将这张表与一个小文件进行组合,这个小文件中存着过去五分钟内发生的事件,其实就是一个由 (UserID, LinkInfo) 对组成的表,存放着过去五分钟内某网站各用户的访问情况。例如,我们可能需要对用户访问其未订阅主题的页面的情况进行统计。
解决方案一:
图解如下:
这段代码可以正确运行,但是不够高效。这是因为在每次调用 processNewLogs() 时都会用到 join() 操作,而我们对数据集是如何分区的却一无所知。默认情况下,连接操作会将两个数据集中的所有键的哈希值都求出来,将该哈希值相同的记录通过网络传到同一台机器上,然后在那台机器上对所有键相同的记录进行连接操作。因为 userData 表比每五分钟出现的访问日志表 events 要大得多,所以要浪费时间做很多额外工作,在每次调用时都对 userData 表进行哈希值计算和跨节点数据混洗,降低了程序的执行效率。
优化方法:
图解如下:
3.3.6 基于分区进行操作
基于分区对数据进行操作可以让我们避免为每个数据元素进行重复的配置工作。诸如打开数据库连接或创建随机数生成器等操作,都是我们应当尽量避免为每个元素都配置一次的工作。Spark 提供基于分区的 mapPartition 和 foreachPartition,让你的部分代码只对 RDD 的每个分区运行一次,这样可以帮助降低这些操作的代价。
3.3.7 从分区中获益的操作
能够从数据分区中获得性能提升的操作有 cogroup()、 groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、combineByKey() 以及 lookup() 等。
第4章 数据读取与保存主要方式
4.1 文本文件输入输出
当我们将一个文本文件读取为 RDD 时,
输入的每一行都会成为 RDD 的一个元素
。也可以将多个完整的文本文件一次性读取为一个 pair RDD,其中键是文件名,值是文件内容。
val input = sc.textFile("./README.md")
如果传递目录,则将目录下的所有文件读取作为 RDD。
文件路径支持通配符。
通过 wholeTextFiles() 对于大量的小文件读取效率比较高,大文件效果没有那么高。
Spark 通过 saveAsTextFile() 进行文本文件的输出,该方法接收一个路径,并将 RDD 中的内容都输入到路径对应的文件中。Spark 将传入的路径作为目录对待,会在那个目录下输出多个文件。这样,Spark 就可以从多个节点上并行输出了。
result.saveAsTextFile(outputFile)
示例代码:
scala> val readme = sc.textFile("./README.md")
readme: org.apache.spark.rdd.RDD[String] = ./README.md MapPartitionsRDD[3] at textFile at <console>:24
scala> readme.collect
res1: Array[String] = Array(# 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, rich set of higher-level tools including Spark SQL for SQL and DataFrames,, MLlib for machine learning, GraphX for graph processing,, and Spark Streaming for stream processing., "", <http://spark.apache.org/>, "", "", ## Online Documentation, "", You can find the latest Spark documentation, including a programming, guide, on the [project web page](http://spark.apache.org/documentation.html)., This README file only contains basic setup instructions., "", ## Building Spark, "", Spark is built using [Apache Maven](...
scala> val readme = sc.textFile("hdfs://hadoop102:9000/README.md")
readme: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/README.md MapPartitionsRDD[7] at textFile at <console>:24
scala> readme.collect
res7: Array[String] = Array(# 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, rich set of higher-level tools including Spark SQL for SQL and DataFrames,, MLlib for machine learning, GraphX for graph processing,, and Spark Streaming for stream processing., "", <http://spark.apache.org/>, "", "", ## Online Documentation, "", You can find the latest Spark documentation, including a programming, guide, on the [project web page](http://spark.apache.org/documentation.html)., This README file only contains basic setup instructions., "", ## Building Spark, "", Spark is built using [Apache Maven](...
scala> readme.saveAsTextFile("hdfs://hadoop102:9000/test")
scala> readme.saveAsTextFile("./saveTest") 注意:Spark Shell 如果开启的集群模式,则文件分散的存储在其他节点上;如果开启的是 Client 模式,则文件存储在本地当前目录
4.2 JSON 文件输入输出
如果 JSON 文件中每一行就是一个 JSON 记录,那么可以通过
将 JSON 文件当做文本文件来读取
,然后利用相关的 JSON 库对每一条数据进行 JSON 解析。
示例代码:
scala> import org.json4s._ 需要导入一些 jar 包支持,或者在打开 spark shell 的时候在 --jars 中导入
import org.json4s._
scala> import org.json4s.jackson.JsonMethods._ 需要导入一些 jar 包支持,或者在打开 spark shell 的时候在 --jars 中导入
import org.json4s.jackson.JsonMethods._
scala> import org.json4s.jackson.Serialization 需要导入一些 jar 包支持,或者在打开 spark shell 的时候在 --jars 中导入
import org.json4s.jackson.Serialization
scala> var result = sc.textFile("/opt/module/spark-2.1.1-bin-hadoop2.7/examples/src/main/resources/people.json")
result: org.apache.spark.rdd.RDD[String] = /opt/module/spark-2.1.1-bin-hadoop2.7/examples/src/main/resources/people.json MapPartitionsRDD[3] at textFile at <console>:40
scala> result.collect
res4: Array[String] = Array({"name":"Michael", "age":30}, {"name":"Andy", "age":30}, {"name":"Justin", "age":19})
scala> class Person(var name: String, var age: Int)
defined class Person
scala> result.collect().foreach(x => {var c = parse(x).extract[Person];println(c.name + "," + c.age)})
Michael,30
Andy,30
Justin,19
如果 JSON 数据是跨行的,那么只能读入整个文件,然后对整个文件进行解析。
JSON 数据的输出主要是通过在输出之前将由结构化数据组成的 RDD 转为字符串 RDD,然后使用 Spark 的文本文件 API 写出去。说白了还是以文本文件的形式存储,只是文本的格式已经在程序中转换为 JSON。
4.3 CSV 文件输入输出
读取 CSV/TSV 数据和读取 JSON 数据相似,都需要先把文件当作普通文本文件来读取数据,然后通过将每一行进行解析实现对 CSV 的读取。
CSV/TSV 数据的输出也是需要将结构化 RDD 通过相关的库转换成字符串 RDD,然后使用 Spark 的文本文件 API 写出去。
4.4 SequenceFile 文件输入输出
SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件(Flat File)。
Spark 有专门用来读取 SequenceFile 文件的接口。在 SparkContext 中,可以调用sequenceFile[keyClass, valueClass](path)
。
类型对应表如下:
图解如下:
示例代码:
scala> val data = sc.parallelize(List((2,"aa"),(3,"bb"),(4,"cc"),(5,"dd"),(6,"ee")))
data: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[16] at parallelize at <console>:24
scala> data.saveAsSequenceFile("hdfs://hadoop102:9000/sequdata")
scala> val sdata = sc.sequenceFile[Int,String]("hdfs://hadoop102:9000/sequdata/p*")
sdata: org.apache.spark.rdd.RDD[(Int, String)] = MapPartitionsRDD[19] at sequenceFile at <console>:24
scala> sdata.collect()
res14: Array[(Int, String)] = Array((2,aa), (3,bb), (4,cc), (5,dd), (6,ee))
可以直接调用 saveAsSequenceFile(path) 保存你的 pair RDD,它会帮你写出数据。需要键和值能够自动转为 Writable 类型。
4.5 对象文件输入输出
对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。可以通过
objectFile[k,v](path)
函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用 saveAsObjectFile() 实现对对象文件的输出。因为是序列化,所以要指定类型。
示例代码:
scala> val data = sc.parallelize(List((2,"aa"),(3,"bb"),(4,"cc"),(5,"dd"),(6,"ee")))
data: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[20] at parallelize at <console>:24
scala> data.saveAsObjectFile("hdfs://hadoop102:9000/objdata")
scala> import org.apache.spark.rdd.RDD
import org.apache.spark.rdd.RDD
scala> val objrdd: org.apache.spark.rdd.RDD[(Int,String)] = sc.objectFile[(Int,String)]("hdfs://hadoop102:9000/objdata/p*")
objrdd: org.apache.spark.rdd.RDD[(Int, String)] = MapPartitionsRDD[28] at objectFile at <console>:25
scala> objrdd.collect()
res20: Array[(Int, String)] = Array((2,aa), (3,bb), (4,cc), (5,dd), (6,ee))
4.6 Hadoop 输入输出格式
Spark 的整个生态系统与 Hadoop 是完全兼容的,所以对于 Hadoop 所支持的文件类型或者数据库类型,Spark 也同样支持。另外,由于 Hadoop 的 API 有新旧两个版本,所以 Spark 为了能够兼容 Hadoop 所有的版本,也提供了两套创建操作接口。对于外部存储创建操作而言,hadoopRDD 和 newHadoopRDD 是最为抽象的两个函数接口,主要包含以下四个参数:
1) 输入格式(InputFormat): 指定数据输入的类型,如 TextInputFormat 等,新旧两个版本所引用的版本分别是 org.apache.hadoop.mapred.InputFormat 和 org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
2) 键类型: 指定 [K,V] 键值对中 K 的类型
3) 值类型: 指定 [K,V] 键值对中 V 的类型
4) 分区值: 指定由外部存储生成的 RDD 的 partition 数量的最小值,如果没有指定,系统会使用默认值 defaultMinSplits
其他创建操作的 API 接口都是为了方便最终的 Spark 程序开发者而设置的,是这两个接口的高效实现版本。例如,对于 textFile 而言,只有 path 这个指定文件路径的参数,其他参数在系统内部指定了默认值。
兼容旧版本 HadoopAPI 的创建操作
兼容新版本 HadoopAPI 的创建操作
注意:
1. 在 Hadoop 中以压缩形式存储的数据,不需要指定解压方式就能够进行读取,因为 Hadoop 本身有一个解压器会根据压缩文件的后缀推断解压算法进行解压。
2. 如果用 Spark 从 Hadoop 中读取某种类型的数据不知道怎么读取的时候,上网查找一个使用 map-reduce 的时候是怎么读取这种这种数据的,然后再将对应的读取方式改写成上面的 hadoopRDD 和 newAPIHadoopRDD 两个类就行了。
示例代码:
scala> val data = sc.parallelize(Array((30,"hadoop"), (71,"hive"), (11,"cat")))
data: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[47] at parallelize at <console>:35
scala> import org.apache.hadoop.io._ 需要导入一些 jar 包支持,或者在打开 spark shell 的时候在 --jars 中导入
scala> import org.apache.hadoop.mapreduce.lib.output._ 需要导入一些 jar 包支持,或者在打开 spark shell 的时候在 --jars 中导入
scala> data.saveAsNewAPIHadoopFile("hdfs://hadoop102:9000/output/", classOf[LongWritable] ,classOf[Text] ,classOf[org.apache.hadoop.mapreduce.lib.output.TextOutputFormat[LongWritable, Text]])
对于 RDD 最后的归宿除了返回为集合和标量,也可以将 RDD 存储到外部文件系统或者数据库中,Spark 系统与 Hadoop 是完全兼容的,所以 MapReduce 所支持的读写文件或者数据库类型 Spark 也同样支持。另外,由于 Hadoop 的 API 有新旧两个版本,所以 Spark 为了能够兼容 Hadoop 所有的版本了,也提供了两套读取 Hadoop 文件 API。
将 RDD 保存到 HDFS 中在通常情况下需要关注或者设置五个参数,即文件保存的路径、Key值的class类型、Value值的class类型、RDD的输出格式(OutputFormat,如 TextOutputFormat/SequenceFileOutputFormat)、以及最后一个相关的参数 codec (这个参数表示压缩存储的压缩形式,如 DefaultCodec、Gzip、Codec 等)
兼容旧版 API
源码:
saveAsObjectFile(path: String): Unit
saveAsTextFile(path: String, codec: Class[_ <: CompressionCodec]): Unit
saveAsTextFile(path: String): Unit
saveAsHadoopFile[F <: OutputFormat[K, V]](path: String)(implicit fm: ClassTag[F]): Unit
saveAsHadoopFile[F <: OutputFormat[K, V]](path: String, codec: Class[_ <: CompressionCodec])(implicit fm: ClassTag[F]): Unit
saveAsHadoopFile(path: String, keyClass: Class[_], valueClass: Class[_], outputFormatClass: Class[_ <: OutputFormat[_, _]], codec: Class[_ <: CompressionCodec]): Unit
saveAsHadoopDataset(conf: JobConf): Unit
这里列出的 API,前面 6 个都是 saveAsHadoopDataset 的简易实现版本,仅仅支持将 RDD 存储到 HDFS 中,而 saveAsHadoopDataset 的参数类型是 JobConf,所以其不仅能够将 RDD 存储到 HDFS 中,也可以将 RDD 存储到其他数据库中,如 HBase、MangoDB、Cassandra 等。
兼容新版 API
源码:
saveAsNewAPIHadoopFile(path: String, keyClass: Class[_], valueClass: Class[_], outputFormatClass: Class[_ <: OutputFormat[_, _]], conf: Configuration = self.context.hadoopConfiguration): Unit
saveAsNewAPIHadoopFile[F <: OutputFormat[K, V]](path: String)(implicit fm: ClassTag[F]): Unit
saveAsNewAPIHadoopDataset(conf: Configuration): Unit
同样的,前 2 个 API 是 saveAsNewAPIHadoopDataset 的简易实现,只能将 RDD 存到 HDFS 中,而 saveAsNewAPIHadoopDataset 比较灵活,新版的 API 没有 codec 的参数,所以要压缩存储文件到 HDFS 中需要使用 hadoopConfiguration 参数,设置对应 mapreduce.map.output.compress.codec 参数和 mapreduce.map.output.compress 参数。
注意:
1. 如果不知道怎么将 RDD 存储到 Hadoop 生态的系统中,主要上网搜索一下对应的 map-reduce 是怎么将数据存储进去的,然后改写成对应的 saveAsHadoopDataset 或 saveAsNewAPIHadoopDataset 就可以了。
示例代码:
scala> import org.apache.hadoop.io._ 需要导入一些 jar 包支持,或者在打开 spark shell 的时候在 --jars 中导入
scala> import org.apache.hadoop.mapreduce.lib.input._ 需要导入一些 jar 包支持,或者在打开 spark shell 的时候在 --jars 中导入
scala> val read = sc.newAPIHadoopFile[LongWritable, Text, org.apache.hadoop.mapreduce.lib.input.TextInputFormat]("hdfs://hadoop102:9000/output/part*", classOf[org.apache.hadoop.mapreduce.lib.input.TextInputFormat], classOf[LongWritable], classOf[Text])
read: org.apache.spark.rdd.RDD[(org.apache.hadoop.io.LongWritable, org.apache.hadoop.io.Text)] = hdfs://hadoop102:9000/output/part* NewHadoopRDD[48] at newAPIHadoopFile at <console>:35
scala> read.map{case (k, v) => v.toString}.collect
res44: Array[String] = Array(30 hadoop, 71 hive, 11 cat)
4.7 文件系统的输入输出
Spark 支持读写很多种文件系统,像本地文件系统、Amazon S3、HDFS 等。
4.8 数据库的输入输出
关系型数据库连接:支持通过 Java JDBC 访问关系型数据库。需要通过 JdbcRDD 进行,示例如下:
注意
:需要先将 mysql-connector-java-5.1.27-bin.jar 拷贝至 spark 的 /opt/module/spark-2.1.1-bin-hadoop2.7/jars 目录下,然后分发至其他机器节点,然后再启动 spark-shell 交互窗口。
Mysql 读取:
示例代码:
def main (args: Array[String] ) {
val sparkConf = new SparkConf ().setMaster ("local[2]").setAppName ("JdbcApp")
val sc = new SparkContext (sparkConf)
val rdd = new org.apache.spark.rdd.JdbcRDD(
sc,
() => {
Class.forName("com.mysql.jdbc.Driver").newInstance()
java.sql.DriverManager.getConnection ("jdbc:mysql://hadoop102:3306/rdd", "root", "123456")
},
"select * from rddtable where id >= ? and id <= ?;",
1,
10,
1,
r => (r.getInt(1), r.getString(2)))
println(rdd.count ())
rdd.foreach(println (_))
sc.stop ()
}
Mysql 写入:
示例代码:
def main(args: Array[String]) {
val sparkConf = new SparkConf().setMaster("local[2]").setAppName("JdbcApp")
val sc = new SparkContext(sparkConf)
val data = sc.parallelize(List("Female", "Male", "Female"))
data.foreachPartition(insertData)
}
def insertData(iterator: Iterator[String]): Unit = {
Class.forName ("com.mysql.jdbc.Driver").newInstance()
val conn = java.sql.DriverManager.getConnection("jdbc:mysql://hadoop102:3306/rdd", "root", "123456")
iterator.foreach(data => {
val ps = conn.prepareStatement("insert into rddtable(name) values (?)")
ps.setString(1, data)
ps.executeUpdate()
})
}
spark-shell 交互界面操作代码:
[atguigu@hadoop102 spark-2.1.1-bin-hadoop2.7]$ bin/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).
19/04/26 11:10:24 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
19/04/26 11:10:29 WARN ObjectStore: Failed to get database global_temp, returning NoSuchObjectException
Spark context Web UI available at http://192.168.25.102:4040
Spark context available as 'sc' (master = spark://hadoop102:7077, app id = app-20190426111025-0004).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 2.1.1
/_/
Using Scala version 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_144)
Type in expressions to have them evaluated.
Type :help for more information.
scala> import com.mysql
import com.mysql
scala> import com.mysql.jdbc.Driver
import com.mysql.jdbc.Driver
# 从 Mysql 的数据库表中读取数据
scala> val rdd = new org.apache.spark.rdd.JdbcRDD(sc,() => {Class.forName("com.mysql.jdbc.Driver").newInstance();java.sql.DriverManager.getConnection ("jdbc:mysql://hadoop102:3306/rdd", "root", "123456")},"select * from rddtable where id >= ? and id <= ?;",1,10,1,r => (r.getInt(1), r.getString(2)))
rdd: org.apache.spark.rdd.JdbcRDD[(Int, String)] = JdbcRDD[0] at JdbcRDD at <console>:26
scala> rdd.collect
res0: Array[(Int, String)] = Array((1,abc), (2,dddd), (3,ddds))
# 向 Mysql 的数据库表中写入数据
scala> :paste
// Entering paste mode (ctrl-D to finish)
def insertData(iterator: Iterator[String]): Unit = {
Class.forName ("com.mysql.jdbc.Driver").newInstance()
val conn = java.sql.DriverManager.getConnection("jdbc:mysql://hadoop102:3306/rdd", "root", "123456")
iterator.foreach(data => {
val ps = conn.prepareStatement("insert into rddtable(name) values (?)")
ps.setString(1, data)
ps.executeUpdate()
})
}
// Exiting paste mode, now interpreting.
insertData: (iterator: Iterator[String])Unit
scala> val data = sc.parallelize(List("Female", "Male", "Female"))
data: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[1] at parallelize at <console>:26
scala> data.foreachPartition(insertData)
# 从 Mysql 的数据库表中再次读取数据
scala> val rdd = new org.apache.spark.rdd.JdbcRDD(sc,() => {Class.forName("com.mysql.jdbc.Driver").newInstance();java.sql.DriverManager.getConnection ("jdbc:mysql://hadoop102:3306/rdd", "root", "123456")},"select * from rddtable where id >= ? and id <= ?;",1,10,1,r => (r.getInt(1), r.getString(2)))
rdd: org.apache.spark.rdd.JdbcRDD[(Int, String)] = JdbcRDD[2] at JdbcRDD at <console>:26
scala> rdd.collect
res2: Array[(Int, String)] = Array((1,abc), (2,dddd), (3,ddds), (4,Female), (5,Female), (6,Male))
JdbcRDD 接收这样几个参数:
首先,要提供一个用于对数据库创建连接的函数。这个函数让每个节点在连接必要的配置后创建自己读取数据的连接。
接下来,要提供一个可以读取一定范围内数据的查询,以及查询参数中 lowerBound 和 upperBound 的值。这些参数可以让 Spark 在不同机器上查询不同范围的数据,这样就不会因尝试在一个节点上读取所有数据而遭遇性能瓶颈。
这个函数的最后一个参数是一个可以将输出结果从转为对操作数据有用的格式的函数。如果这个参数空缺,Spark 会自动将每行结果转为一个对象数组。
Cassandra 数据库和 ElasticSearch 集成:
HBase 数据库
由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现,Spark 可以通过 Hadoop 输入格式访问 HBase。这个输入格式会返回键值对数据,其中键的类型为 org.apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为 org.apache.hadoop.hbase.client.Result。
HBase 读取:
示例代码:
def main(args: Array[String]) {
val sparkConf = new SparkConf().setMaster("local[2]").setAppName("HBaseApp")
val sc = new SparkContext(sparkConf)
val conf = HBaseConfiguration.create()
// HBase 中的表名
conf.set(TableInputFormat.INPUT_TABLE, "fruit")
val hBaseRDD = sc.newAPIHadoopRDD(conf, classOf[TableInputFormat],
classOf[org.apache.hadoop.hbase.io.ImmutableBytesWritable],
classOf[org.apache.hadoop.hbase.client.Result])
val count = hBaseRDD.count()
println("hBaseRDD RDD Count:"+ count)
hBaseRDD.cache()
hBaseRDD.foreach {
case (_, result) =>
val key = Bytes.toString(result.getRow)
val name = Bytes.toString(result.getValue("info".getBytes, "name".getBytes))
val color = Bytes.toString(result.getValue("info".getBytes, "color".getBytes))
println("Row key:" + key + " Name:" + name + " Color:" + color)
}
sc.stop()
}
HBase 写入:
示例代码:
def main(args: Array[String]) {
val sparkConf = new SparkConf().setMaster("local[2]").setAppName("HBaseApp")
val sc = new SparkContext(sparkConf)
val conf = HBaseConfiguration.create()
val jobConf = new JobConf(conf)
jobConf.setOutputFormat(classOf[TableOutputFormat])
jobConf.set(TableOutputFormat.OUTPUT_TABLE, "fruit_spark")
val fruitTable = TableName.valueOf("fruit_spark")
val tableDescr = new HTableDescriptor(fruitTable)
tableDescr.addFamily(new HColumnDescriptor("info".getBytes))
val admin = new HBaseAdmin(conf)
if (admin.tableExists(fruitTable)) {
admin.disableTable(fruitTable)
admin.deleteTable(fruitTable)
}
admin.createTable(tableDescr)
def convert(triple: (Int, String, Int)) = {
val put = new Put(Bytes.toBytes(triple._1))
put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(triple._2))
put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("price"), Bytes.toBytes(triple._3))
(new ImmutableBytesWritable, put)
}
val initialRDD = sc.parallelize(List((1,"apple",11), (2,"banana",12), (3,"pear",13)))
val localData = initialRDD.map(convert)
localData.saveAsHadoopDataset(jobConf)
}
第5章 RDD 编程进阶
5.1 累加器
累加器用来对信息进行聚合,通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。 如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。
针对一个输入的日志文件,如果我们想计算文件中所有空行的数量,我们可以编写以下程序:
示例代码:
scala> val notice = sc.textFile("./NOTICE")
notice: org.apache.spark.rdd.RDD[String] = ./NOTICE MapPartitionsRDD[40] at textFile at <console>:32
scala> val blanklines = sc.accumulator(0) 声明一个累加器,并传入初始值 0
warning: there were two deprecation warnings; re-run with -deprecation for details
blanklines: org.apache.spark.Accumulator[Int] = 0
scala> val tmp = notice.flatMap(line => {
| if (line == "") {
| blanklines += 1 加加
| }
| line.split(" ")
| })
tmp: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[41] at flatMap at <console>:36
scala> tmp.count()
res31: Long = 3213
scala> blanklines.value 在 Driver 中访问累加器的值
res32: Int = 171
累加器的用法如下所示:
通过在驱动器中调用 SparkContext.accumulator(initialValue) 方法,创建出存有初始值的累加器。返回值为 org.apache.spark.Accumulator[T] 对象,其中 T 是初始值 initialValue 的类型。
Spark 闭包里的执行器代码可以使用累加器的 += 方法(在 Java 中是 add)增加累加器的值。
Driver 驱动器程序可以调用累加器的 value 属性(在 Java 中使用 value() 或 setValue() )来访问累加器的值。
注意
:工作节点上的任务不能访问累加器的值。从这些任务的角度来看,累加器是一个只写变量。
对于要在行动操作中使用的累加器,Spark 只会把每个任务对各累加器的修改应用一次。因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在 foreach() 这样的行动操作中使用。转换操作中累加器可能会发生不止一次更新,所以一般不推荐在转换操作中使用。
5.2 自定义累加器
自定义累加器类型的功能在 1.X 版本中就已经提供了,但是使用起来比较麻烦,在 2.0 版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2 来提供更加友好的自定义类型累加器的实现方式。实现自定义类型累加器需要继承 AccumulatorV2 并至少覆写下例中出现的方法,下面这个累加器可以用于在程序运行过程中收集一些文本类信息,最终以 Set[String] 的形式返回。
示例代码:
package com.atguigu.spark
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.JavaConversions._
/**
* 自定义累加器案例
*/
class LogAccumulator extends AccumulatorV2[String, java.util.Set[String]] {
// 定义一个累加器的内存结构,用于保存带有字母的字符串
private val _logArray: java.util.Set[String] = new java.util.HashSet[String]()
// 重写方法检测累加器内部数据结构是否为空
override def isZero: Boolean = {
// 检查 logArray 是否为空
_logArray.isEmpty
}
// 重置你的累加器数据结构
override def reset(): Unit = {
// clear 方法清空 _logArray 的所有内容
_logArray.clear()
}
// 提供转换或者行动操作中添加累加器值的方法
override def add(v: String): Unit = {
// 将带有字幕的字符串添加到 _logArray 内存结构中
_logArray.add(v)
}
// 多个分区的累加器的值进行合并的操作函数
override def merge(other: AccumulatorV2[String, java.util.Set[String]]): Unit = {
// 通过类型监测将o这个累加器的值加入到当前 _logArray 结构中
other match {
case o: LogAccumulator => _logArray.addAll(o.value)
}
}
override def value: java.util.Set[String] = {
java.util.Collections.unmodifiableSet(_logArray)
}
// 让 Spark 框架能够调用 copy 函数产生一个新的系统的类,即累加器实例
override def copy(): AccumulatorV2[String, java.util.Set[String]] = {
val newAcc = new LogAccumulator()
_logArray.synchronized {
newAcc._logArray.addAll(_logArray)
}
newAcc
}
}
// 过滤掉带字母的
object LogAccumulatorDemo {
def main(args: Array[String]) {
val conf = new SparkConf().setMaster("local[*]")setAppName("LogAccumulator")
val sc = new SparkContext(conf)
val accum = new LogAccumulator
sc.register(accum, "logAccum") // 先注册一个自定义的累加器
val sum = sc.parallelize(Array("1", "2a", "3", "4b", "5", "6", "7cd", "8", "9"), 2).filter(line => {
val pattern = """^-?(\d+)"""
val flag = line.matches(pattern)
if (!flag) {
accum.add(line)
}
flag
}).map(_.toInt).reduce(_ + _)
println("sum: " + sum)
for (v <- accum.value) print(v + " ")
println()
sc.stop()
}
}
输出结果如下:
sum: 32
7cd 4b 2a
5.3 广播变量
广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。
传统方式下,Spark 会自动把闭包中所有引用到的变量发送到工作节点上。虽然这很方便,但也很低效。原因有二:首先,默认的任务发射机制是专门为小任务进行优化的;其次,事实上你可能会在多个并行操作中使用同一个变量,但是 Spark 会为每个任务分别发送。
示例代码:
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(35)
scala> broadcastVar.value
res33: Array[Int] = Array(1, 2, 3)
使用广播变量的过程如下:
(1) 通过对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T] 对象。任何可序列化的类型都可以这么实现。
(2) 通过 value 属性访问该对象的值 (在 Java 中为 value() 方法)。
(3) 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。
第6章 Spark Core 实例练习
结合实际生产情况编写一个统计功能,通过分析 CDN 或者 Nginx 的日志文件,统计出访问的 PV、UV、IP 地址、访问来源等相关数据。
日志的格式为:
IP 命中率 响应时间 请求时间 请求方法 请求URL 请求协议 状态码 响应大小 referer 用户代理
ClientIP Hit/Miss ResponseTime [Time Zone] Method URL Protocol StatusCode TrafficSize Referer UserAgent
日志文件 cdh.txt 内容示例:
100.79.121.48 HIT 33 [15/Feb/2017:00:00:46 +0800] "GET http://cdn.v.abc.com.cn/videojs/video.js HTTP/1.1" 200 174055 "http://www.abc.com.cn/" "Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+Trident/4.0;)"
111.19.97.15 HIT 18 [15/Feb/2017:00:00:39 +0800] "GET http://cdn.v.abc.com.cn/videojs/video-js.css HTTP/1.1" 200 14727 "http://www.zzqbsm.com/" "Mozilla/5.0+(Linux;+Android+5.1;+vivo+X6Plus+D+Build/LMY47I)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Version/4.0+Chrome/35.0.1916.138+Mobile+Safari/537.36+T7/7.4+baiduboxapp/8.2.5+(Baidu;+P1+5.1)"
218.108.100.234 HIT 1 [15/Feb/2017:00:00:57 +0800] "GET http://cdn.v.abc.com.cn/videojs/video.js HTTP/1.1" 200 174050 "http://www.abc.com.cn/" "Mozilla/5.0+(Windows+NT+6.1;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/53.0.2785.116+Safari/537.36"
......
......
6.1 计算独立 IP 数
计算思路
1. 从每行日志中筛选出 IP 地址
2. 去除重复的 IP 得到独立 IP 数
计算过程
flatMap(x => IPPattern findFirstIn(x)) 通过正则取出每行日志中的 IP 地址
map(x => (x,1)) 将每行中的 IP 映射成 (IP,1),形成一个 pair RDD
reduceByKey((x,y) => x+y) 将相同的 IP 合并,得到 (IP,数量)
sortBy(_._2, false) 按 IP 大小排序
统计结果
(114.55.227.102,9348)
(220.191.255.197,2640)
(115.236.173.94,2476)
(183.129.221.102,2187)
(112.53.73.66,1794)
(115.236.173.95,1650)
(220.191.254.129,1278)
(218.88.25.200,751)
(183.129.221.104,569)
(115.236.173.93,529)
独立IP数:43649
6.2 统计每个视频独立 IP 数
有时我们不但需要知道全网访问的独立 IP 数,更想知道每个视频访问的独立 IP 数
计算思路
1. 筛选视频文件将每行日志拆分成 (文件名, IP地址) 形式
2. 按文件名分组,相当于数据库的 group by,这时 RDD 的结构为 (文件名, [IP1, IP1, IP2, …]),这时 IP 有重复
3. 将每个文件名中的 IP 地址去重,这时 RDD 的结果为 (文件名, [IP1, IP2, …]),这时 IP 没有重复
计算过程
filter(x => x.matches(“.([0-9]+).mp4.“)) 筛选日志中的视频请求
map(x => getFileNameAndIp(x)) 将每行日志格式化成 (文件名, IP) 这种格式
groupByKey() 按文件名分组,这时 RDD 结构为 (文件名, [IP1, IP1, IP2, …]),IP 有重复
map(x => (x.1, x._2.toList.distinct)) 去除 value 中重复的 IP 地址
sortBy(._2.size, false) 按 IP 数排序
计算结果
视频:141081.mp4 独立IP数:2393
视频:140995.mp4 独立IP数:2050
视频:141027.mp4 独立IP数:1784
视频:141090.mp4 独立IP数:1702
视频:141032.mp4 独立IP数:1528
视频:89973.mp4 独立IP数:1523
视频:141080.mp4 独立IP数:1425
视频:141035.mp4 独立IP数:1321
视频:141082.mp4 独立IP数:1272
视频:140938.mp4 独立IP数:816
6.3 统计一天中每个小时间的流量
有时我想知道网站每小时视频的观看流量,看看用户都喜欢在什么时间段过来看视频
计算思路
1. 将日志中的访问时间及请求大小两个数据提取出来形成 RDD (访问时间, 访问大小),这里要去除 404 之类的非法请求
2. 按访问时间分组形成 RDD (访问时间, [大小1, 大小2, …])
3. 将访问时间对应的大小相加形成 (访问时间, 总大小)
计算过程
filter(x => isMatch(httpSizePattern, x)).filter(x => isMatch(timePattern, x)) 过滤非法请求
map(x => getTimeAndSize(x)) 将日志格式化成 RDD (请求小时, 请求大小)
groupByKey() 按请求时间分组形成 RDD (请求小时, [大小1, 大小2, …])
map(x => (x._1, x._2.sum)) 将每小时的请求大小相加,形成 RDD (请求小时, 总大小)
计算结果
00时 CDN流量=14G
01时 CDN流量=3G
02时 CDN流量=5G
03时 CDN流量=3G
04时 CDN流量=3G
05时 CDN流量=4G
06时 CDN流量=11G
07时 CDN流量=22G
08时 CDN流量=43G
09时 CDN流量=52G
10时 CDN流量=61G
11时 CDN流量=45G
12时 CDN流量=46G
13时 CDN流量=51G
14时 CDN流量=55G
15时 CDN流量=45G
16时 CDN流量=45G
17时 CDN流量=44G
18时 CDN流量=45G
19时 CDN流量=51G
20时 CDN流量=55G
21时 CDN流量=53G
22时 CDN流量=42G
23时 CDN流量=25G
示例代码如下:
package com.atguigu.spark
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import org.slf4j.LoggerFactory
import scala.util.matching.Regex
object CdnStatics {
val logger = LoggerFactory.getLogger(CdnStatics.getClass)
// 匹配 IP 地址
val IPPattern = "((?:(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d)))\\.){3}(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d))))".r
// 匹配视频文件名
val videoPattern = "([0-9]+).mp4".r // .r()方法简介:Scala 中将字符串转换为正则表达式
// [15/Feb/2017:11:17:13 +0800] 匹配 2017:11 按每小时播放量统计
val timePattern = ".*(2017):([0-9]{2}):[0-9]{2}:[0-9]{2}.*".r
// 匹配 http 响应码和请求数据大小
val httpSizePattern = ".*\\s(200|206|304)\\s([0-9]+)\\s.*".r
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("CdnStatics")
val sc = new SparkContext(conf)
val input = sc.textFile("D:\\learn\\JetBrains\\workspace_idea\\spark\\sparkcore_cdh\\src\\main\\resources\\cdn.txt").cache()
// 统计独立 IP 访问量前 10 位
ipStatics(input)
// 统计每个视频独立 IP 数
videoIpStatics(input)
// 统计一天中每个小时间的流量
flowOfHour(input)
sc.stop()
}
// 统计独立IP访问量前 10 位
def ipStatics(data: RDD[String]): Unit = {
// 1、统计独立 IP 数
val ipNums = data.map(x => (IPPattern.findFirstIn(x).get, 1)).reduceByKey(_ + _).sortBy(_._2, false)
// 输出 IP 访问数前量前 10 位
ipNums.take(10).foreach(println)
println("独立IP数:" + ipNums.count())
}
// 统计每个视频独立 IP 数
def videoIpStatics(data: RDD[String]): Unit = {
def getFileNameAndIp(line: String) = {
(videoPattern.findFirstIn(line).mkString, IPPattern.findFirstIn(line).mkString)
}
// 2、统计每个视频独立IP数
data.filter(x => x.matches(".*([0-9]+)\\.mp4.*")).map(x => getFileNameAndIp(x)).groupByKey().map(x => (x._1, x._2.toList.distinct)).
sortBy(_._2.size, false).take(10).foreach(x => println("视频:" + x._1 + " 独立IP数:" + x._2.size))
}
// 统计一天中每个小时间的流量
def flowOfHour(data: RDD[String]): Unit = {
def isMatch(pattern: Regex, str: String) = {
str match {
case pattern(_*) => true
case _ => false
}
}
/**
* 获取日志中小时和http 请求体大小
*
* @param line
* @return
*/
def getTimeAndSize(line: String) = {
var res = ("", 0L)
try {
val httpSizePattern(code, size) = line
val timePattern(year, hour) = line
res = (hour, size.toLong)
} catch {
case ex: Exception => ex.printStackTrace()
}
res
}
// 3、统计一天中每个小时间的流量
data.filter(x => isMatch(httpSizePattern, x)).filter(x => isMatch(timePattern, x)).map(x => getTimeAndSize(x)).groupByKey()
.map(x => (x._1, x._2.sum)).sortByKey().foreach(x => println(x._1 + "时 CDN 流量=" + x._2 / (1024 * 1024 * 1024) + "G"))
}
}
输出结果如下:
(114.55.227.102,9348)
(220.191.255.197,2640)
(115.236.173.94,2476)
(183.129.221.102,2187)
(112.53.73.66,1794)
(115.236.173.95,1650)
(220.191.254.129,1278)
(218.88.25.200,751)
(183.129.221.104,569)
(115.236.173.93,529)
独立IP数:43649
视频:141081.mp4 独立IP数:2393
视频:140995.mp4 独立IP数:2050
视频:141027.mp4 独立IP数:1784
视频:141090.mp4 独立IP数:1702
视频:141032.mp4 独立IP数:1528
视频:89973.mp4 独立IP数:1523
视频:141080.mp4 独立IP数:1425
视频:141035.mp4 独立IP数:1321
视频:141082.mp4 独立IP数:1272
视频:140938.mp4 独立IP数:816
08时 CDN 流量=43G
00时 CDN 流量=14G
16时 CDN 流量=45G
01时 CDN 流量=3G
09时 CDN 流量=52G
02时 CDN 流量=5G
17时 CDN 流量=44G
03时 CDN 流量=3G
10时 CDN 流量=61G
04时 CDN 流量=3G
18时 CDN 流量=45G
05时 CDN 流量=4G
06时 CDN 流量=11G
07时 CDN 流量=22G
11时 CDN 流量=45G
19时 CDN 流量=51G
12时 CDN 流量=46G
20时 CDN 流量=55G
13时 CDN 流量=51G
21时 CDN 流量=53G
14时 CDN 流量=55G
22时 CDN 流量=42G
15时 CDN 流量=45G
23时 CDN 流量=25G
附录
两张图看清 IT 行业
IT行业横向图
IT行业纵向图
2 种计算方式
2 种数据库架构图
【转载文章务必保留出处和署名,谢谢!】