RDD Programming Guide

[TOC]

概述

简单来说,每个Spark程序都包含一个driver(运行用户的main function)以及一个并行执行各种操作的cluster. 首先,Spark提供了RDD这个核心概念,RDD即指在集群中可以并行操作的数据集合,RDD可以由hadoop文件或driver中的集合实例经过一系列转化(transform)生成,用户可以将RDD在内存中持久化,方便后续重用。最后,集群节点故障时,RDD是可以自恢复的。 其次,Spark还有个比较重要的抽象为并行操作中的共享变量。默认情况下,Spark在不同节点上并行执行一组包含同样目标操作的任务时,Spark会将这个目标操作需要用的变量复制到每个任务中去。Spark支持两种类型的共享变量:广播变量(broadcast variables, 在所有节点的内存中均缓存该变量),累加器(accumulator,只能进行累加的跨节点累加器)

RDD (Resilient Distributed Datasets)

Spark提出了RDD的概念,RDD即指可以并行处理且具有容错性的数据集合。Spark中有两种方式创建RDD:将driver中的集合数据并行化;直接从外部存储系统(如HDFS,Hbase或其他的hadoop系列输入源)数据集

Parallelized Collection

在driver中对内存中的集合(如Scala Seq)调用SparkContext提供的并行化接口,即可创建并行化集合。集合的元素会被拷贝到各个节点,最终构成一个可并行计算的分布式数据集。以Scala 为例

val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)
复制代码

分布式数据集(distData)创建好后,就可以对其进行各种并行操作.如,可以求集合元素总和distData.reduce((a, b) => a + b)。后续章节将详细介绍分布式数据集的各种计算操作。 对于parallel collections而言,一个关键参数是分区数, Spark中一个分区即对应着一个task。一般来说,集群中一个CPU最好负责2-4个partition的计算,但Spark会根据集群自动进行partition的分配。当然,你页可以通过设置参数来手动控制分区数(e.g. sc.parallelize(data, 10))。

External Datasets

Spark支持很多Hadoop支持的数据源,如本地文件、HDFS、Cassandra、HBase、Amazon S3等。Spark支持Hadoop支持的文件格式,如纯文本、SequenceFiles 等。 文件类型的RDD可以通过SparkContext的textFile方法创建,该方法需要传参URI(本地路径,hdfs://,s3a://等),并将所有行构成数据集。例如:

scala> val distFile = sc.textFile("data.txt")
distFile: org.apache.spark.rdd.RDD[String] = data.txt MapPartitionsRDD[10] at textFile at <console>:26
复制代码

我们如下可以统计文本所有字符个数distFile.map(s => s.length).reduce((a, b) => a + b)

使用Spark读取文件的一些注意点:

  • 若要读取本地文件,则不管用哪种形式,比如将文件复制到所有节点的相同路径下或直接访问网络地址,该文件地址必须在各个分布式工作节点上均可以正确访问到
  • 所有将文件地址作为输入源的方法(比如textFile)都支持传入目录地址、压缩类文件以及通配符。比如textFile的合法调用包括textFile("/my/directory"), textFile("/my/directory/*.txt") 以及 textFile("/my/directory/*.gz")
  • textFile方法支持第二个传参,用来控制文件分区数。默认情况下,Spark按照文件块大小划分一个分区(HDFS 默认一个块为128M。 通过第二个参数,用户可以扩大分区数,分区数永远不得小于文件块数

除了textFile,Spark还提供了其他Scala API读取其他格式的文件

  • SparkContext.wholeTextFiles lets you read a directory containing multiple small text files, and returns each of them as (filename, content) pairs. This is in contrast with textFile, which would return one record per line in each file. Partitioning is determined by data locality which, in some cases, may result in too few partitions. For those cases, wholeTextFiles provides an optional second argument for controlling the minimal number of partitions.
  • For SequenceFiles, use SparkContext’s sequenceFile[K, V] method where K and V are the types of key and values in the file. These should be subclasses of Hadoop’s Writable interface, like IntWritable and Text. In addition, Spark allows you to specify native types for a few common Writables; for example, sequenceFile[Int, String] will automatically read IntWritables and Texts.
  • For other Hadoop InputFormats, you can use the SparkContext.hadoopRDD method, which takes an arbitrary JobConf and input format class, key class and value class. Set these the same way you would for a Hadoop job with your input source. You can also use SparkContext.newAPIHadoopRDD for InputFormats based on the “new” MapReduce API (org.apache.hadoop.mapreduce).
  • RDD.saveAsObjectFile and SparkContext.objectFile support saving an RDD in a simple format consisting of serialized Java objects. While this is not as efficient as specialized formats like Avro, it offers an easy way to save any RDD.

RDD Operations

RDD支持两种操作:

  • transfromation 转化 : 从已存在的RDD中派生出新的数据集
  • action 计算 : 真正在数据集上进行计算并向driver返回计算结果

对照之前的例子来说,map 将数据集中每个数据进行某种函数转换后生成新的数据集的一个转化操作,reduce则真正负责调用某种函数对数据集进行计算并汇总结果到driver。

Spark中所有的转化操作都是延迟执行的,Spark会记录下对源文件的所有转化操作,只有当action被触发时候,action之前的所有transformation才会可是真正计算。这样设计有利于Spark程序更高效的运行。以上例来说,driver只需要收到最后reduce的结果,而并不需要收到所有的数据。

默认情况下,每个RDD在每次对其调用action操作时候,这个RDD的数据都会重新计算一遍。这个可以通过将RDD持久化在内存或者磁盘里来避免,这样Spark会将数据缓存一份,方便下次的数据调用。Spark提供了多个持久化级别。

基础知识

一个简单的示例

val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)
复制代码

第一行通过读取文件创建了 RDD。此时真实的数据并没有读取 : lines只是个指向目标文件的指针。 第二行lineLengths是map转换后的新RDD。此时lineLengths的数据没有计算出来. 最后, 调用reduce action.此时,Spark将计算拆分成多个任务,运行在不同的机器上。每台机器对它负责的数据进行map之后本地计算reduce,将本地的结果返回给driver

若后续还想调用lineLengths,可以在reduce 之前调用

lineLengths.persist()
复制代码

该方法会在lineLengths首次被计算后将lineLength缓存在内存中

Spark中函数传递

Spark API中经常需要将driver中写的方法传递给executor以便任务在集群中的运行。

  • 匿名方法:适用于比较小的代码块
  • 单例对象中的静态方法,比如
object MyFunctions {
  def func1(s: String): String = { ... }
}
myRdd.map(MyFunctions.func1)
复制代码

注意,跟用单例对象相反,你也可以传递一个类的成员函数,但是这样调用Spark会将包含该方法的类的实例对象也发送到集群里去。例如

class MyClass {
  def func1(s: String): String = { ... }
  def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(func1) }
}
复制代码

