大数据-Spark

什么是Spark?

在这里插入图片描述

Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎,是一种快速、通用、可扩展的大数据分析引擎,2009年诞生于加州大学 伯克利分校 AMPLab,2010年开源,2013年6月成为Apache 孵化项目,2014年2月成为Apache顶级项目。目前,Spark生态系统以及发展成为一个包含多个子项目的集合,其中包括SparkSQL、Spark Streaming、GraphX、MLlib等子项目,Spark是基于内存计算的大数据并行计算框架。Spark基于内存计算,提高了在大数据环境下数据处理的实时性,同时保证了高容错性和高可伸缩性,允许用户将Spark 部署在大量廉价硬件之上,形成集群。Spark得到了众多大数据公司的支持,这些公司包括 Hortonworks、IBM、Intel、Cloudera、MapR、Pivotal、百度、阿里、腾讯、京东、携程、优酷土豆。当前百度的Spark以应用于凤巢、大搜索、直达号、百度大数据等业务。阿里利用 GraphX 构建了大数据的图计算和图挖掘系统,实现了很多生产系统的推荐算法。腾讯 Spark 集群达到了8000台的规模,是当前已知的世界上最大的 Spark 集群。

为什么要学 Spark

Spark 是一个开源的类似于Hadoop Mapreduce 的通用的并行计算框架,Spark 基于 map reduce 算法实现的分布式计算,拥有 Hadoop MapReduce 所具有的优点。但不同于MapReduce的是Spark 中的 Job 中间输出和结果可以保存在 内存中,从而不再需要读写 HDFS,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的 map reduce 的算法。
Spark 是 Map Reduce 的替代方案,而且兼容HDFS Hive,可融入 Hadoop 的生态系统,以弥补 MapReduce 的不足。

Spark 特点


与 Hadoop 的 MapReduce 相比, Spark基于内存的运算要快100倍以上,基于硬盘的运算也要快10倍以上。Spark 实现了高效的 DAG 执行引擎,可以通过 基于内存来高效处理数据流。

在这里插入图片描述
易用
Spark支持Java、Python和Scala的API,还支持超过80种高级算法,使用户可以快速构建不同的应用。而且Spark支持交互式的Python和Scala的shell,可以非常方便地在这些shell中使用Spark集群来验证解决问题的方法。

在这里插入图片描述

通用
Spark 提供了统一的解决方案。 Spark 可以用于批处理、交互式查询 (Spark SQL)、实时流(Spark Streaming)、机器学习(Spark MLlib)和图计算(GraphX)。这些不同类型的处理都可以在同一个应用中无缝使用。Spark 统一的解决方案非常具有吸引力,毕竟任何公司都想用统一的平台去处理遇到的问题,减少开发和维护的人力成本和部署平台的物力成本。

兼容性
Spark 可以非常方便地与其他的开源 产品进行融合。比如,Spark 可以使用 Hadoop 的 YARN 和 Apache Mesos 作为它的资源管理和调度器,并且可以处理所有 Hadoop 支持的数据,包括 HDFS、HBase 和 Cassandra 等。这对于已经部署 Hadoop 集群的用户特别重要、因为不需要做任何数据迁徙就可以使用 Spark的强大处理能力。Spark 也可以不依赖于第三方的资源管理器和调度器,他实现了 Standalone 作为其内置的资源管理和调度框架,这样进一步降低了 Spark 的使用门槛, 使得所有人都可以非常容易的部署和使用 Spark。此外,Spark 还提供了在 EC2 上部署 Standalone 的 Spark 集群的工具。

在这里插入图片描述

Spark 集群安装

下载 spark 安装包
下载地址spark官网:http://spark.apache.org/downloads.html
这里我们使用 spark-2.0.2-bin-hadoop2.7版本.
在这里插入图片描述
规划安装目录
mkdir /opt/bigdata
上传,解压安装包
tar -zxvf spark-2.0.2-bin-hadoop2.7.tgz
重命名
mv spark-2.0.2-bin-hadoop2.7 spark
修改配置文件
配置文件目录在 /opt/bigdata/spark/conf
vim spark-env.sh 修改文件(先把spark-env.sh.template重命名为spark-env.sh)
增加以下:

#配置java环境变量
export JAVA_HOME=/opt/bigdata/jdk1.7.0_67
#指定spark老大Master的IP
export SPARK_MASTER_HOST=hdp-node-01
#指定spark老大Master的端口
export SPARK_MASTER_PORT=7077

vi slaves 修改文件(先把slaves.template重命名为slaves)

hdp-node-02
hdp-node-03
//增加集群中的Datanode主机名

拷贝配置到其他主机

通过scp 命令将spark的安装目录拷贝到其他机器上
scp -r /opt/bigdata/spark hdp-node-02:/opt/bigdata
scp -r /opt/bigdata/spark hdp-node-03:/opt/bigdata

配置环境变量
将spark添加到环境变量,添加以下内容到 /etc/profile

export SPARK_HOME=/opt/bigdata/spark
export PATH=$PATH:$SPARK_HOME/bin

注意最后 source /etc/profile 刷新配置
启动 spark
在主节点上启动spark
/opt/bigdata/spark/sbin/start-all.sh
停止spark
在主节点上停止spark集群
/opt/bigdata/spark/sbin/stop-all.sh
spark的web界面
正常启动spark集群后,可以通过访问 http://主节点名字:8080,查看spark的web界面,查看相关信息。

