Loading

Spark

date: 2020-04-23 19:31:00
updated: 2020-04-24 16:22:00

Spark

VM options: -Dspark.master=local 还可以是 cluster,local[*], Standalone(只支持简单的固定资源分配策略)

Spark Streaming 用来获取不同的源数据流DStream,为了初始化Spark Streaming就需要先创建一个StreamingContext对象,一个StreamingContext对象可以用SparkConf对象创建。

流程:

createStreamingContext() 通过sparkConf来
createKafkaStream() 先先从zk拿到topic的分区情况,再从zk拿到每个分区的offset,从kafka创建一个DStream返回一个rdds
process() 操作rdds

两种方式创建StreamingContext

val conf = new SparkConf().setAppName(appName).setMaster(master);
val ssc = new StreamingContext(conf, Seconds(1)); // 第二个参数是spark从kafka每隔多少秒一个批次

StreamingContext,还可以使用已有的SparkContext来创建
val sc = new SparkContext(conf)
val ssc = new StreamingContext(sc, Seconds(1));
dstream.foreachRDD(rdd => {
    rdd.foreach(record => {
        val connection = createNewConnection()
        connection.send(record)
        connection.close()
    })
})
上面的方法也可以实现读取数据,但是为每个记录创建和销毁连接对象会导致非常高的开支。

下面的方法将连接对象的创建开销分摊到了partition的所有记录上,并且池中的连接对象应该根据需要延迟创建,并且在空闲一段时间后自动超时
dstream.foreachRDD(rdd => {
    rdd.foreachPartition(partitionOfRecords => {
        // ConnectionPool is a static, lazily initialized pool of connections
        val connection = ConnectionPool.getConnection()
        partitionOfRecords.foreach(record => connection.send(record))
        ConnectionPool.returnConnection(connection)  // return to the pool for future reuse
    })
})

内存调优:减少Spark Streaming应用程序垃圾回收的相关暂停,获得更稳定的批处理时间。

  • 默认的持久化级别是序列化数据到内存中,默认的持久化级别是序列化数据到内存中
  • spark.streaming.unpersist = true 使系统找出那些不需要经常保有的RDD,然后去持久化它们。这可以减少Spark RDD的内存使用,也可能改善垃圾回收的行为。
  • Concurrent garbage collector:使用并发的标记-清除垃圾回收可以进一步减少垃圾回收的暂停时间。尽管并发的垃圾回收会减少系统的整体吞吐量,但是仍然推荐使用它以获得更稳定的批处理时间。
  1. RDD 弹性分布式数据集 (Resilient Distributed DataSet)

特性:分区,不可变,并行操作

RDD 只是抽象意义的数据集合,分区内部并不会存储具体的数据,只会存储它在该 RDD 中的 index,通过该 RDD 的 ID 和分区的 index 可以唯一确定对应数据块的编号,然后通过底层存储层的接口提取到数据进行处理。

不可变性是指每个 RDD 都是只读的,它所包含的分区信息是不可变的。由于已有的 RDD 是不可变的,所以我们只有对现有的 RDD 进行转化 (Transformation) 操作,才能得到新的 RDD ,一步一步的计算出我们想要的结果。不需要立刻去存储计算出的数据本身,我们只要记录每个 RDD 是经过哪些转化操作得来的

因为有分区,所以不同节点上的数据可以分别被处理,然后生成一个新的 RDD

RDD结构

a,Partitions
Partitions 就是上面所说的,代表着 RDD 中数据的逻辑结构,每个 Partion 会映射到某个节点内存或者硬盘的一个数据块。

b,SparkContext
SparkContext 是所有 Spark 功能的入口,代表了与 Spark 节点的连接,可以用来创建 RDD 对象以及在节点中的广播变量等等。一个线程只有一个 SparkContext。

c,SparkConf
SparkConf 是一些配置信息。

d,Partitioner
Partitioner 决定了 RDD 的分区方式,目前两种主流的分区方式:Hash partioner 和 Range partitioner。Hash 就是对数据的 Key 进行散列分布,Range 是按照 Key 的排序进行的分区。也可以自定义 Partitioner。

e,Dependencies
Dependencies 也就是依赖关系,记录了该 RDD 的计算过程,也就是说这个 RDD 是通过哪个 RDD 经过怎么样的转化操作得到的。

