例子

object Work02App {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[2]").setAppName(this.getClass.getSimpleName)
    val sc = new SparkContext(sparkConf)
     /* 用户     节目               列表  点击
      001,一起看|电视剧|军旅|亮剑,1,1
      001,一起看|电视剧|军旅|亮剑,1,0
      002,一起看|电视剧|军旅|士兵突击,1,1
      ==>
      001,一起看,2,1
      001,电视剧,2,1
      001,军旅,2,1
      001,亮剑,2,1
      */
    val lines = sc.parallelize(List(
      "001,一起看|电视剧|军旅|亮剑,1,1",
      "001,一起看|电视剧|军旅|亮剑,1,0",
      "002,一起看|电视剧|军旅|士兵突击,1,1"
    ))
    /**
      * ((001,一起看),(1,0))
      * ((001,电视剧),(1,0))
      * ((001,军旅),(1,0))
      * ((001,亮剑),(1,0))
      * ((002,一起看),(1,1))
      * ((002,电视剧),(1,1))
      * ((002,军旅),(1,1))
      * ((002,士兵突击),(1,1))
      * ((001,一起看),(1,1))
      * ((001,电视剧),(1,1))
      * ((001,军旅),(1,1))
      * ((001,亮剑),(1,1))
      */
    val rdd1 = lines.flatMap(x=>{
      val splits = x.split(",")
      val id = splits(0)
      val word = splits(1)
      val imp = splits(2).toInt
      val click = splits(3).toInt
      val words = word.split("\\|")
      words.map(x=>((id,x),(imp,click)))
    })
    /**
      * ((002,军旅),(1,1))
      * ((001,军旅),(2,1))
      * ((002,一起看),(1,1))
      * ((001,亮剑),(2,1))
      * ((001,一起看),(2,1))
      * ((002,电视剧),(1,1))
      * ((001,电视剧),(2,1))
      * ((002,士兵突击),(1,1))
      */
    val rdd2 = rdd1.groupByKey().mapValues(x=>{
      val imps = x.map(_._1).sum
      val clicks = x.map(_._2).sum
      (imps,clicks)
    })

    rdd2.reduceByKey((a,b)=>(a._1+b._1,a._2+b._2)).foreach(println(_))

    sc.stop()
  }
}

多目录输出
1、实现多目录输出自定义类

class MyMultipleTextOutputFormat extends MultipleTextOutputFormat[Any,Any]{
  //生成最终的key类型,这里不要,给null
  override def generateActualKey(key: Any, value: Any): Any = NullWritable.get()
  //生成最终生成的value类型,这里是String
  override def generateActualValue(key: Any, value: Any): Any = {
    value.asInstanceOf[String]
  }
  //生成文件名
  override def generateFileNameForKeyValue(key: Any, value: Any, name: String): String = {
    s"$key/$name"
  }
}

2、主类,使用saveAsHadoopFile(path, keyClass, valueClass, fm.runtimeClass.asInstanceOf[Class[F]])方法保存数据,指定参数

object MultiDirectory {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getSimpleName)
    val sc = new SparkContext(sparkConf)
    val out = "D:\\ssc\\out"
    FileUtils.deleteOutput(sc.hadoopConfiguration,out)
    sc.textFile("D:\\ssc\\access.log")
        .map(x=>{
          val splits = x.split("\t")
          (splits(1),x)
        }).saveAsHadoopFile(out,classOf[String],classOf[String],classOf[MyMultipleTextOutputFormat])
    sc.stop
  }
}

广播变量和累加器

在spark程序中,当一个传递给Spark操作(例如map和reduce)的函数在远程节点上面运行时,Spark操作实际上操作的是这个函数所用变量的一个独立副本。这些变量会被复制到每台机器上,并且这些变量在远程机器上的所有更新都不会传递回驱动程序。通常跨任务的读写变量是低效的,但是,Spark还是为两种常见的使用模式提供了两种有限的共享变量:广播变量(broadcast variable)和累加器(accumulator)

计数器

在spark应用程序中,我们经常会有这样的需求,如异常监控,调试,记录符合某特性的数据的数目,这种需求都需要用到计数器,如果一个变量不被声明为一个累加器,那么它将在被改变时不会再driver端进行全局汇总,即在分布式运行时每个task运行的只是原始变量的一个副本,并不能改变原始变量的值,但是当这个变量被声明为累加器后,该变量就会有分布式计数的功能。

spark 打印SequenceFileInputFormat 的schema_spark


计数器种类很多,但是经常使用的就是两种,longAccumulator和collectionAccumulator

需要注意的是计数器是lazy的,只有触发action才会进行计数,在不持久的情况下重复触发action,计数器会重复累加

1、LongAccumulator
Accumulators是只能通过associative和commutative操作"added"的变量,因此有效的并行支持,他们可以用于实现计数器(如MapReduce)和Spark本身支持数字类型的累加器,程序员还可以添加对新类型的支持