本例中,若在MyClass实例上调用doStuff,因为代码里map中引用了MyClas实例的fun1方法,则这个MyClass实例需要被发送到整个集群,该 map 调用等价于rdd.map(x => this.func1(x)) 同样的,在Spark的调用中若引用了外部对象的变量,则涉及的外部变量都会被Spark引用。

class MyClass {
  val field = "Hello"
  def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) }
}
复制代码

等价于rdd.map(x => this.field + x) 为了避免以上情况,最简单的解决办法是将类成员变量拷贝成一个本地变量,Spark中直接调用本地变量。

def doStuff(rdd: RDD[String]): RDD[String] = {
  val field_ = this.field
  rdd.map(x => field_ + x)
}
复制代码

闭包

集群方式执行代码时候,如何理解变量的生命周期以及作用域的理解是Spark中的一个难点。比如一个常见的费解点:RDD计算中修改了作用域之外的变量,到底会有怎样的输出,以通过foreach进行计数器的累加来说明:

例如计算RDD元素总和,根据executor是否处于同一JVM上(本地执行、集群执行),可能出现不同的结果。

var counter = 0
var rdd = sc.parallelize(data)

// Wrong: Don't do this!!
rdd.foreach(x => counter += x)

println("Counter value: " + counter)

复制代码
本地模式VS.集群模式

以上示例代码的结果是不确定, 在集群模式下,Spark会将计算操作分成多个task,将task分配给executor分别执行。在执行前,Spark会计算这个task的闭包,即executor执行这个task需要使用到的变量、函数,这些需要的东西将会被序列化后,发送到各个executor. 发送到executor的闭包里的变量都是新copy出来的,即在真正执行时候,executor执行foreach时候引用的counter对象已经不是driver里的counter对象了。每个executor累加的都是自己copy出来的counter对象,因此driver中的counter一直是0.

而在local模式下,counter对象可能共享,可能在driver中也能看到counter的增长。

