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 进程上: