spark core简介

1.RDD概述

1.1 什么是 RDD

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据 抽象。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算 的集合。

1.2 RDD 的属性

* Internally, each RDD is characterized by five main properties:
*
*  - A list of partitions
*  - A function for computing each split
*  - A list of dependencies on other RDDs
*  - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
*  - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
*    an HDFS file)
*
* All of the scheduling and execution in Spark is done based on these methods, allowing each RDD
* to implement its own way of computing itself. Indeed, users can implement custom RDDs (e.g. for
* reading data from a new storage system) by overriding these functions. Please refer to the
* <a href="http://people.csail.mit.edu/matei/papers/2012/nsdi_spark.pdf">Spark paper</a>
* for more details on RDD internals.
  1. 一组分区(Partition),即数据集的基本组成单位;
  2. 一个计算每个分区的函数;
  3. RDD 之间的依赖关系;
  4. 一个 Partitioner,即 RDD 的分片函数;
  5. 一个列表,存储存取每个 Partition 的优先位置(preferred location)  (移动数据不如移动计算)
    进程本地化:最好方式是数据和计算在同一个进程中(process)
    节点本地化:同一个NodeManager上启动多个executor,数据发送给同一个NodeManager
    机架本地化:数据发送个同一个机架上的不同节点
  // rdd中定义了分区、依赖、本地化、计算等相关的属性和方法
  // 计算
  @DeveloperApi
  def compute(split: Partition, context: TaskContext): Iterator[T]

  // 分区
  protected def getPartitions: Array[Partition]

  // 依赖
  protected def getDependencies: Seq[Dependency[_]] = deps

  // 本地化
  protected def getPreferredLocations(split: Partition): Seq[String] = Nil

1.3 RDD 特点

RDD 表示只读的分区的数据集,对 RDD 进行改动,只能通过 RDD 的转换操作,由一 个 RDD 得到一个新的 RDD,新的 RDD 包含了从其他 RDD 衍生所必需的信息。RDDs 之间 存在依赖,RDD 的执行是按照血缘关系延时计算的。如果血缘关系较长,可以通过持久化 RDD 来切断血缘关系。

1.3.1 弹性

  •  存储的弹性:内存与磁盘的自动切换;
  •  容错的弹性:数据丢失可以自动恢复
  •  计算的弹性:计算出错重试机制
  • 分片的弹性:可根据需要重新分片

1.3.2 分区

RDD 逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个 compute 函数得到每个分区的数据。如果 RDD 是通过已有的文件系统构建,则 compute 函数是读取 指定文件系统中的数据,如果 RDD 是通过其他 RDD 转换而来,则 compute 函数是执行转 换逻辑将其他 RDD 的数据进行转换。

1.3.3 只读

RDD 是只读的,要想改变 RDD 中的数据,只能在现有的 RDD 基础上创 建新的 RDD。
RDD 的操作算子包括两类,一类叫做 transformations,它是用来将 RDD 进行转化,构 建 RDD 的血缘关系;另一类叫做 actions,它是用来触发 RDD 的计算,得到 RDD 的相关计 算结果或者将 RDD 保存的文件系统中

1.3.4 依赖

RDDs 通过操作算子进行转换,转换得到的新 RDD 包含了从其他 RDDs 衍生所必需的 信息,RDDs 之间维护着这种血缘关系,也称之为依赖。依赖包括两种,一种 是窄依赖,RDDs 之间分区是一一对应的另一种是宽依赖,下游 RDD 的每个分区与上游 RDD(也称之为父 RDD)的每个分区都有关,是多对多的关系

1.3.5 缓存

如果在应用程序中多次使用同一个 RDD,可以将该 RDD 缓存起来,该 RDD 只有在第 一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该 RDD 的时候,会 直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用

1.3.6 CheckPoint

虽然 RDD 的血缘关系天然地可以实现容错,当 RDD 的某个分区数据失败或丢失,可 以通过血缘关系重建。但是对于长时间迭代型应用来说,随着迭代的进行,RDDs 之间的血 缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势 必影响性能。为此,RDD 支持 checkpoint 将数据保存到持久化的存储中,这样就可以切断 之前的血缘关系,因为 checkpoint 后的 RDD 不需要知道它的父 RDDs 了,它可以从 checkpoint 处拿到数据。

1.4设计模式

查看算子代码可以看到,多次调用实际是将自己传入然后生成新的RDD,开启套娃模式

/**
   *  Return a new RDD by first applying a function to all elements of this
   *  RDD, and then flattening the results.
   */
  def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF))
  }

2.编程模型

2.1编程模型

在 Spark 中,RDD 被表示为对象,通过对象上的方法调用来对 RDD 进行转换。经过 一系列的 transformations 定义 RDD 之后,就可以调用 actions 触发 RDD 的计算,action 可以是向应用程序返回结果(count, collect 等),或者是向存储系统保存数据(saveAsTextFile 等)。在 Spark 中,只有遇到 action,才会执行 RDD 的计算(即延迟计算),这样在运行时可 以通过管道的方式传输多个转换。

要使用 Spark,开发者需要编写一个 Driver 程序,它被提交到集群以调度运行 Worker,Driver 中定义了一个或多个 RDD,并调用 RDD 上的 action,Worker 则执行 RDD 分区计算任务

2.2 RDD 的创建

在 Spark 中创建 RDD 的创建方式可以分为三种:

  • 从集合中创建 RDD
  • 从外部存储创建 RDD
  • 从其他 RDD 创建

2.2.1 从集合中创建

从集合中创建 RDD,Spark 主要提供了两种函数:parallelize 和 makeRDD

通过如下代码可以看到makeRDD实际上是调用了parallelize方法

def makeRDD[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
    parallelize(seq, numSlices)
  }


// defaultParallelism取数
// org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend
// totalCoreCount.get()为可用核数总数
override def defaultParallelism(): Int = {
    conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
  }


// org.apache.spark.scheduler.local.LocalSchedulerBackend
override def defaultParallelism(): Int =
    scheduler.conf.getInt("spark.default.parallelism", totalCores)

当未传入分区时,取默认分区数,默认分区数先取 "spark.default.parallelism",如果没有的话则按如下规则

如果为集群模式则为当前可用的core和2取最大值
如果为local模式则为当前系统总核数

代码参考

2.2.2 由外部存储系统的数据集创建

包括本地的文件系统,还有所有 Hadoop 支持的数据集,比如 HDFS、Cassandra、HBase等

注意:传入的分区数为最小分区数,具体分区的个数根据Hadoop的分片规则计算

The textFile method also takes an optional second argument for controlling the number of partitions of the file. By default, Spark creates one partition for each block of the file (blocks being 128MB by default in HDFS), but you can also ask for a higher number of partitions by passing a larger value. Note that you cannot have fewer partitions than blocks.

官网文档

代码参考

2.2.3 从其他 RDD 创建

见2.3

2.3RDD的转换

RDD 整体上分为 transformation 和action 算子。其中transformation 分Value 类型和 Key-Value 类型,其中value又分为单value和双value

算子总结如下:

另外您可以参考: spark RDD英文官网   spark RDD中文官网

2.3.1value类型

单value代码参考  双value代码参考

1.map 和 mapPartitions的区别

➢ 数据处理角度

  • Map 算子是分区内一个数据一个数据的执行,类似于串行操作。而 mapPartitions 算子 是以分区为单位进行批处理操作

➢ 功能的角度

  • Map 算子主要目的将数据源中的数据进行转换和改变。但是不会减少或增多数据。 MapPartitions 算子需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变, 所以可以增加或减少数据

➢ 性能的角度

  • Map 算子因为类似于串行操作,所以性能比较低,而是 mapPartitions 算子类似于批处 理,所以性能较高。但是 mapPartitions 算子会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。所以在内存有限的情况下,不推荐使用。使用 map 操作
// 1.map
    //将处理的数据逐条进行映射转换,这里的转换可以是类型的转换,也可以是值的转换
listRDD.map(_ * 2).collect().foreach(println)

// 2.mapPartitions
// 方法签名传入的是iterator
listRDD.mapPartitions(    // 调用RDD的方法
datas => {
    datas.filter(_ > 2).map(_ * 2) // 此处调用的是scala的方法;可以进行filter过滤数据
}).collect().foreach(println)

2.map和flatMap的区别

map方法和flatMap的区别即scala中的区别,具体见下代码

// map方法
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF)) // 调用scala中迭代器的map方法
  }
// flatmap方法
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF)) // 调用scala中迭代器的flatMap方法
  }

