Spark学习笔记[2]-Spark基本介绍与编程模型

Spark学习笔记[2]-Spark基本介绍与编程模型

  现在大数据领域两个用的比较多的计算框架应该是Spark和Flink,两个计算框架都想做批流整合,一统天下,但是现在比较多的还是使用Spark做批数据处理,使用Flink做流数据处理

  本文会对Spark做简单的介绍,着重介绍其编程模型,后续会有专门的Spark源码分析专题

1、从MapReduce说起

  Spark在大流程上可以看成是对MapReduce的增强,用过MapReduce的人都知道,MapReduce是真的慢啊,MapReduce的两阶段执行逻辑图如下:
在这里插入图片描述

  假设现在处理逻辑比较复杂,大概率是需要多个MapReduce程序进行级联,则处理逻辑链路变成:
在这里插入图片描述

  以这个图为基础,说一下为什么MR慢:

  • 1)、MapReduce任务都是进程级别,进程的启动和调度比较重量级
  • 2)、每个MapReduce程序都要单独申请资源,无法进行JVM重用
  • 3)、Map阶段输出必须排序,输出必须落磁盘,且预聚合操作(combiner)需要额外指定

  当然还有别的原因,这里就不异议列举了

  因为MR慢,所以一些大神就开发了Spark,借鉴了Scala语言中的各种集合处理函数,相比于MR,Spark处理数据的处理速度大大提升,官网那个对比图找不到了😂,上诉的MR流程图在Spark中可以转变成:
在这里插入图片描述

  和MR相比,Spark做了什么?

  • 1)、Spark的每个Task是线程级别,相比于进程,更加轻量
  • 2)、Spark程序只需要申请一次资源(不考虑动态申请资源的情况),在内部启动好Driver和Executor后,后续所有的任务都交由Executor的线程池运行,不需要申请新的JVM
  • 3)、Spark内部有多种数据写出策略,对排序和数据预聚合做了更精细化的控制,且使用了堆外内存,相比MR,速度会更快(详细的内容将在源码分析专题介绍)
2、Spark编程模型

  Spark内部的核心数据抽象就是RDD,RDD的全称是弹性分布式数据集,翻译一下官方的RDD特征:

  • 1)、RDD拥有多个的分区,spark的分区类似于mapreduce的splits,代表的是数据的实际存储位置信息等,RDD是逻辑的概念,分区是物理的概念
  • 2)、操作RDD只会传递一个方法,如rdd.map(),该方法会作用在该RDD的每个分区的每一条记录上
  • 3)、一个RDD会依赖多个RDD,它们之间的依赖关系主要分为宽依赖和窄依赖【有shuffle出现的就是宽依赖】
  • 4)、针对键值对类型的RDD,可以通过分区器指定数据属于哪个分区,例如对key进行hash取模
  • 5)、计算向数据移动,即优先在数据splilt所在机器位置对该split进行计算

  Spark内部的RDD有一个顶级的父类就叫RDD,介绍一下RDD几个比较重要的函数和参数:

  • 1)、_sc:Spark的上下文信息
  • 2)、deps:该RDD分区依赖的父RDD分区有哪些,是一个集合【如果无父RDD,会设置成Nil】
  • 3)、getPartirions函数:模板方法,用于获取该RDD的分区信息,其实就是需要处理数据相关的信息,比如位置、数据量等
  • 4)、getDependencies函数:获取RDD和父RDD的依赖关系
  • 5)、compute函数:模板方法,数据处理的主方法,封装实际业务处理逻辑,该函数返回的是迭代器对象而不是具体的处理结果,迭代器对象内部会封装两个主要的函数:hasnext和next完成对数据集的迭代,在迭代过程中根据传递的业务逻辑函数完成数据处理
  • 6)、Iterator函数:RDD的公共实现,其主要的功能就是调用compute函数获取迭代器对象

  在介绍spark程序的编程模型之前,先来看下scala里的迭代器嵌套,代码如下:

val list03 = List("hello word","hello jeje","hehe hhhhhh")
val iter:Iterator[String] = list03.iterator
val strings = iter.flatMap((x: String) => {
    x.split(" ")
}) //返回的也是迭代器
// strings.foreach(println)
val tuples = strings.map((_, 1)) //strings是迭代器,且已经在调用strings.foreach后指向末尾,再对其进行map迭代,已经无法输出元  
tuples.foreach(println)

  以上代码执行过程中会产生4个对象:

  • 1)、list03:实际的数据存储容器
  • 2)、iter:list03的迭代器对象,可以完成对list03每个元素的迭代
  • 3)、strings:迭代器对象,内部封装了对iter取出数据的处理逻辑
  • 4)、tuples:迭代器对象,内部封装了对strings对象的数据处理逻辑

  通过查看源码,每个对象的关联关系可以表示成下图所示的样子:
