Spark-RDD和共享变量

概览

每个Spark应用程序都由一个driver program 组成,该驱动程序运行我们编写的main函数,并在集群上执行各种 并行操作 。Spark提供的主要抽象是一个 弹性分布式数据集(RDD) ,它是一个跨集群节点分区的元素集合,可以并行操作。RDD是通过从Hadoop文件系统(或任何其他Hadoop支持的文件系统)中的文件或 driver program 中现有的Scala集合开始并转换它来创建的。用户还可以要求Spark在内存中 持久化RDD,允许它在并行操作中有效地重用。最后,RDD会自动从节点故障中恢复。

Spark中的第二个抽象是可以在并行操作中使用的 共享变量 。默认情况下,当Spark在不同节点上作为一组任务并行运行函数时,它会将函数中使用的每个变量的副本发送给每个任务。有时,一个变量需要在任务之间共享,或者在任务和 driver program 之间共享。Spark支持两种类型的共享变量:广播变量 ,可用于在所有节点上的内存中缓存一个值,累加器,是仅可"添加"的变量,例如计数器和求和。

一、RDD

弹性分布式数据集(Resilient Distributed Dataset) 简称 RDD,是一个可以并行操作的元素的容错集合。RDD有五大特性:

1、有一系列分区构成(分区即切片)

2、每个函数都作用在每个分区上(并行计算)

3、和其他RDD是有依赖关系的

4、分区类算子只能作用在kv型RDD上

5、Spark会为每个分区计算最佳的位置,并把函数移动过去(计算向数据移动)

RDD支持两种类型的操作:Transformations 和 Actions ,其中Transformations算子有 lazy特性,只要需要将结果返回给驱动程序时,才会触发计算。默认每个转换后的RDD都可以重新计算,也可以使用persist或cache方法将RDD持久化。这样,当后面的算子用这个RDD时就不用重新计算了。

1、Transformations 算子

map(func)

解释:返回一个新的分布式数据集,该数据集是通过函数func传递源的每个元素而形成的

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

val mapRdd = sourceRdd.map(_+10)

mapRdd.foreach(println)

结果:

11

12

13

14

15

16

17

18

19

20

filter(func)

解释:返回一个新的数据集,该数据集是通过选择func在其上返回true的源的那些元素而形成的。

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

val filterRdd = sourceRdd.filter(_>5)

filterRdd.foreach(println)

结果:

6

7

8

9

10

flatMap(func)

解释:类似于map,但每个输入项都可以映射到0个或多个输出项(因此func应该返回一个Seq而不是单个项)

val sourceRdd2 = sc.parallelize(List("java scala js","c++ c python","java vba python"))

val flatMapRdd = sourceRdd2.flatMap(_.split(" "))

flatMapRdd.foreach(println)

结果:

java

scala

js

c++

c

python

java

vba

python

mapPartitions(func)

解释:类似于map,但在RDD的每个分区(块)上单独运行,因此在T类型的RDD上运行时,func必须是Iterator<T>=>Iterator<U>类型。

mapPartitionsWithIndex(func)

解释:类似于map分区,但也为func提供了一个表示分区索引的整数值,因此func必须是类型(Int, Iterator<T>)=>Iterator<U>当在T类型的RDD上运行时。

复制代码
val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)
//打印每个分区的数据
sourceRdd.mapPartitionsWithIndex((index,partition)=>{
  val sourceData = partition.mkString(",")
  println(index +"-"+sourceData)
  partition
}).collect()
println("------------------------")
val mapPartitionsRDD = sourceRdd.mapPartitions(iter => {
  // 在这个例子中,我们计算每个分区的总和
  val sum = iter.foldLeft(0)(_ + _)
  Iterator(sum) // 返回一个包含分区总和的新迭代器
})
mapPartitionsRDD.foreach(println)

结果:

0-1,2,3

1-4,5,6

2-7,8,9,10


6

15

34

sample(withReplacement, fraction, seed)

解释:使用给定的随机数生成器种子对数据的一小部分进行采样,无论是否进行替换。

所谓抽样那么返回的数据占比就没有那么准确

