前言

自Google发表三大论文GFS、MapReduce、BigTable以来,衍生出来的开源框架越来越多,说到大数据开源生态,就不得不先提一下Hadoop。Hadoop以其高可用、高扩展、高容错等特性早已形成了开源工业界的事实标准。作为一个可以搭建在廉价PC上的分布式集群生态体系,Hadoop用户可以在不清楚底层运行细节的情况下,利用MapReduce简单开发出自己的分布式应用。但是Hadoop MapReduce由于其设计初衷并不是为了满足循环迭代式数据流处理,因此在多并行运行的数据可复用场景(如:机器学习、图挖掘算法、交互式数据挖掘算法)中存在诸多计算效率等问题。所以Spark应运而生,Spark就是在传统的MapReduce 计算框架的基础上,利用其计算过程的优化,从而大大加快了数据分析、挖掘的运行和读写速度,并将计算单元缩小到更适合并行计算和重复使用的RDD。下面我们就进入Spark的世界,看看被Spark先驱们玩坏的那些事儿。

目录
  1. RDD是什么
  2. RDD是怎样形成的
  3. RDD的依赖关系
  4. RDD的内部结构


spark rdd 计算很慢_spark rdd 计算很慢

spark rdd 计算很慢_hadoop_02

RDD是什么

RDD(Resilient Distributed Datasets),是Spark最为核心的概念。

从字面上,直译为弹性分布式数据集。所谓“弹性”,一种简单解释是指RDD是横向多分区的,纵向当计算过程中内存不足时可刷写到磁盘等外存上,可与外存做灵活的数据交换;而另一种个人更偏向的解释是RDD是由虚拟数据结构组成,并不包含真实数据本体,RDD使用了一种“血统”的容错机制,在结构更新和丢失后可随时根据血统进行数据模型的重建。所谓“分布式”,就是可以分布在多台机器上进行并行计算。

从空间结构上,可以理解为是一组只读的、可分区的分布式数据集合,该集合内包含了多个分区。分区就是依照特定规则,将具有相同属性的数据记录放在一起。每个分区相当于一个数据集片段。下图简单表示了一个RDD的结构:


spark rdd 计算很慢_spark rdd 计算很慢

  • RDD是怎样形成的

关于RDD的形成, 主要是通过连接物理存储输入的数据集和在已有RDD基础上进行相关计算操作衍生的。下面我们就通过一个大数据开源生态经典的例子(Wordcount)来描述下RDD的产生过程。强大的Scala代码如下。


spark rdd 计算很慢_spark rdd 计算很慢_04

初识的小伙伴们会感觉很神奇,四行代码就全部搞定了吗,之前的MR代码可是码了一大堆呢……的确如此,想学好Spark的小伙伴们,还是要掌握Scala这门语言,废话不多说,简单解释下这几行代码:

第一行,从HDFS上读取in.txt文件,创建了第一个RDD                                      

第二行,按空格分词,扁平化处理,生成第二个RDD,每个词计数为1,生成了第三个RDD。这里可能有人会问,为什么生成了两个RDD呢,因为此行代码RDD经过了两次算子转换(transformation)操作。关于算子这里不多详述,请关注下期文章

第三行,按每个词分组,累加求和,生成第四个RDD 

第四行,将Wordcount统计结果输出到HDFS

整个产生过程如下图所示:


spark rdd 计算很慢_大数据_05

  • RDD的依赖关系

通过上文的例子可以了解到,一个作业从开始到结束的计算过程中产生了多个RDD,RDD之间是彼此相互依赖的,我们把这种父子依赖的关系,称之为“血统”。如果父RDD的每个分区最多只能被子RDD的一个分区使用,我们称之为(narrow dependency)窄依赖;若一个父RDD的每个分区可以被子RDD的多个分区使用,我们称之为(wide dependency)宽依赖。简单来讲窄依赖就是父子RDD分区间”一对一“的关系,宽依赖就是”一对多“关系,具体理解可参考下图:


spark rdd 计算很慢_spark rdd 计算很慢_06

那么为什么Spark要将依赖分成这两种呢,下面我们就了解下原因:

首先,从计算过程来看,窄依赖是数据以管道方式经一系列计算操作可以运行在了一个集群节点上,如(map、filter等),宽依赖则可能需要将数据通过跨节点传递后运行(如groupByKey),有点类似于MR的shuffle过程。

其次,从失败恢复来看,窄依赖的失败恢复起来更高效,因为它只需找到父RDD的一个对应分区即可,而且可以在不同节点上并行计算做恢复;宽依赖则牵涉到父RDD的多个分区,恢复起来相对复杂些。

综上, 这里引入了一个新的概念Stage。Stage可以简单理解为是由一组RDD组成的可进行优化的执行计划。如果RDD的衍生关系都是窄依赖,则可放在同一个Stage中运行,若RDD的依赖关系为宽依赖,则要划分到不同的Stage。这样Spark在执行作业时,会按照Stage的划分, 生成一个完整的最优的执行计划。下面引用一张比较流行的图片辅助大家理解Stage,如图RDD­-A到RDD-B和RDD-F到RDD-G均属于宽依赖,所以与前面的父RDD划分到了不同的Stage中。


spark rdd 计算很慢_大数据_07

  • RDD的内部结构

到这里,相信大家已经对RDD有了大体的了解,但要详细了解RDD的内部结构,请继续耐心往下看。先贴一张RDD的内部结构图:


spark rdd 计算很慢_spark rdd 计算很慢_08

RDD的属性主要包括(rddname、sparkcontext、sparkconf、parent、StorageLevel、partitioner、checkpoint、Iterator)下面我们就简单逐一了解一下:

Rddname:

即RDD的名称。

Sparkcontext:

SparkContext为Spark job的入口,由Spark driver创建在client端,包括集群连接,RddID,创建抽样,累加器,广播变量等信息。

Sparkconf:

Spark参数配置信息,提供三个位置用来配置系统:

Spark API:控制大部分的应用程序参数,可以用SparkConf对象或者Java系统属性设置。

环境变量:可以通过每个节点的conf/spark-env.sh脚本设置。例如IP地址、端口等信息。

日志配置:可以通过log4j.properties配置。

Parent:

指向依赖父RDD的partition id,利用dependencies方法可以查找该RDD所依赖的Partitonid的List集合,即上图中的parents。

StorageLevel:

一个枚举类型,用来记录RDD的存储级别。存储介质主要包括内存、磁盘和堆外内存,另外还包含是否序列化操作以及副本数量。如:MEMORY_AND_DISK_SER代表数据可以存储在内存和磁盘,并且以序列化的方式存储。

Partitioner:

RDD的分区方式。RDD的分区方式主要包含两种(Hash和Range),这两种分区类型都是针对K-V类型的数据。如是非K-V类型,则分区为None。 Hash是以key作为分区条件的散列分布,分区数据不连续,极端情况也可能散列到少数几个分区上,导致数据不均等;Range按Key的排序平衡分布,分区内数据连续,大小也相对均等。

Checkpoint:

Spark提供的一种缓存机制,当需要计算的RDD过多时,为了避免重新计算之前的RDD,可以对RDD做checkpoint处理,检查RDD是否被物化或计算,并将结果持久化到磁盘或HDFS。与spark提供的另一种缓存机制cache相比, cache缓存数据由executor管理,当executor消失了,被cache的数据将被清除,RDD重新计算。而checkpoint将数据保存到磁盘或HDFS,job可以从checkpoint点继续计算。

Iterator:

迭代器,用来查找当前RDD Partition与父RDD Partition的血缘关系,并通过StorageLevel确定存储位置,从而找到真实数据。迭代方式分为Checkpoint迭代和RDD迭代。 如果StorageLevel为None,则执行computeOrReadCheckpoint方法计算并获取数据,此方法也是一个迭代器,迭代checkpoint数据的存放位置;如果Storagelevel不为None,根据存储级别进入RDD迭代器,继续迭代父RDD的结构,直到获取真实数据的位置。迭代器内部有数据本地化判断,先从本地获取数据,如果没有则远程查找。