RDD-Resilient Distributed Datasets 弹性分布式数据集

RDD- 弹性分布式数据集

RDD 是Spark技术的核心,接下来我们来探讨RDD中的核心概念和问题。

RDD 创建

这里有三种构建RDD的方式:

1.并行化一个内存中的集合。

第一种方法适用于在少量数据集上,并行执行CPU增强型的计算。并行化的程度由单个机器,或者机器集群所有CPU核的总数所决定。

val params = sc.parallelize(1 to 10)
val result = params.map(performExpensiveComputation)

2.利用存储在硬盘的数据集。

第二种方法是为外部数据集创建一个引用。外部数据可以是位于本地或者HDFS上的文件,格式既可以是文本,也可以是SequenceFile等其他类型的文件。 文件分割的形式和Hadoop 类似,其中每一个HDFS block上,都会有一个Spark 分区。

val text: RDD[String] = sc.textFile(inputPath)
sc.textFile(inputPath, 10)

3.转化一个已经存在的RDD。

RDD 转换和行动

Spark 为RDD提供了两种操作,转换和行动。转换是从现存的RDD中产生新的RDD,行动则会在RDD上触发一些计算,计算结果会返回给用户或者存储到外部设备。行动是即时的效果,但转换是惰性的,直到有行动作用在RDD上,转换才会触发。

判断操作是转换还是行动的方法就是看他们返回的类型:转换会返回RDD,返回其他类型则是行动。

聚集转换

reduceByKey(),foldByKey(),aggregateByKey()是三种聚集转换的方法,他们都可以将(K1, List(V1)) 转换为 List(K2, V2)。值得注意的是,在分布式系统中,他们会运行在不同的分区和任务上,他们的执行顺序不会对结果产生影响。

//reduceByKey
val pairs: RDD[(String, Int)] = sc.parallelize(Array(("a", 3), ("a", 1),
                                ("b", 7), ("a", 5)))
val sums: RDD[(String, Int)] = pairs.reduceByKey(_+_)
assert(sums.collect().toSet === Set(("a", 9), ("b", 7)))
//foldByKey
val sums: RDD[(String, Int)] = pairs.foldByKey(0)(_+_)
assert(sums.collect().toSet === Set(("a", 9), ("b", 7)))
//aggregateByKey
val sets: RDD[(String, HashSet[Int])] = pairs.aggregateByKey(
                                new HashSet[Int])(_+=_, _++=_)
//+= Set对每个Value执行的操作:将value添加进set
//++= Set之间的操作:融合成一个Set
assert(sets.collect().toSet === Set(("a", Set(1, 3, 5)), ("b", Set(7))))

RDD缓存

相比MapReduce, Spark的一大优势就在于它非常适合迭代运算,因为它可以将RDD直接存储在内存中,这样在进行迭代计算的时候可以避免读写硬盘。

但是内存中的数据集只能被同一个应用共享。如果想要跨应用,只能用saveAs*()方法写入外部存储。当应用程序停止运行后,所有的RDD缓存也会被销毁。

scala> tuples.cache()
    res1: tuples.type = MappedRDD[4] at map at <console>:18
//cache()的调用不会将tuples直接存入内存,直到Spark任务执行时,tuples才会被加载如内存中
scala> tuples.reduceByKey((a, b) => Math.max(a, b)).foreach(println(_))
    INFO BlockManagerInfo: Added rdd_4_0 in memory on 192.168.1.90:64640
    INFO BlockManagerInfo: Added rdd_4_1 in memory on 192.168.1.90:64640
    (1950,22)
    (1949,111)
//第二次运行直接在本地内存找到tuples RDD
scala> tuples.reduceByKey((a, b) => Math.min(a, b)).foreach(println(_))
    INFO BlockManager: Found block rdd_4_0 locally
    INFO BlockManager: Found block rdd_4_1 locally
    (1949,78)
    (1950,-11)
持久化级别

执行cache()会将RDD的每一个分区都存储到本地的内存。但是当内存不足时,Spark往往不会重新计算分区,而是会执行其他级别的持久化行为。

默认的持久化级别是MEMORY_ONLY,他会将对象直接存入内存。另一个节约内存的方式是MEMORY_ONLY_SER,他将RDD序列化为字节数组存入内存。虽然这种方式比较消耗CPU,但他能降低垃圾回收器的压力,因为所有的对象都变成字节数组。

此外,通过 MEMORY_AND_DISK 或者 MEMORY_AND_DISK_SER, 我们也可以将多余数据存入磁盘。

序列化

在Spark中,序列化有两个方面需要考虑:1.序列化变量 2.序列化方法

  • 变量
    Spark默认的序列化方式是实现Java的序列化接口,他将数据通过网络从一台执行机器传递到另一台。但从数据大小和性能的角度考虑,Java Serialize 不是一个好的选择。

    大多数的Spark应用会采用Kryo 序列化, Kryo不需要类去实现接口,而是通过注册的方式。注册一个Kryo类需要创建一个KryoRegistrator的子类,然后重写registerClasses()方法。

class CustomKryoRegistrator extends KryoRegistrator {
 override def registerClasses(kryo: Kryo) {
 kryo.register(classOf[WeatherRecord])//WeatherRecord 是序列化类的名字
 }
}
//最后在驱动项目中设置registrator属性。
conf.set("spark.kryo.registrator", "CustomKryoRegistrator")
  • 方法
    Spark中采用了Java序列化的机制来实现方法序列化,它可以将方法通过网络传递到远程执行节点。

共享变量

Spark 程序经常需要处理一些不在RDD中的数据,其中一种就是广播变量。

广播变量

不同于常规变量被序列化为闭包函数的一部分,广播变量会直接被序列化并发送到每一个机器。但是广播变量是单向传递的,只会从驱动到任务,所以任务无法更新它,为了解决这个问题只能使用累加变量。

//未使用广播变量
val lookup = Map(1 -> "a", 2 -> "e", 3 -> "i", 4 -> "o", 5 -> "u")
val result = sc.parallelize(Array(2, 1, 3)).map(lookup(_))
assert(result.collect().toSet === Set("a", "e", "i"))

//使用广播变量
val lookup: Broadcast[Map[Int, String]] =sc.broadcast(
                    Map(1 -> "a", 2 -> "e", 3 -> "i", 4 -> "o", 5 -> "u"))
val result = sc.parallelize(Array(2, 1, 3)).map(lookup.value(_))
assert(result.collect().toSet === Set("a", "e", "i"))
累加变量

累计器是一种只能被添加的变量,类似MapReduce里的计数器。在任务结束后,累加器的值可以从驱动中取出。

val count: Accumulator[Int] = sc.accumulator(0)
val result = sc.parallelize(Array(1, 2, 3)).map(i => { count += 1; i })
             .reduce((x, y) => x + y)
assert(count.value === 3)
assert(result === 6)

Spark 运行流程

让我们接下来了解一下Spark应用程序的运行流程。首先要明确的是,这里有两个独立的实体:Driver 和 Executor。Driver用来管理应用和调度任务,executor则用来执行任务。
这里写图片描述

任务提交

首先在Driver的流程中,Spark程序会将工程提交给SparkContext,而SparkContext则会运行调度器。调度器分为两部分:一个是DAG调度器, 它会将整个工程分割成一连串的阶段,第二个是任务调度器,它用来把每个阶段内的任务提交给机器集群。

DAG构建

为了理解如何将工程分割成不同阶段,我们需要了解一下任务的类型。这里有两种任务:shuffle map task和result task。

  • Shuffle Map Task:
    与MapReduce 中Map阶段重排列的功能类似,shuffle会对RDD的一个分区进行运算,然后将运算结果写入一个新的分区,作为下一个任务的输入数据。需要注意的是, Shuffle Map Task 不会运行在最后阶段。
  • Result Task
    Result task 只会运行在最后的阶段,最后将结果返回给用户程序(Driver)。Driver再整合来自不同分区result map 返回的结果。
val hist: Map[Int, Long] = sc.textFile(inputPath)
 .map(word => (word.toLowerCase(), 1))
 .reduceByKey((a, b) => a + b) //Spark 会依据reduceByKey中的重排列将Job分成两个阶段
 .map(_.swap)                  //reduceByKey()在Map阶段处理本地数据时是combiner
 .countByKey()                 //但是在Reduce阶段则是reducer
任务调度

对于特定的执行进程,任务调度器会先去让它执行同一进程下的任务,再去执行位于同一台机器(节点)的任务,然后是同一个rack内,最后才会执行任意非本地的任务。

任务执行

任务分为以下几个阶段执行:
1. 首先,它要更新JAR包和文件依赖。因为执行进程会保存前面任务的依赖到缓存,只有这些依赖发生变化才会更新。
2. 其次,它会将任务代码反序列化。
3. 最后,它会执行任务代码。
对于Result Task, 任务的执行结果会经序列化后返回给驱动程序。Shuffle Map Task 会返回信息来告知下一个阶段的程序来获取他的输出数据。

Spark On YARN

运行在YARN上的Spark分为两种模式:客户模式和集群模式。

  • 客户模式的Spark支持交互组件,像spark-shell或者pyspark。它可以编写脚本执行spark任务。
    Spark程序会运行在用户节点上:

这里写图片描述

  • 集群模式的则将整个应用程序运行在集群上,这方便查看日志文件。
    Spark应用程序会运行在application master 进程上:
    这里写图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值