复制代码
val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)
/ 进行随机采样,采样比例为0.5,有放回的抽样
val sampleRdd = sourceRdd.sample(true, 0.5)
sampleRdd.foreach(println)
println("-------------------------------")
// 进行随机采样,采样比例为0.5,无放回的抽样
val sampleRdd2 = sourceRdd.sample(false, 0.5)
sampleRdd2.foreach(println)

结果:

1

2

5

7

7

9


3

4

5

6

7

9

union(otherDataset)

解释:返回一个新数据集,其中包含源数据集中元素和参数的联合

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

val sourceRdd2 = sc.parallelize(List(1000,2000,9000))

val unionRdd = sourceRdd.union(sourceRdd2)

unionRdd.foreach(println)

结果:

1

2

3

4

5

6

7

8

9

10

1000

2000

9000

intersection(otherDataset)

解释:返回一个新RDD,其中包含源数据集中元素和参数的交集。

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

val sourceRdd2 = sc.parallelize(List(3,5,6,1000,2000,9000))

val intersectionRdd = sourceRdd.intersection(sourceRdd2)

intersectionRdd.foreach(println)

结果:

6

3

5

distinct([numTasks]))

解释:返回一个包含源数据集不同元素的新数据集

val sourceRdd3 = sc.parallelize(List(1,2,3,3,4,4,5,6,6))

val distinctRdd = sourceRdd3.distinct()

distinctRdd.foreach(println)

结果:(无序)

4

1

6

3

5

2

groupByKey([numTasks])

解释:当在(K, V)对的数据集上调用时,返回(K,Iterable<V>)对的数据集

注意: 如果您正在分组以执行聚合(例如总和或 平均)在每个键上,使用reduceByKeyaggregateByKey会产生更好的结果 性能。

注意: 默认情况下,输出中的并行级别取决于父RDD的分区数。 您可以传递一个可选的numTasks参数来设置不同数量的任务。

val sourceRdd4 = sc.parallelize(Seq(

("java", 1),

("java",3),

("java",18),

("c++", 1),

("js", 1),

("R",1),

("python",3),

("python",6),

("scala",2),

("scala", 1),

("scala",21)), 2)

val groupedRDD = sourceRdd4.groupByKey()

groupedRDD.foreach(println)

结果:

(scala,CompactBuffer(1, 2, 21))

(R,CompactBuffer(1))

(python,CompactBuffer(3, 6))

(java,CompactBuffer(1, 3, 18))

(c++,CompactBuffer(1))

(js,CompactBuffer(1))

reduceByKey(func, [numTasks])

解释:当在(K, V)对的数据集上调用时,返回(K,V)对的数据集,其中每个键的值使用给定的duce函数func 聚合,该函数的类型必须是(V,V)=>V。与groupByKey中一样,可以通过可选的第二个参数配置Reduce任务的数量。

val sourceRdd4 = sc.parallelize(Seq(

("java", 1),

("java",3),

("java",18),

("c++", 1),

("js", 1),

("R",1),

("python",3),

("python",6),

("scala",2),

("scala", 1),

("scala",21)), 2)

val reduceByKeyRDD = sourceRdd4.reduceByKey(+)

reduceByKeyRDD.foreach(println)

结果:

(scala,24)

(R,1)

(python,9)

(java,22)

(c++,1)

(js,1)

aggregateByKey(zeroValue)(seqOp, combOp, [numTasks])

解释:当在(K,V)对的数据集上调用时,返回(K,U)对的数据集,其中每个键的值使用给定的组合函数和中性"零"值聚合。允许不同于输入值类型的聚合值类型,同时避免不必要的分配。与groupByKey中一样,可以通过可选的第二个参数来配置Reduce任务的数量。

val sourceRdd4 = sc.parallelize(Seq(

("java", 1),

("java",3),

("java",18),

("c++", 1),

("js", 1),

("R",1),

("python",3),

("python",6),

("scala",2),

("scala", 1),

("scala",21)), 2)