为了保证代码按照预期的累加机制运行,在本例中可以使用Spark提供的累加器(Accumulator)。Spark提供了Accumulator来实现集群模式下对同一变量的更改,详细见后续Accumulator的章节。

总而言之,不要通过loop或者修改driver本地变量来完成全集群的状态修改。

RDD元素打印

另一个常见错误用法即通过调用foreach来打印RDD中的元素 rdd.foreach(println) or rdd.map(println)。在单机下,程序可能会按照预期输出。然而集群模式下,在executor的输出中可以看到相应部分的输出,但是driver的输出中看不到任何东西。 若希望在driver中看到所有的RDD元素,可以先调用collect将所有数据汇总到driver,之后再输出,即rdd.collect().foreach(println),这样做的弊端在于有可能导致driver OOM。安全起见,建议用take():```rdd.take(100).foreach(println)``.

键值对

Spark中有些算子只支持key-value pairs,这类算子多是shuffle操作,比如group by key, aggregating by key。 以Scala为例,计算文件中每行出现的次数

val lines = sc.textFile("data.txt")
val pairs = lines.map(s => (s, 1))
val counts = pairs.reduceByKey((a, b) => a + b)
复制代码

可以调用counts.sortByKey()将line进行文本排序,之后collect()输出所有数据。 注意:key-value中,若 key为自定义类对象,注意重写equals以及hashCode

转换

转换API列表

算子

API列表 有一些对应的异步API

Shuffle

Spark中某些操作会引发shuffle,shuffle会导致数据重新分区,设计到数据在不同的机器之间传输,因此shuffle非常复杂且代价很大

背景

以reduceByKey为例来理解shuffle,reduceByKey会生成按照key聚合成的tuple的RDD,其中tuple由(key, reducefunction的结果)组成,问题在于一个key对应的计算值并不一定在同一个partition中,甚至于可能都不在同一台机器上,因此计算必然涉及到数据的重分布。 在Spark中,data is generally not distributed across partitions to be in the necessary place for a specific operation. During computations, a single task will operate on a single partition - thus, to organize all the data for a single reduceByKey reduce task to execute, Spark needs to perform an all-to-all operation. It must read from all partitions to find all the values for all keys, and then bring together values across partitions to compute the final result for each key - this is called the shuffle

虽然对于新shuffle出来的数据,每个partition中包含哪些数据是确定的,partition的排列顺序也是确定,但是一个partiton内数据的顺序是不能保证的。若需要在shuffle时,保证数据顺序,则有以下几种做法:

  • mapPartitions to sort each partition using, for example, .sorted
  • repartitionAndSortWithinPartitions to efficiently sort partitions while simultaneously repartitioning
  • 调用sortBy,令RDD全局有序

Operations which can cause a shuffle include repartition operations like repartition and coalesce, ‘ByKey operations (except for counting) like groupByKey and reduceByKey, and join operations like cogroup and join.

性能影响

Shuffle操作是个很重的操作,因为shuffle会涉及到磁盘I/O,数据的序列化和反序列化以及网络I/O。为了完成shuffle操作,Spark会生成一系列相关的task-map task用来组织数据、reduce task用来聚合数据,map 和reduce的概念来源于MapReduce但是并不完整一一对应。

Spark内部,单个map task生成的结果会优先保存在内存中直到内存中放不下。之后,这些结果按照最终应该对应的partition的顺序写入到单个文件中。在reduce那端,task执行聚合是来读取其相关的文件块。

一般来说,由于在数据传输前后,需要在内存中创建一些数据结构实例来管理这些数据,shuffle操作会非常耗堆内存。具体说来,reduceByKey以及aggregateByKey会在map端创建上述数据结构实例,而其他一些的'ByKey操作在reduce端创建。如果内存中放不下了这些数据,则需要写到磁盘中,进而引发磁盘I/O,加重垃圾回收。(深入)

Shuffle还会在磁盘上创建大量的中间文件,在Spark 1.3中,直到这些文件对应的RDD被垃圾回收掉之前,这些文件会被一直保留在磁盘上。 This is done so the shuffle files don’t need to be re-created if the lineage is re-computed. Garbage collection may happen only after a long period of time, if the application retains references to these RDDs or if GC does not kick in frequently. This means that long-running Spark jobs may consume a large amount of disk space. The temporary storage directory is specified by the spark.local.dir configuration parameter when configuring the Spark context.

Shuffle behavior can be tuned by adjusting a variety of configuration parameters. See the ‘Shuffle Behavior’ section within the Spark Configuration Guide.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值