大数据技术Spark之Spark Core(三)

一:action

reduce(func) :作用: 通过 func 函数聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据。
collect():作用: 在驱动程序中,以数组的形式返回数据集的所有元素。
count():作用: 返回 RDD 中元素的个数
first():作用:返回RDD中的第一个元素
take(n):作用:返回一个由RDD的n个元素组成的数组
takeOrdered(n):作用:返回改RDD排序后的前n个元素组成的数组
aggregate:作用: aggregate 函数将每个分区里面的元素通过 seqOp 和初始值进行聚合,然后用
combine 函数将每个分区的结果和初始值(zeroValue)进行 combine 操作。这个函数最终返回
的类型不需要和 RDD 中元素类型一致。
fold(num)(func): 作用: 折叠操作, aggregate 的简化操作, seqop 和 combop 一样
saveAsTextFile(path):作用: 将数据集的元素以 textfile 的形式保存到 HDFS 文件系统或者其他支持的文件系统,对于每个元素, Spark 将会调用 toString 方法,将它装换为文件中的文本。
saveAsSequenceFile(path):作用: 将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以使 HDFS或者其他 Hadoop 支持的文件系统
countByKey():作用: 针对(K,V)类型的 RDD,返回一个(K,Int)的 map,表示每一个 key 对应的元素个数。
foreach(func):作用:在数据集的每一个元素上运行函数func进行更新

二:RDD中的函数传递

在实际开发中我们往往需要自己定义一些对于 RDD 的操作,那么此时需要主要的是,初始化工作是在 Driver 端进行的,而实际运行程序是在 Executor 端进行的,这就涉及到了跨进程通信,是需要序列化的。 下面我们看几个例子

package com.ityouxin.action

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

class Search(query:String) extends Serializable {
  def isMatch(s:String):Boolean={
    s.contains(query)
  }
  def getMatch1(rdd:RDD[String]):RDD[String]={
    rdd.filter(this.isMatch)
  }
  def getMatch2(rdd:RDD[String]):RDD[String]={
    rdd.filter(x=>x.contains(query))
  }
}
object SeriTest{
  def main(args: Array[String]): Unit = {
    val config: SparkConf = new SparkConf()
      .setAppName("Spark_Study")
      .setMaster("local[*]") //本地模式
    val sc =  new SparkContext(config)
    val rdd1 = sc.parallelize(Array("hadoop", "spark", "hive", "ityouxin"))
    val search = new Search("oop")
    val rdd2 = search.getMatch1(rdd1)
    println(rdd2.collect().mkString(","))
    val rdd3: RDD[String] = search.getMatch2(rdd1)
    println(rdd3.collect().mkString(","))
  }
}

三:RDD的依赖关系

窄依赖

窄依赖指的是每一个父 RDD 的 Partition 最多被子 RDD 的一个 Partition 使用,窄依赖我们形象的比喻为独生子女

宽依赖

宽依赖指的是多个子 RDD 的 Partition 会依赖同一个父 RDD 的 Partition,会引起 shuffle,总结: 宽依赖我们形象的比喻为超生

DAG:

DAG(Directed Acyclic Graph)叫做有向无环图,原始的 RDD 通过一系列的转换就就形成了 DAG,根据 RDD 之间的依赖关系的不同将 DAG 划分成不同的 Stage,对于窄依赖,partition 的转换处理在 Stage 中完成计算。对于宽依赖,由于有 Shuffle 的存在,只能在 parentRDD 处理完成后,才能开始接下来的计算,因此宽依赖是划分 Stage 的依据 。

任务划分(面试重点)

RDD 任务切分中间分为: Application、 Job、 Stage 和 Task

1) Application:初始化一个 SparkContext 即生成一个 Application
2) Job:一个 Action 算子就会生成一个 Job
3) Stage: 根据 RDD 之间的依赖关系的不同将 Job 划分成不同的 Stage, 遇到一个宽依赖则
划分一个 Stage。

4) Task: Stage 是一个 TaskSet, 将 Stage 划分的结果发送到不同的 Executor 执行即为一个
Task。
注意: Application->Job->Stage-> Task 每一层都是 1 对 n 的关系。

RDD缓存

RDD通过persist方法或cache方法可以将前面的计算结果缓存, 默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 时, 该 RDD 将会被缓存在计算节点的内存中,并供后面重用。

四:数据读取和保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。

文件格式分为: Text 文件、 Json 文件、 Csv 文件、 Sequence 文件以及 Object 文件;
文件系统分为:本地文件系统、 HDFS、 HBASE 以及数据库。

  1. text文件类型的数据
    读取:textFile(String) 保存:saveAsTextFile(String)
  1. Json文件

如果 JSON 文件中每一行就是一个 JSON 记录,那么可以通过将 JSON 文件当做文本
文件来读取,然后利用相关的 JSON 库对每一条数据进行 JSON 解析。
注意:使用 RDD 读取 JSON 文件处理很复杂,同时 SparkSQL 集成了很好的处理 JSON
文件的方式,所以应用中多是采用 SparkSQL 处理 JSON 文件。