@DeveloperApi
abstract class Dependency[T] extends Serializable{
    def rdd: RDD[T]
}
这是RDD指的是父RDD,所以依赖是对父RDD的包装,并且通过Dependency的类型说明当前这个transformation对应的数据处理方式。子类实现主要有两种:

@DeveloperApi
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  /**
   * Get the parent partitions for a child partition.
   * @param partitionId a partition of the child RDD
   * @return the partitions of the parent RDD that the child partition depends upon
   */
  def getParents(partitionId: Int): Seq[Int]

  override def rdd: RDD[T] = _rdd
}
窄依赖是抽象类,具体的实现类有2种

子RDD和父RDD的Partition之间的关系是一对一
@DeveloperApi
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

子RDD和父RDD的Partition之间的关系是一个区间内的1对1对应关系
子的partitionID 3 4 5 6
父的partitionID 8 9 10 11
求子partitionID=5的父partitionID,5-3+8=10
@DeveloperApi
class RangeDependency[T](rdd: RDD[T], inStart: Int, outStart: Int, length: Int)
  extends NarrowDependency[T](rdd) {

  override def getParents(partitionId: Int): List[Int] = {
    if (partitionId >= outStart && partitionId < outStart + length) {
      List(partitionId - outStart + inStart)
    } else {
      Nil
    }
  }
}

宽依赖
@DeveloperApi
class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
    @transient private val _rdd: RDD[_ <: Product2[K, V]],
    val partitioner: Partitioner,
    val serializer: Serializer = SparkEnv.get.serializer,
    val keyOrdering: Option[Ordering[K]] = None,
    val aggregator: Option[Aggregator[K, V, C]] = None,
    val mapSideCombine: Boolean = false)
  extends Dependency[Product2[K, V]] {

  if (mapSideCombine) {
    require(aggregator.isDefined, "Map-side combine without Aggregator specified!")
  }
  override def rdd: RDD[Product2[K, V]] = _rdd.asInstanceOf[RDD[Product2[K, V]]]

  private[spark] val keyClassName: String = reflect.classTag[K].runtimeClass.getName
  private[spark] val valueClassName: String = reflect.classTag[V].runtimeClass.getName
  // Note: It's possible that the combiner class tag is null, if the combineByKey
  // methods in PairRDDFunctions are used instead of combineByKeyWithClassTag.
  private[spark] val combinerClassName: Option[String] =
    Option(reflect.classTag[C]).map(_.runtimeClass.getName)

  val shuffleId: Int = _rdd.context.newShuffleId()

  val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
    shuffleId, _rdd.partitions.length, this)

  _rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}
因为shuffle设计到网络传输,所以要有序列化serializer,为了减少网络传输,可以加map端聚合,通过mapSideCombine和aggregator控制,还有key排序相关的keyOrdering,以及重输出的数据如何分区的partitioner。宽依赖(即shuffle操作)是stage划分的依据

这里有个概念,根据每个 RDD 的分区计算后生成的新的 RDD 的分区的对应关系,可以分成窄依赖和宽依赖。

窄依赖就是父 RDD 的分区可以一一对应到子 RDD 的分区,宽依赖是说父 RDD 的每个分区可以被多个子 RDD 分区使用。如图:

由于窄依赖的特性,窄依赖允许子 RDD 的每个分区可以被并行处理产生,而且支持在同一个节点上链式执行多条指令,无需等待其它父 RDD 的分区操作。


Spark 区分宽窄依赖的原因主要有两点:

窄依赖支持在同一节点上进行链式操作,比如在执行了 map 后,紧接着执行 filter 操作。相反,宽依赖需要所有父分区都是可用的,可能还需要调用类似 MapReduce 之类的操作进行跨节点传递。
从失败恢复的角度考虑,窄依赖失败恢复更有效,因为只要重新计算丢失的父分区即可,而宽依赖涉及到 RDD 的各级多个父分区。

f,Checkpoint
检查点机制,在计算过程中有一些比较耗时的 RDD,我们可以将它缓存到硬盘或者 HDFS 中,标记这个 RDD 有被检查点处理过,并且清空它的所有依赖关系。同时,给它新建一个依赖于 CheckpointRDD 的依赖关系,CheckpintRDD 可以用来从 硬盘中读取 RDD 和生成新的分区信息。

