Big data distributed computing system Spark entry core RDD

Author: Magic Good Source: https://developer.hs.net/thread/2173

Overview

Apache Spark is a fast and general-purpose cluster computing system. Provides high-level APIs in Java, Scala, Python, and R, as well as an optimization engine that supports generic execution graphs. It also supports a rich set of advanced tools, including Spark SQL for SQL and structured data processing, MLlib for machine learning, GraphX ​​for graph processing, and Spark Streaming.

This article mainly introduces the introductory knowledge of Spark, as well as the core RDD related concepts.

R-C.png

Spark RDD

RDD basic concepts

What is RDD

RDD (Resilient Distributed Dataset) is called Resilient Distributed Dataset . It is the most basic data abstraction in Spark. It represents an immutable, partitionable collection whose elements can be computed in parallel. Specific interpretation:

  • Resilient: Elastic scaling, data in RDD can be stored in memory or on disk.
  • Distributed: Elements are distributed and stored and can be used directly for distributed computing.
  • Dataset: A collection of data that can store many elements.

RDD is a read-only data set, which does not actually store the data to be calculated, but records the location of the data and the conversion relationship of the data (the method called, the function passed in).

RDDs are designed for

In many iterative algorithms (such as machine learning, graph algorithms, etc.) and interactive data mining, intermediate results are reused between different computing stages, that is, the output of one stage is used as the input of the next stage. However, the previous MapReduce framework used an acyclic data flow model to write intermediate results to HDFS, which brought a lot of data replication, disk IO and serialization overhead. And these frameworks can only support some specific computing modes (map/reduce), and do not provide a general data abstraction. A paper on RDDs published by AMP Labs: "Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing" addresses these issues. RDD provides an abstract data model, so that users do not need to worry about the distributed nature of the underlying data, but only need to express the specific application logic as a series of transformation operations (functions), and the transformation operations between different RDDs can also form dependencies. relationship, and then implement pipeline, thus avoiding the storage of intermediate results, greatly reducing data replication, disk IO and serialization overhead, and also provides more APIs (map/reduce/filter/groupBy, etc.).

RDD main properties

Inside an RDD, each RDD has five main properties :

  • A list of partitions
  • A function for computing each split
  • A list of dependencies on other RDDs
  • Optionally, a Partitioner for key-value RDDs (eg to say that the RDD is hash-partitioned)
  • Optionally, a list of preferred locations to compute each split on (eg block locations for an HDFS file)

1647242613598-bd7cae1a-b899-4eb6-ab53-cceeb23aebe8.pngProperty description:

Attributes illustrate
A list of partitions A group of partitions (Partition)/a list of partitions (Partition), that is, the basic unit of the data set. For RDD, each shard will be processed by a computing task, and the number of shards determines the degree of parallelism. The user can specify the number of shards of the RDD when creating the RDD. If not specified, the default value will be used.
A function for computing each split A function will be applied to each partition. The calculation of RDD in Spark is in units of shards, and the compute function will be applied to each partition.
A list of dependencies on other RDDs An RDD can depend on multiple other RDDs. Each transformation of RDD will generate a new RDD, so RDD will form a pipeline-like before and after dependencies. When some partition data is lost, Spark can recompute the lost partition data through this dependency, instead of recomputing all partitions of the RDD. (Spark's fault tolerance mechanism)
Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned) -Optional, there will be a Partitioner for KV type RDD, that is, the partition function of RDD, the default is HashPartitioner.
Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file) Optional, a list that stores the preferred location for accessing each Partition. For an HDFS file, this list holds the location of the block where each Partition is located. According to the concept of "mobile data is not as good as mobile computing", when Spark performs task scheduling, it will try its best to select those worker nodes that have data for task computing.

RDD data structure

RDD is the representation of a dataset, not only representing the dataset, but also where the dataset comes from and how to calculate it. The main attributes include:

  • Partition list
  • calculation function
  • dependencies
  • Partition function (default is hash)
  • Best location

Partition list, partition function, and best position, these three attributes actually refer to where the data set is, where the calculation is more suitable, and how to partition; the calculation function and dependency relationship, these two attributes actually refer to how the data set came from of.