读取文件:val json = sc.textFile("/people.json")

解析json数据:val result = json.map(JSON.parseFull)

  1. Sequence 文件

SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文
件(Flat File)。 Spark 有专门用来读取 SequenceFile 的接口。在 SparkContext 中,可以调用
sequenceFile keyClass, valueClass。
注意: SequenceFile 文件只针对 PairRDD

保存RDD:scala> rdd.saveAsSequenceFile(“file:///opt/module/spark/seqFile”)

读取文件:val seq = sc.sequenceFileInt,Int

  1. HDFS
    Spark 的整个生态系统与 Hadoop 是完全兼容的,所以对于 Hadoop 所支持的文件类型或者数据库类型,Spark 也同样支持.另外,由于 Hadoop 的 API 有新旧两个版本,所以 Spark 为了能够兼容 Hadoop 所有的版本,也提供了两套创建操作接口.对于外部存储创建操作而言,hadoopRDD 和 newHadoopRDD 是最为抽象的两个函数接口,主要包含以下四个参数.
    1) 输入格式(InputFormat): 制定数据输入的类型,如 TextInputFormat 等,新旧两个版本所引用的版本分别是 org.apache.hadoop.mapred.InputFormat 和
    org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
    2) 键类型: 指定[K,V]键值对中 K 的类型
    3) 值类型: 指定[K,V]键值对中 V 的类型
    4) 分区值: 指定由外部存储生成的 RDD 的 partition 数量的最小值,如果没有指定,系统
    会使用默认值 defaultMinSplits
    注意:其他创建操作的 API 接口都是为了方便最终的 Spark 程序开发者而设置的,是这两个接口的高效实现版本.例如,对于 textFile 而言,只有 path 这个指定文件路径的参数,其他参数在系统内部指定了默认值。
    1.在 Hadoop 中以压缩形式存储的数据,不需要指定解压方式就能够进行读取,因为Hadoop 本身有一个解压器会根据压缩文件的后缀推断解压算法进行解压.
    2.如果用 Spark 从 Hadoop 中读取某种类型的数据不知道怎么读取的时候,上网查找一个
    使用 map-reduce 的时候是怎么读取这种数据的,然后再将对应的读取方式改写成上面的
    hadoopRDD 和 newAPIHadoopRDD 两个类就行了
  2. MySQL数据库连接
    支持通过 Java JDBC 访问关系型数据库。需要通过 JdbcRDD 进行,示例如下:
1. 添加依赖
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.27</version>
</dependency>
2. mYSQL读取
class MyAvg {
  var spark:SparkSession = null
  @Before
  def init={
    spark = SparkSession.builder().appName("sparkSql").master("local[*]").getOrCreate()
  }
  @Test
  def sqlDataSource={
    //load 默认的加载文件的格式由参数spark.sql.source.default决定
    val df = spark.read.load("datas/users.parquet")
    df.show()
   // spark.sql("select * from parquet.'datas/users.parquet'").show()
    //spark.sql("select * from json.'datas/people.json'").show()
  //jdbc
    spark.read.format("jdbc")
      .option("url","jdbc:mysql://localhost:3306/day10")
      .option("dbtable","user")
      .option("user","root")
      .option("password","123")
      .load().show()
    println("-------------------")
    val option = new Properties
    option.setProperty("user","root")
    option.setProperty("password","123")
    spark.read.jdbc("jdbc:mysql://localhost:3306/day10","user",option).show()
  }
  1. HBASE数据库
    由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现, Spark 可以通过
    Hadoop 输入格式访问 HBase。这个输入格式会返回键值对数据,其中键的类型为 org.
    apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为 org.apache.hadoop.hbase.client.
    Result。
package com.ityouxin.hbase

import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.{Put, Result}
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapred.TableOutputFormat
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.mapred.JobConf
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Spark_Hbase {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("hbase")
    val sc = new SparkContext(conf)
    val datas = sc.makeRDD(List(("1","2","3"),("11","22","33")))
    val rdd2 = datas.map{
      case(rowkey,ename,salay) =>
        val put = new Put(Bytes.toBytes(rowkey))
        put.addColumn(Bytes.toBytes("info"),Bytes.toBytes("ename"),Bytes.toBytes(ename))
        put.addColumn(Bytes.toBytes("info"),Bytes.toBytes("sal"),Bytes.toBytes(salay))
        (new ImmutableBytesWritable,put)
    }
    val hbaseConf = HBaseConfiguration.create
    hbaseConf.set(TableOutputFormat.OUTPUT_TABLE,"hbase_emp_table")
    val jobConf:JobConf = new JobConf(hbaseConf)
    jobConf.setOutputFormat(classOf[TableOutputFormat])
    rdd2.saveAsHadoopDataset(jobConf)
    //注意netty包的冲突,通过执行mvn   dependncy:tree   来进行查找相应jar包,然后排出

    //读Hbase表
    hbaseConf.set(TableInputFormat.INPUT_TABLE,"hbase_emp_table")
    val ReadRdd: RDD[(ImmutableBytesWritable, Result)] =
      sc.newAPIHadoopRDD(hbaseConf,classOf[TableInputFormat],classOf[ImmutableBytesWritable],classOf[Result])
    sc.stop()
  }
}
  1. RDD编程进阶
  1. 累加器
    累加器用来对信息进行聚合,通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。 如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。
  2. 系统累加器
    val notice = sc.textFile("./NOTICE")
    val blanklines = sc.accumulator(0)
    累加器的用法如下所示。
    通过在驱动器中调用 SparkContext.accumulator(initialValue)方法,创建出存有初始值的
    累加器。返回值为 org.apache.spark.Accumulator[T] 对象,其中 T 是初始值 initialValue 的
    类型。 Spark 闭包里的执行器代码可以使用累加器的 += 方法(在 Java 中是 add)增加累加器
    的值。 驱动器程序可以调用累加器的 value 属性(在 Java 中使用 value()或 setValue())来访问
    累加器的值。
    注意: 工作节点上的任务不能访问累加器的值。从这些任务的角度来看,累加器是一个只写
    变量。
    对于要在行动操作中使用的累加器, Spark 只会把每个任务对各累加器的修改应用一次。
    因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在
    foreach() 这样的行动操作中。转化操作中累加器可能会发生不止一次更新
  3. 自定义累加器
    自定义累加器类型的功能在 1.X 版本中就已经提供了,但是使用起来比较麻烦,在 2.0版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2 来提供更加友好的自定义类型累加器的实现方式。实现自定义类型累加器需要继承 AccumulatorV2 并至少覆写下例中出现的方法,下面这个累加器可以用于在程序运行过程中收集一些文本类信息,最终以Set[String]的形式返回。 1