这么做之后,当某个 RDD 需要错误恢复时,回溯到该 RDD,发现它被检查点记录过,就可以直接去硬盘读取该 RDD,无需重新计算。

g,Preferred Location
针对每一个分片,都会选择一个最优的位置来计算,数据不动,代码动。

h,Storage Level
用来记录 RDD 持久化时存储的级别,常用的有:

MEMORY_ONLY:只存在缓存中,如果内存不够,则不缓存剩余的部分。这是 RDD 默认的存储级别。
MEMORY_AND_DISK:缓存在内存中,不够则缓存至内存。
DISK_ONLY:只存硬盘。
MEMORY_ONLY_2 和 MEMORY_AND_DISK_2等:与上面的级别和功能相同,只不过每个分区在集群两个节点上建立副本。
i,Iterator
迭代函数和计算函数是用来表示 RDD 怎样通过父 RDD 计算得到的。

迭代函数首先会判断缓存中是否有想要计算的 RDD,如果有就直接读取,如果没有就查找想要计算的 RDD 是否被检查点处理过。如果有,就直接读取,如果没有,就调用计算函数向上递归,查找父 RDD 进行计算。

  1. map 和 flatMap
都是对 rdd 中的每一个元素进行操作,原先也有几个元素,map就返回含有几个元素的rdd,flatMap = map + flattern 操作,先做map操作,如果元素中含有迭代器,那就把迭代器里的元素循环取出来
val arr2 = sc.parallelize(Array("hello world", "java"))
val t1 = arr2.flatMap(x=>x.split(" "))
t1.foreach(println)
val t2 = arr2.map(x=>x.split(" "))
t2.foreach(x=>{
    x.foreach(println)
})
  1. collect 和 filter

var a = List(1, 2, 3, 4, 5)
a.collect({case i if i % 2 == 0 => i + 10})
a.filter(x=>x%2==0) // filter 只是过滤元素,并不做元素上的改变

  1. offset

一般都会 enable.auto.commit = false 禁止自动提交,自己来管理offset

  • 交给 zk 管理

zkClientUrl 下 zkOffsetPath = "/kafka/consumers/"+ consumer_group_id + "/offsets/" + topic + "/" + partitionId 保存 offsetNum

  • 存储到hbase
    行键 topic:customer_group_id:batch_milliSeconds 通过batch的时间戳可以更好的展示历史的每批次的offsets
    列族 offsets
    列 分区id
    value 偏移量的值
def saveOffsets(TOPIC_NAME:String,GROUP_ID:String,offsetRanges:Array[OffsetRange],
                hbaseTableName:String,batchTime: org.apache.spark.streaming.Time) ={
  val hbaseConf = HBaseConfiguration.create()
  hbaseConf.addResource("src/main/resources/hbase-site.xml")
  val conn = ConnectionFactory.createConnection(hbaseConf)
  val table = conn.getTable(TableName.valueOf(hbaseTableName))
  val rowKey = TOPIC_NAME + ":" + GROUP_ID + ":" +String.valueOf(batchTime.milliseconds)
  val put = new Put(rowKey.getBytes)
  for(offset <- offsetRanges){
    put.addColumn(Bytes.toBytes("offsets"),Bytes.toBytes(offset.partition.toString),
          Bytes.toBytes(offset.untilOffset.toString))
  }
  table.put(put)
  conn.close()
}
  1. foreachRDD作用于DStream中每一个时间间隔的RDD
    foreachPartition作用于每一个时间间隔的RDD中的每一个partition
    foreach作用于每一个时间间隔的RDD中的每一个元素。
从 foreachRDD 的源码上来看,并没有任何的Iterator,而 foreach 和 foreachPartition 都用到了迭代器,说明这两个是会遍历RDD的。
private def foreachRDD(
    foreachFunc: (RDD[T], Time) => Unit,
    displayInnerRDDOps: Boolean): Unit = {
    new ForEachDStream(this,
    context.sparkContext.clean(foreachFunc, false), displayInnerRDDOps).register()
}

def foreach(f: T => Unit): Unit = withScope {
  val cleanF = sc.clean(f)
  sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}

def foreachPartition(f: Iterator[T] => Unit): Unit = withScope {
  val cleanF = sc.clean(f)
  sc.runJob(this, (iter: Iterator[T]) => cleanF(iter))
}
  1. DAG 有向无环图