Spark HA高可用部署

Spark Standalone 集群是 Master-Slaves 架构的集群模式,和大部分的 Master-Slaves 结构集群一样,存在着 Master 单点故障的问题。解决这个问题,Spark提供了两种方案:

  1. 基于文件系统的单点恢复(Single-Node Recovery with Local File System)。
    主要用于开发或测试环境。当spark提供目录保存spark Application和worker的注册信息,并将他们的恢复状态写入该目录中,这时,一旦Master发生故障,就可以通过重新启动Master进程(sbin/start-master.sh),恢复已运行的spark Application和worker的注册信息。
  2. 基于zookeeper的Standby Masters(Standby Masters with ZooKeeper)。
    用于生产模式。其基本原理是通过zookeeper来选举一个Master,其他的Master处于Standby状态。将spark集群连接到同一个ZooKeeper实例并启动多个Master,利用zookeeper提供的选举和状态保存功能,可以使一个Master被选举成活着的master,而其他Master处于Standby状态。如果现任Master死去,另一个Master会通过选举产生,并恢复到旧的Master状态,然后恢复调度。整个恢复过程可能要1-2分钟。

基于zookeeper的Spark HA高可用集群部署
该HA方案使用起来很简单,首先需要搭建一个zookeeper集群,然后启动zooKeeper集群,最后在不同节点上启动Master。具体配置如下:

  1. vim spark-env.sh
    注释掉export SPARK_MASTER_HOST=hdp-node-01
  2. 在spark-env.sh添加SPARK_DAEMON_JAVA_OPTS,内容如下
    	export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER  -	
    	Dspark.deploy.zookeeper.url=hdp-node-01:2181,hdp-node-02:2181,hdp-node-03:2181  -
    	Dspark.deploy.zookeeper.dir=/spark"
    

参数说明

  1. spark.deploy.recoveryMode:恢复模式(Master重新启动的模式)
    有三种:(1)ZooKeeper (2) FileSystem (3)NONE
  2. spark.deploy.zookeeper.url:ZooKeeper的Server地址
  3. spark.deploy.zookeeper.dir:保存集群元数据信息的文件、目录。
    包括Worker,Driver和Application。

注意:
在普通模式下启动spark集群,只需要在主机上面执行start-all.sh 就可以了。
在高可用模式下启动spark集群,先需要在任意一台节点上启动start-all.sh命令。然后在另外一台节点上单独启动master。命令start-master.sh。

Spark角色介绍

Spark是基于内存计算的大数据并行计算框架。因为其基于内存计算,比Hadoop中MapReduce计算框架具有更高的实时性,同时保证了高效容错性和可伸缩性。从2009年诞生于AMPLab到现在已经成为Apache顶级开源项目,并成功应用于商业集群中,学习Spark就需要了解其架构。
Spark架构图如下:
在这里插入图片描述
Spark架构使用了分布式计算中master-slave模型,master是集群中含有master进程的节点,slave是集群中含有worker进程的节点。

  1. Driver Program : 运⾏main函数并且新建SparkContext的程序。
  2. Application : 基于Spark的应用程序,包含了driver程序和集群上的executor。
  3. Cluster Manager :指的是在集群上获取资源的外部服务。目前有三种类型
    1. Standalone: spark原生的资源管理,由Master负责资源的分配.
    2. Apache Mesos:与 hadoop MR 兼容性良好的一种资源调度框架
    3. Hadoop Yarn: 主要是指Yarn中的ResourceManager
  4. Worker Node: 集群中任何可以运行Application代码的节点,在Standalone模式中指的是通过slaves文件配置的Worker节点,在Spark on Yarn模式下就是NodeManager节点。
  5. Executor:是在一个worker node上为某应⽤启动的⼀个进程,该进程负责运⾏行任务,并且负责将数据存在内存或者磁盘上。每个应⽤都有各自独立的executor。
  6. Task:被送到某个executor上的工作单元。
  7. Job : 提交给spark的任务,从数据源(Data blocks)加载生成RDD(每个数据分片Partition一般是128M,最后一条记录横跨2个blocks),后将RDD经过一系列转换(包括基本类型转换和洗牌)最终得到计算结果(result),再将结果汇总到driver端。由Action触发,每个Action对应一个Job。

简单的 Spark 程序

执行第一个 spark 程序

普通模式提交任务:

bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://node5:7077 \
--executor-memory 1G \
--total-executor-cores 2 \
examples/jars/spark-examples_2.11-2.0.2.jar \
10

该算法是利用蒙特·卡罗算法求圆周率PI,通过计算机模拟大量的随机数,最终会计算出比较精确的π。
在这里插入图片描述

高可用模式提交任务:
在高可用模式下,因为涉及到多个Master,所以对于应用程序的提交就有了一点变化,因为应用程序需要知道当前的Master的IP地址和端口。这种HA方案处理这种情况很简单,只需要在SparkContext指向一个Master列表就可以了,
如spark://host1:port1,host2:port2,host3:port3,应用程序会轮询列表,找到活着的Master。

bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://hdp-node-01:7077,hdp-node-02:7077,hdp-node-03:7077 \
//轮询
--executor-memory 1G \
--total-executor-cores 2 \
examples/jars/spark-examples_2.11-2.0.2.jar \
10

Spark-Shell

spark-shell是Spark自带的交互式Shell程序,方便用户进行交互式编程,用户可以在该命令行下用scala编写spark程序。

弹性分布式数据集RDD

RDD概述

什么是RDD

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据抽象,他代表一个不可变、可分区、里面的元素可并行计算的集合。RDD 具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。 RDD 允许用户在执行多个查询时显式地将数据缓存在内存中,后续的查询能够重用这些数据,极大地提升了查询的速度。
Dataset:一个数据的集合,用于存放数据。
Distributed:RDD 中的数据是分布式存储的,可用于分布式计算。
Resilient: RDD 中的数据可以存储在内存或者磁盘中。

RDD的属性

在这里插入图片描述

  1. A list of partitions:一个分区 (Partition)列表,数据集的基本组成单位
    对于 RDD 来说, 每个分区都会被一个计算任务处理,并决定并行计算的力度。用户可以在创建 RDD 时指定 RDD 的分区个数,如果没有指定,那么就会采取默认值,一般为2。(比如:读取 HDFS 上数据文件产生的 RDD 分区数跟 block 的个数相等。)

  2. A function for computing each split : 一个计算每个分区的函数
    Spark 中 RDD 的计算是 以分区为单位的,每个RDD都会实现compute函数以达到这个目的。

  3. A list of dependencies on other RDDs:一个 RDD 会依赖于其他多个 RDD ,RDD 之间的依赖关系。
    RDD 的每次转换都会生成一个新的 RDD ,所以 RDD 之间都会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系计算丢失的分区数据,而不是对 RDD 的所有分区进行重新计算。

  4. Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned):一个 Partition,即 RDD 的分区函数(可选项)
    当前 Spark 中实现了两种类型的分区函数,一个基于哈希的 HashParttioner,另外一个是基于范围的 RangePartitioner 。 只有对于 key-vaue 的 RDD ,才会有 Partitioner,非 key-value 的 RDD 的 Partitioner 的值是 None。 Partitioner 函数决定了 parent RDD Shuffle 输出时的分区数量。

  5. Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file):一个列表,存储每个 Partitioner 的优先位置(可选项)。
    对于一个HDFS 文件来说,这个列表保存的就是每个 Partitioner 所在的快的位置。按照“移动数据不如移动计算”的理念,Spark 在进行任务调度的时候,会尽可能地将计算任务分配其所要处理数据块的存储位置(Spark进行任务分配的时候尽可能选择那些存有数据的 worker 节点来进行任务计算)。

为什么会产生RDD?

  1. 传统的 MapReduce 虽然具有自动容错、平衡负载和可拓展性的有点,但是其最大的缺点是采用非循环式的数据流模型,使得在迭代计算中要进行大量的磁盘 IO 操作。 RDD 正是解决这一缺点的抽象方法。
  2. RDD 是 Spark 提供的最重要的抽象的概念,他是一种具有容错机制的特殊集合,可以在分布在集群的节点上,以函数式编程来操作集合,进行各种并行操作。可以吧 RDD 的结果数据进行缓存,方便进行多次重用,避免重复计算。

RDD在Spark中的地位及作用

  1. 为什么会有 Spark
    因为传统的并行计算模型无法有效的解决迭代计算(iterative)和交互式计算(interactive)。而 Spark 的使命便是解决这两个问题,这也是他存在的价值和理由。

  2. Spark 如何解决迭代计算?
    其实主要实现思想就是 RDD ,把所有计算的数据保存在分布式的内存中。迭代计算通常情况下都是对同一个数据集做反复的迭代计算,数据在内存中将大大提升 IO 操作。这也是 Spark 涉及的核心:内存计算

  3. Spark 如何实现交互式计算?
    因为Spark 是用 scala 语言实现的,Spark 和 scala 能够紧密的继承,所以 Spark 可以完美的运用 scala的解释器,使得其中的 scala 可以向操作本地集合对象一样轻松操作分布式数据集。

  4. Spark 和 RDD 的关系
    RDD 是一种具有容错性、基于内存计算的抽象方法,RDD 是 Spark Core 的底层核心, Spark 则是这个抽象算法的实现。

创建 RDD

sc 为 SparkContext 的实例

  1. 由一个已经存在的 Scala 集合创建
    val rdd: RDD[Int] = sc.parallelize(Array(1,2,3,4,5,6,7,8))
  2. 由外部存储系统的文件创建。包括本地的文件系统,还有所有Hadoop 支持的数据集,比如 HDFS、Cassandra、HBase等。
    **val rdd:RDD[String] = sc.textFile("/word.txt")
  3. 已有的 RDD 经过算子转换生成新的 RDD
    **val rdd2:RDD[String] = rdd1.flatMap(_.split(" "))

RDD 编程 API

RDD 的算子分类

  1. Transformation(转换): 根据数据集创建一个新的数据集,计算后返回一个新的 RDD 。例子:一个rdd 进行 map 操作后生成了一个新的 rdd(返回一个RDD)。
  2. Action(动作):对 rdd 结果计算后返回一个数值 value 给驱动程序。例如:collect 算子将数据集的所有元素收集完成返回给驱动程序(返回一个数组Array)。