package com.ityouxin

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.util.AccumulatorV2

object accumulator {
  def main(args: Array[String]): Unit = {
    val conf:SparkConf = new SparkConf().setMaster("local[*]").setAppName("accumulator")
    val sc = new SparkContext(conf)
    val rdd1 = sc.makeRDD(List(1,2,3,4,5,6),2)
    val sum = new Myacc
    sc.register(sum)
    rdd1.foreach(i=>{
      sum.sum =sum.sum+i
    })
    println(sum.sum)
    sc.stop()
  }
}
class Myacc extends AccumulatorV2[Int,Int]{
  var sum = 0
  override def isZero: Boolean = {
    if (sum==0)true else false
  }

  override def copy(): AccumulatorV2[Int, Int] = {
    val acc =new Myacc
    acc.sum=this.sum
    acc
  }

  override def reset(): Unit = {
    this.sum=0
  }

  override def add(v: Int): Unit = {
    this.sum = this.sum +v
  }

  override def merge(other: AccumulatorV2[Int, Int]): Unit = {
    this.sum = this.sum+other.value
  }

  override def value: Int = {
    this.sum
  }
}
  1. 自定义累加器案例(二)
package com.ityouxin

import java.util

import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}

//过滤掉字母
object Spark_Accumulator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("hbase")
    val sc = new SparkContext(conf)

    val accumulator = new LogAccumulator
    sc.register(accumulator)
    //rdd,过滤掉带字母的元素
    val sum =sc.makeRDD(Array(("1"),("2a"),("33"),("7lhx"),("5")),2).filter(
      line=>{
        //正则表达书,匹配带字母的元组
       val pattern = """^-?(\d+)"""
       val flag =line.matches(pattern)
        //当flag匹配到字母后,将字段进行调用LogAccumulator.class类的add方法进行处理
       if (!flag){
        accumulator.add(line)
       }
        flag//进行map映射转换,然后reduce算子实现过滤后的value相加

     }).map(_.toInt).reduce(_+_)
    println("sum : " + sum)
    val str: util.Iterator[String] = accumulator.value.iterator()
    while (str.hasNext){
      println(str.next())
    }
  }
class LogAccumulator extends AccumulatorV2[String,java.util.Set[String]]{
  private val  _logArray:java.util.Set[String] = new java.util.HashSet[String]()
  override def isZero: Boolean = {
    _logArray.isEmpty
  }

  override def copy():org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]] = {
    val accumulator = new LogAccumulator()
    accumulator.synchronized(
      accumulator._logArray.addAll(_logArray)
    )
    accumulator
  }

  override def reset(): Unit = {
    _logArray.clear()
  }

  override def add(v: String): Unit = {
    _logArray.add(v)
  }

  override def merge(other: org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]]): Unit = {
    other match {
      case o:LogAccumulator => _logArray.addAll(o.value)
    }
  }

  override def value: java.util.Set[String] = {
    java.util.Collections.unmodifiableSet(_logArray)
  }
}
}
  1. 广播变量(调优策略)
    广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。 在多个并行操作中使用同一个变量,但是Spark 会为每个任务分别发送 。
    使用广播变量的过程如下:
    (1) 通过对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T]对象。 任何可序列化的类型都可以这么实现。
    (2) 通过 value 属性访问该对象的值(在 Java 中为 value() 方法)。
    (3) 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。