val sparkConf = new SparkConf().setMaster("local[*]").setAppName("MyLongAccumulator")
    val sc = new SparkContext(sparkConf)
    val acc = sc.longAccumulator("计数")
    val rdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9))
    val forRDD = rdd.map(x=>{
      acc.add(1L)
    })
    forRDD.count()
    println(acc.value) //9
    forRDD.count()
    println(acc.value) //18
    forRDD.count()
    println(acc.value) //27
    sc.stop()

使用longAccumulator做计数的时候要小心重复执行action导致的acc.value的变化,这是因为重复执行了count,累加器的数量成倍增长,解决方法,在action操作之前调用rdd的cache方法(或persist),这样在count后数据集就会被缓存下来,而无需从头开始计算

forRDD.cache()
    forRDD.count()
    println(acc.value)  //9
    forRDD.count()
    println(acc.value)  //9
    forRDD.count()
    println(acc.value)  //9

2、CollectionAccumulator
CollectionAccumulator,集合计数器,计数器中保存的是集合元素,通过泛型指定

def main(args: Array[String]): Unit = {
    /**
      * 需求:id后三位相同的加入计数器
      */
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("MyLongAccumulator")
    val sc = new SparkContext(sparkConf)
    //生成集合计数器
    val acc = sc.collectionAccumulator[People]("集合计数器")
    //生成RDD
    val rdd = sc.parallelize(Array(People("p1", 100000), People("p2", 100001),
      People("p3", 100222), People("p4", 100003)))
    rdd.map(x=>{
      val id = x.id.toString.reverse
      //满足条件就加入计数器
      if(id(0) == id(1) && id(0) == id(2)){
        acc.add(x)
      }
    }).count()
    println(acc.value) //[People(p1,100000), People(p3,100222)]
    sc.stop()
  }
  case class People(name:String,id:Long);

注意

  1. 计数器在Driver端定义赋初始值,计数器只能在Driver端读取最后的值
  2. 计数器不是一个调优操作

广播变量

如果我们要在分布式计算里面分发大对象,例如:字典,集合,黑白名单等,这个都会由Driver端进行分发,一般来讲,如果这个变量不是广播变量,那么每个task就会分发一份,这在task数目十分多的情况下Driver的带宽会成为系统的瓶颈,而且会大量消耗task服务器上的资源,如果将这个变量声明为广播变量,那么只是每个executor拥有一份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。

spark 打印SequenceFileInputFormat 的schema_spark_02


小表广播案例

spark有一种常见的优化方式就是小表广播,使用map join来代替reduce join,我们通过把小表的数据集广播到各个节点上,节省了shuffle操作

def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[2]")
      .setAppName(this.getClass.getSimpleName)
    val sc = new SparkContext(sparkConf)

    // Fact table  航线(起点机场, 终点机场, 航空公司, 起飞时间)
    val flights = sc.parallelize(List(
      ("SEA", "JFK", "DL",  "7:00"),
      ("SFO", "LAX", "AA",  "7:05"),
      ("SFO", "JFK", "VX", "7:05"),
      ("JFK", "LAX", "DL", "7:10"),
      ("LAX", "SEA", "DL",  "7:10")))

    // Dimension table 机场(简称, 全称, 城市, 所处城市简称)
    val airports = sc.parallelize(List(
      ("JFK", "John F. Kennedy International Airport", "New York", "NY"),
      ("LAX", "Los Angeles International Airport", "Los Angeles", "CA"),
      ("SEA", "Seattle-Tacoma International Airport", "Seattle", "WA"),
      ("SFO", "San Francisco International Airport", "San Francisco", "CA")))

    // Dimension table  航空公司(简称,全称)
    val airlines = sc.parallelize(List(
      ("AA", "American Airlines"),
      ("DL", "Delta Airlines"),
      ("VX", "Virgin America")))

    //最终统计结果:
    //出发城市           终点城市           航空公司名称         起飞时间
    //Seattle           New York       Delta Airlines           7:00
    //San Francisco     Los Angeles    American Airlines       7:05
    //San Francisco     New York       Virgin America            7:05
    //New York          Los Angeles    Delta Airlines           7:10
    //Los Angeles       Seattle        Delta Airlines          7:10

    val airportsBc = sc.broadcast(airports.map(x => (x._1, x._3)).collectAsMap())
    val airlinesBc = sc.broadcast(airlines.collectAsMap())

    flights.map{
      case (a,b,c,d) => (airportsBc.value.get(a).get,
        airportsBc.value.get(b).get,
        airlinesBc.value.get(c).get,
        d
      )
    }.foreach(println)
    sc.stop()
  }