Transformation

RDD 中的所有转换都是延迟加载的,也就是说,他们并不会直接计算结果。相反的,它们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当发生一个要求返回结果给 Driver 的动作时,这些转换才会真正运行。这种设计让 Spark 更加有效率的运行。
常用的Transformation

转换含义
map(func)返回一个新的 RDD ,该 RDD 由每一个输入元素经过 func 函数转换后组成。
filter(func)返回一个新的 RDD,该 RDD 由经过 func 函数计算后返回值为true的元素组成。
flatMap(func)类似于 map ,但是每一个输入元素可以被映射为 0 或多个输出元素(所以func 应该返回一个 序列,而不是单一元素)
mapPartitions(func)类似于map ,但独立地在 RDD 的每一个分片上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是Iterator[T] => Iterator[U]
mapPartitionsWithIndex(func)类似于mapPartitions,但 func 带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U]
union(otherDataset)对源 RDD 和参数 RDD 求并集后返回一个新的 RDD
intersection(otherDataset)对源RDD和参数RDD求交集后返回一个新的RDD
distinct([numTasks]))对源RDD进行去重后返回一个新的RDD
groupByKey([numTasks])在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD
reduceByKey(func, [numTasks])在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置
sortByKey([ascending], [numTasks])在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
sortBy(func,[ascending], [numTasks])与sortByKey类似,但是更灵活
join(otherDataset, [numTasks])在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD
cogroup(otherDataset, [numTasks])在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD
coalesce(numPartitions)减少 RDD 的分区数到指定值。
repartition(numPartitions)重新给 RDD 分区
repartitionAndSortWithinPartitions(partitioner)重新给 RDD 分区,并且每个分区内以记录的 key 排序

Action

常用的Action

动作含义
reduce(func)reduce将RDD中元素前两个传给输入函数,产生一个新的return值,新产生的return值与RDD中下一个元素(第三个元素)组成两个元素,再被传给输入函数,直到最后只有一个值为止。
collect()在驱动程序中,以数组的形式返回数据集的所有元素
count()返回RDD的元素个数
first()返回RDD的第一个元素(类似于take(1))
take(n)返回一个由数据集的前n个元素组成的数组
takeOrdered(n, [ordering])返回自然顺序或者自定义顺序的前 n 个元素
saveAsTextFile(path)将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本
saveAsSequenceFile(path)将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。
saveAsObjectFile(path)将数据集的元素,以 Java 序列化的方式保存到指定的目录下
countByKey()针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。
foreach(func)在数据集的每一个元素上,运行函数func
foreachPartition(func)在数据集的每一个分区上,运行函数func

RDD常用的算子操作

启动spark-shell 进行测试:
spark-shell --master spark://主机名

  1. 案例算子演示1 :map、filter
    //通过并行化生成rdd
    val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9, 1, 10))
    //对rdd1里的每一个元素乘2然后排序
    val rdd2 = rdd1.map(_ * 2).sortBy(x => x, true)
    //过滤出大于等于5的元素
    val rdd3 = rdd2.filter(_ >= 5)
    //将元素以数组的方式在客户端显示
    rdd3.collect

  2. 案例算子演示2:flapMap
    val rdd1 = sc.parallelize(Array(“a b c”, “d e f”, “h i j”))
    //将rdd1里面的每一个元素先切分在压平
    val rdd2 = rdd1.flatMap(_.split(" "))
    rdd2.collect

  3. 案例算子演示3:交集、并集
    val rdd1 = sc.parallelize(List(5, 6, 4, 3))
    val rdd2 = sc.parallelize(List(1, 2, 3, 4))
    //求并集
    val rdd3 = rdd1.union(rdd2)
    //求交集
    val rdd4 = rdd1.intersection(rdd2)
    //去重
    rdd3.distinct.collect
    rdd4.collect

  4. 案例算子演示4:join、groupByKey
    val rdd1 = sc.parallelize(List((“tom”, 1), (“jerry”, 3), (“kitty”, 2)))
    val rdd2 = sc.parallelize(List((“jerry”, 2), (“tom”, 1), (“shuke”, 2)))
    //求join
    val rdd3 = rdd1.join(rdd2)
    rdd3.collect
    //求并集
    val rdd4 = rdd1 union rdd2
    rdd4.collect
    //按key进行分组
    val rdd5=rdd4.groupByKey
    rdd5.collect

  5. 案例算子演示5:cogroup
    val rdd1 = sc.parallelize(List((“tom”, 1), (“tom”, 2), (“jerry”, 3), (“kitty”, 2)))
    val rdd2 = sc.parallelize(List((“jerry”, 2), (“tom”, 1), (“jim”, 2)))
    //cogroup
    val rdd3 = rdd1.cogroup(rdd2)
    //注意cogroup与groupByKey的区别
    rdd3.collect

  6. 案例算子演示6:reduce
    val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5))
    //reduce聚合
    val i= rdd1.reduce(_ + _)
    println(i)

  7. 案例算子演示7:reduceByKey、sortByKey
    val rdd1 = sc.parallelize(List((“tom”, 1), (“jerry”, 3), (“kitty”, 2), (“shuke”, 1)))
    val rdd2 = sc.parallelize(List((“jerry”, 2), (“tom”, 3), (“shuke”, 2), (“kitty”, 5)))
    val rdd3 = rdd1.union(rdd2)
    //按key进行聚合
    val rdd4 = rdd3.reduceByKey(_ + _)
    rdd4.collect
    //按value的降序排序
    val rdd5 = rdd4.map(t => (t._2, t._1)).sortByKey(false).map(t => (t._2, t._1))
    rdd5.collect

  8. 案例算子演示8:repartition、coalesce
    val rdd1 = sc.parallelize(1 to 10,3)
    //利用repartition改变rdd1分区数
    //减少分区
    rdd1.repartition(2).partitions.size
    //增加分区
    rdd1.repartition(4).partitions.size
    //利用coalesce改变rdd1分区数
    //减少分区
    rdd1.coalesce(2).partitions.size