val aggregateRdd = sourceRdd4.aggregateByKey((0, 0))(//初始值

(acc, num) => (acc._1 + num, acc._2 + 1), // 部分聚合

(acc1, acc2) => (acc1._1 + acc2._1, acc1._2 + acc2._2) // 合并聚合结果

)

aggregateRdd.foreach(println)

结果:

(scala,(24,3))

(R,(1,1))

(python,(9,2))

(java,(22,3))

(c++,(1,1))

(js,(1,1))

sortByKey([ascending], [numTasks])

解释:当在K实现Order的(K, V)对的数据集上调用时,返回(K,V)对的数据集,这些数据集按键按升序或降序排序,如布尔ascending参数中指定的那样。

复制代码
val sourceRdd5 = sc.parallelize(Seq(
  ("java", 96),
  ("c++", 94),
  ("js", 85),
  ("R",88),
  ("python",96),
  ("scala",91)))
//使用 sortByKey 进行排序,默认为升序
val sortedAscRdd = sourceRdd5.sortByKey()
//降序排序
val sortedDescRdd = sourceRdd5.sortByKey(false)
sortedAscRdd.foreach(println)
println("-------------------------------")
sortedDescRdd.foreach(println)
println("------------将key和value调换-------------------")
val sortedAscRdd2 = sourceRdd5.map(x=>{(x._2,x._1)}).sortByKey()
val sortedDescRdd2 = sourceRdd5.map(x=>{(x._2,x._1)}).sortByKey(false)
sortedAscRdd2.foreach(println)
println("-------------------------------")
sortedDescRdd2.foreach(println)

结果:

(R,88)

(c++,94)

(java,96)

(js,85)

(python,96)

(scala,91)


(scala,91)

(python,96)

(js,85)

(java,96)

(c++,94)

(R,88)

------------将key和value调换-------------------

(85,js)

(88,R)

(91,scala)

(94,c++)

(96,java)

(96,python)


(96,java)

(96,python)

(94,c++)

(91,scala)

(88,R)

(85,js)

join(otherDataset, [numTasks])

解释:当在(K,V)和(K,W)类型的数据集上调用时,返回(K,(V,W))对的数据集,其中每个键的所有元素对。 外连接通过leftOuterJoinrightOuterJoin和fullOuterJoinfullOuterJoin

val languageTimeRdd = sc.parallelize(List(("pascal",1970), ("c", 1972),("c++", 1983),("perl", 1987),("python", 1991),("java", 1995)))

val languageCreatorRdd2 = sc.parallelize(List(("java", "James Gosling"), ("python", "Guido van Rossum"), ("perl", "Larry Wall"), ("ruby", "Yukihiro Matsumoto")))

//inner join

println("----------inner join-------------")

val innerJoinResult = languageTimeRdd.join(languageCreatorRdd2)

innerJoinResult.foreach(println)

//left join

println("----------left join-------------")

val leftJoinResult = languageTimeRdd.leftOuterJoin(languageCreatorRdd2)

leftJoinResult.foreach(println)

//right join

println("----------right join-------------")

val rightJoinResult = languageTimeRdd.rightOuterJoin(languageCreatorRdd2)

rightJoinResult.foreach(println)

//full outer join

println("----------full outer join-------------")

val fullJoinResult = languageTimeRdd.fullOuterJoin(languageCreatorRdd2)

fullJoinResult.foreach(println)

结果:

----------inner join-------------

(python,(1991,Guido van Rossum))

(java,(1995,James Gosling))

(perl,(1987,Larry Wall))

----------left join-------------

(c++,(1983,None))

(python,(1991,Some(Guido van Rossum)))

(pascal,(1970,None))

(java,(1995,Some(James Gosling)))

(perl,(1987,Some(Larry Wall)))

(c,(1972,None))

----------right join-------------

(python,(Some(1991),Guido van Rossum))

(java,(Some(1995),James Gosling))

(perl,(Some(1987),Larry Wall))

(ruby,(None,Yukihiro Matsumoto))

----------full outer join-------------

(c++,(Some(1983),None))

(python,(Some(1991),Some(Guido van Rossum)))

(pascal,(Some(1970),None))