在这里插入图片描述

  在调用foreach之前只是构造了如上图所示的对象关系图,并没有触发真正的迭代计算,触发foreach后,会在内部依次调用迭代器的hasnext和next方法将数据取出计算,整个调用代码可以简化成

while (hasNext) f(next()) //f是传入的函数,next()可以取出数据,将数据作用于函数上即完成该条数据的处理

  在调用foreach之前只是构造了如上图所示的对象关系图,并没有触发真正的迭代计算,触发foreach后,会在内部依次调用迭代器的hasnext和next方法将数据取出计算,先调用hasnext判断是否有下一条数据,调用关系如下:在这里插入图片描述

  在Strings迭代器的hasNext方法中会完成从iter迭代器取数(最终会从list03容器取待处理字符串),取出字符串后会对输入字符串运用flatMap操作,也就是f(next()),并将其赋值给新的迭代器cur,后续next函数直接从cur取值,如果hasnext发现cur已经为空,则会从iter取下一条数据,如果iter也指向空,则数据处理结束,调用next函数的调用流程如下:
在这里插入图片描述

  Spark的编程模型和scala的迭代器嵌套非常的像,以wordCount为例,示例代码如下:

val conf = new SparkConf()
conf.setAppName("wordcount")
conf.setMaster("local")
val sc = new SparkContext(conf) //读取文件
val fileRdd:RDD[String] = sc.textFile("D:\\JAVA_WORKSPACE\\spark_test\\src\\main\\scala\\com\\cmb\\spark\\test\\wc_data.txt")

val res = fileRdd.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)

res.foreach(println)

  就是通过不断转换出新的RDD(类似于scala的不断生成新的迭代器),最终通过RDD的某个方法触发计算流程(类似于最开始scala代码的foreach函数)通过跟踪其源码

  可以说Spark的操作都是面向RDD的,其操作RDD的函数大体可以分成以下几类(这些函数在内部被称为算子):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K3oEXigz-1645015300729)(./image/introduction/image-20220111170152345.png)]
在这里插入图片描述

  一般而言,我们在写Spark程序时都是通过转换算子封装数据处理逻辑,再通过action算子触发真正的计算流程,数据会沿着封装好的数据处理逻辑流动,类似于PipeLine

  为了加深理解,剖析一下WordCount的源码,整个调用流程如下图,图很大,可以下载原图看看【图片编号:image-20220111165319035.png】:

在这里插入图片描述

  图中的处理流程被称为一个Job,从图中可以看出,这个Job被切割成了两个阶段,ShuffleTask1和ShuffleTask2(实际应该称为ShuffleMapStage和ResultStage),切割的依据就是是否发生shuffle操作,是否发生shuffle操作在Spark内部由RDD的依赖关系决定,也就是RDD基本特性的第3点,RDD与其他RDD的关系被称为血缘或者血统,在大类上分为窄依赖和shuffle依赖(中文也叫宽依赖),整个RDD依赖的类集成关系如下:
在这里插入图片描述

  Spark的调度会以Task为单位进行,下面以WordCount的ShuffeTask1为例进行说明,整个流程其实可以简写成两步:

  • 1)、调用MapPartitionsRDD_3.iterator方法获取迭代器
  • 2)、对迭代器进行遍历(依次触发前面RDD的iterator方法)

  调用MapPartitionsRDD_3.iterator方法获取迭代器的程序调用栈如下:
在这里插入图片描述

  假如将代码展开,可以变成如下代码:

NextIterator = file.iter【指向文件的迭代器,用于遍历文件数据】
HadoopRDD.iter=NextIterator
MapPartitionRDD_1.iter=HadoopRDD.iter.map(cleanF)
MapPartitionRDD_2.iter=MapPartitionRDD_1.iter.flatMap(cleanF)
MapPartitionRDD_3.iter=MapPartitionRDD_2.iter.map(cleanF)
MapPartitionRDD_3.iter.foreach(XXX) //遍历迭代器元素

  从展开的代码可以看出,这个模式和之前分析scala的迭代器嵌套的形式一样,数据读取流程不再重复描述,因为框架做了很多工作,所以写Spark程序可以像写单机程序一样

3、总结

  本文从MR出发,首先介绍了MR程序存在的问题,继而引出Spark,之后介绍了Spark的基本变成方式和编程模型,介绍了Spark的RDD算子种类和RDD依赖关系,最后一WordCount为例从源码的角度分析了其调用执行流程,调用流程大图的gitee地址:https://gitee.com/source-code-note/graph/tree/master/spark

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值