// scala的flatMap方法如下
def flatMap[B](f: A => GenTraversableOnce[B]): Iterator[B] = new AbstractIterator[B] {
    private var cur: Iterator[B] = empty
    // 其实就是对输入的每个元素调用f方法,然后调用toIterator方法生成迭代器,再次进行迭代
    // 因此f方法返回的必须是可迭代对象
    private def nextCur() { cur = f(self.next()).toIterator }
    // 对生成的cur迭代器再次进行迭代
    def hasNext: Boolean = {
      while (!cur.hasNext) {
        if (!self.hasNext) return false
        nextCur()
      }
      true
    }
    def next(): B = (if (hasNext) cur else empty).next()
  }
  • map和flatmap最终调用的是scala迭代器的map方法和flatMap方法
  • flatMap中传入的函数必须返回可迭代对象,内部对生成的迭代器再次进行迭代

3.coalesce和repartition的区别

  1. coalesce 重新分区,可以选择是否进行 shuffle 过程。由参数 shuffle: Boolean = false/true 决定。
  2. repartition 实际上是调用的 coalesce,进行 shuffle
  3. coalesce 中如果未设置shuffle,当传入分区数b大于原来分区数a时,则返回rdd分区数还是b,即分区数不会变大
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    coalesce(numPartitions, shuffle = true)
  }

2.3.2key-value类型

代码参考

1.groupByKey和reduceByKey的区别

  • reduceByKey:按照 key 进行聚合,在 shuffle 之前有 combine(预聚合)操作,返回结果 是 RDD[k,v]。
  • groupByKey:按照 key 进行分组,直接进行 shuffle。
  • 开发指导:reduceByKey 比 groupByKey,建议使用。但是需要注意是否会影响业务逻 辑。

2.sortBy和sortByKey的区别

sortBy底层就是调用的 sortByKey方法

  1. 先调用keyBy方法,生成key-value形式,key的生成调用sortBy输入的f函数
  2. 调用sortByKey,按照key进行排序
  3. 调用values方法,取出value值即可
  def sortBy[K](
      f: (T) => K,
      ascending: Boolean = true,
      numPartitions: Int = this.partitions.length)
      (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T] = withScope {
    this.keyBy[K](f) // 调用 keyBy生成键值对
        .sortByKey(ascending, numPartitions) // 调用sortByKey方法排序
        .values  // 取value的值
  }

def keyBy[K](f: T => K): RDD[(K, T)] = withScope {
    val cleanedF = sc.clean(f)
    map(x => (cleanedF(x), x))   // 此处生成键值对,键为f函数生成
  }

3.join和cogroup的区别

join底层实际上是调用cogroup,然后对生成的迭代器进行展开生成新的RDD

  def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))] = self.withScope {
    this.cogroup(other, partitioner).flatMapValues( pair =>
      for (v <- pair._1.iterator; w <- pair._2.iterator) yield (v, w)
    )
  }

2.4 action

action代码参考

1.countByKey

countByKey为执行算子,但是也包含shuffle。本质是调用了reduceByKey方法,然后进行collect

// 可以看到是先调用了 reduceByKey 方法,然后进行collect
def countByKey(): Map[K, Long] = self.withScope {
    self.mapValues(_ => 1L).reduceByKey(_ + _).collect().toMap
  }

笔者比较存在疑惑:为什么要将countByKey设计成shuffle和collect一起,而其它的groupByKey、reduceByKey算子都是转换算子,可能作者设计有自己的考量

2.forearch

正常情况下,driver初始化执行信息,然后将任务发送给executor,executor执行完成后将结果返回给driver。foreach内容在executor端执行

val rdd6: RDD[Int] = sc.makeRDD(1 to 5,2)
rdd6.foreach(_ * 2)  // _ * 2在executor中执行
rdd6.collect().foreach(_ * 2) // _ * 2 在driver中执行

2.5RDD中函数的传递

在实际开发中我们往往需要自己定义溢写对RDD的操作,那么此时需要注意点是,初始化工作是在Driver端进行的,而实际运行程序是在Executor端进行的,这就涉及到了跨进程通信,是需要序列化的

2.5.1传递一个方法

使类继承Scala.Serializable即可

代码参考

2.5.2传递一个属性

  1. 使类继承Scala.Serializable
  2. 将类变量赋值给局部变量

以上方法二选一即可

2.6RDD依赖

2.6.1Lineage

RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血缘)记录下来,以便恢复丢失的分区。RDD的lineage会记录RDD元素的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据

dataRDD1.toDebugString // 查看依赖关系
dataRDD1.dependencies  // 查看依赖类型 宽依赖还是窄依赖

参考代码

2.6.2窄依赖

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

