核心内容:
1、Spark中的持久化
2、Spark中的广播
3、Spark中的累加器
OK,其实这篇博客应该是昨天就完成的,所以自我反省一下,最近这段时间任务比较多,也在一直准备数据结构的事情,估计还有6天左右数据结构与Spark就开始并行学习了,其实当时学完C语言就应该学习数据结构的,呵呵,还好自己当时基础扎实,OK,进入本篇博客的正题……
(一)Spark中的持久化
在Spark当中,持久化包括两方面的内容:
①在操作RDD的时候,如何保存结果(即Action部分)
②RDD在构建高效算法时所涉及的Persist(内含Cache)和CheckPoint
好的,先讲述Spark在操作RDD时如何将数据进行保存,即Action级别的RDD:
reduce:将上一次的计算结果作为下一次计算结果的第一个数字
示例代码:
val data = 1 to 10
val rdd1:RDD[Int] = sc.parallelize(data)
val sum:Int = rdd1.reduce((x:Int,y:Int)=>x+y)
println(sum)
运行结果:55
collect:collect就是将结果从集群中收集过来,如果想在命令终端中看到执行结果,就必须用collect
源码示例:
def collect(): Array[T] = withScope {
val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
Array.concat(results: _*)
}
到这里我们可以发现:凡是Action级别的算子都会触发一个作业(sc.runJob),注意:在Spark当中,一个应用程序里面可以有很多个Job(作业),但是在Hadoop当中一个应用程序就是一个Job(作业)。
count操作:计数操作
代码示例1:
val data = 1 to 10
val rdd1 = sc.parallelize(data)
val sum:Long = rdd1.count() //Action级别的算子会触发相应的作业
println(sum)
运行结果1:
10
示例代码2:
val data:Array[(String,Int)] = Array("Spark"->1,"Hadoop"->10,"Spark"->10)
val rdd1:RDD[(String,Int)] = sc.parallelize(data)
val sum:Long = rdd1.count()
println(sum)
运行结果2:
3
countByKey:相同Key的value数值有多少个(即相同分数的学生有多少个)
代码示例:
val data:Array[(Int,String)] = Array(100->"Spark",100->"Hadoop",90->"Java",90->"Scala")
val rdd1:RDD[(Int,String)] = sc.parallelize(data)
val sum:Map[Int, Long] = rdd1.countByKey()
println(sum)
for(ele<- sum) println(ele._1+"\t"+ele._2)
运行结果:
Map(100 -> 2, 90 -> 2)
100 2
90 2
take:获取RDD中前n个元素,并构成一个新的集合
代码示例:
val data = 1 to 10
val rdd1:RDD[Int] = sc.parallelize(data)
val rdd2:Array[Int] = rdd1.take(5)
for(ele<- rdd2) println(ele)
运行结果:
1
2
3
4
5
saveAsTextFile:将最终的结果存储到HDFS上面,当然也可以将结果输出到本地,主要看运行模式,在命令指定的时候之所以不写Hadoop中HDFS的IP地址和端口号,主要是因为有上下文可以进行自动推断。
代码示例:(本例子采用的是Spark的本地运行模式,也是第一次试验)
package com.appache.spark.app
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* Created by hp on 2016/12/7.
*/
object App
{
def main(args:Array[String]):Unit=
{
val conf = new SparkConf()
conf.setAppName("App")
conf.setMaster("local")//本地模式下进行测试
val sc = new SparkContext(conf)
val lines:RDD[String] = sc.textFile("C:\\word.txt")
val words:RDD[String] = lines.flatMap(line=>line.split(" "))
val pairs:RDD[(String,Int)] = words.map(word=>(word,1))
val wordCounts:RDD[(String,Int)] = pairs.reduceByKey((x:Int,y:Int)=>x+y)
wordCounts.saveAsTextFile("C:\\dir1209\\") //看了嘛,将结果输出到了本地C盘
sc.stop()
}
}
运行结果:
OK,到这里我们将Spark中持久化的第一方面—如何保存结果就讲完了,接下来我们讲述
Spark持久化的第二方面—RDD在构建高效算法时所涉及的Persist(内含Cache)和CheckPoint,即我们通过persist进行持久化。
首先我们需要讲述一下persist持久化的原因:
在Spark当中,凡是稍微复杂的算法里面都含有persist持久化,默认情况下Spark中的数据是放在内存当中的,放在内存中比较适合高速的迭代(例如在我们的Stage中含有1000个步骤,其实在执行的时候,在第一个步骤会输入数据,在第1000个步骤会产生输出数据,中间并不会产生临时数据。),这样的迭代对于我们的迭代是非常好的事情。还有我们都知道,RDD是存在血统继承关系的,如果后面的数据分片出错之后或者RDD本身出错之后,可以根据前面依赖的血统关系算出来(比如91步出错,我们可以从第90步重新计算而不是从第1步进行计算)。但是呢话虽然是这么说,但是就像是刚才的1000个步骤,如果我们曾经没有对父RDD进行Persist持久化的话,我们此时还需要从头开始计算。
(呵呵,在此插入一个缓存和内存的介绍,转载网址:http://wenda.so.com/q/1384669792067437?src=300)
在Spark当中,需要Persist持久化的场景(5种场景):
1、在Spark的应用程序中,如果某个步骤计算特别耗时,那么再次重新计算的话代价就很大,此时我们需要Persist持久化保存数据。
2、在Spark的应用程序中,如果某个计算链条特别长的话,重新计算的话代价也是非常大的,此时我们需要Persist持久化保存数据。
3、CheckPoint所在的RDD也要进行持久化,从而持久化数据:
CheckPoint本身是lazy级别的,在你触发一个Job之后,你开始算你的Job,Job算完之后,转过来Spark调度框架如果发现你的RDD有CheckPoint标记的话,转过来调度框架会基于CheckPoint会在提交一个作业。即我们的CheckPoint也会触发一个新的作业(呵呵,是lazy的也会触发作业啊)。由于CheckPoint已经将数据计算过一遍,但是如果我们不持久化的话,CheckPoint就需要在重新计算一遍,因此如果在第一次计算的时候我们就进行CheckPoint,那么后续在进行计算的时候速度就会非常的块,所以CheckPoint所在的RDD的时候一定要进行Persist持久化。
当然,CheckPoint是RDD的操作算子,肯定是手动的—-即我们会写一个具体的RDD.persist或者RDD.cache,然后在写RDD.checkpoint。(呵呵,就是RDD.cache.checkpoint或者RDD.persist.checkpoint吧!)
4、Shuffle之后要进行持久化:
因为Shuffle要进行网络的传输,而网络传输的风险是很大的,如果数据丢失的话就需要重来,但是如果我们进行了Persist,数据丢失的话就会基于Persist进行恢复。
注意:上面这4个步骤都是手动持久化的
5、Shuffle之前(系统框架默认帮我们做的,即Spark框架默认帮助我们把数据持久化到本地磁盘)
当然,在这里强调一下Cache和Persist的关系:
Cache是Persist持久化的一种特殊情况,Persist可以将结果放在内存、可以放在磁盘,当然也可以同时将结果放在内存和磁盘当中(以放多份副本);但是Cache就只是将结果在内存中,这就是Cache的特殊之处!
即Cache == StorageLevel.MEMORY_ONLY = persist()(当然这是我现在的理解)
从源码中我们可以发现Persist持久化有很多种方式:
/**
* Set this RDD's storage level to persist its values across operations after the first time
* it is computed. This can only be used to assign a new storage level if the RDD does not
* have a storage level set yet. Local checkpointing is an exception.
*/
def persist(newLevel: StorageLevel): this.type = {
if (isLocallyCheckpointed) {
// This means the user previously called localCheckpoint(), which should have already
// marked this RDD for persisting. Here we should override the old storage level with
// one that is explicitly requested by the user (after adapting it to use disk).
persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
} else {
persist(newLevel, allowOverride = false)
}
}
/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */
def cache(): this.type = persist()
Persist中的org.apache.spark.storage.StorageLevel(伴生对象)有很多种不同的方式,见源码:
object StorageLevel {
//不存储,进行Persist持久化的时候不存储
val NONE = new StorageLevel(false, false, false, false)
//将数据只存储的硬盘上面
val DISK_ONLY = new StorageLevel(true, false, false, false)
//在两个节点的磁盘上有副本
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
//MEMORY_ONLY是默认的持久化方式,当然默认的情况下肯定也是最高效的情况,也就是说Spark本身程序在运
行的情况下,数据都是放在内存中,这也给大家留下了一个Spark耗内存的概念,但是耗内存是方便进行迭代,
所以这种耗内存是正确的,耗内存越多越好
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
//在两台机器上缓存同样的数据,这样当一台机器缓存挂掉之后,可以到另外一台机器寻找数据
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
//序列化
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
//MEMORY_ONLY_SER_2序列化且有两份,为什么要进行序列化呢?序列化是为了减少数据的体积,减少存储空间
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
//切记:MEMORY_AND_DISK不是将数据同时放到内存和磁盘中,这个不是的!Spark会优先考虑使用内存,只要
内存足够,它都会将数据放在内存的,不会考虑将数据存放在磁盘的,但是如果内存不够的话,它会将数据放在
磁盘上。
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
//在两台机器上有两台副本
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
//序列化
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
//2份
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
//TachYon 非常重要
val OFF_HEAP = new StorageLevel(false, false, true, false)
上面的这些存储级别基本上满足我们的需求了。
但是有几个问题要特别声明一下:
问题1:MEMORY_ONLY持久化的相关理解?
MEMORY_ONLY是默认的持久化方式,当然默认的情况下肯定也是最高效的情况,也就是说Spark本身程序在运行的情况下,数据都是放在内存中,这也给大家留下了一个Spark耗内存的概念,但是耗内存是方便进行迭代, 所以这种耗内存是正确的,耗内存越多越好!
问题2:MEMORY_AND_DISK是不是将数据同时放到内存和磁盘中呢?
MEMORY_AND_DISK不是将数据同时放到内存和磁盘中,这个不是的!Spark会优先考虑使用内存,只要内存足够,它都会将数据放在内存的,不会考虑将数据存放在磁盘的,但是如果内存不够的话,它会将数据放在磁盘上。
问题3:MEMORY_ONLY_SER_2的相关概念?
如果你发现内存不够用,或者经常出现OOM的话,好的处理方式就是将内存中的内容进行序列化,但是序列化
的不好之处在于当我们要使用数据的时候要进行反序列化,这样会耗费CPU。
MEMORY_ONLY_SER_2代表序列化且有两份,为什么要进行序列化呢?序列化是为了减少数据的体积,减少存储空间。
问题4:MEMORY_AND_DISK和MEMORY的区别???
大家都是优先考虑基于内存,但是Memory的角度就必须是内存,MEMORY的角度如果内存不够的话就会出现OOM,或者说数据会丢失。但是如果是MEMORY_AND_DISK就会极大的降低OOM,只要磁盘没问题,不会导致数据丢失。
问题5:为什么有两份副本啊?
如果一个计算步骤特别耗时并且是基于内存的,如果有两份数据副本并且其中一份内存崩溃掉,此时可以切换到
另外一台机器进行计算,此时可以提高效率。即虽然耗了内存,但是提高了效率,即空间换时间(效率)。
问题6:既然这么多Persist持久化方式,我们应该如何选择呢?
MEMORY_ONLY —> MEMORY_ONLY_2 或者 MEMORY_ONLY_SER,但是MEMORY_AND_DISK虽然比较安全,但是不是最快的方式,而且磁盘IO读取太麻烦,所以能用内存尽量用内存,能用两份副本的内存尽量用两份副本的内存。
OK,到现在为止我们演示一下Persist持久化对计算的影响……
val wordCounts:RDD[(String,Int)] = pairs.reduceByKey((x:Int,y:Int)=>x+y).persist(StorageLevel.MEMORY_ONLY)
注意:Cache之后一定不能有其它算子(Cache不是Action),在实际工作的时候如果Cache后面有其它算子的话,它每次都会重新触发这个计算过程!
scala> val cached = sc.textFile("/word.txt").flatMap(line=>line.split(" ")).map(word=>(word,1)).reduceByKey(_+_).cache
scala> cached.count()
运行结果:
OK,在第一次触发作业之后,我们已经将作业的运行结果缓存起来了,此时我们在一次的运行作业:
从运行结果上面我们可以看出,经过缓存之后,运行时间从3s变成了73ms,时间大大减小。其实从运行结果我们可以看出,我们是在Shuffle之后进行持久化的。
通过缓存我们可以将数据缓存得以重复利用,当然我们也可以清除掉缓存中的数据,利用unpersist即可以将缓存中的内容进行清空:
scala> cached.unpersist()
16/12/09 16:47:12 INFO rdd.ShuffledRDD: Removing RDD 4 from persistence list
16/12/09 16:47:12 INFO storage.BlockManager: Removing RDD 4
res3: cached.type = ShuffledRDD[4] at reduceByKey at <console>:27
清空缓存之后我们在Storage中的内容也进行清空了。
注意:Persist是lazy级别的,unpersist是eager级别的,而之所以说persist是lazy级别的,是因为我们根本就没有计算数据,所以怎么缓存数据啊!
当然,上面的缓存方式和我们下面的缓存方式是一样的:
scala> val cached = sc.textFile("/word.txt").flatMap(line=>line.split(" ")).map(word=>(word,1)).reduceByKey(_+_).persist(StorageLevel.MEMORY_ONLY)
呵呵,看来以后向reduceByKey这种产生Shuffle的Transformation操作要进行缓存了!!!
OK,接下来我们谈论广播……,写到这里有点累了,哎,周日还要给工程硕士讲课,还是接着写吧!
应用实例:
package com.appache.spark.app
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* Created by hp on 2016/12/7.
*/
object App
{
def main(args:Array[String]):Unit=
{
val conf = new SparkConf()
conf.setAppName("App")
conf.setMaster("local")
val sc = new SparkContext(conf)
val number = 10
val broadCastNumber: Broadcast[Int] = sc.broadcast(number)
val data = sc.parallelize(1 to 10)
val result:RDD[Int] = data.map(x=>x * broadCastNumber.value)
for(ele<- result) println(ele)
sc.stop()
}
}
运行结果:
10
20
30
40
50
60
70
80
90
100
好的,图示和程序过后,我们详述一下广播的概念:
为什么要有广播?
广播的时候是将广播变量广播到Executor的内存中,所有的任务都会只享有这个全局只读的唯一一份变量所以会极大的减少网络传输,而且会极大的节省内存,减少OOM的可能。广播是由Driver发给当前Application分配的所有Executor内存级别的全局只读变量,Executor中的线程池中的线程共享该全局变量,极大的减少了网络传输(否则的话每个Task都要传输一次该变量),并极大的节省了内存,当然也隐形的提高了CPU的有效工作。
对于上面的实例,在广播之前number要发送4次(因为有4个Task),此时将会有4个number的数据副本,这种结果至于网络传输还是内存消耗都是极大的浪费。但是在广播之后,在Executor里面,就直接广播一份变量过来,即BroadCast到Executor的内存,此时在内存中只有这样的一份数据副本,大家共享这样的一份数据副本!
呵呵,广播变量类似于Java当中的静态的属性和方法:由操作系统只分配一块内存空间,大家共同使用这样的一块内存空间。
广播变量不需要手动销毁,应用程序存在,广播变量就存在,应用程序销毁,则广播变量也销毁。第一种情况下一般情况下都是没有优势的,但是数据量小的情况下没有必要广播,因为广播需要管理和维护的。
呵呵,接下来具体介绍累加器:Accumulator
为什么需要累加器?
从实际的角度讲,我们有广播全局变量的能力和局部变量是不是就完美了呢?其实还不是这样的……
累加器(Accumulator)的特征:全局级别的,且Executor中的Task只能修改,即只能增加累加器的内容,即对于Executor中的Task只能修改但不可读,只对Driver可读。
累加器在记录集群的状态,尤其是全局唯一状态的时候是至关重要的,因为作为分布式集群肯定是需要一个全局唯一状态的东西!累加器是非常重要的,它可以记录集群全局唯一的状态,而且只有Driver可读,因为Driver控制整个集群的程序,所以它有必要知道整个集群的状态。
累加器的要点:
1、累加器(集群全局唯一变量)在全局是唯一的,每次操作只增不减!
2、在Executor中只能修改,即只能增加其数值,同时在Driver中可以读取,如果想保持全局唯一的变量,使用累加器是不错的!
Excuter是集群共享的,可以理解为在Executor中是全局共享的。两个App不可以共享累加器,但是不同的job(作业)可以共享累加器!累加器是通过SparkContext进行创建的,获取全局唯一的状态对象。在Driver中可读,Executor中可以修改(让Executor只能修改,这是权限控制的问题),累加器既在Driver上面,又在Executor上面,但是核心在Driver上面。
实例程序:
val sc = new SparkContext(conf)
var sum = sc.accumulator(0) //累加器是Sc创建的!!
val data = sc.parallelize(1 to 10)
data.foreach(item=> sum += item)
println(sum)
运行结果:55
总结:Spark中所谓的累计器其实就是Hadoop中的计数器,是全局的,Spark将跨所有的Task来聚集累计器,并在Application(不是Job)结束时产生一个最终的结果。
OK,继续努力!!!