(java,(Some(1995),Some(James Gosling)))

(perl,(Some(1987),Some(Larry Wall)))

(ruby,(None,Some(Yukihiro Matsumoto)))

(c,(Some(1972),None))

cogroup(otherDataset, [numTasks])

解释:当在(K, V)和(K,W)类型的数据集上调用时,返回(K,(Iterable<V>,Iterable<W>))元组的数据集。此操作也称为groupWith.

val languageTimeRdd = sc.parallelize(List(("pascal",1970), ("c", 1972),("c++", 1983),("perl", 1987),("python", 1991),("java", 1995)))

val languageCreatorRdd2 = sc.parallelize(List(("java", "James Gosling"), ("python", "Guido van Rossum"), ("perl", "Larry Wall"), ("ruby", "Yukihiro Matsumoto")))

val cogroupRdd = languageTimeRdd.cogroup(languageCreatorRdd2)

cogroupRdd.foreach(println)

结果:

(c++,(CompactBuffer(1983),CompactBuffer()))

(python,(CompactBuffer(1991),CompactBuffer(Guido van Rossum)))

(pascal,(CompactBuffer(1970),CompactBuffer()))

(java,(CompactBuffer(1995),CompactBuffer(James Gosling)))

(perl,(CompactBuffer(1987),CompactBuffer(Larry Wall)))

(ruby,(CompactBuffer(),CompactBuffer(Yukihiro Matsumoto)))

(c,(CompactBuffer(1972),CompactBuffer()))

cartesian(otherDataset)

解释:当在T和U类型的数据集上调用时,返回(T,U)对(所有元素对)的数据集。

val languageTimeRdd = sc.parallelize(List(("pascal",1970), ("c", 1972),("c++", 1983),("perl", 1987),("python", 1991),("java", 1995)))

val languageCreatorRdd2 = sc.parallelize(List(("java", "James Gosling"), ("python", "Guido van Rossum"), ("perl", "Larry Wall"), ("ruby", "Yukihiro Matsumoto")))

val cartesianRdd = languageTimeRdd.cartesian(languageCreatorRdd2)

cartesianRdd.foreach(println)

结果:

((pascal,1970),(java,James Gosling))

((pascal,1970),(python,Guido van Rossum))

((pascal,1970),(perl,Larry Wall))

((pascal,1970),(ruby,Yukihiro Matsumoto))

((c,1972),(java,James Gosling))

((c,1972),(python,Guido van Rossum))

((c,1972),(perl,Larry Wall))

((c,1972),(ruby,Yukihiro Matsumoto))

((c++,1983),(java,James Gosling))

((c++,1983),(python,Guido van Rossum))

((c++,1983),(perl,Larry Wall))

((c++,1983),(ruby,Yukihiro Matsumoto))

((perl,1987),(java,James Gosling))

((perl,1987),(python,Guido van Rossum))

((perl,1987),(perl,Larry Wall))

((perl,1987),(ruby,Yukihiro Matsumoto))

((python,1991),(java,James Gosling))

((python,1991),(python,Guido van Rossum))

((python,1991),(perl,Larry Wall))

((python,1991),(ruby,Yukihiro Matsumoto))

((java,1995),(java,James Gosling))

((java,1995),(python,Guido van Rossum))

((java,1995),(perl,Larry Wall))

((java,1995),(ruby,Yukihiro Matsumoto))

pipe(command, [envVars])

解释:通过shell命令(例如Perl或bash脚本)管道RDD的每个分区。RDD元素被写入进程的标准输入,输出到其标准输出的行作为字符串的RDD返回。

因为需要用到脚本,所以必须要到linux环境中

我们执行spark-shell --master yarn 来测试该算子

val sourceRdd6 = sc.parallelize(List("24/07/08 16:43:39 INFO Executor:",

"24/07/08 16:43:40 INFO Utils:",

"24/07/08 16:43:36 INFO SecurityManager",

"24/07/08 16:43:35 INFO SparkContext"))

val pipeRdd = sourceRdd6.pipe("cut -c 19-")

pipeRdd.collect().foreach(println)

