一、RDD持久化
RDD缓存
- RDD通过persist方法或cache方法可以将前面的计算结果缓存,默认情况下 persist() 会把数据以序列化的形式缓存在 JVM的堆空间中。
- 但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
- 源码显示cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。
RDD CheckPoint
- Spark中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能。
- 为当前RDD设置检查点。该函数将会创建一个二进制的文件,并存储到checkpoint目录中,该目录是用SparkContext.setCheckpointDir()设置的。在checkpoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除。对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。
import org.apache.log4j.{
Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.storage.StorageLevel
import org.apache.spark.{
SparkConf, SparkContext}
/**
* CREATE BY zhang
* DATE 2020.09.21 14:10 星期一
* DESC: spark 持久化
*/
object Demo01Cache {
def main(args: Array[String]): Unit = {
//控制日志输出
Logger.getLogger("org").setLevel(Level.ERROR)
/* 初始化 SparkContext */
val sc = new SparkContext(new SparkConf().setAppName(this.getClass.getName).setMaster("local[2]"))
/* 并行化生成RDD */
val scRDD: RDD[String] = sc.textFile("E:\\景航\\aaa.txt")
/**
* 多次被复用的RDD,可以用 cache
* 也可以用 persist
* 持久化是优化的一种重要手段
*/
/* 用默认的存储级别 */
val d: scRDD.type = scRDD.cache()
/* 可以定义存储的级别 参数用于传递持久化存储级别 */
val d1: scRDD.type = scRDD.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)
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)
*/
/* 持久化 仅内存存储 */
val d2: scRDD.type = scRDD.persist(StorageLevel.MEMORY_ONLY)
/*
设置ck的路径,生产环境 一般设置在分布式文件系统
分布式文件系统 常有 hdfs s3 touchyong fastdfs GFS
*/
sc.setCheckpointDir("E:\\景航\\aaa.txt")
scRDD.checkpoint() //为了防止节点挂掉等 导致数据丢失,可以直接从检查点 恢复数据
/* 关闭资源 */
sc.stop()
}
}
二、分区
- Spark目前支持Hash分区和Range分区
- 用户也可以自定义分区,Hash分区为当前的默认分区,
- Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce的个数?
- 只有Key-Value类型的RDD才有分区器的,非Key-Value类型的RDD分区器的值是None
- 每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的。
import org.apache.log4j.{
Level, Logger}
import org.apache.spark.{
Partitioner, SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
object Demo02DefindPartition {
def main(args: Array[String]): Unit = {
//控制日志输出
Logger.getLogger("org").setLevel(Level.ERROR)
/* 初始化 SparkContext */
val sc = new SparkContext(new SparkConf().setAppName(this.getClass.getName).setMaster("local[2]"))
/* 并行化生成RDD */
val scRDD: RDD[String] = sc.textFile("E:\\aaa.txt")
/* 扁平化操作 根据空格进行分割 */
val flatMapRDD: RDD[String] = scRDD.flatMap(_.split(" "))
/* 将value类型的RDD 转换成 key-value的RDD (String, Int) */
val pairRDD1: RDD[(String, Int)] = flatMapRDD.map((_, 1))
/* 将value类型的RDD 转换成 key-value的RDD (String, String) */
val pairRDD2: RDD[(String, String)] = flatMapRDD.map(num => (num, num))
/*
自定义分区
参数是 Partitioner
需要自建一个类继承 Partitioner
*/
val value: RDD[(String, Int)] = pairRDD1.partitionBy(new Mypartition(3))
/* 关闭资源 */
sc.stop()
}
}
/* 自建一个类继承 Partitioner */
class Mypartition(num: Int) extends Partitioner{
/* 指定分区个数 */
override def numPartitions: Int = num
/*
设置分区规则
这里按照单词的最后一个字母分区
*/
override def getPartition(key: Any): Int = {
/* 获取单词 */
val word = key.toString
/*
获取单词的最后一个字符
将这个字符根据ASCll表 转换成 Int类型
对分区个数取余
*/
word.substring(word.length - 1).toInt % numPartitions
}
}
三、保存和读取 SequenceFile 文件
import org.apache.log4j.{
Level, Logger}
import org.apache.spark.{
SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
/**
* CREATE BY zhang
* DATE 2020.09.21 15:06 星期一
* DESC: 文件类型读取和保存
* SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。
* Spark 有专门用来读取 SequenceFile 的接口。
* 在 SparkContext 中,可以调用 sequenceFile[ keyClass, valueClass](path)。
* 注意:SequenceFile文件只针对PairRDD
*/
object Demo03SequenceFile {
def main(args: Array[String]): Unit = {
//控制日志输出
Logger.getLogger("org").setLevel(Level.ERROR)
/* 初始化 SparkContext */
val sc = new SparkContext(new SparkConf().setAppName(this.getClass.getName).setMaster("local[2]"))
/*
保存为 SequenceFile 文件
*/
/* 并行化生成RDD */
val scRDD1: RDD[String] = sc.textFile("E:\\aaa.txt")
/* 扁平化操作 返回 value 的RDD */
val flatMapRDD: RDD[String] = scRDD1.flatMap(_.split(" "))
/* 用map 将 value 的RDD 映射成 key-value 的RDD */
val pairRDD: RDD[(String, Int)] = flatMapRDD.map((_, 1))
/* 保存为SequenceFile */
pairRDD.saveAsSequenceFile("E:/outputfile")
/*
读取 SequenceFile 文件
并行化生成RDD
*/
val scRDD2: RDD[(String, Int)] = sc.sequenceFile[String, Int]("E:/outputfile")
/* 遍历 */
scRDD2.foreach(println(_))
/* 关闭资源 */
sc.stop()
}
}
四、共享变量
累加器
累加器用来对信息进行聚合,通常在向 Spark传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果
import org.apache.log4j.{
Level, Logger}
import org.apache.spark.util.LongAccumulator
import org.apache.spark.{
SparkConf, SparkContext}
object Demo02Accumulator {
def main(args: Array[String]): Unit = {
//控制日志输出
Logger.getLogger("org").setLevel(Level.ERROR)
/* 初始化 SparkContext */
val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[2]")
val sc = new SparkContext(conf)
//driver 端的变量 声明一个累加器
val accumulator: LongAccumulator = sc.longAccumulator
println("accumulator:"+accumulator.value)
// sc.accumulator(0)
/* 并行化生成RDD,设置分区1个 */
val numRdd = sc.parallelize(Array(1,2,3,4,5),1)
numRdd.map(i => {
/* excutor端改变累加器的值 */
accumulator.add(i)
println("excutor端的accumulator:"+accumulator.value)
}).collect()
println("-----------")
println("driver端的accumulator:"+accumulator.value)
/* 关闭资源 */
sc.stop()
}
}
自定义累加器
import java.util
import org.apache.log4j.{
Level, Logger}
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{
SparkConf, SparkContext}
object Demo03DefindAccu {
def main(args: Array[String]): Unit = {
//控制日志输出
Logger.getLogger("org").setLevel(Level.ERROR)
/* 初始化 SparkContext */
val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[2]")
val sc = new SparkContext(conf)
//自定义的accumulator
val accum = new DefindAccumulator
//Accumulator must be registered before send to executor
sc.register(accum)
//过滤掉带字母的,然后求和
val sumRdd = sc.parallelize(Array("1", "2a", "3", "4b", "5", "6", "7cd", "8", "9"), 2)
val sum = sumRdd.filter(line =>{
val pattern = """^-?(\d+)"""
val flag = line.matches(pattern)
//纯数字 累加求和,非纯数字,累加到累加器
if (!flag) {
accum.add(line)
}
flag
}).map(_.toInt).reduce(_+_)
println("sum: " + sum) //纯数字 累加求和
//非纯数字 累加到累加器 最后结果做遍历
val value: util.HashSet[String] = accum.value
println(value)
/* 关闭资源 */
sc.stop()
}
}
//AccumulatorV2[String,Set[String]] 输入的是每个单词,输出的是一个集合
class DefindAccumulator extends AccumulatorV2[String,util.HashSet[String]]{
private val hashSet = new util.HashSet[String]()
//判断是否为空
override def isZero: Boolean = hashSet.isEmpty
//重置
override def reset(): Unit = hashSet.clear()
//添加元素
override def add(v: String): Unit = {
hashSet.add(v)
}
//多个分区的合并
override def merge(other: AccumulatorV2[String, util.HashSet[String]]): Unit = {
other match {
//每个分区计算的结果(accu.value)都是 HashSet,现在要多个分区的结果合并
case accu:AccumulatorV2[String, util.HashSet[String]] =>{
hashSet.addAll(accu.value) //多个hashSet合并
}
}
}
//将当前的结果 复制到新的的一个累加器里面
override def copy(): AccumulatorV2[String, util.HashSet[String]] = {
val accumulator = new DefindAccumulator
//复制
accumulator.hashSet.addAll(hashSet)
accumulator //返回对象
}
//返回值
override def value: util.HashSet[String] = hashSet
}
广播变量(调优策略)
- 广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark会为每个任务分别发送。
- 使用广播变量的过程如下:
(1) 通过对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T] 对象。 任何可序列化的类型都可以这么实现。
(2) 通过 value 属性访问该对象的值(在 Java 中为 value() 方法)。
(3) 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。
import org.apache.log4j.{
Level, Logger}
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.{
SparkConf, SparkContext}
object Demo04Broadcast {
def main(args: Array[String]): Unit = {
/* 常用作广播大变量 */
//控制日志输出
Logger.getLogger("org").setLevel(Level.ERROR)
/* 初始化 SparkContext */
val conf = new SparkConf().setAppName(this.getClass.getSimpleName).setMaster("local[2]")
val sc = new SparkContext(conf)
val num = 100
val numRdd = sc.parallelize(Array(1,2,3,4,5,6,7,8,9),4)
// 不广播时 会将变量 在每一个task 都要copy 一份
numRdd.map(_+num)
// 先将变量广播出去
val broadcastvalue: Broadcast[Int] = sc.broadcast(num)
numRdd.map(i => {
//广播变量的取值
var value = broadcastvalue.value
value += i
println(value)
}).collect()
/* 关闭资源 */
sc.stop()
}
}