通过DAG来对RDD的关系进行建模,描述RDD之间的依赖关系,这个关系叫 lineage

名词 解释
Job 调用RDD的一个action,如count,即触发一个Job,spark中对应实现为ActiveJob,DAGScheduler中使用集合activeJobs和jobIdToActiveJob维护Job
Stage 代表一个Job的DAG,会在发生shuffle处被切分,切分后每一个部分即为一个Stage,Stage实现分为ShuffleMapStage和ResultStage,一个Job切分的结果是0个或多个ShuffleMapStage加一个ResultStage
Task 最终被发送到Executor执行的任务,和stage的ShuffleMapStage和ResultStage对应,其实现分为ShuffleMapTask和ResultTask

mr 只有一个job,分为map和reduce两个阶段;spark每执行一次action算子就会产生一个job

spark划分stage的整体思路是:从后往前推,遇到宽依赖就断开,划分为一个stage;遇到窄依赖就将这个RDD加入该stage中

RDD RDD 是 Spark 的灵魂,也称为弹性分布式数据集。一个 RDD 代表一个可以被分区的只读数据集。RDD 内部可以有许多分区(partitions),每个分区又拥有大量的记录(records)。

DAG Spark 中使用 DAG 对 RDD 的关系进行建模,描述了 RDD 的依赖关系,这种关系也被称之为 lineage(血缘),RDD 的依赖关系使用 Dependency 维护。

Stage 在 DAG 中又进行 Stage 的划分,划分的依据是依赖是否是 shuffle 的,每个 Stage 又可以划分成若干 Task。接下来的事情就是 Driver 发送 Task 到 Executor,Executor 线程池去执行这些 task,完成之后将结果返回给 Driver。

Job 每一次action动作都会产生一个job。