结果:

INFO Executor:

INFO Utils:

INFO SecurityManager

INFO SparkContext

coalesce(numPartitions)

解释:将RDD中的分区数量减少到num分区。对于过滤大型数据集后更有效地运行操作很有用。

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

//分区变少 无shuffle

val coalesceRDD1 = sourceRdd.coalesce(1,false)

//分区变少 有shuffle

val coalesceRDD2 = sourceRdd.coalesce(1,true)

//分区变多 无shuffle

val coalesceRDD3 = sourceRdd.coalesce(5,false)

//分区变多 有shuffle

val coalesceRDD4 = sourceRdd.coalesce(5,true)

println(coalesceRDD1.getNumPartitions)

println(coalesceRDD2.getNumPartitions)

println(coalesceRDD3.getNumPartitions)

println(coalesceRDD4.getNumPartitions)

结果:

1

1

3

5

repartition(numPartitions)

解释:随机重新洗牌RDD中的数据以创建更多或更少的分区并在它们之间进行平衡。 这总是在网络上打乱所有数据。(内部调用的也是coalesce 默认是进行shuffle)

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

//分区变少

val repartitionRdd1 = sourceRdd.repartition(2)

//分区变多

val repartitionRdd2 = sourceRdd.repartition(6)

println(repartitionRdd1.getNumPartitions)

println(repartitionRdd2.getNumPartitions)

结果:

2

6

repartitionAndSortWithinPartitions(partitioner)

解释:根据给定的分区器重新分区RDD,并且在每个结果分区中, 按键对记录进行排序。这比调用repartition然后在其中排序更有效 每个分区,因为它可以将排序向下推到洗牌机械中。

repartitionAndSortWithinPartitions是Spark官网推荐的一个算子,官方建议,如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。

val sourceRdd4 = sc.parallelize(Seq(

("java", 1),

("java",3),

("java",18),

("c++", 1),

("js", 1),

("R",1),

("python",3),

("python",6),

("scala",2),

("scala", 1),

("scala",21)), 2)

println("-----------------------key是字符串--分区变多--------------------------")

val repartitionAndSortWithinPartitionsRdd1 = sourceRdd4.repartitionAndSortWithinPartitions(new MyPartitioner(4))

repartitionAndSortWithinPartitionsRdd1.foreach(println)

println("-----------------------key是字符串--分区变少--------------------------")

val repartitionAndSortWithinPartitionsRdd2 = sourceRdd4.repartitionAndSortWithinPartitions(new MyPartitioner(1))

repartitionAndSortWithinPartitionsRdd2.foreach(println)

println("-----------------------key是数字--分区变多--------------------------")

val repartitionAndSortWithinPartitionsRdd3 = sourceRdd4.map(x=>{(x._2,x._1)}).repartitionAndSortWithinPartitions(new MyPartitioner(4))

repartitionAndSortWithinPartitionsRdd3.foreach(println)

println("-----------------------key是数字--分区变少--------------------------")

val repartitionAndSortWithinPartitionsRdd4 = sourceRdd4.map(x=>{(x._2,x._1)}).repartitionAndSortWithinPartitions(new MyPartitioner(1))

repartitionAndSortWithinPartitionsRdd4.foreach(println)

复制代码
class MyPartitioner(partitions: Int) extends Partitioner {
  require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")

  override def numPartitions: Int = partitions

  override def getPartition(key: Any): Int = {
    val index = Math.abs(key.hashCode()) % numPartitions
    println("key:"+key+"---所属分区:"+index)
    index
  }
}

结果:

-----------------------key是字符串--分区变多--------------------------

key:java---所属分区:2

key:java---所属分区:2

key:java---所属分区:2

key:c++---所属分区:3

key:js---所属分区:1

key:R---所属分区:2

key:python---所属分区:0

key:python---所属分区:0

key:scala---所属分区:2

key:scala---所属分区:2

key:scala---所属分区:2

(python,3)

(python,6)

(js,1)

(R,1)

(java,1)

(java,3)

(java,18)

(scala,2)

(scala,1)

(scala,21)