2.6.3宽依赖

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

2.6.4DAG

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

2.6.5任务划分

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的关系

2.7RDD缓存

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

// cache方法 实际调用了 persist() 方法
def cache(): this.type = persist()

// persist() 方法传入默认 StorageLevel.MEMORY_ONLY 级别
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)


// 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)

// 上面枚举值的输入参数如下
def apply( 
      useDisk: Boolean,   // 是否使用磁盘
      useMemory: Boolean,  // 是否使用内存
      useOffHeap: Boolean,   // 是否使用堆外内存
      deserialized: Boolean,   // 是否需要反序列化
      replication: Int): StorageLevel = { // 副本数量
    getCachedStorageLevel(
      new StorageLevel(useDisk, useMemory, useOffHeap, deserialized, replication))
  }

 

Storage Level(存储级别)Meaning(含义)
MEMORY_ONLY将 RDD 以反序列化的 Java 对象的形式存储在 JVM 中。如果内存空间不够,部分数据分区将不再缓存,在每次需要用到这些数据时重新进行计算。这是默认的级别。
MEMORY_AND_DISK将 RDD 以反序列化的 Java 对象的形式存储在 JVM 中。如果内存空间不够,将未缓存的数据分区存储到磁盘,在需要使用这些分区时从磁盘读取。
MEMORY_ONLY_SER 
(Java and Scala)将 RDD 以序列化的 Java 对象的形式进行存储(每个分区为一个 byte 数组)。这种方式会比反序列化对象的方式节省很多空间,尤其是在使用 fast serializer 时会节省更多的空间,但是在读取时会增加 CPU 的计算负担。
MEMORY_AND_DISK_SER 
(Java and Scala)类似于 MEMORY_ONLY_SER,但是溢出的分区会存储到磁盘,而不是在用到它们时重新计算。
DISK_ONLY只在磁盘上缓存 RDD。
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc。 与上面的级别功能相同,只不过每个分区在集群中两个节点上建立副本。 
OFF_HEAP(experimental 实验性)类似于 MEMORY_ONLY_SER,但是将数据存储在 off-heap memory 中。这需要启用 off-heap 内存。
  • 通过查看源码发现cache最终也是调用了persist方法,默认的存储级别是尽在内存存储一份,Spark的存储级别还有好多种,存储级别在Object StorageLevel定义
  • RDD 可以使用 persist() 方法或 cache() 方法进行持久化。数据将会在第一次 action 操作时进行计算,并缓存在节点的内存中。Spark 的缓存具有容错机制,如果一个缓存的 RDD 的某个分区丢失了,Spark 将按照原来的计算过程,自动重新计算并进行缓存
  • Spark 会自动监视每个节点上的缓存使用情况,并使用 least-recently-used(LRU)的方式来丢弃旧数据分区。如果您想手动删除 RDD 而不是等待它掉出缓存,使用 RDD.unpersist() 方法

参考官网文档:中文 英文   

代码参考

2.8RDD Checkpoint

  • Spark中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能
  • 为当前RDD设置检查点。该函数将会创建一个二进制文件,并存储到checkpoint目录中,该目录是用SparkContext.setCheckpointDir()设置的。在checkpoint过程中,该RDD的所有依赖于父RDD中的信息将全部被移除。对RDD进行checkpoint操作不会马上执行,必须执行action算子才能触发

代码参考

3.键值对RDD分区

Spark 目前支持 Hash 分区和 Range 分区,用户也可以自定义分区,Hash 分区为当前 的默认分区,Spark 中分区器直接决定了 RDD 中分区的个数、RDD 中每条数据经过 Shuffle 过程属于哪个分区和 Reduce 的个数

3.1获得RDD分区

可以通过使用 RDD 的 partitioner 属性来获取 RDD 的分区方式。它会返回一个scala.Option 对象, 通过 get 方法获取其中的值

3.2Hash分区

HashPartitioner 分区的原理:对于给定的 key,计算其 hashCode,并除以分区的个数 取余,如果余数小于 0,则用余数+分区的个数(否则加 0),最后返回的值就是这个 key 所 属的分区 ID。

3.3Range分区