为什么只能 broadcast 只读的变量
这就涉及一致性的问题,如果变量可以被更新,那么变量被某个节点更新,其他节点需要一块更新,这涉及了事务一致性。
注意事项

  1. 变量一旦被定义为一个广播变量,那么这个变量只能读,不能修改
  2. 能不能将一个RDD使用广播变量广播出去?因为RDD是不存储数据的。可以将RDD的结果广播出去。
  3. 广播变量只能在Driver端定义,不能在Executor端定义。
  4. 在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。
  5. 如果Executor端用到了Driver的变量,不使用广播变量在Executor有多少task就有多少Driver端的变量副本。
  6. 如果Executor端用到了Driver的变量,使用广播变量在每个Executor中只有一份Driver端的变量副本。

持久化
Spark非常重要的一个功能特性就是可以将RDD持久化在内存中。当对RDD执行持久化操作时,每个节点都会将自己操作的RDD的partition持久化到内存中,并且在之后对该RDD的反复使用中,直接使用内存缓存的partition。这样的话,对于针对一个RDD反复执行多个操作的场景,就只要对RDD计算一次即可,后面直接使用该RDD,而不需要反复计算多次该RDD。

巧妙使用RDD持久化,甚至在某些场景下,可以将spark应用程序的性能提升10倍。对于迭代式算法和快速交互式应用来说,RDD持久化,是非常重要的。
持久化的存储级别很多,常用的是MEMORY_ONLY、MEMORY_ONLY_SER、MEMORY_AND_DISK

Storage Level

Meaning

MEMORY_ONLY

将RDD作为不序列化的Java对象存储在JVM中。如果RDD不适合内存,那么一些分区将不会被缓存,而是在需要它们时动态地重新计算。这是默认级别。

MEMORY_AND_DISK

将RDD作为不序列化的Java对象存储在JVM中。如果RDD不适合内存,那么将不适合的分区存储在磁盘上,并在需要时从磁盘中读取它们。

MEMORY_ONLY_SER (Java and Scala)

将RDD存储为序列化的Java对象(每个分区一个字节数组)。这通常比反序列化对象更节省空间,特别是在使用快速序列化器时,但读取时需要更多cpu。

如何选择存储级别
Storage Level的选择是内存和CPU的权衡

  • 内存多:MEMORY_ONLY (不进行序列化)
  • CPU跟的上:MEMORY_ONLY_SER (进行了序列化,推介)
  • 不建议写Disk

使用cache()和persist()进行持久化操作,它们都是lazy的,需要action才能触发,默认使用MEMORY_ONLY

scala> forRDD.cache
res18: forRDD.type = MapPartitionsRDD[9] at map at <console>:27

scala> forRDD.count
res19: Long = 8

结果可以在Web UI的Storage中查看
如果需要清除缓存,使用unpersist(),清除缓存数据是立即执行的

scala> forRDD.unpersist()
res8: forRDD.type = MapPartitionsRDD[3] at map at <console>:28

修改存储级别

val forRDD = rdd.map(x => {
    //计数器做累加
    acc.add(1L)
}).persist(StorageLevel.MEMORY_ONLY_SER).count()

cache和persist有什么区别?

  • cache调用的persist,persist调用的persist(storage level)

序列化和非序列化有什么区别?

  • 序列化将对象转换成字节数组了,节省空间,占CPU
  • 开启kyro序列化(需要注册
  • spark默认序列化是java的序列化: implements java.io.Serializable
  • 使用spark的序列化必须要注册:conf.set(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)
object SerializationApp {

  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf()
    /**
     * 采用spark的序列化需要注册的
     */
    sparkConf.registerKryoClasses(Array(classOf[Info]))

    val sc = new SparkContext(sparkConf)

    val flag = sc.getConf.getInt("spark.flag",0)

    val infos = new ArrayBuffer[Info]()
    val names = Array("feifei","jiajia","jianren")
    val genders = Array("male","female")
    val addresses = Array("beijing","shanghai","hangzhou","chengdu")

    for (i <- 1 to 5000000){
      val name = names(Random.nextInt(3))
      val age = Random.nextInt(100)
      val gender = genders(Random.nextInt(2))
      val address= addresses(Random.nextInt(4))
      infos += Info(name,age,gender,address)
    }

    val rdd = sc.parallelize(infos)

    if(flag == 0) {
      rdd.persist(StorageLevel.MEMORY_ONLY)
    } else {
      rdd.persist(StorageLevel.MEMORY_ONLY_SER)
    }

    println(rdd.count())

    Thread.sleep(1000 * 60)

    sc.stop()
  }

  case class Info(name: String, age: Int, gender: String,address:String)
}

启动脚本

spark-submit \
--class com.ruozedata.bigdata.SerializationApp \
--name SerializationApp \
--master local[2] \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.flag=1 \
/home/ruoze/lib/ruozedata-spark-core-1.0.jar

结果说明:
不使用序列化:上述数据保存到内存是171.7MB
使用java序列化:126.8MB
spark序列化不注册:165.0MB
spark序列化注册:120.1MB

缓存的移除

  • Spark自动监视每个节点上的缓存使用情况,并以最近最少使用(LRU)的方式删除旧的数据分区。如果想要手动删除一个RDD,而不是等待它从缓存中消失,那么可以使用RDD.unpersist()方法。