(c++,1)

-----------------------key是字符串--分区变少--------------------------

key:java---所属分区:0

key:java---所属分区:0

key:java---所属分区:0

key:c++---所属分区:0

key:js---所属分区:0

key:R---所属分区:0

key:python---所属分区:0

key:python---所属分区:0

key:scala---所属分区:0

key:scala---所属分区:0

key:scala---所属分区:0

(R,1)

(c++,1)

(java,1)

(java,3)

(java,18)

(js,1)

(python,3)

(python,6)

(scala,2)

(scala,1)

(scala,21)

-----------------------key是数字--分区变多--------------------------

key:1---所属分区:1

key:3---所属分区:3

key:18---所属分区:2

key:1---所属分区:1

key:1---所属分区:1

key:1---所属分区:1

key:3---所属分区:3

key:6---所属分区:2

key:2---所属分区:2

key:1---所属分区:1

key:21---所属分区:1

(1,java)

(1,c++)

(1,js)

(1,R)

(1,scala)

(21,scala)

(2,scala)

(6,python)

(18,java)

(3,java)

(3,python)

-----------------------key是数字--分区变少--------------------------

key:1---所属分区:0

key:3---所属分区:0

key:18---所属分区:0

key:1---所属分区:0

key:1---所属分区:0

key:1---所属分区:0

key:3---所属分区:0

key:6---所属分区:0

key:2---所属分区:0

key:1---所属分区:0

key:21---所属分区:0

(1,java)

(1,c++)

(1,js)

(1,R)

(1,scala)

(2,scala)

(3,java)

(3,python)

(6,python)

(18,java)

(21,scala)

2、Actions 算子

reduce(func)

解释:使用函数func(它接受两个参数并返回一个)聚合数据集的元素。该函数应该是可交换和关联的,以便可以并行正确计算。

复制代码
val sourceRdd4 = sc.parallelize(Seq(
  ("java", 1),
  ("java",3),
  ("java",18),
  ("c++", 1),
  ("js", 1),
  ("R",1),
  ("python",3),
  ("python",6),
  ("scala",2),
  ("scala", 1),
  ("scala",21)), 2)

val reduceRdd = sourceRdd4.reduce((kv1,kv2)=>{

var newKey:String = kv1._1

val sumNum:Int = kv1._2+kv2._2

if(!kv1._1.contains(kv2._1)){

newKey = kv1.1 +""+kv2._1

}

(newKey,sumNum)

})

println(reduceRdd._1+"----"+reduceRdd._2)

结果:

java_c++_js_R_python_scala----58

collect()

解释:在驱动程序中以数组的形式返回数据集的所有元素。这通常在返回足够小的数据子集的过滤器或其他操作之后很有用。

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

sourceRdd.collect().foreach(println)

结果:

1

2

3

4

5

6

7

8

9

10

count()

解释:返回数据集中元素的数量。

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

println(sourceRdd.count())

结果:

10

first()

解释:返回数据集的第一个元素(类似于take(1) )。

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

println(sourceRdd.first())

结果:

1

take(n)

解释:返回包含数据集前n个元素的数组

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

sourceRdd.take(3).foreach(println)

结果:

1

2

3

takeSample(withReplacement, num, [seed])

解释:返回一个数组,其中包含数据集num元素的随机样本,有或没有替换,可选地预先指定随机数生成器种子。

val sourceRdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10),3)

sourceRdd.takeSample(false,5).foreach(println)

println("---------------")

sourceRdd.takeSample(true,5).foreach(println)

结果:

6

1

9

4

3


3

2

9

4

2

takeOrdered(n, [ordering])

解释:使用它们的自然顺序或自定义比较器返回RDD的前n个元素

val sourceRd8 = sc.parallelize(List(13,2,6,4,8,3,7,8,91,1),3)

sourceRd8.takeOrdered(5).foreach(println)

结果:

1

2

3

4

6

saveAsTextFile(path)

解释:将数据集的元素作为文本文件(或一组文本文件)写入本地文件系统、HDFS或任何其他Hadoop支持的文件的给定目录中系统。Spark将在每个元素上调用toString将其转换为文件中的一行文本。