RDD basic operations

RDD object creation

API Documentation Reference: https://spark.apache.org/docs/2.4.5/api/scala/index.html#org.apache.spark.SparkContext

Through the SparkContext.scalaAPI source code, we can understand that RDD can be created by the following methods:

  • Created from datasets in external storage systems, including local file systems, and all Hadoop-supported datasets such as HDFS, Cassandra, HBase, etc.

1647244703608-b8acabb7-d077-4771-a8ac-48772ba28a53.png

// 创建一个 SparkContext 对象
val spConf = new SparkConf().setAppName("WordCount").setMaster("local")
val spContext = new SparkContext(spConf)
// 本地系统文件创建 RDD
val textFile = spContext.textFile("src/wordCount/temp/test.txt")
// HDFS 文件创建 RDD
val textFile1 = spContext.textFile("hdfs://node1:8088/wordCount/temp/test.txt")
  1. Generate a new RDD from an existing RDD through operator transformation:
val rdd = textFile.flatMap(_.split(" "))
  1. Create a new RDD from an existing collection:
val rdd1 = spContext.parallelize(Array(1,2,3,4,5,6,7,8))
val rdd2 = spContext.makeRDD(List(1,2,3,4,5,6,7,8))

1647330208280-73edc821-8a85-4ba4-9929-ca76807d538b.png

parallelizeMethods are executed with lazy parallelization. If seqis a mutable collection, and is changed after calling parallelization and before the first operation on the RDD, the resulting RDD will reflect the modified collection, passing a copy of the parameter to avoid this. Avoid parallelize(Seq())creating an empty RDD with . Consider emptyRDD for an RDD with no partitions, or an RDD parallelize(Seq[T]())for T with empty partitions.

1647330252866-c289c72f-ea8a-4c4c-b4fd-dffeb7299d25.png

Discover the bottom layer of the method or the method called makeRDDthrough the source codeparallelize

Overview of RDD Operators

The operator in RDD is officially called Operator (operation), which corresponds to the method in the RDD object. Why not just call the method directly?

  • The reason why it is called an operator is to distinguish it from the method of the object in the original Scala, because the method of the object in Scala is completed in the memory of the same node, and the operator of the RDD is to send the calculation logic to the distributed node (Executor side). ) executed in
  • The external operations of the RDD method are executed on the Driver side, while the logic code inside the method is executed on the Executor side

Refer to the blog post to understand: https://blog.csdn.net/weixin_43101402/article/details/116065748

RDD operators are divided into two categories:

  • Transformations transformation operations, returning a new RDD
  • Actions Action operations, the return value is not an RDD (no return value or other calculation result value)

All transformations in RDDs are lazily evaluated/lazy, meaning they are not computed directly. These transitions will only actually run when an Action occurs that requires a result to be returned to the Driver. The reason for using lazy evaluation/delayed execution is that it can form a DAG (directed acyclic graph) of RDD operations for Stage division and parallel optimization at the time of Action. This design makes Spark run more efficiently.

Spark also provides statistics related operators for RDDs:

Operator illustrate
count number
mean mean
sum sum
max maximum value
min minimum
variance variance
sampleVariance Calculate variance from sampling
stdev Standard Deviation: A measure of how discrete the data is
sampleStdev sample standard deviation
stats View Statistics

Transformations

Spark RDD supports general transformations transformation operators as shown in the following list:

conversion operator Function Description
map(func) 返回一个新的 RDD,该 RDD 由每一个输入元素经过 func 函数转换后组成
filter(func) 返回一个新的 RDD,该 RDD 由经过 func 函数计算后返回值为 true 的输入元素组成
flatMap(func) 类似于 map,但是每一个输入元素可以被映射为 0 或多个输出元素(所以 func 应该返回一个序列,而不是单一元素)
mapPartitions(func) 类似于 map,但独立地在 RDD 的每一个分片上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是 Iterator[T] => Iterator[U]
mapPartitionsWithIndex(func) 类似于 mapPartitions,只是func 多了一个整型的分区索引值,因此如果RDD包含元素类型为T,则 func 必须是 Iterator <T> => Iterator <U> 的映射函数。
sample(withReplacement, fraction, seed) 采样部分(比例取决于 fraction )数据,同时可以指定是否使用回置采样(withReplacement),以及随机数种子(seed)
union(otherDataset) 返回源数据集和参数数据集(otherDataset)的并集
intersection(otherDataset) 返回源数据集和参数数据集(otherDataset)的交集
distinct([numPartitions])) 返回对源数据集做元素去重后的新数据集
groupByKey([numPartitions]) 只对包含键值对的RDD有效,如源RDD包含 (K, V) 对,则该算子返回一个新的数据集包含 (K, Iterable <V>) 对。注意:如果你需要按key分组聚合的话(如sum或average),推荐使用 reduceByKey或者 aggregateByKey 以获得更好的性能。注意:默认情况下,输出计算的并行度取决于源RDD的分区个数。当然,你也可以通过设置可选参数 numTasks 来指定并行任务的个数。
reduceByKey(func, [numPartitions]) 如果源RDD包含元素类型 (K, V) 对,则该算子也返回包含(K, V) 对的RDD,只不过每个key对应的value是经过func聚合后的结果,而func本身是一个 (V, V) => V 的映射函数。另外,和 groupByKey 类似,可以通过可选参数 numTasks 指定reduce任务的个数。
aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions]) 如果源RDD包含 (K, V) 对,则返回新RDD包含 (K, U) 对,其中每个key对应的value都是由 combOp 函数 和 一个“0”值zeroValue 聚合得到。允许聚合后value类型和输入value类型不同,避免了不必要的开销。和 groupByKey 类似,可以通过可选参数 numTasks 指定reduce任务的个数。
sortByKey([ascending], [numPartitions]) 如果源RDD包含元素类型 (K, V) 对,其中K可排序,则返回新的RDD包含 (K, V) 对,并按照 K 排序(升序还是降序取决于 ascending 参数)
join(otherDataset, [numPartitions]) 如果源RDD包含元素类型 (K, V) 且参数RDD(otherDataset)包含元素类型(K, W),则返回的新RDD中将包含内关联后key对应的 (K, (V, W)) 对。外关联(Outer joins)操作请参考 leftOuterJoin、rightOuterJoin 以及 fullOuterJoin 算子。
cogroup(otherDataset, [numPartitions]) 如果源RDD包含元素类型 (K, V) 且参数RDD(otherDataset)包含元素类型(K, W),则返回的新RDD中包含 (K, (Iterable <V>, Iterable <W>))。该算子还有个别名:groupWith
cartesian(otherDataset) 如果源RDD包含元素类型 T 且参数RDD(otherDataset)包含元素类型 U,则返回的新RDD包含前二者的笛卡尔积,其元素类型为 (T, U) 对。
pipe(command, [envVars]) 以shell命令行管道处理RDD的每个分区,如:Perl 或者 bash 脚本。RDD中每个元素都将依次写入进程的标准输入(stdin),然后按行输出到标准输出(stdout),每一行输出字符串即成为一个新的RDD元素。
coalesce(numPartitions) 将RDD的分区数减少到numPartitions。当以后大数据集被过滤成小数据集后,减少分区数,可以提升效率。
repartition(numPartitions) 将RDD数据重新混洗(reshuffle)并随机分布到新的分区中,使数据分布更均衡,新的分区个数取决于numPartitions。该算子总是需要通过网络混洗所有数据。
repartitionAndSortWithinPartitions(partitioner) 根据partitioner(spark自带有HashPartitioner和RangePartitioner等)重新分区RDD,并且在每个结果分区中按key做排序。这是一个组合算子,功能上等价于先 repartition 再在每个分区内排序,但这个算子内部做了优化(将排序过程下推到混洗同时进行),因此性能更好。

参考文档:https://spark.apache.org/docs/2.4.5/rdd-programming-guide.html#transformations

Actions

Spark RDD 支持通用的 Actions 动作算子如下列表所示:

动作算子 功能说明
reduce(func) 将RDD中元素按func进行聚合(func是一个 (T,T) => T 的映射函数,其中T为源RDD元素类型,并且func需要满足 交换律 和 结合律 以便支持并行计算)
collect() 将数据集中所有元素以数组形式返回驱动器(driver)程序。通常用于,在RDD进行了filter或其他过滤操作后,将一个足够小的数据子集返回到驱动器内存中。
count() 返回数据集中元素个数
first() 返回数据集中首个元素(类似于 take(1) )
take(n) 返回数据集中前 n 个元素
takeSample(withReplacement, num, [seed]) 返回数据集的随机采样子集,最多包含 num 个元素,withReplacement 表示是否使用回置采样,最后一个参数为可选参数seed,随机数生成器的种子。
takeOrdered(n, [ordering]) 按元素排序(可以通过 ordering 自定义排序规则)后,返回前 n 个元素
saveAsTextFile(path) 将数据集中元素保存到指定目录下的文本文件中(或者多个文本文件),支持本地文件系统、HDFS 或者其他任何Hadoop支持的文件系统。保存过程中,Spark会调用每个元素的toString方法,并将结果保存成文件中的一行。
saveAsSequenceFile(path)(Java and Scala) 将数据集中元素保存到指定目录下的Hadoop Sequence文件中,支持本地文件系统、HDFS 或者其他任何Hadoop支持的文件系统。适用于实现了Writable接口的键值对RDD。在Scala中,同样也适用于能够被隐式转换为Writable的类型(Spark实现了所有基本类型的隐式转换,如:Int,Double,String 等)
saveAsObjectFile(path)(Java and Scala) 将RDD元素以Java序列化的格式保存成文件,保存结果文件可以使用 SparkContext.objectFile 来读取。
countByKey() 只适用于包含键值对(K, V)的RDD,并返回一个哈希表,包含 (K, Int) 对,表示每个key的个数。
foreach(func) 在RDD的每个元素上运行 func 函数。通常被用于累加操作,如:更新一个累加器(Accumulator ) 或者 和外部存储系统互操作。注意:用 foreach 操作出累加器之外的变量可能导致未定义的行为。更详细请参考前面的“理解闭包”(Understanding closures)这一小节。

参考文档:https://spark.apache.org/docs/2.4.5/rdd-programming-guide.html#actions

RDD 算子实践

  /**
   * 给定一个键值对 RDD: val rdd = sc.parallelize(Array(("spark",2),("hadoop",6),("hadoop",4),("spark",6)))
   * key 表示图书名称,value 表示某天图书销量 请计算每个键对应的平均值,也就是计算每种图书的每天平均销量。
   * 最终结果:("spark",4),("hadoop",5)。
   */
  def calRddDemo(sc: SparkContext): Unit = {
    val rdd = sc.parallelize(Array(("spark", 2), ("hadoop", 6), ("hadoop", 4), ("spark", 6)))
    val mapRdd = rdd.groupByKey()
  
    //    方法1
    val args1 = mapRdd.mapValues(o => o.sum / o.size).collect()
    args1.foreach(println)
  
    //    方法2
    val args2 = mapRdd.map(t => (t._1, t._2.sum / t._2.size)).collect()
    args2.foreach(println)
  }

RDD 持久化

Spark 中最重要的功能之一就是将数据集持久化(或缓存)在内存中,当持久化一个 RDD 时,每个节点都会将它计算的任何分区存储在内存中,并在对该数据集(或从它派生的数据集)的其他操作中重用它们。这使得未来的行动更快(通常超过 10 倍)。缓存是迭代算法和快速交互使用的关键工具。 通过使用 persist()cache() 方法将 RDD 标记为持久化,在第一次在 Action 计算时触发,它将计算的结果保存在节点的内存中。在触发后面的 Action 时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。 1647342966653-46c4f512-d25c-40f7-b71f-c561d477b911.png

通过查看 RDD 的源码发现 cache 最终也是调用了 persist 无参方法(默认存储只存在内存中)。

Spark 的缓存是容错的——如果 RDD 的任何分区丢失,它将使用最初创建它的转换自动重新计算。

RDD 存储级别

每个持久化的 RDD 都可以使用不同的存储级别进行存储,例如:允许将数据集持久化到磁盘上、将其持久化在内存中或者作为序列化的 Java 对象(以节省空间),跨节点复制它。这些通过将 StorageLevel 对象传递给 persist() 来设置级别。 cache() 方法是使用默认存储级别的简写,即 StorageLevel.MEMORY_ONLY(将反序列化的对象存储在内存中)。

存储级别 说明
MEMORY_ONLY -默认级别
-将 RDD 以非序列化的 Java 对象存储在 JVM 中。
-如果没有足够的内存存储 RDD,则某些分区将不会被缓存,每次需要时都会重新计算。
MEMORY_AND_DISK -将 RDD 以非序列化的 Java 对象存储在 JVM 中。
-如果数据在内存中放不下,则溢写到磁盘上.需要时则会从磁盘上读
MEMORY_ONLY_SER
(Java and Scala) -将 RDD 以序列化的 Java 对象(每个分区一个字节数组)的方式存储。
-通常比非序列化对象(deserialized objects)更具空间效率,特别是在使用快速序列化的情况下,但是这种方式读取数据会消耗更多的 CPU
MEMORY_AND_DISK_SER
(Java and Scala) -与 MEMORY_ONLY_SER 类似,但如果数据在内存中放不下,则溢写到磁盘上,而不是每次需要重新计算它们
DISK_ONLY -将 RDD 分区存储在磁盘上
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. -与上面的储存级别相同,只不过将持久化数据存为两份,备份每个分区存储在两个集群节点上
OFF_HEAP (experimental) -与 MEMORY_ONLY_SER 类似,但将数据存储在堆外内存中。(即不是直接存储在 JVM 内存中)

使用注意:

  • RDD 持久化/缓存的目的是为了提高后续操作的速度
  • 缓存的级别有很多,默认只存在内存中,开发中使用 memory_and_disk
  • 只有执行 Action 操作的时候才会真正将 RDD 数据进行持久化/缓存
  • 实际开发中如果某一个 RDD 后续会被频繁的使用,可以将该 RDD 进行持久化/缓存

RDD 容错机制

持久化可以把数据放在内存中,虽然是快速的,但并不能完全保证数据可靠;也可以把数据放在磁盘上,也不能保证数据的完全可靠性,如磁盘使用中途损坏等。所以持久化存在一定的局限性,为了解决这一问题,就需要利用 RDD 中的 checkpoint 检查点。 ![image.png](F:\01mynotes\0317-大数据分布式计算系统 Spark\大数据分布式计算系统Spark核心之RDD.assets\1647344646657-aaa54f40-969f-4241-b963-ce61b078f512.png) checkpoint 的产生就是为了更加可靠的数据持久化,在 checkpoint 的时候一般把数据放在在 HDFS 上,这就天然的借助了 HDFS 天生的高容错、高可靠来实现数据最大程度上的安全,实现了 RDD 的容错和高可用。 将此 RDD 标记为 checkpoint 检查点,它将被保存到使用 SparkContext#setCheckpointDir 设置的检查点目录内的文件中,并且将删除对其父 RDD 的所有引用。必须在此 RDD 上执行任何作业之前调用此函数。

使用注意:

  • 开发中为了保证数据的安全性性及读取效率,可以对频繁使用且重要的数据,先做缓存/持久化,再做 checkpint 操作。
  • 强烈建议将此 RDD 持久化在内存中,否则将其保存在文件中将需要重新计算。
  • 持久化和 checkpoint 的区别:
    • 存储位置: Persist 或 Cache 方法只能保存在本地的磁盘和内存中(或者堆外内存--实验中) ,而 checkpoint 可以保存数据到 HDFS 相关的分布式存储系统上。
    • 生命周期: Cache 和 Persist 的 RDD 会在程序结束后会被清除或者手动调用 unpersist 方法,而 checkpoint 的 RDD 在程序结束后依然存在,不会被删除。

RDD 依赖与DAG

RDD 依赖分类

RDD 和 其子 RDD 之间的依赖关系分为两类:宽依赖(wide dependency/shuffle dependency) 、窄依赖(narrow dependency) 。

1647391499607-c6caa584-fb25-4bf3-bb10-af8f22ecae0e.png 窄依赖:父 RDD 的一个分区只会被子 RDD 的一个分区依赖; 宽依赖:父 RDD 的一个分区会被子 RDD 的多个分区依赖(涉及到 shuffle)。

RDD 依赖意义

  1. 对于窄依赖:
  • 窄依赖的多个分区可以并行计算。
  • 窄依赖的一个分区的数据如果丢失只需要重新计算对应的分区的数据就可以了。
  1. 对于宽依赖:
  • 划分 Stage(阶段)的依据:对于宽依赖,必须等到上一阶段计算完成才能计算下 一阶段。

RDD DAG

DAG (Directed Acyclic Graph 有向无环图)指的是数据转换执行的过程,有方向,无闭环(其实就是 RDD 执行的流程)。 RDD 从一开始的创建通过一系列的转换操作就形成了 DAG 有向无环图,任务执行时,可以按照 DAG 的描述,执行真正的计算(数据被操作的一个过程)。 DAG的边界:

  • 开始:通过 SparkContext 创建的 RDD;
  • 结束:触发 Action,一旦触发 Action 就形成了一个完整的 DAG。

DAG 划分 Stage

一个 Spark 程序可以有多个 DAG(有几个 Action,就有几个 DAG,图中最后只有一个 Action,那么就是一个 DAG)。 一个 DAG 可以有多个 Stage(根据宽依赖/shuffle 进行划分)。 1647395414584-24d13540-8ed1-4b8c-9d27-0dbe79d358cb.png 同一个 Stage 可以有多个 Task 并行执行(task 数=分区数,如上图,Stage1 中有三个分区 P1、P2、P3,对应的也有三个 Task)。 可以看到这个 DAG 中只 reduceByKey 操作是一个宽依赖,Spark 内核会以此为边界将其前后划分成不同的 Stage。 同时可以注意到,在图中 Stage1 中,从 textFile 到 flatMap 到 map 都是窄依赖,这几步操作可以形成一个流水线操作,通过 flatMap 操作生成的 partition 可以不用等待整个 RDD 计算结束,而是继续进行 map 操作,这样大大 提高了计算的效率。

划分的意义: 一个复杂的业务逻辑如果有 shuffle,那么就意味着前面阶段产生结果后,才能执行下一个阶段,即下一个阶段的计算要依赖上一个阶段的数据。那么按照 shuffle 进行划分(也就是按照宽依赖就行划分),就可以将一个 DAG 划分成多个 Stage/阶段,在同一个 Stage 中,会有多个算子操作,可以形成一个 pipeline 流水线,流水线内的多个平行的分区可以并行执行。

如何划分: 对于窄依赖,partition 的转换处理在 stage 中完成计算,不划分(将窄依赖尽量放在在同一个 stage 中,可以实现流水线计算)。 对于宽依赖,由于有 shuffle 的存在,只能在父 RDD 处理完成后,才能开始接下来的计算,也就是说需要要划分 stage。

Spark 会根据 shuffle(宽依赖)使用回溯算法来对 DAG 进行 Stage 划分,从后往前,遇到宽依赖就断开,遇到窄依赖就把当前的 RDD 加入到当前的 stage 阶段中。

数据混洗

部分 Spark 算子会触发众所周知的**混洗(Shuffle)**事件。Spark中的混洗机制是用于将数据重新分布,其结果是所有数据将在各个分区间重新分组。一般情况下,混洗需要跨执行器(executor)或跨机器复制数据,这也是混洗操作一般都比较复杂而且开销大的原因。

基本说明