注意: repartition 可以增加和减少 rdd 中的分区数,coalesce 只能减少 rdd 分区数,增加不会生效。

RDD 的依赖关系

RDD 的依赖

RDD 和他依赖的父 RDD 的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。

在这里插入图片描述

窄依赖

窄依赖指的是每一个父 RDD 的 Partition 最多被子 RDD 的一个 Partition 使用
总结:窄依赖我们形象的比喻为独生子女

宽依赖

宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition
总结:宽依赖我们形象的比喻为超生

**窄依赖的函数有:
map, filter, union, join, mapPartitions, mapValues
该算子没有经过 shuffle 处理就是窄依赖
宽依赖的函数有:
groupByKey,partitionBy
该算子经过 shuffle 处理生成多个 partition就是宽依赖
**

RDD 的缓存

Spark 速度非常快的原因之一,就是在不同操作中可以在内存中持久化或者缓存数据集。当持久化某个 RDD 后,每一个节点都将把计算分区结果保存在内存中,对此 RDD 或衍生出的 RDD 进行其他动作中重用。这使得后续的动作变得更加迅速。 RDD 相关的持久化和缓存,是 Spark 最重要的特征之一。可以说,缓存是 Spark 构建迭代算法和快速交互式查询的关键。当你持久化一个RDD,每一个结点都将把它的计算分块结果保存在内存中,并在对此数据集(或者衍生出的数据集)进行的其它动作中重用。这将使得后续的动作(action)变得更加迅速(通常快10倍)。缓存是用Spark构建迭代算法的关键。RDD的缓存能够在第一次计算完成后,将计算结果保存到内存、本地文件系统或者Tachyon(分布式内存文件系统)中。通过缓存,Spark避免了RDD上的重复计算,能够极大地提升计算速度。

RDD 缓存方式

RDD 通过 persist 方法或 cache 方法可以将前面计算结果缓存,但是并不会这两个方法被调用时立即缓存,而是触发后面的 action 时,该 RDD 将会被缓存在计算节点的内存中,并提供后面的重用。
在这里插入图片描述
通过查看源码发现 cache 最终也是调用了 persist方法,可以看出 cache 是一个默认的存储级别,而 persist 是可以自定义存储级别的。
默认的存储级别都是仅在内存存储一分,Spark 的存储级别还有好多种,存储级别在 object StorageLevel 中定义的。
在这里插入图片描述
缓存有可能会丢失,或者存储于内存的数据由于内存不足而被删除, RDD 的缓存容错机制保证了及时缓存丢失也能保证计算的正确执行。通过基于 RDD 的一系列转换,丢失的数据会被重算,由于 RDD 的各个 Partition 是相对独立的,因此 只需要计算丢失的部分即可,并不需要重算全部 Partition。

DAG 的生成

什么是 DAG

DAG(Directed Acyclic Graph)叫做有向无环图,原始的 RDD 通过一系列的转换就形成了 DAG ,根据 RDD 之间的依赖关系的不同将 DAG 划分成不同的 Stage(调度阶段)。对于窄依赖,partition 的转换处理在 一个 Stage 中完成计算。对于宽依赖,由于有Shuffle 的存在,只能在 parentRDD 处理完成后,才能开始接下来的计算,因此宽依赖是划分 Stage 的依据
在这里插入图片描述

Spark 任务调度

任务调度流程图

在这里插入图片描述
各个 RDD 之间存在着依赖关系,这些依赖关系就形成有向无环图 DAG ,DAGScheduler 对这些依赖关系形成的 DAG 进行 Stage 划分,划分的规则为从后往前回溯,遇到窄依赖加入本 stage,遇到宽依赖则进行 Stage 切分。完成了 Stage 的划分,DAGScheduler 基于每个 Stage 生成 TeskSet,并将 TaskSet 提交给 TaskScheduler。TaskScheduler 负责具体的 task 调度,最后在 Worker 节点上启动 task。

DAGScheduler

  1. DAGScheduler 对 DAG 有向无环图进行 Stage 划分。
  2. 记录那个 RDD 或者 Stage 输出被物化(缓存),通常在一个复杂的 Shuffle 之后,通常物化一下(cache、persist),方便在之后的计算。
  3. 重新提交 shuffle 输出丢失的 stage (stage 内部计算出错)给TaskScheduler。
  4. 将 TaskSet 传给底层调度器
    – spark-cluster TaskScheduler
    – yarn-cluster YarnClusterScheduler
    – yarn-client YarnClientClusterScheduler