saveAsSequenceFile(path)

解释:将数据集的元素写入本地文件系统、HDFS或任何其他Hadoop支持的文件系统的给定路径中的Hadoop Sequence File。这在实现Hadoop的Writable接口的键值对的RDD上可用。在Scala中,它也适用于隐式转换为Writable的类型(Spark包括Int、Double、String等基本类型的转换)。

saveAsObjectFile(path)

解释:使用Java序列化以简单格式写入数据集的元素,然后可以使用 SparkContext.objectFile()。

countByKey()

解释:仅在(K,V)类型的RDD上可用。返回(K,Int)对的哈希图以及每个键的计数。

val sourceRdd4 = sc.parallelize(Seq(

("java", 1),

("java",3),

("java",18),

("c++", 1),

("js", 1),

("R",1),

("python",3),

("python",6),

("scala",2),

("scala", 1),

("scala",21)), 2)

sourceRdd4.countByKey().foreach(println)

结果:

(java,3)

(c++,1)

(scala,3)

(python,2)

(js,1)

(R,1)

foreach(func)(上面已经演示过)

解释:对数据集的每个元素运行一个函数func。这通常用于副作用,例如更新累加器或与外部存储系统交互。

注意:在foreach()之外修改除Accumulator之外的变量可能会导致未定义的行为。有关详细信息,请参阅了解闭包。

3、Shuffle操作

Spark中的某些操作会触发称为shuffle的事件。shuffle是Spark重新分配数据的机制,以便在分区之间以不同的方式分组。这通常涉及跨 executors 和 节点 复制数据,因此shuffle是复杂且成本高昂的操作。

我们以 reduceByKey 为例说明:

reduceByKey 生成一个新的RDD,相同key的所有value会被组合到一起,这就需要将本分区中不同的key发送到不同的节点。这对于Spark来说是一个全向操作。它必须从所有分区中读取以查找所有key的所有value, 然后将跨分区的value聚集在一起以计算每个key的最终结果- 这就是Shuffle

尽管新洗牌数据的每个分区中的元素集是确定的,分区本身的顺序也是确定的,但这些元素的顺序不是。如果希望在洗牌后可预测地排序数据,那么可以使用:

mapPartitions(对每个分区进行排序,例如,使用.sorted)

