文章目录
高级应用
1、RDD的分片数量
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
parallelize(seq, numSlices)
}
def defaultParallelism: Int = {
assertNotStopped()
taskScheduler.defaultParallelism
}
numSlices:分片数,一个分片就是一个任务
2、函数转换的问题
class SearchFunctions(val query: String) extends java.io.Serializable{
def isMatch(s: String): Boolean = {
s.contains(query) }
def getMatchesFunctionReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
// 问题:"isMatch"表示"this.isMatch",因此我们要传递整个"this"
rdd.filter(isMatch)
}
def getMatchesFieldReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
// 问题:"query"表示"this.query",因此我们要传递整个"this"
rdd.filter(x => x.contains(query))
}
def getMatchesNoReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
// 安全:只把我们需要的字段拿出来放入局部变量中
val query_ = this.query
rdd.filter(x => x.contains(query_))
}
}
<1>如果RDD的转换中使用到了class中的方法或者变量,那么该class需要支持序列化
<2>如果通过局部变量的方式将class中的变量赋值为局部变量,那么不需要传递对象
3、RDD的持久化
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)
RDD是通过persist方法或者是cache方法可以将前面的计算结果缓存,默认情况下persist()会把数据以序列化的形式缓存在JVM的堆空间中;但是并不是这俩个方法被调用的时候立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点内存中,给后面提供重用。
而且通过观察源码我么可以发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别有很多种,存储级别在object的StorageLevel中定义的。
object StorageLevel {
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)
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)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
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)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
注意:在存储级别的末尾加上"_2"来把持久化数据存为俩份。
scala> val rdd = sc.makeRDD(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at makeRDD at <console>:25
scala> val nocache = rdd.map(_.toString+"["+System.currentTimeMillis+"]")
nocache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[20] at map at <console>:27
scala> val cache = rdd.map(_.toString+"["+System.currentTimeMillis+"]")
cache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[21] at map at <console>:27
scala> cache.cache
res24: cache.type = MapPartitionsRDD[21] at map at <console>:27
scala> nocache.collect
res25: Array[String] = Array(1[1505479375155], 2[1505479374674], 3[1505479374674], 4[1505479375153], 5[1505479375153], 6[1505479374675], 7[1505479375154], 8[1505479375154], 9[1505479374676], 10[1505479374676])
scala> nocache.collect
res26: Array[String] = Array(1[1505479375679], 2[1505479376157], 3[1505479376157], 4[1505479375680], 5[1505479375680], 6[1505479376159], 7[1505479375680], 8[1505479375680], 9[1505479376158], 10[1505479376158])
scala> nocache.collect
res27: Array[String] = Array(1[1505479376743], 2[1505479377218], 3[1505479377218], 4[1505479376745], 5[1505479376745], 6[1505479377219], 7[1505479376747], 8[1505479376747], 9[1505479377218], 10[1505479377218])
scala> cache.collect
res28: Array[String] = Array(1[1505479382745], 2[1505479382253], 3[1505479382253], 4[1505479382748], 5[1505479382748], 6[1505479382257], 7[1505479382747], 8[1505479382747], 9[1505479382253], 10[1505479382253])
scala> cache.collect
res29: Array[String] = Array(1[1505479382745], 2[1505479382253], 3[1505479382253], 4[1505479382748], 5[1505479382748], 6[1505479382257], 7[1505479382747], 8[1505479382747], 9[1505479382253], 10[1505479382253])
scala> cache.collect
res30: Array[String] = Array(1[1505479382745], 2[1505479382253], 3[1505479382253], 4[1505479382748], 5[1505479382748], 6[1505479382257], 7[1505479382747], 8[1505479382747], 9[1505479382253], 10[1505479382253])
cache.persist(org.apache.spark.storage.StorageLevel.MEMORY_ONLY)
缓存是有可能丢失的,或者存储于内存中的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也可以保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,是因为由于RDD的各个Partition是相互独立的,因此只需要将丢失的部分计算即可,并不需要重新计算全部的Partition。
注意:使用Tachyon可以实现堆外缓存。
4、RDD的检查点机制
Spark中对于数据的保存除了持久化之外,还提供了一种检查点机制。虽然cache和checkpoint都是给RDD提供缓存作用的,但是俩这存在着显著地区别:cache是把RDD计算出来之后放在内存中,checkpoint是把RDD保存在HDFS中,是多副本可靠存储,所以依赖链可以丢掉了,就斩断了依赖链是通过赋值实现的高容错性。
适合场景:
<1>DAG中的Lineage过长,如果要是重新计算的话,开销太大
<2>在宽依赖上做checkpoint获得的收益会更大
scala> val data = sc.parallelize(1 to 100 , 5)
data: org.apache.spark.rdd.RDD[Int] =ParallelCollectionRDD[12] at parallelize at <console>:12
scala> sc.setCheckpointDir("hdfs://master01:9000/checkpoint")
scala> data.checkpoint
scala> data.count
scala> val ch1 = sc.parallelize(1 to 2)
ch1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[33] at parallelize at <console>:25
scala> val ch2 = ch1.map(_.toString+"["+System.currentTimeMillis+"]")
ch2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[36] at map at <console>:27
scala> val ch3 = ch1.map(_.toString+"["+System.currentTimeMillis+"]")
ch3: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[37] at map at <console>:27
scala> ch3.checkpoint
scala> ch2.collect
res62: Array[String] = Array(1[1505480940726], 2[1505480940243])
scala> ch2.collect
res63: Array[String] = Array(1[1505480941957], 2[1505480941480])
scala> ch2.collect
res64: Array[String] = Array(1[1505480942736], 2[1505480942257])
在checkpoint过程中,该RDD的所有依赖于父RDD中的信息将全部转移出。对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。
5、RDD运行方式
<1>RDD任务划分
小结:
- 一个jar包就是一个Application
- 一个行动操作就是一个job,对应Hadoop中的一个MapReduce任务
- 一个Job有很多Stage组成,划分Stage是从后往前划分,遇到宽依赖则将前面的所有转换分为一个Stage
- 一个Stage是由很多Task组成,一个分区被一个Task所处理,所有分区数也叫做并行度
RDD的运行规划图如下:
<2>RDD依赖关系
<1>宽依赖:指的是多个子RDD的Partition会依赖同一个父RDD的Partition,同时会引起shuffle
<2>窄依赖:指的是每一个父RDD的Partition最多被子RDD的一个Partition使用
<3>DAG的生成
需要用Oozie来实现资源的调度
<4>Lineage
RDD只支持粗粒度转换,即使是在大量记录上执行的单个操作。将会创建RDD的一系列Lineage(即血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转化安行为,当该RDD的部分分区数据丢失时,它就能够根据这些信息来重新计算和恢复丢失的数据分区。
6、键值对RDD分区
Spark目前支持Hash分区和Range分区,用户也可以自定义分区,Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce个数。
但是HashPartitioner存在一些弊端:可能会导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据。这是由于HashPartitioner分区原理就是对于给定的value,计算器hashCode并除以分区个数取余,如果余数小于0,则用余数+分区个数,最后返回值就是这个key所属分区的ID
所以更多使用RangePartitioner:尽量保存每个分区中的数据量的均匀,而且分区与分区之间是有序的,一个分区中的 元素肯定都是比另一个分区内的元素小或者是大的。这种分区的作用:将一定范围内的数映射到某一个分区内,在实现中分界算法尤为重要,用到了水塘抽样算法。
注意:
1. 只有key-value类型的RDD才有分区的,非key-value类型的RDD分区值是None
2. 每个RDD的分区范围:0~numPartitions-1,决定这个值是属于哪个分区
3. 自定义分区主要通过继承partitioner抽象类实现,需要实现俩个方法
import org.apache.spark.{Partitioner, SparkConf, SparkContext}
class CustomerPartitioner(numPartition:Int) extends Partitioner{
//返回分区的总数
override def numPartitions: Int = {
numPartition
}
//根据传入的key返回分区的索引
override def getPartition(key: Any): Int ={
key.toString.toInt % numPartition
}
}
object CustomerPartitioner {
def main(args: Array[String]): Unit = {
val sc = new SparkContext(new SparkConf().setAppName("partitions").setMaster("local[*]"))
val rdd = sc.makeRDD(0 to 10,1).zipWithIndex()
val r = rdd.mapPartitionsWithIndex((index,items) => Iterator(index+"["+items.mkString(",")+"]")).collect()
for (i <- r){
println(i)
}
val rdd1 = rdd.partitionBy(new CustomerPartitioner(5))
val r1 = rdd1.mapPartitionsWithIndex((index,items) => Iterator(index+"["+items.mkString(",")+"]")).collect()
for (i <- r1){
println(i)
}
sc.stop()
}
}
运行结果如下:
7、RDD进阶
<1>累计器
Spark内部提供了一个默认的累加器,但是只能用于求和(局限性)
使用方法:
- 通过accumulator声明一个累加器,0是初始值
- 转换或者是行动操作中,通过blanklines+=n
- 在driver程序中,通过blanklines.value来获取值
- 累加器是懒执行的,需要行动触发
scala> val license = sc.textFile("./LICENSE")
license: org.apache.spark.rdd.RDD[String] = ./LICENSE MapPartitionsRDD[1] at textFile at <console>:24
scala> val blanklines = sc.accumulator(0)
warning: there were two deprecation warnings; re-run with -deprecation for details
blanklines: org.apache.spark.Accumulator[Int] = 0
scala> val rdd = license.flatMap(line => {
| if (line == "") {
| blanklines += 1
| }
| line.split(" ")
| })
rdd: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[2] at flatMap at <console>:28
scala> rdd.count
res4: Long = 3453
scala> blanklines.value
res5: Int = 48
<2>自定义累加器
使用方法:
- 需要继承AccumulatorV2抽象类,In就是输入的类型,Out是累加器输出的数据
- 如何使用:
. [1] 通过accumulator声明一个累加器,0位初始值
. [2]通过sc.register注册一个新的累加器
. [3]通过累加器实例名.add来添加数据
. [4]通过累加器实例名.value来获取累加器的值
3.最好不要在转换的时候访问累加器,而是在行动操作时候访问
4.转换或者行动操作过程中不能访问累加器的值,只能添加
具体操作如下:
package sparkAccumlator
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.util.AccumulatorV2
import scala.collection.mutable
class Accumlator extends AccumulatorV2[String,mutable.HashMap[String,Int]]{
//定义一个累加器
private val _hashAcc = new mutable.HashMap[String,Int]()
//检测是否为空
override def isZero: Boolean = {
_hashAcc.isEmpty
}
//拷贝一个新的累计器
override def copy(): AccumulatorV2[String,mutable.HashMap[String,Int]] = {
val newAcc = new Accumlator()
_hashAcc.synchronized{
newAcc._hashAcc ++= (_hashAcc)
}
newAcc
}
//重置一个新的累加器
override def reset(): Unit = {
_hashAcc.clear()
}
//每一个分区中用于添加数据的方法
override def add(v: String): Unit = {
_hashAcc.get(v) match {
case None => _hashAcc += ((v, 1))
case Some(a) => _hashAcc += ((v, a+1))
}
}
//合并每一个分区的输出
override def merge(other: AccumulatorV2[String,mutable.HashMap[String,Int]]): Unit = {
other match {
case o:AccumulatorV2[String,mutable.HashMap[String,Int]] => {
for ((k, v) <- o.value){
_hashAcc.get(k) match {
case None => _hashAcc += ((k, v))
case Some(a) => _hashAcc += ((k,a+v))
}
}
}
}
}
//输出值
override def value: mutable.HashMap[String, Int] = {
_hashAcc
}
}
object Accumlator {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("accumlator").setMaster("local[*]")
val sc = new SparkContext(sparkConf)
val hashAcc = new Accumlator()
//注册累计器
sc.register(hashAcc)
val rdd = sc.makeRDD(Array("a", "b", "c", "a", "b", "c", "d"))
rdd.foreach(hashAcc.add(_))
for ((k, v) <- hashAcc.value) {
println("【" + k + ":" + v + "】")
}
sc.stop()
}
}
<3>广播变量
- [1]如果使用本地变量不采用广播变量形式,那么每个分区需要进行一个拷贝
- [2]如果使用了广播变,那么每一个Excutor中会有该变量的一次拷贝,一个Excutor[JVM进程]中有很多分区
使用方法: - [1] 通过sc.broadcast来创建一个广播变量
- [2]通过value方法来获取广播变量的内容
- [3]广播变量只会给每个节点分发一次,因此用该作为只读值处理(修改这个值不会影响别的节点)
具体代码如下:
scala> val broadcast = sc.broadcast(Array(1, 2, 3, 4, 5))
broadcast: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(2)
scala> broadcast.value
res6: Array[Int] = Array(1, 2, 3, 4, 5)
注意:广播变量用来处理高效分发较大的对象;主要用在百兆数据的分发。