为了理解混洗阶段都发生了哪些事,首先以 reduceByKey 算子为例来看一下。reduceByKey算子会生成一个新的RDD,将源RDD中一个key对应的多个value组合进一个tuple - 然后将这些values输入给reduce函数,得到的result再和key关联放入新的RDD中。这个算子的难点在于对于某一个key来说,并非其对应的所有values都在同一个分区(partition)中,甚至有可能都不在同一台机器上,但是这些values又必须放到一起计算reduce结果。 在Spark中,通常是由于为了进行某种计算操作,而将数据分布到所需要的各个分区当中。而在计算阶段,单个任务(task)只会操作单个分区中的数据 – 因此,为了组织好每个reduceByKey中reduce任务执行时所需的数据,Spark需要执行一个多对多操作。即,Spark需要读取RDD的所有分区,并找到所有key对应的所有values,然后跨分区传输这些values,并将每个key对应的所有values放到同一分区,以便后续计算各个key对应values的reduce结果 – 这个过程就叫做混洗(Shuffle)。 虽然混洗好后,各个分区中的元素和分区自身的顺序都是确定的,但是分区中元素的顺序并非确定的。如果需要混洗后分区内的元素有序,可以参考使用以下混洗操作:

  • mapPartitions 使用 .sorted 对每个分区排序
  • repartitionAndSortWithinPartitions 重分区的同时,对分区进行排序,比自行组合repartition和sort更高效
  • sortBy 创建一个全局有序的RDD

会导致混洗的算子有:**重分区(repartition)**类算子,如: repartitioncoalesceByKey 类算子(除了计数类的,如 countByKey) 如:groupByKeyreduceByKey;以及Join类算子,如:cogroupjoin.

性能影响

混洗(Shuffle)之所以开销大,是因为混洗操作需要引入磁盘I/O,数据序列化以及网络I/O等操作。为了组织好混洗数据,Spark需要生成对应的任务集 – 一系列map任务用于组织数据,再用一系列reduce任务来聚合数据。注意这里的map、reduce是来自MapReduce的术语,和Spark的map、reduce算子并没有直接关系。 在Spark内部,单个map任务的输出会尽量保存在内存中,直至放不下为止。然后,这些输出会基于目标分区重新排序,并写到一个文件里。在reduce端,reduce任务只读取与之相关的并已经排序好的blocks。 某些混洗算子会导致非常明显的内存开销增长,因为这些算子需要在数据传输前后,在内存中维护组织数据记录的各种数据结构。特别地,reduceByKey和aggregateByKey都会在map端创建这些数据结构,而ByKey系列算子都会在reduce端创建这些数据结构。如果数据在内存中存不下,Spark会把数据吐到磁盘上,当然这回导致额外的磁盘I/O以及垃圾回收的开销。 混洗还会再磁盘上生成很多临时文件。以Spark-1.3来说,这些临时文件会一直保留到其对应的RDD被垃圾回收才删除。之所以这样做,是因为如果血统信息需要重新计算的时候,这些混洗文件可以不必重新生成。如果程序持续引用这些RDD或者垃圾回收启动频率较低,那么这些垃圾回收可能需要等较长的一段时间。这就意味着,长时间运行的Spark作业可能会消耗大量的磁盘。Spark的临时存储目录,是由spark.local.dir 配置参数指定的。 混洗行为可以由一系列配置参数来调优。参考Spark配置指南(Spark Configuration Guide)中“混洗行为”这一小节。

共享变量

默认情况下,当 Spark 在集群的多个不同节点的多个任务上并行运行一个函数时,它会把函数中涉及到的每个变量,在每个任务上都生成一个副本。但有时需要在多个任务之间共享变量,或者在任务(Task)和任务控制节点(Driver Program)之间共享变量。 为了满足这种需求,Spark 提供了两种类型的变量:

  1. 累加器(Accumulators):累加器支持在所有不同节点之间进行累加计算(比如:计数、求和)。
  2. 广播变量(Broadcast Variables):广播变量用来把变量在所有节点的内存之间进行共享,在每个机器上缓存一个只读的变量,而不是为机器上的每个任务都生成一个副本。

Accumulators

累加器(Accumulators)是仅通过关联和交换操作“添加”到的变量,因此可以有效地并行支持。它们可用于实现计数器(如在 MapReduce 中)或求和。 Spark 原生支持数值类型的累加器,程序员可以添加对新类型的支持。 作为用户,您可以创建命名或未命名的累加器。如下图所示,一个命名的累加器(在此实例中为计数器)将显示在修改该累加器的阶段的 Web UI 中。 Spark 在“Tasks”表中显示由任务修改的每个累加器的值。