TaskScheduler

  1. 为每一个 TaskSet 构建一个 TaskSetManager 实例管理这个 TaskSet 的生命周期。
  2. 数据本地性地决定每个 Task 最佳位置。
  3. 提交 taskset(一组task)到集群运行并监控。
  4. 推测执行,碰到计算缓慢任务需要放到别的节点上重试。
  5. 重新提交 Shuffle 输出丢失的 Stage 给 DAGScheduler。

RDD容错机制之 checkpoint

checkpoint 是什么

  1. Spark 在生产环境下经常会面临 transformation 的 RDD 非常多(例如一个Job中包含1万个RDD)或者具体的 transformation 的 RDD 本身计算特别复杂或者耗时(例如计算时长超过1个小时),这个时候就要考虑对计算结果数据持久化保存。
  2. Spark 是擅长多步骤迭代的,同时擅长基于 Job 的复用,这个时候如果能够对曾经计算的过程产生的数据进行复用,就可以极大的提升效率。
  3. 如果采用 persist 把数据放在内存中,虽然是快速的,但是也是最不可靠的。如果把数据放在磁盘上,也不是完全可靠的!例如磁盘会损坏,系统管理员可能清空磁盘。
  4. Checkpoint的产生就是为了相对而言更加可靠的持久化数据,在 Checkpoint 的时候可以指定把数据放在本地,并且是多副本的方式,但是在生产环境下是放在 HDFS 上,这就天然的借助了 HDFS 高容错、高可靠的特征来完成了最大化的可靠的持久化数据的方式。
    假如进行1万个算子操作,在9000个算子的时候 persist, 数据还是有可能丢失的,但是如果 checkpoint,数据丢失的概率为0.

checkpoint 原理机制

  1. 当 RDD 使用 cache 机制从内存中读取数据,如果数据没有读到,会使用 checkpoint 机制读取数据。此时如果没有 checkpoint 机制,那么就需要找到父 RDD 重新计算,因此 checkopint 是个很重要的容错机制。ckeckpoint 就是对于一个 RDD chain(链)如果后面需要反复使用某些中间结果 RDD,可能因为一些故障导致该中间数据丢失,那么就可以针对该 RDD 启动 checkpoint 机制,使用 checkpoint 首先需要调用 sparkContext 的 setCheckpoint 方法,设置一个容错文件系统目录,比如 hdfs ,然后对 RDD 调用 checkpoint 方法。之后在 RDD 所处的job 运行结束后,会启动一个单独的 job 来将 checkpoint 过的数据写入之前设置的文件系统持久化,进行高可用。所以后面的计算在使用该 RDD 时,如果数据丢失了,但是还是可以从它的 checkpoint 中读取数据,不需要重新计算。
  2. persist 或者 cache 与 checkpoint 的区别在于,前者持久化只是将数据保存在 BlockManager 中,但是其 lineage 是不变的,但是后者 checkpoint 执行完后,RDD 已经没有依赖 RDD,只有一个 checkpointRDD ,checkpoint 之后,RDD 的lineage 就改变了。persist 或者 cache 持久化的数据丢失的可能性更大,因为可能磁盘活内存被清理,但是 checkpoint 的数据通常保存在 hdfs 上,放在了高容错文件系统。

Spark 运行架构

Spark 运行基本流程

Spark的运行模式:

  1. 本地(local)模式
  2. 基于 Standalone 模式的运行
  3. 基于 yarn 的运行模式
  4. 基于 mesos 模式的运行

Spark运行基本流程参见下面示意图:

在这里插入图片描述

  1. 构建 Spark Application 的运行环境(启动 SparkContext),SparkContext 向 资源管理器(可以是 Standalone、Mesos 或 YARN)注册并申请运行 Executor 资源。
  2. 资源管理器分配 Executor 资源并启动 Executor 运行情况将随着心跳发送到资源管理器上。
  3. SparkContext 构建成 DAG 图,DAGScheduler 将 DAG 图按照宽窄依赖分解成 Stage,一个 Stage 就是一个 TaskSet ,在把 TaskSet 发送给 TaskScheduler。Executor 向 SparkContext 申请 Task,TaskScheduler 将 Task 发送给 Executor 运行,同时 SparkContext 将应用程序代码发给 Executor 。
  4. Task 在 Executor 上运行,运行完毕后释放资源。

Spark 运行架构特点

Spark运行架构特点:

  1. 每个 Application 获取专属 的 executor 进程,该进程在 Application 期间一直驻留,并以多线程方式运行 tasks。
  2. Spark 任务与资源管理器无关,只要能够获取 executor 进程,并且保持相互通信就可以了。
  3. 提交 SparkContext 的 Client 应该靠近 Worker 节点(运行 Executor 的节点),最好是在同一个 Rack 里,因为 Spark 程序运行 过程中 SparkContext 和 Executor 之间有大量的信息交换。如果想在远程集群中运行,最好使用 RPC 将 SparkContext 提交给集群,不要远离 Worker 运行 SparkContext。
  4. Task 采用了数据本地性和推测执行的优化机制。

Spark Shuffle详解

     因为 RDD 是分布式弹性数据集,他的 Partition 极有可能分布在各个节点上,每一个 Key 对应的 Value 也不一定在同一个节点或者 partition 上。所有当需要具有相应的 Key 和 Value 最终集合到同一个节点上运行的时候会发生 shuffle 的过程。
     Spark 中, 当发生 Shuffle 时,根据运行模式的不同,主要分为两大类:HashShuffle 和 SortShuffle。

  1. HashShuffle 运行机制:

在这里插入图片描述

     shuffle write 阶段,主要是在一个 stage 结束计算之后,为了下一个 stage 可以执行 shuffle 类的算子(比如 reduceByKey),从而将每个 task 处理的数据按 key 进行“分类”。所谓“分类”,就是对相同的 key 执行 hash 算法,从而将相同的 key 写入同一个磁盘文件中,而每一个磁盘文件都只属于下游 stage 的task。 在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满后,才会溢写到磁盘文件中。

     那么每个执行 shuffle write 的 task,要为下一个 stage 创建多少个磁盘文件呢?下一个 stage 的task 有多少个,当前 stage 的每个 task 就要创建多少粉磁盘文件。比如下一个 stage 总共有 100 个 task 那么当前 stage 的每个 task 都要创建 100 份磁盘文件。如果当前 stage 有50个 task ,总共有10个 Executor ,每个 Executor 执行 5个 task,那么每个 Executor 上总共要创建500个磁盘文件,所有 Executor 上创建 5000 个磁盘文件。由此可见,未经优化的 shuffle write 操作所产生的磁盘文件的数量是极其惊人的。

     接着我们来说说 shuffle read。shuffle read ,通常就是一个 stage 刚开始时要做的事情。此时该 stage 的每一个 task 就需要将 上一个 stage 的计算结果中的所有相同 key ,从各个节点上通过网络拉取到自己所在的节点上,拉取属于自己的哪一个磁盘文件即可。

     shuffle read 的拉取过程是一边拉取一遍进行聚合的。每个 shuffle read task 都会有一个自己的 buffer 缓冲,每次都只能拉取与 buffer 缓冲相同大小的数据,然后通过内存中的一个 Map 进行聚合等操作。聚合完一批数据后,在拉取下一批数据,并放到 buffer 缓冲中进行聚合操作。以此类推,直到最后将所有数据拉取完毕,并取得最终结果。

  1. 问题:
    在使用HashShuffle的时候会出现以下的问题:
    一个map task都会产生R个磁盘小文件,共产生m*r个小文件,磁盘小文件过多会有什么影响?
    1. 小文件过多,频繁大量的通信,耗时低效的 IO 操作。
    2. 对象过多 -> 内存不足 -> GC -> OOM.
    3. 写磁盘的时候会产生大量的写文件对象。
    4. 读文件的时候会产生大量的读文件对象。
      HashShuffle优化
      在这里插入图片描述

     数据会先写入一个内存数据结构中,此时根据不同的 shuffle 算子,可能选用不同的数据结构。如果是 reduceByKey 这种聚合类的 shuffle 算子,那么会选用 Map 数据结构,一边通过 Map 进行聚合,一边写入内存。如果是 join 这种普通的 shuffle 算子,那么会选用 Array 数据结构,直接写入内存。接着,每写一条数据进入内存数据结构之后就会判断一下,是否达到了某个临界阀值。如果达到临界阀值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。

     在溢写到磁盘文件之前,会根据 key 对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入到磁盘文件。默认的batch数量是1000条,也就是说,排序好的数据,会以每批 1 万条数据的形式分批写入磁盘文件。首先会将数据缓冲在内存中,当缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘 IO 次数,提高性能。

    一个 task 将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有临时磁盘文件都进行合并,这就是 merge 过程。此时会将之前所有温江中的磁盘数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个 task 就只对应一个磁盘文件。

  1. 问题
    当数据量不是特别大的时候,依旧按照普通的 SortShuffle 会对资源进行浪费,并产生不必要的磁盘临时文件,对集群处理性能产生影响。为了解决这一问题,当 task 数量不是特别大的时候,会自动触发 bypass 机制。
    shuffle map task 数量小于 spark.shuffle.sort.bypassMergeThreshold 参数的值(默认为200)
    Bypass 机制
    在这里插入图片描述

广播变量

  1. 概述

         通常在向 Spark 传递函数时,比如使用 map()函数或者用 filter()传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。Spark 的两个共享变量,累加器与广播变量。

    广播变量的引入

    Spark 会自动吧闭包中所有引用到的变量发送到工作节点上。虽然这很方便,但也很低效。原因有二:首先,默认的任务发射机制是专门为小任务进行优化的。其次,事实上你可能会在多个并行操作中使用同一个变量,但是 Spark 会为每个操作分别发送变量的副本。
    在这里插入图片描述
    为了改变这一问题,提高 Spark 的计算效率,我们引入了广播变量。
    在这里插入图片描述
    注意点:

    1. 广播变量必须在 Driver 定义。
    2. 广播变量的值只能在 Driver 修改。
    3. 广播变量的值在 Executor 只能读取,不能修改。
  2. 语法
    使用广播变量的过程很简单:

    1. 通过对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T] 对象。任何可序列化的类型都可以这么实现。
    2. 通过value 属性访问该对象的值(在 java 中为 value()方法)
    3. 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)
  3. 案例:

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext

/**
 * 广播变量
 */
object GuangBoTest {
 
  
  def main(args: Array[String]): Unit = {

    //配置spark
    var conf = new SparkConf().setAppName("wc").setMaster("local")

    //获取spark上下文对象
    var sc = new SparkContext(conf)

    //读取数据文件
    var dataRdd = sc.textFile("wc.txt")

    //broadcast定义广播变量
    var list = sc.broadcast(List("hello world"))

    //默认情况下,一个task任务复制一份变量,会造成重复复制
    //广播变量,把变量复制一份,存放在blockmanager中,每个executor都有一个对应的blockmanager。
    //数据是只读的
    dataRdd.foreach(s => {
      
      //使用.value进行获取数据
      if (list.value.contains(s)) {
        println(s)
      }
      
    })

  }
}

累加器

累加器用来对信息进行聚合,而广播变量用来高效分发较大的对象。

  1. 概述
    当我们需要进行如异常监控、调试、记录符合某特性的数据的数目的时候,这种需求都需要使用的计数器,但是普通变量在进行计数的时候,只是单纯的在运行 Task 的时候产生一个原始变量的副本,而不会在 Driver 端进行汇总,进而不会影响原始变量的值。
    在这里插入图片描述
    为了解决这一问题,我们引入了累加器的概念:
    在这里插入图片描述
    注意点:
    1. 累加器只能在 Driver 端进行定义。
    2. 累加器在 Executor 端只能操作不能读取。
    3. 累加器只能在 Driver 端读取。

  2. 语法:

    1. 通过在 driver 中调用 SparkContext.accumulator(initialValue) 方法,创建出存有初始值为initialValue的累加器。返回值为 org.apache.spark.Accumulator[T] 对象,其中 T 是初始值 initialValue的类型。
    2. Spark 闭包(函数序列化)里的 executor 代码可以使用累加器的 += 方法(在 Java 中是 add)增加累加器的值。
    3. driver 程序可以调用累加器的 value 属性(在 Java 中使用 value() 或 setValue() )来访问累加器的值。
  3. 案例:

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext

/**
 * 累加器
 */
object LeiJiaQiTest {
  
//  var i = 0
  
  def main(args: Array[String]): Unit = {
    //配置spark
    var conf = new SparkConf().setAppName("wc").setMaster("local")

    //获取spark上下文对象
    var sc = new SparkContext(conf)

    //读取数据文件
    var dataRdd = sc.textFile("wc.txt")
    
    //默认情况下,是没办法修改i的值的
//    var i = 0
    
    //定义累加器
    var i = sc.accumulator(0)
    
    dataRdd.foreach(s=>{
      i += 1
      println(s + i)
    })

    println(i)
  }
}

Spark 2.x 介绍

概述: Spark2.x 新特性,和Spark1.x 的对比,以及 Spark2.x 的编码操作。

Spark 2.x 新特性

描述

  1. 更简单:支持标准 SQL 和简化 API

         Spark2.0 依然拥有标准的 SQL 支持和统一的 DataFrame/DataSet API。但我们扩展了 Spark 的 SQL 性能,引进了一个新的 ANSI SQL 解析器并支持子查询。 Spark2.0 可以运行所有的 99 TPC-DS 的查询,这需要很多的 SQL:2003 功能。

  2. 更快:Spark 作为一个编译器
    在这里插入图片描述

         在 Spark2.0 中,把 DataFrames 当做一种特殊的 DataSets,DataFrames = DataSets[Row] 吧两者统一为 DataSets。

  3. 更智能:DataSet 结构化数据流

         通过在 DataFrame 之上构建持久化的应用程序来不断简化数据流,允许我们统一数据流,支持交互和批量查询。

         如果使用 Java、Scala 语言实现以下 SQL 语句的运行效果,有两种情况,一种是通用的,一种是硬编码的。

    select count(*) from store sales where ss _item_sk = 1000

         以下为通用的写法,已经使用了 30 多年的 vplcanp(火山)迭代模型,几乎所有的数据库都是用这个模型,他可以处理多种不同的操作符和函数。

    在这里插入图片描述

         以下为硬编码的写法,他只能处理这一条指定的语句。


    在这里插入图片描述

总结

Sparl2.x 和 Spark1.x 相比之下的新特性为:

     更简单:支持标准 SQL 和简化的API 。

     更快:Spark 作为一个编译器。

     更智能:DataFrame 结构化数据流。

Spark 2.x 代码案例

实现wordcount

import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}

object SQLWordCount {

  def main(args: Array[String]): Unit = {

    //创建SparkSession
    val spark = SparkSession.builder()
      .appName("SQLWordCount")
      .master("local[*]")
      .getOrCreate()

    //(指定以后从哪里)读数据,是lazy

    //Dataset分布式数据集,是对RDD的进一步封装,是更加智能的RDD
    //dataset只有一列,默认这列叫value
    val lines: Dataset[String] = spark.read.textFile("wc.txt")

    //整理数据(切分压平)
    //导入隐式转换
    import spark.implicits._
    val words: Dataset[String] = lines.flatMap(_.split(" "))

    //注册视图
    words.createTempView("v_wc")

    //执行SQL(Transformation,lazy)
    val result: DataFrame = spark.sql("SELECT value word, COUNT(*) counts FROM v_wc GROUP BY word ORDER BY counts DESC")

    //执行Action
    result.show()

    spark.stop()

  }
}



仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值