RDD概述
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象。
代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
RDD编程
为了防止在日志文件中出现太多的日志信息,影响大家的学习,大家可以在配置一下日志文件
# Set everything to be logged to the console
rootLogger.level = ERROR
rootLogger.appenderRef.stdout.ref = console
# In the pattern layout configuration below, we specify an explicit `%ex` conversion
# pattern for logging Throwables. If this was omitted, then (by default) Log4J would
# implicitly add an `%xEx` conversion pattern which logs stacktraces with additional
# class packaging information. That extra information can sometimes add a substantial
# performance overhead, so we disable it in our default logging config.
# For more information, see SPARK-39361.
appender.console.type = Console
appender.console.name = console
appender.console.target = SYSTEM_ERR
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n%ex
# Set the default spark-shell/spark-sql log level to WARN. When running the
# spark-shell/spark-sql, the log level for these classes is used to overwrite
# the root logger's log level, so that the user can have different defaults
# for the shell and regular Spark apps.
logger.repl.name = org.apache.spark.repl.Main
logger.repl.level = warn
logger.thriftserver.name = org.apache.spark.sql.hive.thriftserver.SparkSQLCLIDriver
logger.thriftserver.level = warn
# Settings to quiet third party logs that are too verbose
logger.jetty1.name = org.sparkproject.jetty
logger.jetty1.level = warn
logger.jetty2.name = org.sparkproject.jetty.util.component.AbstractLifeCycle
logger.jetty2.level = error
logger.replexprTyper.name = org.apache.spark.repl.SparkIMain$exprTyper
logger.replexprTyper.level = info
logger.replSparkILoopInterpreter.name = org.apache.spark.repl.SparkILoop$SparkILoopInterpreter
logger.replSparkILoopInterpreter.level = info
logger.parquet1.name = org.apache.parquet
logger.parquet1.level = error
logger.parquet2.name = parquet
logger.parquet2.level = error
# SPARK-9183: Settings to avoid annoying messages when looking up nonexistent UDFs in SparkSQL with Hive support
logger.RetryingHMSHandler.name = org.apache.hadoop.hive.metastore.RetryingHMSHandler
logger.RetryingHMSHandler.level = fatal
logger.FunctionRegistry.name = org.apache.hadoop.hive.ql.exec.FunctionRegistry
logger.FunctionRegistry.level = error
# For deploying Spark ThriftServer
# SPARK-34128: Suppress undesirable TTransportException warnings involved in THRIFT-4805
appender.console.filter.1.type = RegexFilter
appender.console.filter.1.regex = .*Thrift error occurred during processing of message.*
创建项目导入依赖:
创建一个项目大家都会,这里直接省略了,现在开始导入依赖
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>3.3.1</version>
</dependency>
</dependencies>
创建执行环境
以下代码用于创建 Spark 的运行环境。可以注意到,Java 版本实际上是在 Scala 版本的基础上进行了一层封装。在此需要提醒各位的是,如果没有安装 Scala 的运行环境,将无法直接查看或运行 .scala 文件中的代码。因此,建议大家自行安装 Scala 的开发环境,以确保能够顺利进行后续的开发与调试工作。
public class Spark01_env {
public static void main(String[] args) {
//创建conf
SparkConf sparkConf = new SparkConf();
sparkConf.setMaster("local[*]")
.setAppName("spark_es_sink");
JavaSparkContext jsc = new JavaSparkContext(sparkConf);
jsc.close();
}
}
foreach 和 collect.foreach 有什么区别
RDD操作
转换算子(Transformations)
|
算子 |
说明 |
类别 |
|
|
一对一映射 |
基础 |
|
|
一对多映射 |
基础 |
|
|
过滤 |
基础 |
|
|
分区级批量操作 |
基础 |
|
|
带分区索引的批量操作 |
基础 |
|
|
去重 |
基础 |
|
|
采样 |
基础 |
|
|
合并 |
集合操作 |
|
|
交集 |
集合操作 |
|
|
差集 |
集合操作 |
|
|
笛卡尔积 |
集合操作 |
|
|
减少分区 |
分区操作 |
|
|
重新分区 |
分区操作 |
|
|
分组 |
分组聚合 |
PairRDD专属转换算子
|
算子 |
说明 |
|
|
只修改value |
|
|
扁平化修改value |
|
|
按key聚合(推荐) |
|
|
按key分组 |
|
|
灵活聚合 |
|
|
最通用的聚合 |
|
|
按key排序 |
|
|
内连接 |
|
|
左外连接 |
|
|
右外连接 |
|
|
全外连接 |
|
|
协同分组 |
行动算子(Actions)
|
算子 |
说明 |
|
|
收集所有到Driver |
|
|
计数 |
|
|
第一个元素 |
|
|
取前n个 |
|
|
取排序前n个 |
|
|
取最大的n个 |
|
|
聚合 |
|
|
带初始值聚合 |
|
|
灵活聚合 |
|
|
遍历 |
|
|
保存为文本 |
|
|
按key计数 |
|
|
按值计数 |
上面的使用方式都大致相同,这里为大家展示一个简单的Java使用的方式,其他的大家可以自己尝试一下:
public static void main(String[] args) {
JavaSparkContext jsc = new JavaSparkContext(new SparkConf().setMaster("local[*]").setAppName("spark_es"));
//创建数据
JavaPairRDD<String, Integer> stringIntegerJavaPairRDD = jsc.parallelizePairs(Arrays.asList(
new Tuple2<>("a", 1),
new Tuple2<>("b", 2),
new Tuple2<>("b", 4),
new Tuple2<>("a", 3)
));
stringIntegerJavaPairRDD.groupByKey().collect().forEach(System.out::println);
jsc.close();
}
Shuffle 操作
什么是shuffle
Shuffle是Spark中的一种数据重组操作,它在不同节点间重新分配数据,以确保相同key的数据元素被分组在一起。这通常发生在某些转换操作之后,如groupByKey、reduceByKey、join等
产生shuffle的原因
Shuffle操作通常是为了实现数据的聚合或者连接操作。例如,在进行聚合操作时,需要将所有具有相同key的数据元素发送到同一个节点上进行处理。
产生shuffle的具体操作
- 以下操作可能会导致Shuffle:
-
groupByKey:按照key对数据进行分组。reduceByKey:按照key对数据进行聚合。join:连接两个RDD中具有相同key的数据。cogroup:类似于join,但是可以同时连接多个RDD。repartition和coalesce:重新分区数据,可能会导致数据在节点间移动。
数据依赖和血缘
依赖
在Spark中,每个RDD都依赖于它的父RDD。这种依赖关系定义了数据是如何从一个RDD流向另一个RDD的。例如,如果一个RDD是通过map操作从另一个RDD创建的,那么它就依赖于父RDD。
宽依赖
在 Apache Spark 的执行引擎中,"宽依赖(Wide Dependency)" 是 RDD(弹性分布式数据集)之间的一种依赖关系,它表示父 RDD 的一个分区可能被多个子 RDD 的分区所依赖。这种依赖关系通常发生在需要进行跨分区数据交换(Shuffle)的操作中,例如 groupByKey、reduceByKey、sortByKey、join 等操作。
宽依赖的特点:
1. 需要 Shuffle 操作:
-
- 在宽依赖中,子 RDD 的每个分区可能依赖于多个父 RDD 的分区,这就要求数据必须跨分区重新分布,也就是 Shuffle。Shuffle 是一个代价较高的操作,因为它涉及磁盘 I/O 和网络传输。
a. 划分 Stage 的依据:
-
- Spark 在调度任务时,会根据 RDD 的依赖关系将整个计算图划分为不同的 Stage。宽依赖是划分 Stage 的关键点,每个 Stage 内部的 RDD 之间都是窄依赖,而 Stage 之间的依赖是宽依赖。
b. 容错机制:
-
- 如果某个分区计算失败,Spark 可以根据父 RDD 的分区重新计算该分区。但由于宽依赖涉及多个父分区,恢复失败的分区可能需要从多个父分区中获取数据,增加了恢复的复杂性和开销。
c. 性能影响:
-
- 宽依赖通常意味着更高的计算和通信开销。由于 Shuffle 操作会引入磁盘 I/O 和网络传输,因此在设计 Spark 应用程序时,应尽量减少宽依赖操作的使用,或者通过优化数据结构和算法来降低 Shuffle 的开销。
宽依赖的典型操作:
groupByKey:将相同键的所有值分组在一起,通常需要将数据按照键重新分布到不同的分区。reduceByKey:对相同键的值进行聚合操作,同样需要跨分区的数据重分布。join:两个 RDD 根据键进行连接操作,通常需要两个 RDD 的数据都按照键进行 Shuffle。sortByKey:根据键对数据进行排序,需要将数据重新分布到不同的分区以保证有序性。
示例:
假设我们有两个 RDD:rdd1 和 rdd2,其中 rdd2 是通过对 rdd1 进行 groupByKey 操作得到的。
rdd1 = sc.parallelize([(1, 'a'), (2, 'b'), (1, 'c'), (3, 'd')])
rdd2 = rdd1.groupByKey()
在这个例子中,rdd2 的每个分区可能依赖于 rdd1 的多个分区,因为 groupByKey 需要将相同键的数据聚集在一起。因此,rdd1 和 rdd2 之间的依赖关系是宽依赖。
总结:
在 Apache Spark 的上下文中,“窄依赖”(Narrow Dependency)是 RDD(弹性分布式数据集)之间的一种依赖关系。它描述了父 RDD 与子 RDD 之间的分区关系,具体是指子 RDD 的每个分区最多依赖于父 RDD 的一个分区。
窄依赖
窄依赖的特点:
- 一对一的分区依赖:子 RDD 的每个分区只依赖于父 RDD 的一个分区。例如,
map或filter操作会生成窄依赖,因为每个子分区的数据仅来源于一个父分区。 - 无需 Shuffle:由于数据不需要跨分区重新分布,因此窄依赖操作通常不需要触发 Shuffle(数据洗牌)过程,这使得它们在计算上更加高效。
- 容错高效:当某个分区丢失时,Spark 只需要重新计算该分区对应父 RDD 的那个分区,而不需要重新计算整个父 RDD,从而提高了容错效率。
窄依赖的典型场景:
- Map 操作:例如,
rdd.map(x => x * 2)会生成一个窄依赖,因为每个子分区的数据仅依赖于父分区的一个分区。 - Filter 操作:例如,
rdd.filter(x => x > 10)也会生成窄依赖,因为它不会改变数据的分区方式,只是过滤掉一些数据。 - Union 操作:当两个 RDD 被合并时(
rdd1.union(rdd2)),它们之间的依赖关系也是窄依赖,因为每个子分区的数据仅来源于其中一个父 RDD 的分区。
窄依赖 vs 宽依赖:
与窄依赖相对的是宽依赖(Wide Dependency),它指的是子 RDD 的每个分区依赖于父 RDD 的多个分区。宽依赖通常发生在需要进行 Shuffle 的操作中,例如 groupBy、reduceByKey 或 join。宽依赖的容错成本更高,因为一个分区的丢失可能导致父 RDD 的多个分区需要重新计算。
窄依赖的意义:
窄依赖的设计对 Spark 的性能优化至关重要:
- 任务调度:窄依赖使得 Spark 可以将多个操作合并为一个阶段(Stage),从而减少任务调度的开销。
- 高效计算:由于不需要 Shuffle 数据,窄依赖操作通常执行得更快。
- 容错机制:窄依赖支持更高效的恢复机制,减少了因故障导致的重新计算量。
总结来说,窄依赖是 Spark 高性能计算的关键之一,它通过减少数据的跨分区依赖和避免不必要的 Shuffle 操作,提升了整体的计算效率和容错能力。
宽依赖是 Spark 中一种重要的依赖关系,它通常伴随着 Shuffle 操作,影响着任务的划分、执行效率和容错机制。理解宽依赖的特性有助于我们更好地优化 Spark 应用程序,减少不必要的 Shuffle 操作,从而提升整体性能。
血缘
血缘(Lineage)是指RDD的创建历史,包括它是如何从其他RDD通过一系列转换操作得到的。Spark利用血缘信息来优化执行计划,并且在数据丢失时可以从血缘信息中重建丢失的RDD。
作业,阶段和任务的关系
|
层级 |
单位 |
触发/划分依据 |
描述 |
|
作业(Job) |
Job |
由 Action 操作(如 `count()`、`collect()`)触发 |
包含多个 Stage,是 Spark 调度执行的基本单位 |
|
阶段(Stage) |
Stage |
根据 RDD 的宽依赖(Shuffle)划分 |
每个 Job 会被划分为多个 Stage,Stage 是调度的最小单位 |
|
任务(Task) |
Task |
根据数据分区划分 |
每个 Stage 会生成多个 Task,Task 是 Executor 上实际执行的最小单位 |
阶段的数量
阶段的数量与 Shuffle 依赖的数量之间存在密切关系。在分布式计算框架(如 Apache Spark)中,任务的执行过程通常被划分为多个阶段(Stage)。这些阶段的划分主要依据数据处理过程中是否存在 Shuffle 操作,即数据需要在不同节点之间重新分布的情况。
具体来说,每当作业中出现 Shuffle 操作时(例如 groupByKey、reduceByKey 等转换操作),计算引擎会在此处将一个阶段划分成前后两个独立的阶段。这是因为 Shuffle 操作需要将前一阶段的输出作为输入,经过网络传输和重新分区后,才能继续后续的计算任务。因此,Shuffle 操作是阶段划分的关键依据。
由此可以看出,阶段的数量通常等于 Shuffle 依赖的数量加一。例如,如果一个作业中包含两个 Shuffle 操作,则至少会生成三个阶段。每个阶段内部的任务(Task)可以并行执行,而阶段之间的依赖关系则决定了它们必须按顺序执行,前一个阶段完成后,下一个阶段才能开始。
总结来说,阶段的数量与 Shuffle 依赖的数量密切相关,前者通常由后者决定,并在此基础上增加一定的数量以反映任务执行的阶段性流程。这种关系是构建高效分布式计算任务调度和执行计划的基础。
任务(task)的数量
在 Apache Spark 中,RDD(弹性分布式数据集)是其核心的数据抽象之一。RDD 是由多个分区(partition)组成的,这些分区可以在集群中的不同节点上并行处理。在 Spark 应用程序中,任务(task)是 Spark 调度和执行的最小单位,每个 task 负责处理一个 RDD 分区中的数据。
因此,当我们提交一个 Spark 作业(job)时,该作业会被分解为多个 stage,而每个 stage 中的 task 数量通常与该 stage 对应的 RDD 的分区数量一致。也就是说,一个 stage 中有多少个分区,就会生成多少个 task 来处理这些分区。这也就意味着,task 的数量等于该阶段最终需要处理的 RDD 的分区数量。
此外,task 的数量也会受到转换操作(transformation)和行动操作(action)的影响。例如,像 map、filter 这样的转换操作通常不会改变分区数量,因此 task 数量也不会改变;而像 repartition、coalesce 或 groupBy 这样的操作可能会改变 RDD 的分区数,从而影响后续 stage 中 task 的数量。
总结来说,在 Spark 的执行过程中,task 的数量通常由当前 stage 所处理的 RDD 的分区数量决定。分区数量越多,task 的数量也越多,从而可以提高并行度,但同时也会带来更多的任务调度开销。因此,合理设置 RDD 的分区数量对于 Spark 作业的性能调优至关重要。
分区的数量
在实际开发过程中,分区(Partition)的数量与任务(Task)的数量密切相关。然而,在具体设计时,我们应如何合理设定分区的数量以达到最优性能呢?
在spark中推荐将分区数量设置成资源核数的2~3倍。
RDD的持久化
在 Apache Spark 中,RDD(弹性分布式数据集)的持久化(Persistence) 是一项非常重要的机制,它允许将 RDD 的数据缓存或存储在内存或磁盘中,以便在多个操作中重复使用,从而提升 Spark 应用程序的性能。
一、什么是 RDD 的持久化?
RDD 是惰性求值的,即只有遇到“行动操作”(Action)时才会真正执行计算。Spark 默认情况下,在每次遇到行动操作时都会重新计算整个 RDD。这种机制虽然提高了容错性(通过血统 lineage 实现),但会导致重复计算,降低效率。
为了优化这种重复计算的问题,Spark 提供了 持久化机制(Persistence),允许将某个 RDD 的数据保存在内存、磁盘或两者中,供后续操作重复使用。
二、持久化的类型
Spark 提供了多种持久化级别(Storage Level),用于控制 RDD 数据的存储方式。这些级别通过 StorageLevel 对象定义,常见的包括:
|
存储级别 |
描述 |
|
|
默认级别,将 RDD 以反序列化 Java 对象的形式存储在 JVM 内存中。如果内存不足,部分分区不会被缓存,需要在使用时重新计算。 |
|
|
将 RDD 以反序列化 Java 对象的形式存储在内存中,内存不足时写入磁盘。 |
|
|
与 |
|
|
类似于 |
|
|
RDD 数据只存储在磁盘上。 |
|
|
存储在堆外内存中,通常用于 Tungsten 引擎和外部存储系统。 |
三、如何使用持久化
在 Spark 中,cache、persist 和 checkpoint 都是用于避免重复计算、提升性能的持久化方式,但它们在存储位置、生命周期和核心用途上有着根本的不同。
cache 是最简单的用法,persist 提供了更多灵活性,而 checkpoint 则侧重于切断依赖链和容错。
1. 🧊 cache:最便捷的内存缓存
cache 是 Spark 中最简单的持久化方式,它是 persist 的简化版本。
- 用法:直接在 RDD 或 DataFrame 上调用
.cache()方法即可。 - 默认存储级别:它的效果等同于
persist(StorageLevel.MEMORY_ONLY),即只将数据存储在 JVM 堆内存中。 - 特点:
-
- 最快速度:纯内存存储,如果数据能完全放入内存,访问速度是最快的。
- 内存风险:如果数据集太大,放不进内存,多余的分区将不会被缓存。当需要这些分区时,Spark 必须重新计算,这可能导致性能下降甚至 OOM 错误。
2. ⚙️ persist:灵活可配的持久化
persist 提供了最丰富的配置选项,允许开发者根据数据大小和计算资源,手动选择最合适的存储级别。
- 用法:调用
.persist()方法,可以传入一个StorageLevel参数来指定存储策略。 - 存储级别:通过
StorageLevel可以组合多种存储方式,下面是几个常用的级别:
|
存储级别 |
描述 |
适用场景 |
|
MEMORY_ONLY |
非序列化的 Java 对象存于内存,空间不足则重新计算。 |
数据量小,内存充足,追求最快速度(也是 |
|
MEMORY_ONLY_SER |
序列化后存于内存,节省空间,但读写需额外 CPU 开销。 |
内存有限,但计算资源(CPU)相对富裕,需要高效利用内存。 |
|
MEMORY_AND_DISK |
优先存内存,内存放不下则溢写到磁盘。 |
数据量较大,内存可能放不下,但磁盘 IO 可接受,避免大量重新计算。 |
|
DISK_ONLY |
仅存于磁盘。 |
内存极度紧缺,或计算结果非常昂贵,但磁盘访问慢。 |
注意:persist() 和 cache() 都是延迟执行的,即调用时不会立即缓存,只有当后续的 Action 操作(如 count()、collect())触发计算时,数据才会被真正持久化。
3. 📁 checkpoint:切断依赖的可靠存储
checkpoint 的机制与前两者有本质不同。它不仅仅是缓存,更是一种用于容错和切断计算血统(Lineage) 的机制。
- 用法:使用
checkpoint前,必须先设置一个可靠的存储目录(通常是 HDFS),然后调用.checkpoint()方法。
jsc.setCheckpointDir("doc");
//为了解决chectpoint重复计算问题,执行命令之前执行缓存命令
calculatedRDD.persist(StorageLevel.MEMORY_AND_DISK_SER());
calculatedRDD.checkpoint();
//执行行动算子执行就算,为了之后的清理内存做准备
long count = calculatedRDD.count();
logger.info("Processed record count: {}", count);
//执行行动算子之后,可以清空缓存了,数据存储到了checkpoint指定的文件中
calculatedRDD.unpersist();
logger.info("Cache unpersisted successfully.");
//之后在使用的时候就是直接使用我们已经计算之后的RDD了
4.缓存和检查点区别
(1)Cache缓存只是将数据保存起来,不切断血缘依赖。Checkpoint检查点切断血缘依赖。
(2)Cache缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint的数据通常存储在HDFS等容错、高可用的文件系统,可靠性高。
(3)建议对checkpoint()的RDD使用Cache缓存,这样checkpoint的job只需从Cache缓存中读取数据即可,否则需要再从头计算一次RDD。
(4)如果使用完了缓存,可以通过unpersist()方法释放缓存。
四、持久化的注意事项
- 选择合适的存储级别:
-
- 如果内存足够,优先使用
MEMORY_ONLY,性能最好。 - 若内存有限,可选择
MEMORY_AND_DISK或MEMORY_ONLY_SER。 - 避免频繁使用磁盘,以免影响性能。
- 如果内存足够,优先使用
- 释放资源:
-
- 使用完后可以通过
rdd.unpersist()手动释放缓存资源,避免占用不必要的内存或磁盘空间。
- 使用完后可以通过
- 容错性:
-
- 即使某个节点失败,Spark 也可以通过 lineage 重新计算丢失的分区。
- 序列化与反序列化开销:
-
- 使用
MEMORY_ONLY_SER等序列化存储方式时,会增加 CPU 开销。
- 使用
五、示例代码
val data = sc.parallelize(Seq(1, 2, 3, 4, 5))
val squared = data.map(x => x * x)
// 持久化 squared RDD
squared.persist(StorageLevel.MEMORY_AND_DISK)
// 第一次行动操作,触发计算并缓存
squared.count()
// 后续操作将使用缓存数据
squared.sum()
// 手动释放缓存
squared.unpersist()
六、总结
RDD 的持久化机制是 Spark 性能优化的重要手段之一。通过合理选择存储级别、控制缓存生命周期,可以显著减少重复计算带来的开销,提高作业执行效率。但在实际使用中,应根据集群资源和数据规模灵活选择持久化策略,以达到最佳性能表现。
RDD分区器
待完善

1260

被折叠的 条评论
为什么被折叠?