scala> val accum = sc.longAccumulator("My Accumulator")
accum: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 0, name: Some(My Accumulator), value: 0)

scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

scala> accum.value
res2: Long = 10

累加器使用实例:

  /**
   * 累加器 操作实例
   *
   * 假设需要统计数据的操作数量
   *
   */
  def AccumulatorDemo(sc: SparkContext): Unit = {
    val dealRdd = sc.parallelize(Array(1, 2, 3, 4, 5, 6))
    // 1.不使用累加器
    // 直接使用普通变量进行计数,最后计算结果为0
    // 因为 foreach 中的函数是传递给 Worker 中的 Executor 执行
    // 而 counter 变量在 Driver 端定义的,会以副本形式传递给 Executor
    // 最后各个 Executor 中的 counter 累加,但 Driver 端的 counter 并不会被操作
    var counter = 0;
    dealRdd.foreach(f => counter += 1)
    println("counter = " + counter) // counter = 0

    // 2.使用累加器
    // 通过累计器能够解决普通变量无法正常计数的问题
    // 从 Spark2.0 开始 Accumulator 类已经过时,计数可以使用 LongAccumulator 工具类
    var accu: LongAccumulator = sc.longAccumulator("count name")
    dealRdd.foreach(f => accu.add(1))
    println("accumulator = " + accu.value) // accumulator = 6
  }

Broadcast Variables

Broadcast Variables refers to caching a read-only variable on each machine without sending a copy of it with the task. For example, they can be used to provide each node with a copy of a large input dataset in an efficient manner. Spark also tries to use efficient broadcast algorithms to distribute broadcast variables to reduce communication costs. Spark actions are executed through a set of stages, separated by distributed "shuffle" operations. Spark automatically broadcasts the public data required by tasks within each stage. Data broadcast in this way is cached in serialized form and deserialized before running each task. This means that explicitly creating broadcast variables is only useful when tasks across multiple stages require the same data or when caching the data in deserialized form is important. Create a broadcast variable from variable v by calling SparkContext.broadcast(v). A broadcast variable is a wrapper around v whose value can be accessed by calling the value method.

scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)

scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)

Example of using broadcast variables:

 /**
   * 广播变量 操作实例
   *
   * 假设需要将部门id的集合:List(2, 1, 3, 2, 0)
   * 需要通过字典索引的方式翻译成对应部门的名字,字典项如下:
   * List((0, "研发部门"), (1, "财务部门"), (2, "营销部门"), (3, "人力部门"))
   *
   */
  def BroadCastDemo(sc: SparkContext): Unit = {
    val depListRdd = sc.parallelize(List(2, 1, 3, 2, 0))
    val depDictMap: collection.Map[Int, String] = Map((0, "研发部门"), (1, "财务部门"), (2, "营销部门"), (3, "人力部门"))
    // 1.不使用广播变量
    // 不使用广播变量虽然也能实现对于部门集合id的映射
    // 但是使用的 depDictMap 变量会在 task 之间以副本形式传递,如果计算量较大对性能影响也会增加
    var depNameRdd = depListRdd.map(v => (v, depDictMap(v)))
    depNameRdd.foreach(println) // (1,财务部门) (3,人力部门) (2,营销部门) (0,研发部门) (2,营销部门)

    // 2.使用广播变量
    // 通过使用广播变量,各个 task 中都是复用同一个 broadcast 变量,减少了 task 之间的传输
    val broadcast : Broadcast[collection.Map[Int, String]]  = sc.broadcast(depDictMap)
    depNameRdd = depListRdd.map(v => (v, broadcast.value(v)))
    depNameRdd.foreach(println) // (1,财务部门) (3,人力部门) (2,营销部门) (0,研发部门) (2,营销部门)
  }

Summarize

This article makes an in-depth analysis of the introductory knowledge of the big data computing system Spark and the core technology RDD, hoping to gain something for the students who study this article.

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324257964&siteId=291194637