在《第一篇|Spark概览》一文中,对Spark的整体面貌进行了阐述。本文将深入探究Spark的核心组件–Spark core,Spark Core是Spark平台的基础通用执行引擎,所有其他功能均建立在该引擎之上。它不仅提供了内存计算功能来提高速度,而且还提供了通用的执行模型以支持各种应用程序,另外,用户可以使用Java,Scala和Python API开发应用程序。Spark core是建立在统一的抽象RDD之上的,这使得Spark的各个组件可以随意集成,可以在同一个应用程序中使用不同的组件以完成复杂的大数据处理任务。本文主要讨论的内容有:
- 什么是RDD
- RDD的设计初衷
- RDD的基本概念与主要特点
- 宽依赖与窄依赖
- stage划分与作业调度
- RDD操作算子
- Transformations
- Actions
- 共享变量
- 广播变量
- 累加器
- 持久化
- 综合案例
什么是RDD
设计初衷
RDD(Resilient Distributed Datasets)的设计之初是为了解决目前存在的一些计算框架对于两类应用场景的处理效率不高的问题,这两类应用场景是迭代式算法和交互式数据挖掘。在这两种应用场景中,通过将数据保存在内存中,可以将性能提高到几个数量级。对于迭代式算法而言,比如PageRank、K-means聚类、逻辑回归等,经常需要重用中间结果。另一种应用场景是交互式数据挖掘,比如在同一份数据集上运行多个即席查询。大部分的计算框架(比如Hadoop),使用中间计算结果的方式是将其写入到一个外部存储设备(比如HDFS),这会增加额外的负载(数据复制、磁盘IO和序列化),由此会增加应用的执行时间。
RDD可以有效地支持多数应用中的数据重用,它是一种容错的、并行的数据结构,可以让用户显性地将中间结果持久化到内存中,并且可以通过分区来优化数据的存放,另外,RDD支持丰富的算子操作,用户可以很容易地使用这些算子对RDD进行操作。
基本概念
一个RDD是一个分布式对象集合,其本质是一个只读的、分区的记录集合。每个RDD可以分成多个分区,不同的分区保存在不同的集群节点上(具体如下图所示)。RDD是一种高度受限的共享内存模型,即RDD是只读的分区记录集合,所以也就不能对其进行修改。只能通过两种方式创建RDD,一种是基于物理存储的数据创建RDD,另一种是通过在其他RDD上作用转换操作(transformation,比如map、filter、join等)得到新的RDD。
RDD不需要被物化,它通过血缘关系(lineage)来确定其是从RDD计算得来的。另外,用户可以控制RDD的持久化和分区,用户可以将需要被重用的RDD进行持久化操作(比如内存、或者磁盘)以提高计算效率。也可以按照记录的key将RDD的元素分布在不同的机器上,比如在对两个数据集进行JOIN操作时,可以确保以相同的方式进行hash分区。
主要特点
-
基于内存
RDD是位于内存中的对象集合。RDD可以存储在内存、磁盘或者内存加磁盘中,但是,Spark之所以速度快,是基于这样一个事实:数据存储在内存中,并且每个算子不会从磁盘上提取数据。
-
分区
分区是对逻辑数据集划分成不同的独立部分,分区是分布式系统性能优化的一种技术手段,可以减少网络流量传输,将相同的key的元素分布在相同的分区中可以减少shuffle带来的影响。RDD被分成了多个分区,这些分区分布在集群中的不同节点。
-
强类型
RDD中的数据是强类型的,当创建RDD的时候,所有的元素都是相同的类型,该类型依赖于数据集的数据类型。
-
懒加载
Spark的转换操作是懒加载模式,这就意味着只有在执行了action(比如count、collect等)操作之后,才会去执行一些列的算子操作。
-
不可修改
RDD一旦被创建,就不能被修改。只能从一个RDD转换成另外一个RDD。
-
并行化
RDD是可以被并行操作的,由于RDD是分区的,每个分区分布在不同的机器上,所以每个分区可以被并行操作。
-
持久化
由于RDD是懒加载的,只有action操作才会导致RDD的转换操作被执行,进而创建出相对应的RDD。对于一些被重复使用的RDD,可以对其进行持久化操作(比如将其保存在内存或磁盘中,Spark支持多种持久化策略),从而提高计算效率。
宽依赖和窄依赖
RDD中不同的操作会使得不同RDD中的分区产不同的依赖,主要有两种依赖:宽依赖和窄依赖。宽依赖是指一个父RDD的一个分区对应一个子RDD的多个分区,窄依赖是指一个父RDD的分区对应与一个子RDD的分区,或者多个父RDD的分区对应一个子RDD分区。关于宽依赖与窄依赖,如下图所示:
Stage划分
窄依赖会被划分到同一个stage中,这样可以以管道的形式迭代执行。宽依赖所依赖的分区一般有多个,所以需要跨节点传输数据。从容灾方面看,两种依赖的计算结果恢复的方式是不同的,窄依赖只需要恢复父RDD丢失的分区即可,而宽依赖则需要考虑恢复所有父RDD丢失的分区。
DAGScheduler会将Job的RDD划分到不同的stage中,并构建一个stage的依赖关系,即DAG。这样划分的目的是既可以保障没有依赖关系的stage可以并行执行,又可以保证存在依赖关系的stage顺序执行。stage主要分为两种类型,一种是ShuffleMapStage,另一种是ResultStage。其中ShuffleMapStage是属于上游的stage,而ResulStage属于最下游的stage,这意味着上游的stage先执行,最后执行ResultStage。
- ShuffleMapStage
ShuffleMapStage是DAG调度流程的中间stage,它可以包含一个或者多个ShuffleMapTask,用与生成Shuffle的数据,ShuffleMapStage可以是ShuffleMapStage的前置stage,但一定是ResultStage的前置stage。部分源码如下:
private[spark] class ShuffleMapStage(
id: Int,
rdd: RDD[_],
numTasks: Int,
parents: List[Stage],
firstJobId: Int,
callSite: CallSite,
val shuffleDep: ShuffleDependency[_, _, _],
mapOutputTrackerMaster: MapOutputTrackerMaster)
extends Stage(id, rdd, numTasks, parents, firstJobId, callSite) {
// 省略代码
}
}
- ResultStage
ResultStage可以使用指定的函数对RDD中的分区进行计算并得到最终结果,ResultStage是最后执行的stage,比如打印数据到控制台,或者将数据写入到外部存储设备等。部分源码如下:
private[spark] class ResultStage(
id: Int,
rdd: RDD[_],
val func: (TaskContext, Iterator[_]) => _,
val partitions: Array[Int],
parents: List[Stage],
firstJobId: Int,
callSite: CallSite)
extends Stage(id, rdd, partitions.length, parents, firstJobId,