@Author : Spinach | GHB
@Link : http://blog.csdn.net/bocai8058
Google 在 2003 年和 2004 年先后发表了 Google 文件系统 GFS 和 MapReduce 编程模型两篇文章,基于这两篇开源文档,06 年 Nutch 项目子项目之一的 Hadoop 实现了两个强有力的开源产品:HDFS 和 MapReduce。 Hadoop 成为了典型的大数据批量处理架构,由 HDFS 负责静态数据的存储,并通过 MapReduce 将计算逻辑分配到各数据节点进行数据计算和价值发现。之后以 HDFS 和 MapReduce 为基础建立了很多项目,形成了Hadoop生态圈。
而Spark则是UC Berkeley AMP lab(加州大学伯克利分校AMP实验室)所开源的类Hadoop MapReduce的通用并行框架, 专门用于大数据量下的迭代式计算。是为了跟 Hadoop 配合而开发出来的,不是为了取代 Hadoop, Spark 运算比 Hadoop 的 MapReduce 框架快的原因是因为 Hadoop 在一次 MapReduce 运算之后,会将数据的运算结果从内存写入到磁盘中,第二次 Mapreduce 运算时在从磁盘中读取数据,所以其瓶颈在2次运算间的多余 IO 消耗。 Spark 则是将数据一直缓存在内存中,直到计算得到最后的结果,再将结果写入到磁盘,所以多次运算的情况下, Spark 是比较快的。其优化了迭代式工作负载。具体区别如下:
Spark是专为大规模数据处理而设计的快速通用的计算引擎。
特点
Spark 的主要特点还包括:
RDD(Resilient Distributed Datasets) ,弹性分布式数据集,是Spark底层的分布式存储的数据结构,可以说是Spark的核心,Spark API的所有操作都是基于RDD的。 是分布式内存的一个抽象概念,RDD提供了一种高度受限的共享内存模型,即RDD是只读的记录分区的集合,只能通过在其他RDD执行确定的转换操作(如map、join和group by)而创建,然而这些限制使得实现容错的开销很低。
Spark的基本计算单元,可以通过一系列算子进行操作(主要有Transformation和Action操作)
窄依赖:父RDD每一个分区最多被一个子RDD的分区所用;表现为一个父RDD的分区对应于一个子RDD的分区,或两个父RDD的分区对应于一个子RDD 的分区。
宽依赖:父RDD的每个分区都可能被多个子RDD分区所使用,子RDD分区通常对应所有的父RDD分区。
常见的窄依赖有:map、filter、union、mapPartitions、mapValues、join(父RDD是hash-partitioned :如果JoinAPI之前被调用的RDD API是宽依赖(存在shuffle),而且两个join的RDD的分区数量一致,join结果的rdd分区数量也一样,这个时候join api是窄依赖)。
常见的宽依赖有:groupByKey、partitionBy、reduceByKey、join(父RDD不是hash-partitioned :除此之外的,rdd 的join api是宽依赖)。
Directed Acycle graph,反应RDD之间的依赖关系。
基于DAG划分Stage 并以TaskSet的形式提交Stage给TaskScheduler;负责将作业拆分成不同阶段的具有依赖关系的多批任务;最重要的任务之一就是:计算作业和任务的依赖关系,制定调度逻辑。在SparkContext初始化的过程中被实例化,一个SparkContext对应创建一个DAGScheduler。
Transformation函数
Transformation函数 | 描述 | 类型 |
---|---|---|
map | 将原来 RDD 的每个数据项通过 map 中的用户自定义函数f映射转变为一个新的元素。源码中map算子相当于初始化一个RDD, 新RDD叫做MappedRDD(this, sc.clean(f))。 | Value数据类型,输入分区与输出分区一对一型 |
flatMap | 将原来 RDD 中的每个元素通过函数f转换为新的元素,并将生成的RDD的每个集合中的元素合并为一个集合,内部创建FlatMappedRDD(this,sc.clean(f))。 | Value数据类型,输入分区与输出分区一对一型 |
mapPartitions | 获取到每个分区的迭代器,在函数中通过这个分区整体的迭代器对整个分区的元素进行操作。 内部实现是生成MapPartitionsRDD。 | Value数据类型,输入分区与输出分区一对一型 |
glom | glom函数将每个分区形成一个数组,内部实现是返回的GlommedRDD。 | Value数据类型,输入分区与输出分区一对一型 |
union | 使用 union 函数时需要保证两个RDD元素的数据类型相同,返回的RDD数据类型和被合并的RDD元素数据类型相同,并不进行去重操作,保存所有元素。 | Value数据类型,输入分区与输出分区多对一型 |
cartesian | 对两个RDD内的所有元素进行笛卡尔积操作。操作后,内部实现返回CartesianRDD。 | Value数据类型,输入分区与输出分区多对一型 |
groupBy | 将元素通过函数生成相应的 Key,数据就转化为Key-Value格式,之后将Key相同的元素分为一组。 | Value数据类型,输入分区与输出分区多对多型 |
filter | 对元素进行过滤,对每个元素应用f函 数,返回值为true的元素在RDD中保留,返回值为false的元素将被过滤掉。内部实现相当于生成FilteredRDD(this,sc.clean(f))。 | Value数据类型,输出分区为输入分区子集型 |
distinct | 将RDD中的元素进行去重操作。图9中的每个方框代表一个RDD分区,通过distinct函数,将数据去重。 | Value数据类型,输出分区为输入分区子集型 |
subtract | subtract相当于进行集合的差操作,RDD 1去除RDD 1和RDD 2交集中的所有元素。 | Value数据类型,输出分区为输入分区子集型 |
sample | sample 将 RDD 这个集合内的元素进行采样,获取所有元素的子集。用户可以设定是否有放回的抽样、百分比、随机种子,进而决定采样方式。内部实现是生成SampledRDD(withReplacement, fraction, seed)。 | Value数据类型,输出分区为输入分区子集型 |
takeSample | takeSample()函数和上面的sample函数是一个原理,但是不使用相对比例采样,而是按设定的采样个数进行采样,同时返回结果不再是RDD,而是相当于对采样后的数据进行Collect(),返回结果的集合为单机的数组。 | Value数据类型,输出分区为输入分区子集型 |
cache | cache 将 RDD 元素从磁盘缓存到内存。 相当于 persist(MEMORY_ONLY) 函数的功能。 | Value数据类型,Cache型 |
persist | persist 函数对 RDD 进行缓存操作。数据缓存在哪里依据 StorageLevel 这个枚举类型进行确定。 | Value数据类型,Cache型 |
mapValues | 针对(Key, Value)型数据中的 Value 进行 Map 操作,而不对 Key 进行处理。 | Key-Value数据类型,输入分区与输出分区一对一 |
combineByKey | 只是两个值合并成一个值。 | Key-Value数据类型,单个RDD聚集 |
reduceByKey | reduceByKey 是比 combineByKey 更简单的一种情况,只是两个值合并成一个值,( Int, Int V)to (Int, Int C),比如叠加。 | Key-Value数据类型,单个RDD聚集 |
partitionBy | 对RDD进行分区操作。 | Key-Value数据类型,单个RDD聚集 |
Cogroup | 将两个RDD进行协同划分。 | Key-Value数据类型,两个RDD聚集 |
join | 对两个需要连接的 RDD 进行 cogroup函数操作,将相同 key 的数据能够放到一个分区,在 cogroup 操作之后形成的新 RDD 对每个key 下的元素进行笛卡尔积的操作,返回的结果再展平,对应 key 下的所有元组形成一个集合。最后返回 RDD[(K, (V, W))]。 | Key-Value数据类型,连接 |
leftOutJoin和rightOutJoin | LeftOutJoin(左外连接)和RightOutJoin(右外连接)相当于在join的基础上先判断一侧的RDD元素是否为空,如果为空,则填充为空。 如果不为空,则将数据进行连接运算,并返回结果。 | Key-Value数据类型,连接 |
Action函数
Action函数 | 描述 | 类型 |
---|---|---|
foreach | 对 RDD 中的每个元素都应用 f 函数操作,不返回 RDD 和 Array, 而是返回Uint。 | 无输出 |
saveAsTextFile | 将数据输出,存储到 HDFS 的指定目录。 | HDFS |
saveAsObjectFile | 将分区中的每10个元素组成一个Array,然后将这个Array序列化,映射为(Null,BytesWritable(Y))的元素,写入HDFS为SequenceFile的格式。 | HDFS |
collect | collect 相当于 toArray, toArray 已经过时不推荐使用, collect 将分布式的 RDD 返回为一个单机的 scala Array 数组。在这个数组上运用 scala 的函数式操作。 | Scala集合和数据类型 |
collectAsMap | 对(K,V)型的RDD数据返回一个单机HashMap。 对于重复K的RDD元素,后面的元素覆盖前面的元素。 | Scala集合和数据类型 |
reduceByKeyLocally | 先reduce再collectAsMap的功能,先对RDD的整体进行reduce操作,然后再收集所有结果返回为一个HashMap。 | Scala集合和数据类型 |
lookup | 对(Key,Value)型的RDD操作,返回指定Key对应的元素形成的Seq。 | Scala集合和数据类型 |
count | 返回整个 RDD 的元素个数。 | Scala集合和数据类型 |
top | 返回最大的k个元素。 | Scala集合和数据类型 |
reduce | reduce函数相当于对RDD中的元素进行reduceLeft函数的操作。 | Scala集合和数据类型 |
fold | fold和reduce的原理相同,但是与reduce不同,相当于每个reduce时,迭代器取的第一个元素是zeroValue。 | Scala集合和数据类型 |
aggregate | 先对每个分区的所有元素进行aggregate操作,再对分区的结果进行fold操作。 | Scala集合和数据类型 |
// Spark program
Val lines1 = sc.textFile(inputPath1). map(···)). map(···)
Val lines2 = sc.textFile(inputPath2) . map(···)
Val lines3 = sc.textFile(inputPath3)
Val dtinone1 = lines2.union(lines3)
Val dtinone = lines1.join(dtinone1)
dtinone.saveAsTextFile(···)
dtinone.filter(···).foreach(···)
Spark的计算发生在RDD的Action操作,而对Action之前的所有Transformation,Spark只是记录下RDD生成的计划,并不会触发真正的计算。Spark内核会在需要计算发生的时刻绘制一张关于计算路径的有向无环图,也就是DAG。
Application多个job多个Stage:Spark Application中可以因为不同的Action触发众多的job,一个Application中可以有很多的job,每个job是由一个或者多个Stage构成的,后面的Stage依赖于前面的Stage,也就是说只有前面依赖的Stage计算完毕后,后面的Stage才会运行。
划分依据:Stage划分的依据就是宽依赖,何时产生宽依赖,reduceByKey, groupByKey等算子,会导致宽依赖的产生。
核心算法:从后往前回溯,遇到窄依赖加入本stage,遇见宽依赖进行Stage切分。Spark内核会从触发Action操作的那个RDD开始从后往前推,首先会为最后一个RDD创建一个stage,然后继续倒推,如果发现对某个RDD是宽依赖,那么就会将宽依赖的那个RDD创建一个新的stage,那个RDD就是新的stage的最后一个RDD。然后依次类推,继续继续倒推,根据窄依赖或者宽依赖进行stage的划分,直到所有的RDD全部遍历完成为止。
从HDFS中读入数据生成3个不同的RDD,通过一系列transformation操作后再将计算结果保存回HDFS。可以看到这个DAG中只有join操作是一个宽依赖,Spark内核会以此为边界将其前后划分成不同的Stage。 同时我们可以注意到,在图中Stage2中,从map到union都是窄依赖,这两步操作可以形成一个流水线操作,通过map操作生成的partition可以不用等待整个RDD计算结束,而是继续进行union操作,这样大大提高了计算的效率。
调度阶段的提交,最终会被转换成一个任务集的提交,DAGScheduler通过TaskScheduler接口提交任务集,这个任务集最终会触发TaskScheduler构建一个TaskSetManager的实例来管理这个任务集的生命周期,对于DAGScheduler来说,提交调度阶段的工作到此就完成了。而TaskScheduler的具体实现则会在得到计算资源的时候,进一步通过TaskSetManager调度具体的任务到对应的Executor节点上进行运算。
DAGScheduler监控Job与Task:要保证相互依赖的作业调度阶段能够得到顺利的调度执行,DAGScheduler需要监控当前作业调度阶段乃至任务的完成情况。这通过对外暴露一系列的回调函数来实现的,对于TaskScheduler来说,这些回调函数主要包括任务的开始结束失败、任务集的失败,DAGScheduler根据这些任务的生命周期信息进一步维护作业和调度阶段的状态信息。
DAGScheduler监控Executor的生命状态:TaskScheduler通过回调函数通知DAGScheduler具体的Executor的生命状态,如果某一个Executor崩溃了,则对应的调度阶段任务集的ShuffleMapTask的输出结果也将标志为不可用,这将导致对应任务集状态的变更,进而重新执行相关计算任务,以获取丢失的相关数据。
结果DAGScheduler:一个具体的任务在Executor中执行完毕后,其结果需要以某种形式返回给DAGScheduler,根据任务类型的不同,任务结果的返回方式也不同。
两种结果,中间结果与最终结果:对于FinalStage所对应的任务,返回给DAGScheduler的是运算结果本身,而对于中间调度阶段对应的任务ShuffleMapTask,返回给DAGScheduler的是一个MapStatus里的相关存储信息,而非结果本身,这些存储位置信息将作为下一个调度阶段的任务获取输入数据的依据。
两种类型,DirectTaskResult与IndirectTaskResult:根据任务结果大小的不同,ResultTask返回的结果又分为两类,如果结果足够小,则直接放在DirectTaskResult对象内中,如果超过特定尺寸则在Executor端会将DirectTaskResult先序列化,再把序列化的结果作为一个数据块存放在BlockManager中,然后将BlockManager返回的BlockID放在IndirectTaskResult对象中返回给TaskScheduler,TaskScheduler进而调用TaskResultGetter将IndirectTaskResult中的BlockID取出并通过BlockManager最终取得对应的DirectTaskResult。
引用:https://blog.csdn.net/databatman/article/details/53023818#11-hadoop-和-spark-的关系 | https://www.jianshu.com/p/cedbebfeea8c | https://blog.csdn.net/liuxiangke0210/article/details/79687240 | https://blog.csdn.net/xwc35047/article/details/78732738