- Hadoop 与 Spark 的对比
- 什么是Spark
部分摘自https://www.zhihu.com/question/26568496/answer/41608400
Hadoop
Hadoop解决了大数据(大到一台计算机无法进行存储,大到一台计算机无法在要求的时间内进行处理)的可靠存储和处理。
- HDFS通过在普通PC组成的集群上提供高可靠的文件存储,通过将块保存多个副本的方法解决服务器或硬盘坏掉的问题
- MapReduce,通过简单的Mapper和Reducer的抽象提供一个变成模型,可以在一个非常多PC组成的不可靠集群上并发地,分布式地处理解决大量数据集,从而把并发、分布式(如机器间通信)和故障恢复等计算细节隐藏起来。Mapper和Reducer的抽象,是各种复杂数据处理都可以分解的基本元素。这样复杂的数据处理就可以分解为多个Job(一个Job只包含一个Mapper和一个Reducer)组成的有向无环图DAG。然后每个Mapper和Reducer放到Hadoop集群上执行,就可以得出结果。在Map和Reduce中间的过程中存在一个shuffle过程对Map得出的结果进行洗牌整理,排序,之后在将所有的输出数据合并成一个文件的过程。如果数据量大将会非常耗时。
Hadoop的局限于不足:
- 抽象层次低,需要手工编写代码来完成,使用上难以上手。
- 只提供两个操作,Map和Reducer,表达力欠缺。
- 一个Job只有Map和Reducer两个阶段,复杂的计算需要大量的Job完成,(通过中间变量来连接Job之间的关系),Job之间的依赖关系是由开发者自己管理的。
- 处理逻辑隐含在代码细节中,没有整体逻辑
- 中间结果也放在HDFS文件系统中(造成了大量数据复制,磁盘IO和序列化开销)
- ReduceTask需要等待所有MapTask都完成后才可以开始
- 时延高,只适用于Batch数据处理,对于流式数据(交互式数据,实时数据)处理的支持不够
- 对于迭代式数据处理性能比较差
在Hadoop推出后,出现了很多相关技术对其局限性进行改进,其中Spark就是一个。
Spark
Apache Spark 是一个新兴的大数据处理引擎,主要特点是提供了一个集群的分布式内存抽象,以支持需要工作集的应用。
注意
Hadoop主要用于解决普通应尽存储和计算问题;而Spark用于构建大型的,低延迟的数据分析应用程序,不实现存储。
RDD设计背景
在实际应用中,存在许多迭代式计算,这些迭代计算的共同之处是,不同计算阶段之间会重用中间结果,即一个阶段的输出结果会作为下一阶段的输出。MapReduce将中间结果写入到HDFS中,造成了局限性。如果能将中间结果保存在内存中,就可以大量减少IO。RDD就是为满足这一需求而出现的,提供了一个抽象的数据架构,我们不必担心底层数据的分布式特性,只需将具体的应用逻辑表达为一系列转换处理,不同RDD之间的转换操作形成依赖关系,以实现管道化。RDD概念
一个RDD就是一个分布式对象集合。本质上是一个只读的(不可改变的)分区记录集合。每个RDD可以分成多个分区,每个分区就是一个数据集片段(HDFS上的块),并且一个RDD的不同分区可以被保存到集群中不同的节点上,从而可以在集群的不同节点上进行并行计算。
RDD提供了一种高度受限的 共享内存模型 ,也就是RDD的不能直接修改,只能基于稳定的物理存储中的数据集来创建RDD,或者通过其他RDD上执行确定的转换操作(map,join和groupBy)来创建得到新的RDD。
RDD提供了一组抽象的数据运算(Transformations和Actions)。Transformation指定RDD之间的相互依赖关系,Actions用于执行计算并指定输出的形式。两个操作的主要区别是,Transformation(比如map,filter,groupBy,join等)接收RDD并返回RDD,而行动操作(比如count,collect等)接收RDD但是返回非RDD,输出一个值或一个结果。RDD的典型执行过程:
RDD读入外部数据源(或内存中的集合)进行创建
RDD经过一系列的Transformation进行处理,每一次都会产生不同的RDD供下一个Transformation使用
最后一个RDD经过Action操作进行处理,并输出到外部数据源。(或者变成Scala/Java集合或变量)
RDD采用了惰性调用,在RDD的执行过程中,真正的计算发生在RDD的Action操作,对于Transformation操作,Spark只是记录下Transformation操作莹莹的一些基础数据集以及RDD生成的轨迹(即相互依赖关系)而不会触发真正的计算。
这一系列处理称为“血缘关系”,即DAG拓扑排序的结果。采用惰性调用,通过学院关系连接起来的RDD操作就可以实现管道化,避免了多次转换操作之间数据同步的等待,而且不用担心有过多的中间数据。因为管道化,一个操作得到的结果不需要保存为中间数据,而是直接管道式流入下一个操作进行处理。也是的管道中每次操作的计算变得相对简单,保证了每个操作在逻辑上的单一性。
RDD特性:
(1)高效的容错性。现有的分布式共享内存,键值存储,内存数据库等,为了实现容错,必须在集群节点之间进行数据复制或者记录日志,也就会在节点之间发生大量的数据传输。在RDD设计中,数据只读,不可修改,如果要修改数据,必须从父RDD转换到子RDD,因此在不同的RDD之间建立了血缘关系。所以RDD是一种天生具有容错机制的特殊集合,不需要通过数据冗余的方式(如checkpoint)来实现容错。只需要通过RDD学院关系计算得到丢失的分区来实现容错,无需回滚整个系统。并且RDD依赖关系只需要记录这种粗粒度的转换操作,而不需要记录具体的数据和各种细粒度操作的日志,大大降低了数据密集型应用中的容错开销。
(2)中间结果持久化到内存。数据在内存中的多个RDD操作之间进行传递,不需要落地到磁盘上,避免了不必要的读写开销
(3)存放的数据可以是Java对象,避免了不必要的对象序列化和反序列化开销。tips:对象序列化是一个将对象状态转换为字节流的过程,可以将其保存在磁盘文件中,或通过网络发送到其他程序;从字节流创建对象的过程称为反序列化。创建的字节流是与平台无关的,在一个平台上序列化的对象可以在不同的平台上反序列化。
RDD之间的依赖关系
RDD中的不同操作会使得不同的RDD中分区产生不同的依赖。RDD中的依赖关系分为窄依赖(Narrow Dependency)和宽依赖(Wide Dependency)。
窄依赖表现为一个父RDD的分区对应于一个子RDD的分区。或多个父RDD的分区对应于一个子RDD的分区。多到1映射。典型操作包括map,filter,union等。Join对输入进行协同划分为窄依赖。
宽依赖表现为一个父RDD的一个分区,对应一个子RDD的多个分区。1到多映射。典型操作包括groupByKey,sortByKey。Join对输入进行非协同划分为宽依赖。
(图片来源于海牛大数据)tips: 这里的1和多都指的是RDD的一个分区。
相对而言,窄依赖的失败恢复更为高效,它只需要根据父RDD分区重新计算丢失的分区即可,不需要计算所有分区,而且可以并行地在不同几点进行重新计算。(也就是,子RDD的某些分区丢失后,只需要找到部分父RDD分区就可以恢复)而宽依赖,单个节点失效通常意味着重新计算过程会涉及多个父RDD分区,开销比较大。此外,Spark还提供了数据检查点和记录日志,用于持久化中间RDD,从而使得在进行失败恢复是不需要追溯到最开始的阶段。在进行故障恢复时,spark会对数据检查点开销和重新计算RDD分区的开销进行比较,自动选择最优的恢复策略。
Spark阶段的划分
Spark通过分析各个RDD的依赖关系生成了DAG,再通过分析各个RDD中的分区之间的依赖关系来决定如何划分阶段。具体划分方法:
在DAG中进行反向解析,遇到宽依赖就断开,遇到窄依赖就把当前RDD加入到当前的阶段;
将窄依赖尽量划分在同一个阶段中,可以实现流水线计算。
(图片源于海牛大数据)如上图例子。
从HDFS中读取分区A和C,经过一系列操作后得出结果,再写入HDFS中。
在操作中A->B, 和F->G是宽依赖,所以断开。其余的是窄依赖,尽量将窄依赖归到一个pipeline上实现流水线作业。
在分区7到分区9再到分区13这个过程中,可以不用等待分区8到分区10这个转换操作的计算结束,而是直接从分区9进行union操作到分区13,这样流水线执行大大提高了计算的效率。
将一个DAG图划分成多个阶段以后,每个阶段都代表了一组关联的、 相互之间没有shuffle依赖关系 的任务组成的任务集合。每个任务集合会被提交给任务调度器(TsakScheduler)进行处理,由任务调度器将任务分发给Executor运行。RDD在Spar框架中的运行过程:
(1)创建RDD对象;
(2)SparkContext负责计算RDD之间的依赖关系,构建DAG
(3)DAG Scheduler负责把DAG图分解成多个阶段,每个阶段包含了多个任务,每个任务会被任务调度器分发给各个工作节点上的Executor去执行。默认情况下,每个transformation RDD在执行action操作时都会重新计算。即使两个action操作会使用同一个转换RDD,该RDD也会重新计算。除非使用persist方法或者cache方法将RDD缓存到内存,这样下次使用这个RDD时将会提高计算效率,也支持将RDD持久化到硬盘上,或在多个节点上复制。
Spark Transformation部分操作
- map(func) 将原来RDD的每个数据项,使用map中用户自定义的函数func进行映射,转变为一个新的元素,并返回一个新的RDD
- filter(func) 使用func对原RDD中数据项进行过滤,将符合func中条件的数据项组成新的RDD返回。
- flatMap(func) 类似于map,但是输入数据项可以被映射到0个或多个输入数据集合中,所以func返回值是一个数据项集合而不是一个单一的数据项。
- mapPartitions(func) 类似于map,但是该操作时在每个分区上分别执行,所以当操作一个类型为T的RDD是func的格式必须是Iterator=>Iterator。即mapPartitions需要获取到每个分区的迭代器,在函数中通过这个分区的迭代器对整个分区的元素进行操作。
- union(otherDataset) 返回数据结构和参数指定的数据集合并后的数据集。使用union函数是要保证两个RDD元素的数据类型相同。返回的RDD数据类型和被合并的RDD元素数据类型相同。该操作不进行去冲操作,返回的结果会保留所有元素。如果想去重,可以使用distinct()。
- distinct([numTasks]) 将RDD中的元素进行去重操作。
Spark Actions部分操作
- reduce(func) 使用func聚集数据集中的元素,这个函数func输入为两个元素,返回一个元素。这个函数符合结合律和交换律,这样才能保证数据集中各个元素计算的正确性。
- collect() 在驱动程序中,以数组的形式返回数据集的所有元素。通常用于filter或其他产生了大量小数据集的情况。
- count() 返回数据集中元素的个数。
- first() 返回数据集中的第一个元素
- saveAsTestFile(path) 将数据集中的元素以文本文件的形式保存到指定的本地文件系统、HDFS或其他Hadoop支持的文件系统中。