HashPartitioner 分区弊端:可能导致每个分区中数据量的不均匀,极端情况下会导致 某些分区拥有 RDD 的全部数据。
RangePartitioner 作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区 中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分 区内的元素小或者大,但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内 的数映射到某一个分区内。实现过程为:
第一步:先重整个 RDD 中抽取出样本数据,将样本数据排序,计算出每个分区的最 大 key 值,形成一个 Array[KEY]类型的数组变量 rangeBounds;
第二步:判断 key 在 rangeBounds 中所处的范围,给出该 key 值在下一个 RDD 中的 分区 id 下标;该分区器要求 RDD 中的 KEY 类型必须是可以排序的

 

3.4自定义分区

要实现自定义的分区器,你需要继承 org.apache.spark.Partitioner 类并实现下面三个方法
(1)numPartitions: Int:返回创建出来的分区数。
(2)getPartition(key: Any): Int:返回给定键的分区编号(0 到 numPartitions-1)。 
(3)equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个 方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同。

4.数据读取与保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。 文件格式分为:Text 文件、Json 文件、Csv 文件、Sequence 文件以及 Object 文件; 文件系统分为:本地文件系统、HDFS、HBASE 以及数据库

4.1 文件类数据读取与保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。 文件格式分为:Text 文件、Json 文件、Csv 文件、Sequence 文件以及 Object 文件; 文件系统分为:本地文件系统、HDFS、HBASE 以及数据库


4.1.1 Text 文件

代码参考wordcount

4.1.2 Json 文件

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

代码参考

4.1.3 Sequence 文件

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

代码参考上文 saveAsSequenceFile action算子

4.1.4 对象文件

对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。可以通过 objectFile[k,v](path) 函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调 用 saveAsObjectFile() 实现对对象文件的输出。因为是序列化所以要指定类型

代码参考上文 saveAsObjectFile action算子

4.2 文件系统类数据读取与保存

4.2.1 HDFS

Spark 的整个生态系统与 Hadoop 是完全兼容的,所以对于 Hadoop 所支持的文件类型 或者数据库类型,Spark 也同样支持.另外,由于 Hadoop 的 API 有新旧两个版本,所以 Spark 为 了能够兼容 Hadoop 所有的版本,也提供了两套创建操作接口.对于外部存储创建操作而 言,hadoopRDD 和 newHadoopRDD 是最为抽象的两个函数接口,主要包含以下四个参数.

  • 输入格式(InputFormat): 制定数据输入的类型,如 TextInputFormat 等,新旧两个版本 所引用的版本分别是 org.apache.hadoop.mapred.InputFormat 和 org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
  • 键类型: 指定[K,V]键值对中 K 的类型
  • 值类型: 指定[K,V]键值对中 V 的类型
  • 分区值: 指定由外部存储生成的 RDD 的 partition 数量的最小值,如果没有指定,系统会使用默认值 defaultMinSplits

注意:其他创建操作的 API 接口都是为了方便最终的 Spark 程序开发者而设置的,是这两个 接口的高效实现版本.例如,对于 textFile 而言,只有 path 这个指定文件路径的参数,其他参数 在系统内部指定了默认值

4.2.2 MySQL 数据库连接

代码参考

4.2.3 HBase 数据库

暂略

5.编程进阶

5.1累加器

累加器用来对信息进行聚合,通常在向 Spark 传递函数时,比如使用 map() 函数或者 用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会 得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。如果我 们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果

5.1.1系统累加器

通过在驱动器中调用 SparkContext.accumulator(initialValue)方法,创建出存有初始值的 累加器。返回值为 org.apache.spark.Accumulator[T] 对象,其中 T 是初始值 initialValue 的 类型。Spark 闭包里的执行器代码可以使用累加器的 += 方法(在 Java 中是 add)增加累加器 的值。 驱动器程序可以调用累加器的 value 属性(在 Java 中使用 value()或 setValue())来访问 累加器的值。 注意:工作节点上的任务不能访问累加器的值。从这些任务的角度来看,累加器是一个只写 变量。

对于要在行动操作中使用的累加器,Spark 只会把每个任务对各累加器的修改应用一次。 因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在 foreach() 这样的行动操作中。转化操作中累加器可能会发生不止一次更新

代码参考

5.1.2自定义累加器

实现自定义类型累加器需 要继承 AccumulatorV2 并至少覆写

代码参考

5.2广播变量(调优策略)

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一 个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询 表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。 在多个并 行操作中使用同一个变量,但是 Spark 会为每个任务分别发送

使用广播变量的过程如下:

  1. 通过对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T]对 象。任何可序列化的类型都可以这么实现
  2. 通过 value 属性访问该对象的值(在 Java 中为 value()方法)
  3. 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节 点)

代码参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值