Spark弹性分布式数据集介绍

弹性分布式数据集 RDD

我们都知道Spark主要时以RDD的概念为中心,在对数据处理的过程中对RDD进行操作得到我我们想要的数据结果。

RDD是一个容错可以执行并行的元素的集合,但是并不是实际保存数据,是对数据的引用。

有两种方法创建RDD:1、在驱动程序中parallelizing一个已经存在的集合 2、外部存储中引用一个数据集(HDFS/HBase....等)。

  • 并行集合

可以调用SparkContext的parallelize方法在已存在的集合上进行创建,该集合的元素从一个可以并行操作的distributed dataset(分布式数据集)中复制到另一个dataset中去,

scala中:

val data = Array(1,2,3,4,5,6,7,8,9)

val distData = sc.parallelize(data)

这样之后我们就可以对转化后的Array集合进行并行化操作,如调用reduce方法求和:

distData.reduce(_+_) 或者distDara.reduce((a,b)=>a+b)来合计数据中的元素。

并行集合中很重要的一个参数是partition(分区)的数量,他可以用来切割dataset,spark在运行时实在每个分区上运行一个任务的。一般情况下spark尝试根据集群的情况来自动设置分区的数量,也可以参数指定,parallelize(data,5).或者时.repartition(numPartition)来指定

  • 外部数据集

Spark可从Hadoop所支持的任何存储源创建distributed dataset,如本地,hdfs,Hbase,S3等

可使用sc.textFile("path")来创建文本文件的RDD,path可以时本地、hdfs等url,并且读取他们作为一个行集合

 scala> val lines = sc.textFile("data.txt")

 创建后lines可以使用dataset的操作,如lines.map(line => line.length).reduce((a,b)=>a+b)

  • 一些注意事项:
  1. 本地路径的话,工作节点访问的该路径下的文件必须可访问
  2. 所有Spark中基于文件的输入方法,包括textFile(文本文件),支持目录压缩文件,或通配符来操作。即可读一个目录下的所有文件,可读压缩文件,利用通配符过滤读取文件。如:sc.textFile("..../directory/*.dat"),这样只会读取以.dat结尾的文件。
  3. textFile()方法可通过第二个参数控制文件的分区数量,默认情况下spark默认为文件的每一个分块创建一个分区,
  • 除文本文件外,其他数据格式支持:
  1. 。。。
  • RDD操作

RDD支持两种的操作:1、transformation(转换):在一个dataset上创建一个新的dataset 2、actions(动作):在dataset上运行结果返回到驱动程序

例如map()通过对数据集中的每个元素都执行一个函数,返回新的dataset。reduce通过执行函数,聚合RDD中所有元素,结果返回给驱动程序的action。

转换操作都是lazy的,仅记录数据集的转换,不会立即计算结果。仅当需要 action操作时才进行计算。Q:这样为什么使Spark高效?A:map()创建的数据集合是要被用在reduce上的,仅reduce的计算结果才返回给驱动程序,而不是映射一个更大的数据集。即:我们最终要的的是action的计算结果,如果每一次转换都计算出结果映射起来的话,这样会映射出很大的数据集。

action是每个transformation RDD都会被计算,对于经常要使用的RDD,可persist或chache将RDD持久化到内存中,需要时更快的访问。也支持持久化到磁盘,或复制到多个节点。

基础讲解:

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

第一行定义基本RDD,并未加载到内存或action,line仅仅是类似指针的东西指向文件。

此时:lines是一行一行的数据集合,因为读的时候是一行一行读的

第二行是定义map()执行计算每一行长度,此时不会立即计算,因为laziness

第三行执行action,此时spark(通过Driver)分发计算任务task到不同机器上的Excuter运行,每个机器运行他这一部分的map和本地reduce仅返回聚合结果给驱动程序。

如果以后需要多次使用到lineLengths ,我们可以进行持久化操作:lineLengths.persist()

注意:sc.textFile(param) 中的 param 可以是以逗号分隔的字符串路径。

"/directory1/xxx.dat,/directory2/xxx.dat,/directory3/xxx.dat"

 传递函数给spark

驱动程序在集群上运行时,Spark的API很大程度上依赖传递函数。2中方式实现

  • 匿名函数,用于短暂代码片段
  • 全局单例对象中的静态方法,如:定义MF,传递func1
object MyFunctions {
  def func1(s: String): String = { ... }
} 
myRdd.map(MyFunctions.func1)

 也会有传递一个类的实例的方法的引用,需要发送整个对象,包括类的其他方法。

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

如果我们创建一个MtClass的实例,调用doStuff,在map内中有func1的引用,所以整个对象是要发送到集群的。Q:这是闭包??

类似rdd.map(x => this.func1(x)),类似的方式,访问外部的字段将引用整个对象:


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

 

相当于写:rdd.map( x => this.field +x),他将引用对象的所有东西,为避免这个问题。最简单的方法是field到本地变量,而不是从外部访问他:


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

理解闭包

在集群执行代码时,理解变量和生命周期时很难的

例子:简单RDD求和

var counter = 0
var rdd = sc.parallelize(data)
// Wrong: Don't do this!!
rdd.foreach(x => counter += x)
println("Counter value: " + counter)

以上代码可能无法按预期工作,Spark在执行作业时,会分解RDD操作到每个执行单元里,执行前,spark计算任务的closure(闭包)。二闭包时在RDD上的执行者必须能够访问的变量和方法,闭包被序列化发送到每个执行器。

闭包的变量副本发给每个 executor ,当 counter 被 foreach 函数引用的时候,它已经不再是 driver node 的 counter 了。虽然在 driver node 仍然有一个 counter 在内存中,但是对 executors 已经不可见。executor 看到的只是序列化的闭包一个副本。所以 counter 最终的值还是 0,因为对 counter 所有的操作所有操作均引用序列化的 closure 内的值。

打印 RDD 的元素

一台机器上:打印 RDD 的所有元素使用 rdd.foreach(println) 或 rdd.map(println)

集群上:得先收集到Driver端, rdd.collect().foreach(println),这样可能driver内存耗尽,可rdd.take(100).foreach(println)

使用Key-Value对工作

。。。

Transformat(转换)

map(func) ---------返回新的数据集,有数据源对每一个元素进行func

filter(f)       ---------返回新的数据集,返回为true的元素

flatMap()   ---------返回Seq而非item

。。。

Actions(动作)

reduce()

collect()

count()

take()

。。。

Shuffle操作

spark重新分发数据的一种机制,使这些数据可以跨不同区域进行分组,涉及executer和机器之间的拷贝数据。

以reduceByKey为例,key相同的所有的值被组合成一个tuple-key以及在reduce函数上执行结果,但一个key的所有值不一定都在一个分区里,甚至不在同一台机器,但必须被共同计算。

在 spark 里,特定的操作需要数据不跨分区分布。在计算期间,一个任务在一个分区上执行,为了所有数据都在单个 reduceByKey 的 reduce 任务上运行,我们需要执行一个 all-to-all 操作。它必须从所有分区读取所有的 key 和 key对应的所有的值,并且跨分区聚集去计算每个 key 的结果 - 这个过程就叫做 shuffle

尽管每个分区新 shuffle 的数据集将是确定的,分区本身的顺序也是这样,但是这些数据的顺序是不确定的。如果希望 shuffle 后的数据是有序的,可以使用 : 

  • mapPartitions 对每个分区进行排序,例如 .sorted
  • repartitionAndSortWithinPartitions 在分区的同时对分区进行高效的排序。
  • sortBy 做一个整体的排序。

触发 shuffle 的操作包括 repartition 操作,如 repartitioncoalesce;'ByKey' 操作(除了 counting 相关操作),如 groupByKeyreduceByKey 和 join 操作,如 cogroup 和 join

shuffle代价很高,涉及IO,序列化,网络io。

Spark启动一些列map和reduce,map组织数据,reduce聚合数据。内部,map任务的结果都会保存在内存中,知道内存不能完全存储,这些数据将基于目标分区进行排序写入到一个单独文件中,reduce时读取已排序的数据块;

某些 shuffle 操作会大量消耗堆内存空间, shuffle 操作在数据转换前后,需要在使用内存中的数据结构对数据进行组织。reduceByKey 和 aggregateByKey 在 map 时会创建这些数据结构,ByKey 操作在 reduce 时创建这些数据结构。当内存满的时候,Spark 会把溢出的数据存到磁盘上,这将导致额外的磁盘 IO 开销和垃圾回收开销的增加。

shuffle操作会磁盘生成大量文件,长期运行spark将会消耗大量的磁盘空间,临时存储路径可设置。

RDD持久化(缓存)

持久化一个RDD时,每个节点的其他分区都可以使用RDD在内存中进行计算,在该数据上的其他 action 操作将直接使用内存中的数据。action速度提升。缓存时迭代算法和快速的交互式使用的重要工具。

RDD使用cache和persist进行持久化,第一次action时进行计算,并缓存在节点的内存中。缓存具有容错机制,某个缓存的rdd在某个分区中丢失,spark照原来计算过程,重新计算并缓存。

缓存有多种存储级别,cache默认内存,persist可以设置,各个存储级别如下:

shuffle操作中,即使没调用persist方法,spark也会自动缓存部分数据。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

一些小知识点: 

  • cache和persist的区别:

cache()调用了persist(),cache只有一个默认的缓存级别MEMORY_ONLY ,而persist可以根据情况设置其它的缓存级别。

/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */
def cache(): this.type = persist()
/** Persist this RDD with the default storage level (`MEMORY_ONLY`). */
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值