Spark
简介
Apache Spark是一个分布式、内存级计算框架,是专为大规模数据处理而设计的
特点
- 高性能: Spark 比 Hadoop 的 MapReduce 快 100 倍以上,无论是基于内存还是硬盘的运算都快 10 倍以上。它采用高效的 DAG 执行引擎,能够有效处理数据流。
- 易用性: Spark 提供 Java、Python、R 和 Scala 的 API,以及超过 80 种高级算法,使用户能够快速构建各种应用。此外,支持交互式的 Python 和 Scala shell,方便用户验证解决问题的方法。
- 通用性: Spark 提供了统一的解决方案,可用于批处理、交互式查询(Spark SQL)、实时流处理(Spark Streaming)、机器学习(Spark MLlib)和图计算(GraphX),可以在同一个应用中无缝使用。
- 兼容性: Spark 能与其他开源产品轻松融合。它可以使用 Hadoop 的 YARN 和 Apache Mesos 作为资源管理和调度器,并处理所有 Hadoop 支持的数据,包括 HDFS、HBase 和 Cassandra 等。对于已部署 Hadoop 集群的用户来说,这尤为重要,因为可以无需数据迁移即可利用 Spark 的强大处理能力。
Spark为什么这么快
- 内存计算。Spark优先将数据加载到内存中,数据可以被快速处理,并可启用缓存。
- shuffle过程优化。和Mapreduce的shuffle过程中间文件频繁落盘不同,Spark对Shuffle机制进行了优化, 降低中间文件的数量并保证内存优先。
- RDD计算模型。Spark具有高效的DAG调度算法,同时将RDD计算结果存储在内存中,避免重复计算。
Spark的生态体系
Spark体系包含Spark Core
、Spark SQL
、Spark Streaming
、Spark MLlib
及 Spark Graphx
。其中Spark Core为核心组件,提供RDD计算模型。在其基础上的众组件分别提供查询分析
、实时计算
、机器学
、图计算
等功能。
部署方式
Local: 运行在单台机器上,通常用于练习和测试。
Standalone: 构建基于 Master+Slaves 的资源调度集群,Spark 任务提交给 Master 运行,是 Spark 自身的调度系统。
Yarn: Spark 客户端直接连接 Yarn,无需额外构建 Spark 集群。有 yarn-client 和 yarn-cluster 两种模式,主要区别在于 Driver 程序的运行节点。
Mesos: 在国内使用较少
Spark RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据抽象,代表一个不可变、可分区、里面的元素可并行计算的集合。
Resilient :它是弹性的,RDD 里面的中的数据可以保存在内存中或者磁盘里面;
Distributed : 它里面的元素是分布式存储的,可以用于分布式计算;
Dataset: 它是一个集合,可以存放很多元素
Spark中的血统概念(RDD)
在Spark中,RDD的血统(lineage)指的是每个RDD如何从原始数据集或其他RDD中经过一系列转换操作得到的依赖关系链。血统图记录了这些转换关系,是一个有向无环图(DAG)。血统概念保证了Spark的容错性、优化性和惰性计算。通过血统信息,Spark能够重新计算丢失的分区数据、进行优化操作,并延迟转换操作的执行,提高了计算的效率和灵活性。
容错性(Fault Tolerance):RDD的血统信息是弹性的,即使某个RDD分区丢失,Spark也能根据RDD的血统信息重新计算出丢失的分区数据,从而保证计算的容错性。
优化(Optimization):Spark可以根据RDD的血统信息进行优化,例如通过重新排序RDD转换操作的执行顺序或者选择合适的执行策略来提高计算性能。
惰性计算(Lazy Evaluation):RDD的转换操作并不会立即执行,而是会生成一个新的RDD,并记录其父RDD的血统信息。只有当行动(Action)操作触发时,Spark才会根据RDD的血统信息执行转换操作并计算结果
RDD宽窄变换
针对不同的函数转换,RDD之间的依赖关系分为宽依赖和窄依赖。宽依赖会产生shuffle
行为,经历map输出、中间文件落地和reduce聚合等过程。
首先,Spark官网中对于宽依赖和窄依赖的定义:
- 宽依赖: 父RDD每个分区被多个子RDD分区使用
- 窄依赖: 父RDD每个分区被子RDD的一个分区使用
常见的窄转换操作包括 map
、flatMap
、filter
、mapPartitions
、mapPartitionsWithIndex
、filter
等。常见的宽转换操作包括 groupBy
、reduceByKey
、join
、cogroup
、sortByKey
等。
总的来说,窄转换是高效的,适用于局部处理数据的情况,而宽转换是需要跨分区处理数据的情况下使用,但是它会涉及到数据的重新分区和重新组织,因此会更加昂贵。在设计 Spark 作业时,应该尽量避免过多的宽转换操作,以提高作业的性能和效率。
下面我们结合示意图,分别列出宽依赖和窄依赖存在的四种情况:
- 窄依赖(一个父RDD对应一个子RDD:map/filter、union算子)
- 窄依赖(多个父RDD对应一个子RDD:co-partioned join算子)
- 宽依赖(一个父RDD对应多个非全部子RDD: groupByKey算子等)
- 宽依赖(一个父RDD对应全部子RDD: not co-partioned join算子)
RDD分区
在 Spark 中,RDD 分区决定了数据在集群中的并行处理方式。它们允许数据分布到集群的多个节点上,并且决定了任务的划分和调度方式。RDD 分区的设计考虑了数据本地性、任务划分和容错性。通常情况下,Spark 会根据数据源和集群配置自动确定 RDD 的分区方式,但用户也可以通过手动调整分区数量和分布方式来优化计算性能。
一般情况下,Spark 会根据数据源的特性和集群的配置自动确定 RDD 的分区方式。用户也可以通过 repartition
、coalesce
等方法来手动调整 RDD 的分区数量和分布方式,以满足具体的计算需求和性能优化目标。
// 案例1: coalesce(numPartitions) 改变分区数,用于大数据集过滤后,提高小数据集的执行效率。
val rdd = sc.parallelize(1 to 10, 4)
val coalesceRDD = rdd.coalesce(2)
coalesceRDD.partitions.size -> 2
// 案例2: repartition(numPartitions) 根据分区数,重新通过网络随机shuffle所有数据。
val rdd = sc.parallelize(1 to 16, 4)
val rerdd = rdd.repartition(2)
// 案例3: partitionBy 对 pairRDD 进行分区操作,如果原有的 partionRDD 和现有的 partionRDD 是一致的话就不进行分区,否则会生成数据 shuffle,会产生 shuffle 过程。
val rdd = sc.parallelize(Array((1,"aaa"),(2,"bbb"),(3,"ccc"),(4,"ddd")), 4)
rdd.partitions.size
var rdd2 = rdd.partitionBy(new org.apache.spark.HashPartitioner(2))
rdd2.partitions.size
/*coalesce 和 repartition 的区别:
coalesce 重新分区,可以选择是否进行 shuffle 过程。由参数 shuffle: Boolean = false/true 决定。
repartition 实际上是调用的 coalesce,默认是进行 shuffle 的。源码如下:*/
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] =
withScope {
coalesce(numPartitions, shuffle = true)
}
RDD属性
- 分片列表: 数据集的基本组成单位,决定并行度。在创建 RDD 时可以指定分片个数,如果未指定则使用默认值。
- 计算每个分片的函数: Spark 中 RDD 的计算以分片为单位进行,该函数会应用于每个分片。
- 依赖其他 RDD 的列表: RDD 的转换会生成新的 RDD,形成前后依赖关系。Spark 的容错机制可以通过这些依赖关系重新计算丢失的分片数据。
- 分区函数: 对于键值对类型的 RDD,可能会有一个分区函数。默认情况下为 HashPartitioner。
- 最佳位置: 存储每个分片优先位置的列表。在任务调度时,Spark 会尽可能选择存有数据的节点进行计算,遵循"移动数据不如移动计算"的理念。
RDD 是一个数据集的表示,不仅表示了数据集,还表示了这个数据集从哪来,如何计算。
RDD创建
//第一种方式:基于现有数据形成RDD
val rdd1 = sc.parallelize(Array(1,2,3,4,5,6,7))
val rdd2 = sc.makeRDD(Array(1,2,3,4,5,6,7,8))
//第二种:由外部存储系统的数据集创建,包括本地的文件系统,还有所有 Hadoop 支持的数据集,比如 HDFS、Cassandra、HBase 等
val rdd1 =
sc.textFile("hdfs://hadoop5/shopping_data/customer_details/customer_details.csv")
RDD算子
RDD 的算子分为两类:
- Transformation转换操作:返回一个新的 RDD
- Action动作操作:返回值不是 RDD(无返回值或返回其他的)
RDD 不存储实际数据,而是记录数据位置和转换关系。所有转换操作都是惰性求值/延迟执行的,只有在执行 Action 动作时才会真正运行。
之所以使用惰性求值/延迟执行,是因为这样可以在 Action 时对 RDD 操作形成 DAG有向无环图进行 Stage 的划分和并行优化,这种设计让 Spark 更加有效率地运行。Action动作操作不延迟
//案例 rdd2的值为多少?
val data = Array(100, 200, 300, 400) //定义一个数组
val rdd1 = sc.parallelize(data) //定义rdd1
val rdd2 = rdd1.map(x => x / 100) //通过rdd1 生成rdd2
data(2) = 900 //修改 data的值
rdd2.take(4)
/* take触发执行,rdd2 -> 1 2 3 4
如果没有action操作,rdd2的数据保持不变
data值的修改跟rdd2的数据没关系
*/
Transformation
MAP
基于RDD进行数据变换,返回的结果是一个新的RDD
al str = "aa bb cc"
val rdd1 = rdd1.parallelize(str)
val data1 = rdd1.map(s => s.split(" ").map(s => (s,1)))
//Array((aa,1),(bb,1),(cc,1))
val data2 = rdd2.map(s => s.split(" ")).map(s => (s,1))
//Array((aa,1),(bb,1),(cc,1))
/*
在这两种写法中,第二种写法更优,因为它只进行了一次字符串拆分操作。
*/
glom
glom作用:将每个分区的数据放到一个数组
//需求:假设您想要在给定的RDD中找出最大值。现在你可以使用map和reduce操作来完成它,如下所示。
val dataList = List(50.0,40.0,40.0,70.0)
val dataRDD = sc.makeRDD(dataList)
val maxValue = dataRDD.reduce (_ max _)
//尽管它起作用,但分区之间会有很多混洗进行比较。这些不好,特别是对于大数据。
/*我们可以,比较分区之间的最大值以获得最终的最大值。
现在我们需要一种方法来比较给定分区中的所有值。这可以使用glom轻松完成,如下所示*/
val maxValue = dataRDD.glom().map((value:Array[Double]) => value.max).reduce(_ max
_)
map((value:Array[Double]) => value.max)
对每个分区的数据进行映射操作,返回每个分区中的最大值。
reduce(_ max _)
对所有分区的最大值进行比较,得出最终的最大值。
这种方法避免了不必要的混洗操作,因为每个分区的最大值是在本地计算的,而不是在不同分区之间进行比较。这样可以提高性能,特别是对于大数据集来说。
val data = List(1, 2, 3, 4, 5, 6, 7, 8)
val rdd = sc.parallelize(data, 3) // 创建一个包含 3 个分区的 RDD
val glommedRDD = rdd.glom() // 对 RDD 进行 glom() 转换
val collectedData = glommedRDD.collect() // 收集 glom 后的结果
collectedData.foreach(arr => println(arr.mkString(","))) // 打印每个分区的数据
结果:
1,2
3,4
5,6,7,8
flatMap
flatMap
是 Spark 中的一个转换操作,它类似于 map
,但是可以将每个输入元素映射(转换)为零个或多个输出元素。flatMap
操作将函数应用于 RDD 中的每个元素,并将结果扁平化为单个 RDD。
val data = List("hello world", "how are you")
val rdd = sc.parallelize(data)
// 使用flatMap将每个字符串拆分为单词,并扁平化结果
val flatMappedRDD = rdd.flatMap(_.split(" "))
// 打印扁平化后的结果
flatMappedRDD.collect().foreach(println)
在这个示例中,我们创建了一个包含两个字符串的 RDD,并使用 flatMap
将每个字符串拆分为单词。然后,我们使用 collect()
将扁平化后的结果收集到驱动程序节点,并通过 foreach
方法打印每个单词。
假设 data
中的数据是 ["hello world", "how are you"]
,那么经过 flatMap
操作后,得到的扁平化的结果将会是 ["hello", "world", "how", "are", "you"]
。
filter
filter
是 Spark 中的一个转换操作,它用于筛选出满足特定条件的元素,返回一个包含满足条件的元素的新 RDD。具体来说,filter
接受一个函数作为参数,该函数返回一个布尔值,用于确定是否保留每个输入元素。
val data = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val rdd = sc.parallelize(data)
// 使用filter筛选出大于5的元素
val filteredRDD = rdd.filter(_ > 5)
// 打印筛选后的结果
filteredRDD.collect().foreach(println)
sample
sample
是 Spark 中的一个转换操作,用于从 RDD 中随机抽样出一部分元素。它可以用于从大数据集中获取一个较小的样本,以便进行测试或分析。
sample
操作有几种不同的形式,最常用的形式是接受三个参数:withReplacement
、fraction
和 seed
。具体说明如下:
withReplacement
: 一个布尔值,表示抽样时是否允许重复抽取同一个元素。如果为true
,则允许重复抽取;如果为false
,则不允许重复抽取。fraction
: 一个双精度浮点数,表示要抽样的比例,取值范围为 [0, 1]。例如,如果fraction
为 0.1,则表示抽样 10% 的数据。seed
: 一个长整型数值,用于指定随机数生成器的种子,以便可重现性。
val data = 1 to 100
val rdd = sc.parallelize(data)
// 从 RDD 中随机抽样出一部分元素
val sampledRDD = rdd.sample(false, 0.1, 42) // 不允许重复抽取,抽样比例为 0.1,种子为 42
// 打印抽样后的结果
sampledRDD.collect().foreach(println)
distinct
distinct
是 Spark 中的一个转换操作,用于去除 RDD 中的重复元素,返回一个新的 RDD,其中包含了去重后的元素。
distinct
操作会将 RDD 中的所有元素进行扫描,并将重复的元素去除,保留唯一的元素。它是一种常用的操作,用于处理数据中的重复记录或重复值。
val data = List(1, 2, 3, 1, 2, 4, 5, 3)
val rdd = sc.parallelize(data)
// 对 RDD 进行去重操作
val distinctRDD = rdd.distinct()
// 打印去重后的结果
distinctRDD.collect().foreach(println)
假设 data
中的数据是 [1, 2, 3, 1, 2, 4, 5, 3]
,经过 distinct
操作后,去除了重复的元素,得到的结果将会是 [1, 2, 3, 4, 5]
。
reduceByKey
reduceByKey
是 Spark 中的一个转换操作,用于对键值对 (key, value) 形式的 RDD 进行聚合操作,按键对相同键的值进行聚合。它对具有相同键的值进行分组,并对每组的值应用一个指定的聚合函数,然后返回一个新的键值对 RDD,其中每个键对应于一个聚合后的结果。
val data = List(("a", 1), ("b", 2), ("a", 3), ("b", 4), ("c", 5))
val rdd = sc.parallelize(data)
// 使用 reduceByKey 对具有相同键的值进行聚合操作,求和
val reducedRDD = rdd.reduceByKey(_ + _)
// 打印聚合后的结果
reducedRDD.collect().foreach(println)
(a, 4)
(b, 6)
(c, 5)
groupBy,groupByKey
groupBy
和 groupByKey
都是 Spark 中的转换操作,用于对键值对 (key, value) 形式的 RDD 进行分组操作。它们都根据键对 RDD 中的元素进行分组,但在某些方面有所不同。
groupBy
: groupBy
方法将 RDD 中的每个元素应用于一个函数,该函数返回一个分组键,然后根据这些键对元素进行分组。返回的结果是一个元组 (key, Iterable[T])
的 RDD,其中 key
是分组键,Iterable[T]
是具有相同分组键的元素的迭代器。
val data = List(("a", 1), ("b", 2), ("a", 3), ("b", 4), ("c", 5))
val rdd = sc.parallelize(data)
val groupedRDD = rdd.groupBy(_._1)
groupedRDD.collect().foreach(println)
---------------
(a,CompactBuffer((a,1), (a,3)))
(b,CompactBuffer((b,2), (b,4)))
(c,CompactBuffer((c,5)))
groupByKey
: groupByKey
方法仅用于键值对 RDD,它根据键对 RDD 中的元素进行分组,并将具有相同键的值进行聚合。返回的结果是一个元组 (key, Iterable[V])
的 RDD,其中 key
是分组键,Iterable[V]
是具有相同分组键的值的迭代器
val data = List(("a", 1), ("b", 2), ("a", 3), ("b", 4), ("c", 5))
val rdd = sc.parallelize(data)
val groupByKeyRDD = rdd.groupByKey()
groupByKeyRDD.collect().foreach(println)
-------
(a,CompactBuffer(1, 3))
(b,CompactBuffer(2, 4))
(c,CompactBuffer(5))
不同之处在于,groupBy
返回的结果是元组 (key, Iterable[T])
的 RDD,而 groupByKey
返回的结果是元组 (key, Iterable[V])
的 RDD,其中 V
是键对应的值的类型。
reduceByKey & groupByKey优势比较
reduceByKey
相比于groupByKey
更具优势,特别是在处理大数据集时。因为reduceByKey
在每个分区内先进行局部聚合,然后在分区间进行全局聚合。可以减少数据传输量和计算量,从而提高了性能。- 另外,由于
groupByKey
返回的结果是一个包含迭代器的列表,可能会导致内存消耗过大,特别是当某个键对应的值非常多时。而reduceByKey
返回的结果是一个键对应一个聚合后的值,对内存的消耗相对较小。 - 因此,通常情况下建议使用
reduceByKey
,除非需要保留每个键对应的所有值的列表,才会使用groupByKey
。
sortBy&sortByKe
sortBy
和 sortByKey
都是 Spark 中的转换操作,用于对 RDD 进行排序。它们可以对 RDD 中的元素按照指定的排序规则进行排序,并返回一个新的排序后的 RDD。
sortBy
:
sortBy
方法对 RDD 中的元素进行排序,并返回一个新的 RDD,其中元素按照指定的排序规则进行排序。- 排序规则可以是任何可以比较的类型,例如数字、字符串等。
sortBy
方法需要传递一个函数作为参数,该函数用于为每个元素生成一个排序键,然后根据这些排序键对元素进行排序。- 可以选择是否升序或降序排序,默认为升序排序。
val rdd = sc.parallelize(List(3, 1, 2, 5, 4))
val sortedRDD = rdd.sortBy(x => x, ascending = false)
//x => x 表示一个以 x 作为参数的函数,它返回参数 x 自身的值。换句话说,这个函数会将传入的参数原封不动地返回
sortedRDD.collect().foreach(println)
--------
5
4
3
2
1
sortByKey
:
sortByKey
方法专门用于对键值对 RDD 进行排序,它会根据键对 RDD 中的元素进行排序。- 返回的结果是一个元组
(key, value)
的 RDD,其中元素按照键的顺序进行排序。 - 默认情况下,按照键进行升序排序,但也可以通过传递参数来指定排序顺序。
val pairRDD = sc.parallelize(List(("b", 2), ("a", 1), ("c", 3)))
val sortedRDD = pairRDD.sortByKey()
sortedRDD.collect().foreach(println)
--------------------
(a,1)
(b,2)
(c,3)
mapValues
mapValues
是 Spark 中用于对键值对 RDD 进行转换的一种操作。它类似于 map
,但是仅对值部分进行转换,而键部分保持不变。这意味着,mapValues
只对 RDD 中的值部分应用指定的函数,而键部分保持不变。
val data = List(("a", 1), ("b", 2), ("c", 3))
val pairRDD = sc.parallelize(data)
val transformedRDD = pairRDD.mapValues(_ * 2)
transformedRDD.collect().foreach(println)
----
(a,2)
(b,4)
(c,6)
union
union
是 Spark 中的一个转换操作,用于将两个 RDD 合并为一个 RDD。它将两个 RDD 中的元素合并到一个新的 RDD 中,不去重,保留所有元素
val data1 = sc.parallelize(List(1, 2, 3))
val data2 = sc.parallelize(List(4, 5, 6))
val rdd1 = data1.map(x => ("A", x))
val rdd2 = data2.map(x => ("B", x))
val unionRDD = rdd1.union(rdd2)
unionRDD.collect().foreach(println)
-----
(A,1)
(A,2)
(A,3)
(B,4)
(B,5)
(B,6)
Action
collect()
: 以数组的形式返回 RDD 中的所有元素到驱动程序。这个操作通常用于在本地处理 RDD 中的数据。
count()
: 返回 RDD 中的元素个数。
first()
: 返回 RDD 中的第一个元素。
take(n: Int)
: 返回 RDD 中的前 n 个元素
saveAsTextFile(path: String)
: 将 RDD 中的内容保存为文本文件。
saveAsObjectFile(path: String)
: 将 RDD 中的内容保存为序列化对象文件。
saveAsSequenceFile(path: String)
: 将 RDD 中的内容保存为 Hadoop SequenceFile。
)
(A,1)
(A,2)
(A,3)
(B,4)
(B,5)
(B,6)
### Action
**`collect()`:** 以数组的形式返回 RDD 中的所有元素到驱动程序。这个操作通常用于在本地处理 RDD 中的数据。
**`count()`:** 返回 RDD 中的元素个数。
**`first()`:** 返回 RDD 中的第一个元素。
**`take(n: Int)`:** 返回 RDD 中的前 n 个元素
**`saveAsTextFile(path: String)`:** 将 RDD 中的内容保存为文本文件。
**`saveAsObjectFile(path: String)`:** 将 RDD 中的内容保存为序列化对象文件。
**`saveAsSequenceFile(path: String)`:** 将 RDD 中的内容保存为 Hadoop SequenceFile。
**`foreach(func: T => Unit)`:** 对 RDD 中的每个元素应用指定的函数 func。