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 是一个开源的类似于Hadoop Mapreduce 的通用的并行计算框架,Spark 基于 map reduce 算法实现的分布式计算,拥有 Hadoop MapReduce 所具有的优点。但不同于MapReduce的是Spark 中的 Job 中间输出和结果可以保存在 内存中,从而不再需要读写 HDFS,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的 map reduce 的算法。
Spark 是 Map Reduce 的替代方案,而且兼容HDFS Hive,可融入 Hadoop 的生态系统,以弥补 MapReduce 的不足。
快
与 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官网: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 Standalone 集群是 Master-Slaves 架构的集群模式,和大部分的 Master-Slaves 结构集群一样,存在着 Master 单点故障的问题。解决这个问题,Spark提供了两种方案:
基于zookeeper的Spark HA高可用集群部署
该HA方案使用起来很简单,首先需要搭建一个zookeeper集群,然后启动zooKeeper集群,最后在不同节点上启动Master。具体配置如下:
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"
参数说明
注意:
在普通模式下启动spark集群,只需要在主机上面执行start-all.sh 就可以了。
在高可用模式下启动spark集群,先需要在任意一台节点上启动start-all.sh命令。然后在另外一台节点上单独启动master。命令start-master.sh。
Spark是基于内存计算的大数据并行计算框架。因为其基于内存计算,比Hadoop中MapReduce计算框架具有更高的实时性,同时保证了高效容错性和可伸缩性。从2009年诞生于AMPLab到现在已经成为Apache顶级开源项目,并成功应用于商业集群中,学习Spark就需要了解其架构。
Spark架构图如下:
Spark架构使用了分布式计算中master-slave模型,master是集群中含有master进程的节点,slave是集群中含有worker进程的节点。
普通模式提交任务:
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程序,方便用户进行交互式编程,用户可以在该命令行下用scala编写spark程序。
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据抽象,他代表一个不可变、可分区、里面的元素可并行计算的集合。RDD 具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。 RDD 允许用户在执行多个查询时显式地将数据缓存在内存中,后续的查询能够重用这些数据,极大地提升了查询的速度。
Dataset:一个数据的集合,用于存放数据。
Distributed:RDD 中的数据是分布式存储的,可用于分布式计算。
Resilient: RDD 中的数据可以存储在内存或者磁盘中。
A list of partitions:一个分区 (Partition)列表,数据集的基本组成单位。
对于 RDD 来说, 每个分区都会被一个计算任务处理,并决定并行计算的力度。用户可以在创建 RDD 时指定 RDD 的分区个数,如果没有指定,那么就会采取默认值,一般为2。(比如:读取 HDFS 上数据文件产生的 RDD 分区数跟 block 的个数相等。)
A function for computing each split : 一个计算每个分区的函数。
Spark 中 RDD 的计算是 以分区为单位的,每个RDD都会实现compute函数以达到这个目的。
A list of dependencies on other RDDs:一个 RDD 会依赖于其他多个 RDD ,RDD 之间的依赖关系。
RDD 的每次转换都会生成一个新的 RDD ,所以 RDD 之间都会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系计算丢失的分区数据,而不是对 RDD 的所有分区进行重新计算。
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 输出时的分区数量。
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 节点来进行任务计算)。
为什么会有 Spark
因为传统的并行计算模型无法有效的解决迭代计算(iterative)和交互式计算(interactive)。而 Spark 的使命便是解决这两个问题,这也是他存在的价值和理由。
Spark 如何解决迭代计算?
其实主要实现思想就是 RDD ,把所有计算的数据保存在分布式的内存中。迭代计算通常情况下都是对同一个数据集做反复的迭代计算,数据在内存中将大大提升 IO 操作。这也是 Spark 涉及的核心:内存计算。
Spark 如何实现交互式计算?
因为Spark 是用 scala 语言实现的,Spark 和 scala 能够紧密的继承,所以 Spark 可以完美的运用 scala的解释器,使得其中的 scala 可以向操作本地集合对象一样轻松操作分布式数据集。
Spark 和 RDD 的关系
RDD 是一种具有容错性、基于内存计算的抽象方法,RDD 是 Spark Core 的底层核心, Spark 则是这个抽象算法的实现。
sc 为 SparkContext 的实例
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
动作 | 含义 |
---|---|
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 |
启动spark-shell 进行测试:
spark-shell --master spark://主机名
案例算子演示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:flapMap
val rdd1 = sc.parallelize(Array(“a b c”, “d e f”, “h i j”))
//将rdd1里面的每一个元素先切分在压平
val rdd2 = rdd1.flatMap(_.split(" "))
rdd2.collect
案例算子演示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: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: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:reduce
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5))
//reduce聚合
val i= rdd1.reduce(_ + _)
println(i)
案例算子演示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: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 的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。
窄依赖指的是每一个父 RDD 的 Partition 最多被子 RDD 的一个 Partition 使用
总结:窄依赖我们形象的比喻为独生子女
宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition
总结:宽依赖我们形象的比喻为超生
**窄依赖的函数有:
map, filter, union, join, mapPartitions, mapValues
该算子没有经过 shuffle 处理就是窄依赖
宽依赖的函数有:
groupByKey,partitionBy
该算子经过 shuffle 处理生成多个 partition就是宽依赖
**
Spark 速度非常快的原因之一,就是在不同操作中可以在内存中持久化或者缓存数据集。当持久化某个 RDD 后,每一个节点都将把计算分区结果保存在内存中,对此 RDD 或衍生出的 RDD 进行其他动作中重用。这使得后续的动作变得更加迅速。 RDD 相关的持久化和缓存,是 Spark 最重要的特征之一。可以说,缓存是 Spark 构建迭代算法和快速交互式查询的关键。当你持久化一个RDD,每一个结点都将把它的计算分块结果保存在内存中,并在对此数据集(或者衍生出的数据集)进行的其它动作中重用。这将使得后续的动作(action)变得更加迅速(通常快10倍)。缓存是用Spark构建迭代算法的关键。RDD的缓存能够在第一次计算完成后,将计算结果保存到内存、本地文件系统或者Tachyon(分布式内存文件系统)中。通过缓存,Spark避免了RDD上的重复计算,能够极大地提升计算速度。
RDD 通过 persist 方法或 cache 方法可以将前面计算结果缓存,但是并不会这两个方法被调用时立即缓存,而是触发后面的 action 时,该 RDD 将会被缓存在计算节点的内存中,并提供后面的重用。
通过查看源码发现 cache 最终也是调用了 persist方法,可以看出 cache 是一个默认的存储级别,而 persist 是可以自定义存储级别的。
默认的存储级别都是仅在内存存储一分,Spark 的存储级别还有好多种,存储级别在 object StorageLevel 中定义的。
缓存有可能会丢失,或者存储于内存的数据由于内存不足而被删除, RDD 的缓存容错机制保证了及时缓存丢失也能保证计算的正确执行。通过基于 RDD 的一系列转换,丢失的数据会被重算,由于 RDD 的各个 Partition 是相对独立的,因此 只需要计算丢失的部分即可,并不需要重算全部 Partition。
DAG(Directed Acyclic Graph)叫做有向无环图,原始的 RDD 通过一系列的转换就形成了 DAG ,根据 RDD 之间的依赖关系的不同将 DAG 划分成不同的 Stage(调度阶段)。对于窄依赖,partition 的转换处理在 一个 Stage 中完成计算。对于宽依赖,由于有Shuffle 的存在,只能在 parentRDD 处理完成后,才能开始接下来的计算,因此宽依赖是划分 Stage 的依据
各个 RDD 之间存在着依赖关系,这些依赖关系就形成有向无环图 DAG ,DAGScheduler 对这些依赖关系形成的 DAG 进行 Stage 划分,划分的规则为从后往前回溯,遇到窄依赖加入本 stage,遇到宽依赖则进行 Stage 切分。完成了 Stage 的划分,DAGScheduler 基于每个 Stage 生成 TeskSet,并将 TaskSet 提交给 TaskScheduler。TaskScheduler 负责具体的 task 调度,最后在 Worker 节点上启动 task。
Spark的运行模式:
Spark运行基本流程参见下面示意图:
Spark运行架构特点:
因为 RDD 是分布式弹性数据集,他的 Partition 极有可能分布在各个节点上,每一个 Key 对应的 Value 也不一定在同一个节点或者 partition 上。所有当需要具有相应的 Key 和 Value 最终集合到同一个节点上运行的时候会发生 shuffle 的过程。
Spark 中, 当发生 Shuffle 时,根据运行模式的不同,主要分为两大类:HashShuffle 和 SortShuffle。
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 缓冲中进行聚合操作。以此类推,直到最后将所有数据拉取完毕,并取得最终结果。
数据会先写入一个内存数据结构中,此时根据不同的 shuffle 算子,可能选用不同的数据结构。如果是 reduceByKey 这种聚合类的 shuffle 算子,那么会选用 Map 数据结构,一边通过 Map 进行聚合,一边写入内存。如果是 join 这种普通的 shuffle 算子,那么会选用 Array 数据结构,直接写入内存。接着,每写一条数据进入内存数据结构之后就会判断一下,是否达到了某个临界阀值。如果达到临界阀值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。
在溢写到磁盘文件之前,会根据 key 对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入到磁盘文件。默认的batch数量是1000条,也就是说,排序好的数据,会以每批 1 万条数据的形式分批写入磁盘文件。首先会将数据缓冲在内存中,当缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘 IO 次数,提高性能。
一个 task 将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有临时磁盘文件都进行合并,这就是 merge 过程。此时会将之前所有温江中的磁盘数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个 task 就只对应一个磁盘文件。
概述
通常在向 Spark 传递函数时,比如使用 map()函数或者用 filter()传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。Spark 的两个共享变量,累加器与广播变量。
广播变量的引入
Spark 会自动吧闭包中所有引用到的变量发送到工作节点上。虽然这很方便,但也很低效。原因有二:首先,默认的任务发射机制是专门为小任务进行优化的。其次,事实上你可能会在多个并行操作中使用同一个变量,但是 Spark 会为每个操作分别发送变量的副本。
为了改变这一问题,提高 Spark 的计算效率,我们引入了广播变量。
注意点:
语法
使用广播变量的过程很简单:
案例:
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)
}
})
}
}
累加器用来对信息进行聚合,而广播变量用来高效分发较大的对象。
概述
当我们需要进行如异常监控、调试、记录符合某特性的数据的数目的时候,这种需求都需要使用的计数器,但是普通变量在进行计数的时候,只是单纯的在运行 Task 的时候产生一个原始变量的副本,而不会在 Driver 端进行汇总,进而不会影响原始变量的值。
为了解决这一问题,我们引入了累加器的概念:
注意点:
1. 累加器只能在 Driver 端进行定义。
2. 累加器在 Executor 端只能操作不能读取。
3. 累加器只能在 Driver 端读取。
语法:
案例:
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)
}
}
概述: Spark2.x 新特性,和Spark1.x 的对比,以及 Spark2.x 的编码操作。
更简单:支持标准 SQL 和简化 API
Spark2.0 依然拥有标准的 SQL 支持和统一的 DataFrame/DataSet API。但我们扩展了 Spark 的 SQL 性能,引进了一个新的 ANSI SQL 解析器并支持子查询。 Spark2.0 可以运行所有的 99 TPC-DS 的查询,这需要很多的 SQL:2003 功能。
在 Spark2.0 中,把 DataFrames 当做一种特殊的 DataSets,DataFrames = DataSets[Row] 吧两者统一为 DataSets。
更智能: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 结构化数据流。
实现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()
}
}