Task 一个 Stage 内,最终的 RDD 有多少个 partition,就会产生多少个 task。repartition 会减少小文件,减少 task 的个数,但是会增加 task 处理的数据量,可能会延长处理时间。

  1. 算子
  • Value数据类型的Transformation算子,这种变换并不触发提交作业,针对处理的数据项是Value型的数据

    • 输入分区与输出分区一对一型
      • map
      • flatMap
        • map 操作后再把所有元素拍扁
      • mapPartitions
        • 对每个分区里的所有数据进行批处理
        • 如果有2个分区,每个分区2条数,通过map做批处理的话需要修改4次,使用mapPartitions其实只运算了2次
        • listRDD.mapPartitions(datas =>{ datas.map(x => x * 2) }) datas 这里指的是 Scala 的 迭代器,对于里面的 *2 计算是scala的计算,不是spark的计算(spark的计算需要发送到Executor),所以相当于是向 N个 Executor(个数等于分区个数)直接发送了结果,不牵扯计算,提高了效率。但是因为是一次性发送最终的结果,有可能存在处理完后数据的容量大于 Executor 的最大容量,产生内存溢出OOM
      • glom
        • 将同一个分区里的元素合并到一个array里
    • 输入分区与输出分区多对一型
      • union
      • cartesian
        • 笛卡尔
    • 输入分区与输出分区多对多型
      • groupBy
        • 生成相应的key,相同的放在一起
        val a = sc.parallelize(1 to 9, 3)
        a.groupBy(x => { if (x % 2 == 0) "even" else "odd" }).collect
        res42: Array[(String, Seq[Int])] = Array((even,ArrayBuffer(2, 4, 6, 8)), (odd,ArrayBuffer(1, 3, 5, 7, 9)))
        
        返回值是 [String, Seq] String 相当于一个序列的昵称
    • 输出分区为输入分区子集型
      • filter
      • distinct
      • subtract
        • 去掉含有重复的项
        val a = sc.parallelize(1 to 9, 3)
        val b = sc.parallelize(1 to 3, 3)
        val c = a.subtract(b)
        c.collect
        res3: Array[Int] = Array(6, 9, 4, 7, 5, 8)
        
      • sample
        • 随机抽样
        val a = sc.parallelize(1 to 10000, 3)
        a.sample(false, 0.1, 0).count
        res24: Long = 960
        
      • takeSample
    • Cache型
      • cache
        • cache调用了persist()方法,无参,cache只有一个默认的缓存级别MEMORY_ONLY
      • persist
        • persist(StorageLevel)可以根据情况设置其它的缓存级别,比如缓存到硬盘disk_only,内存和硬盘都缓存memory_and_disk。每个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(false, false, true, false)
          ......
          }
          
          class StorageLevel private(
              private var _useDisk: Boolean,
              private var _useMemory: Boolean,
              private var _useOffHeap: Boolean,
              private var _deserialized: Boolean,
              private var _replication: Int = 1)
          extends Externalizable {
          ......
          def useDisk: Boolean = _useDisk // 使用硬盘
          def useMemory: Boolean = _useMemory // 使用内存
          def useOffHeap: Boolean = _useOffHeap // 使用堆外内存,这是Java虚拟机里面的概念,堆外内存意味着把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机)。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。
          def deserialized: Boolean = _deserialized // 反序列化,其逆过程序列化(Serialization)是java提供的一种机制,将对象表示成一连串的字节;而反序列化就表示将字节恢复为对象的过程。序列化是对象永久化的一种机制,可以将对象及其属性保存起来,并能在反序列化后直接恢复这个对象
          def replication: Int = _replication // 备份数(在多个节点上备份)
          ......
          }
          
  • Key-Value数据类型的Transfromation算子,这种变换并不触发提交作业,针对处理的数据项是Key-Value型的数据对

    • 输入分区与输出分区一对一
      • mapValues
        • 针对(key,value)型数据中的Value进行操作,而不对Key进行处理
    • 对单个RDD或两个RDD聚集
      • 单个RDD聚集
        • combineByKey
        • reduceByKey
          • 会先在局部聚合再shuffle,比起groupbykey(先shuffle再聚合)在大量数据时可以减轻网络压力
        • partitionBy
      • 两个RDD聚集
        • cogroup
    • 连接
      • join
      • leftOutJoin和 rightOutJoin
  • Action算子,这类算子会调用 sc.runjob() 方法触发SparkContext提交Job作业

    • 无输出
      • foreach
      • foreachPartition
    • HDFS
      • saveAsTextFile
      • saveAsObjectFile
    • Scala集合和数据类型
      • collect
        • 将rdd中的所有数据都从网络端下载到本地,返回结果为一个array。数据量大的时候容易引起溢出
      • collectAsMap
      • reduceByKeyLocally
      • lookup
      • count
      • top
      • reduce
        • 底层实现是 reduceLeft
      • fold
        • 和reduce类似,不同的是fold()操作需要从一个初始值开始,并以该值作为上下文,处理集合中的每个元素
      • aggregate
  1. spark 和 mapreduce
  • 消除了冗余的 HDFS 读写: Hadoop 每次 shuffle 操作后,必须写到磁盘,而 Spark 在 shuffle 后不一定落盘,可以 persist 到内存中,以便迭代时使用。如果操作复杂,很多的 shufle 操作,那么 Hadoop 的读写 IO 时间会大大增加,也是 Hive 更慢的主要原因了。
  • 消除了冗余的 MapReduce 阶段: Hadoop 的 shuffle 操作一定连着完整的 MapReduce 操作,冗余繁琐。而 Spark 基于 RDD 提供了丰富的算子操作,且 reduce 操作产生 shuffle 数据,可以缓存在内存中。
  • JVM 的优化: Hadoop 每次 MapReduce 操作,启动一个 Task 便会启动一次 JVM,基于进程的操作。而 Spark 每次 MapReduce 操作是基于线程的,只在启动 Executor 是启动一次 JVM,内存的 Task 操作是在线程复用的。每次启动 JVM 的时间可能就需要几秒甚至十几秒,那么当 Task 多了,这个时间 Hadoop 不知道比 Spark 慢了多少。

xx. Driver 创建spark上下文对象的应用程序就是 Driver,拆分任务,发送任务到 Executor。发送涉及到网络传输,类对象需要 extends java.io.Serializable
Executor 执行器用于接收任务并执行任务,并可以返回执行情况给 Driver。所有算子都交给 Executor

  1. spark 提交流程
    spark-submit(Driver) 向Master提交任务,申请运行资源。在参数中备注上master、executor地址,运行内存等等。

spark-submit --class cn.enn.realtime.business.ContractDetailStream
--master yarn
--deploy-mode cluster
--driver-memory 1g
--executor-memory 1g
--executor-cores 1
--num-executors 1 \

Master负责资源调度,分配资源,记录Executor在哪些Worker里启动。Master和Worker进行RPC通信,让worker启动executor,将分区的参数传递过去。
Driver里有具体的业务逻辑编写,生成执行任务,发送给executor执行,也就是executor启动会主动和driver进行通信,获取Task

  1. spark数据倾斜

SET spark.sql.autoBroadcastJoinThreshold=104857600;
适用于:参与Join的一边数据集足够小,可被加载进Driver并通过Broadcast方法广播到各个Executor中。
解决方案:在Java/Scala代码中将小数据集数据拉取到Driver,然后通过Broadcast方案将小数据集的数据广播到各Executor。或者在使用SQL前,将Broadcast的阈值调整得足够大,从而使用Broadcast生效。进而将Reduce侧Join替换为Map侧Join。
优势:避免了Shuffle,彻底消除了数据倾斜产生的条件,可极大提升性能。
劣势:要求参与Join的一侧数据集足够小,并且主要适用于Join的场景,不适合聚合的场景,适用条件有限。

  1. spark-streaming连接kafka两种方式 / kafkastream 的两种形式
    Receiver模式:KafkaUtils.createDStream() kafka支持发布订阅,所以kafka把消息全部封装好,提供给spark去调用,本来kafka的消息分布在不同的partition上面,相当于做了一步数据合并,在发送给spark,故spark可以设置executor个数去消费这部分数据,效率相对慢一些
    Direct模式:KafkaUtils.createDirectStream() 每次到topic的每个partition依据偏移量进行获取数据,拉取数据以后进行处理,可以实现高可用

  2. RDD、DataFrame、Dataset
    共性:
    1、RDD、DataFrame、Dataset全都是spark平台下的分布式弹性数据集,为处理超大型数据提供便利
    2、三者都有惰性机制,在进行创建、转换,如map方法时,不会立即执行,只有在遇到Action如foreach时,三者才会开始遍历运算,极端情况下,如果代码里面有创建、转换,但是后面没有在Action中使用对应的结果,在执行时会被直接跳过
    3、三者都会根据spark的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出
    4、三者都有partition的概念
    5、三者有许多共同的函数,如filter,排序等
    6、在对DataFrame和Dataset进行操作许多操作都需要这个包进行支持 import spark.implicits._

区别:
RDD不支持sparksql操作
与RDD和Dataset不同,DataFrame每一行的类型固定为Row,只有通过解析才能获取各个字段的值,每一列的值没法直接访问
DataFrame与Dataset均支持sparksql的操作,比如select,groupby之类
DataFrame与Dataset支持一些特别方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然

** Dataset和DataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同 **
DataFrame也可以叫Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的getAS方法或者模式匹配拿出特定字段
Dataset中,每一行是什么类型是不一定的,在自定义了case class之后可以很自由的获得每一行的信息

case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
/**
    rdd
    ("a", 1)
    ("b", 1)
    ("a", 1)
    * */
val test: Dataset[Coltest]=rdd.map{line=>
    Coltest(line._1,line._2)
}.toDS
test.map{
    line=>
    println(line.col1)
    println(line.col2)
}
可以看出,Dataset在需要访问列中的某个字段时是非常方便的,然而,如果要写一些适配性很强的函数时,如果使用Dataset,行的类型又不确定,可能是各种case class,无法实现适配,这时候用DataFrame即Dataset[Row]就能比较好的解决问题  
  1. 共享变量:累加器和广播变量

Spark中分布式执行的代码需要传递到各个Executor的Task上运行。对于一些只读、固定的数据(比如从DB中读出的数据),每次都需要Driver广播到各个Task上,这样效率低下。广播变量允许将变量只广播(提前广播)给各个Executor。该Executor上的各个Task再从所在节点的BlockManager获取变量,而不是从Driver获取变量,从而提升了效率。

一个Executor只需要在第一个Task启动时,获得一份Broadcast数据,之后的Task都从本节点的BlockManager中获取相关数据。

val values = ListInt
val broadcastValues = sparkContext.broadcast(values)
rdd.mapPartitions(iter => {
  broadcastValues.getValue.foreach(println)
})

posted @ 2020-10-22 10:54  猫熊小才天  阅读(148)  评论(0编辑  收藏  举报