repartitionAndSortWithinPartitions(在重新分区的同时有效地对分区进行排序

sortBy(创建全局排序的RDD

由于Shuffle涉及磁盘I/O、数据序列化和 网络输入输出。为了整理这些数据,Spark会产生map任务和reduce任务来实现(这里说的并不是MapReduce中的mapTask和reduceTask)

在内部,单个map任务的结果会保存在内存中,当数据不合适再内存中时会溢写到磁盘(会产生磁盘IO开销和垃圾回收机制)

Shuffle会在磁盘产生大量的中间文件,这些文件会一直保留,直到不再使用相关RDD并被垃圾收集。这是为了再次使用RDD时提升Shuffle的性能。

4、持久化

当我们有迭代算法的需求时,我们就需要将之前的RDD进行缓存或者持久化,可以直接使用persist()或cache()来实现。Spark的缓存也是容错的,如果发现缓存的RDD有分区丢失,会通过血缘关系再次创建它。

每个RDD都可以选择不同的持久化级别,比如磁盘、内存。这些级别通过传递一个 StorageLevel对象来实现。

存储级别 含义
MEMORY_ONLY 将RDD存储为JVM中反序列化的Java对象。如果RDD不适合内存,某些分区将不会被缓存,并且每次需要时都会立即重新计算。这是默认级别。
MEMORY_AND_DISK 将RDD存储为JVM中反序列化的Java对象。如果RDD不适合内存,则将不适合磁盘的分区存储在磁盘上,并在需要时从那里读取它们。
MEMORY_ONLY_SER 只适用于Java和Scala,将RDD存储为序列化的Java对象(每个分区一个字节数组)。 这通常比反序列化对象更节省空间,尤其是在使用 快速序列化程序,但读取更占用CPU。
MEMORY_AND_DISK_SER 只适用于Java和Scala,类似于MEMORY_ONLY_SER,但是将不适合内存的分区溢出到磁盘,而不是每次需要时立即重新计算它们。
DISK_ONLY 仅将RDD分区存储在磁盘上
MEMORY_ONLY_2、MEMORY_AND_DISK_2等。 与上面的级别相同,但在两个集群节点上复制每个分区。
OFF_HEAP (实验阶段)类似于MEMORY_ONLY_SER,但将数据存储在 堆外内存。这需要启用堆外内存。

Spark还会自动在shuffle操作中持久化一些中间数据(例如reduceByKey),即使没有用户调用persist。这样做是为了避免在洗牌过程中节点失败时重新计算整个输入。

那么选择哪种存储级别呢?

Spark的存储级别旨在在内存使用和CPU效率之间提供不同的权衡。官网给了我们一下建议:

1、如果您的RDD适合默认存储级别(MEMORY_ONLY),请保持不变。 这是CPU效率最高的选项,允许RDD上的操作尽可能快地运行。

2、如果没有,请尝试使用MEMORY_ONLY_SER并选择一个快速序列化库来使对象更加节省空间,但访问速度仍然相当快。(适用于Java和Scala)

3、不要溢出到磁盘,除非计算数据集的函数很昂贵,或者它们过滤了大量数据。否则,重新计算分区可能与从磁盘读取分区一样快

4、如果您想要快速故障恢复(例如,如果使用Spark处理来自Web应用程序的请求),请使用复制的存储级别。所有存储级别都通过重新计算丢失的数据提供完全的容错能力,但复制的存储级别允许您继续在RDD上运行任务,而无需等待重新计算丢失的分区。

5、删除数据

Spark自动监控每个节点上的缓存使用情况,并在 最近最少使用(LRU)方式。如果您想手动删除RDD而不是等待 如果要从缓存中删除,请使用RDD.unpersist()方法。

二、共享变量

通常情况下,我们传递给算子的函数是作用在集群中各个worker上的,其中的变量被复制到每个worker,那么如何跨任务共享变量或者将变量回传给driver呢?Spark给我们提供了两种类型的共享变量即:广播变量和累加器。

1、广播变量

广播变量是将只读变量缓存在每台机器上,而不是将其副本与任务一起发送。例如,它们可以用来以有效的方式为每个节点提供大型输入数据集的副本。Spark还尝试使用有效的广播算法来分发广播变量,以降低通信成本。

创建方法为:

val broadcastVar = sc.broadcast(Array(1, 2, 3))

使用方法为:

broadcastVar.value

2、累加器

累加器是仅通过关联和交换操作"添加"到的变量,因此可以有效地并行支持。它们可用于实现计数器(如在MapReduce中)或求和。Spark原生支持数字类型的累加器,我们可以添加对其他类型的支持。

累加器在了解整体任务运行进度时很有用,使用方法如下:

创建累加器

val accum = sc.longAccumulator("My Accumulator")

//各个节点累加操作

accum.add(x)

//driver端获取结果

accum.value

相关推荐
材料苦逼不会梦到计算机白富美1 小时前
golang分布式缓存项目 Day 1
分布式·缓存·golang
拓端研究室TRL1 小时前
【梯度提升专题】XGBoost、Adaboost、CatBoost预测合集:抗乳腺癌药物优化、信贷风控、比特币应用|附数据代码...
大数据
黄焖鸡能干四碗1 小时前
信息化运维方案,实施方案,开发方案,信息中心安全运维资料(软件资料word)
大数据·人工智能·软件需求·设计规范·规格说明书
想进大厂的小王1 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情1 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
编码小袁1 小时前
探索数据科学与大数据技术专业本科生的广阔就业前景
大数据
WeeJot嵌入式2 小时前
大数据治理:确保数据的可持续性和价值
大数据
ZHOU西口3 小时前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
zmd-zk3 小时前
kafka+zookeeper的搭建
大数据·分布式·zookeeper·中间件·kafka
激流丶3 小时前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic