Linux环境Spark安装配置及使用
1. 认识Spark
(1) Spark介绍
- 大数据计算引擎
- 官网:spark.apache.org/
- 官方介绍:Apache Spark™ is a unified analytics engine for large-scale data processing.(Apache Spark™是一个用于大规模数据处理的统一分析引擎。)
- Spark是一种快速、通用、可扩展的大数据分析引擎,2009年诞生于加州大学伯克利分校AMPLab,2010年开源,2013年6月成为Apache孵化项目,2014年2月成为Apache顶级项目。目前,Spark生态系统已经发展成为一个包含多个子项目的集合,其中包含SparkSQL、Spark Streaming、GraphX、MLlib等子项目,Spark是基于内存计算的大数据并行计算框架。Spark基于内存计算,提高了在大数据环境下数据处理的实时性,同时保证了高容错性和高可伸缩性,允许用户将Spark部署在大量廉价硬件之上,形成集群。
- Spark生态圈:
- Spark Core:RDD(弹性分布式数据集)
- Spark SQL
- Spark Streaming
- Spark MLLib:协同过滤,ALS,逻辑回归等等 --> 机器学习
- Spark Graphx:图计算
(2) 为什么要学习Spark
- Hadoop的MapReduce计算模型存在的问题:
-
MapReduce的核心是Shuffle(洗牌)。在整个Shuffle的过程中,至少会产生6次的I/O。
-
中间结果输出:基于MapReduce的计算引擎通常会将中间结果输出到磁盘上,进行存储和容错。另外,当一些查询(如:Hive)翻译到MapReduce任务时,往往会产生多个Stage(阶段),而这些串联的Stage又依赖于底层文件系统(如HDFS)来存储每一个Stage的输出结果,而I/O的效率往往较低,从而影响了MapReduce的运行速度。
-
- Spark的最大特点:基于内存
- Spark是MapReduce的替代方案,而且兼容HDFS、Hive,可融入Hadoop的生态系统,弥补MapReduce的不足。
(3) Spark的特点:快、易用、通用、兼容
- 快——与Hadoop的MapReduce相比,Spark基于内存的运算速度要快100倍以上,即使,Spark基于硬盘的运算也要快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还可以很好的融入Hadoop的体系结构中可以直接操作HDFS,并提供Hive on Spark、Pig on Spark的框架集成Hadoop。
- 兼容——Spark可以非常方便地与其他的开源产品进行融合。比如,Spark可以使用Hadoop的YARN和ApacheMesos作为它的资源管理和调度器,并且可以处理所有Hadoop支持的数据,包括HDFS、HBase和Cassandra等。这对于已经部署Hadoop集群的用户特别重要,因为不需要做任何数据迁移就可以使用Spark的强大处理能力。Spark也可以不依赖于第三方的资源管理和调度器,它实现了Standalone作为其内置的资源管理和调度框架,这样进一步降低了Spark的使用门槛,使得所有人都可以非常容易地部署和使用Spark。此外,Spark还提供了在EC2上部署Standalone的Spark集群的工具。
2. Spark体系架构
- Spark的运行方式
- Yarn
- Standalone:本机调试(demo)
- Worker(从节点):每个服务器上,资源和任务的管理者,只负责管理一个节点。
- 执行过程:
- 一个Worker 有多个 Executor。 Executor是任务的执行者,按阶段(stage)划分任务。—> RDD
- 客户端:Driver Program 提交任务到集群中。
- spark-submit
- spark-shell
3. Spark-2.1.0安装流程
(1) 准备工作
- 具备java环境
- 配置主机名
- 配置免密码登录
- 防火墙关闭
(2) 解压spark-2.1.0-bin-hadoop2.7.tgz安装包到目标目录下:
tar -zxvf .tar.gz -C 目标目录
(3) 为后续方便,重命名Spark文件夹:
mv spark-2.1.0-bin-hadoop2.7/ spark-2.1.0
(4) Spark目录介绍
- bin —— Spark操作命令
- conf —— 配置文件
- data —— Spark测试文件
- examples —— Spark示例程序
- jars
- LICENSE
- licenses
- NOTICE
- python
- R
- README.md
- RELEASE
- sbin —— Spark集群命令
- yarn —— Spark-yarn配置
(5) 修改配置文件:
- <1>. 配置spark-env.sh:
- 进入spark-2.1.0/conf路径,重命名配置文件:
mv spark-env.sh.template spark-env.sh
- 修改spark-env.sh信息:
vi spark-env.sh
-
export JAVA_HOME=/opt/module/jdk1.8.0_144 export SPARK_MASTER_HOST=bigdata01 export SPARK_MASTER_PORT=7077 复制代码
- 进入spark-2.1.0/conf路径,重命名配置文件:
- <2>. 配置slaves:
- 进入spark-2.1.0/conf路径,重命名配置文件:
mv slaves.template slaves
- 修改slaves信息:
vi slaves
-
bigdata02 bigdata03 复制代码
- 进入spark-2.1.0/conf路径,重命名配置文件:
(6) 配置环境变量:
- 修改配置文件:
vi /etc/profile
- 增加以下内容:
export SPARK_HOME=spark安装路径
export PATH=$PATH:$SPARK_HOME/bin
export PATH=$PATH:$SPARK_HOME/sbin
- 声明环境变量:
source /etc/profile
(6) 集群配置:
- 拷贝配置好的spark到其他机器上
scp -r spark-2.1.0/ bigdata02:$PWD
scp -r spark-2.1.0/ bigdata03:$PWD
(7) 启动:
- 启动主节点:
start-master.sh
- 启动从节点:
start-slaves.sh
- 启动shell:
spark-shell
- 通过网页端查看:
- http://bigdata01:8080/
- Spark中内置有Tomcat,故端口号默认为8080
(8) 关闭:
- 关闭主节点:
stop-master.sh
- 关闭从节点:
stop-slaves.sh
4. Spark HA的实现
(1) 基于文件系统的单点恢复
-
主要用于开发或测试环境。
-
当spark提供目录保存spark Application和worker的注册信息,并将他们的恢复状态写入该目录中,一旦Master发生故障,就可以通过重新启动Master进程(sbin/start-master.sh),恢复已运行的spark Application和worker的注册信息。
-
基于文件系统的单点恢复,主要是在spark-env.sh里对SPARK_DAEMON_JAVA_OPTS设置
- 创建存放文件夹:
mkdir /opt/module/spark-2.1.0/recovery
- 修改配置信息:
vi spark-env.sh
- 增加内容:
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=FILESYSTEM -Dspark.deploy.recoveryDirectory=/opt/module/spark-2.1.0/recovery"
- 创建存放文件夹:
(2) 基于Zookeeper的Standby Masters
-
适用于现实生产。
-
ZooKeeper提供了一个Leader Election机制,利用这个机制可以保证虽然集群存在多个Master,但是只有一个是Active的,其他的都是Standby。当Active的Master出现故障时,另外的一个Standby Master会被选举出来。由于集群的信息,包括Worker,Driver和Application的信息都已经持久化到ZooKeeper,因此在切换的过程中只会影响新Job的提交,对于正在进行的Job没有任何的影响。加入ZooKeeper的集群整体架构如下图所示:
-
修改配置信息:
vi spark-env.sh
- 增加内容:
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=bigdata01:2181,bigdata02:2181,bigdata03:2181 -Dspark.deploy.zookeeper.dir=/spark"
- 注释掉:
export SPARK_MASTER_HOST
和export SPARK_MASTER_PORT
-
发送新的配置文件到集群其余节点:
scp spark-env.sh bigdata02:$PWD
scp spark-env.sh bigdata03:$PWD
5. 执行Spark的任务
(1) spark-submit
- 用于提交Spark的任务(任务即相关jar包)
- e.g.: 蒙特卡洛求PI(圆周率)
- 原理:如下图所示,随机向正方形内落点,通过统计正方形内所有点数和落入圆内的点数来计算占比,得出正方形与圆的面积近似比值,进而近似出PI值。
- 命令:
spark-submit --master spark://XXXX:7077
(指明master地址)--class org.apache.spark.examples.SparkPi
(指明主程序的名字)/XXXX/spark/examples/jars/spark-examples_2.11-2.1.0.jar
(指明jar包地址)100
(指明运行次数)
(2) spark-shell
- 相当于REPL,作为一个独立的Application运行
- spark-shell是Spark自带的交互式Shell程序,方便用户进行交互式编程,用户可以在该命令行下用scala编写spark程序。
- 参数说明:
--master spark://XXXX:7077
指定Master的地址--executor-memory 2g
指定每个worker可用内存为2G--total-executor-cores 2
指定整个集群使用的cup核数为2个
- Spark Session 是 2.0 以后提供的,利用 SparkSession 可以访问spark所有组件
- 两种运行模式:
- <1>. 本地模式
- 启动:
spark-shell
(后面不接任何参数)
- 启动:
- <2>. 集群模式
- 启动:
spark-shell --master spark://XXXX:7077
(指明master地址)
- 启动:
- <1>. 本地模式
- e.g.: 编写WordCount程序
- <1>. 处理本地文件,把结果打印到屏幕上
- 启动:
spark-shell
- 传入文件:
sc.textFile("/XXXX/WordCount.txt")
(本地文件路径).flatMap(_.split(" "))
(按照空格分割).map((_,1))
(单词遍历).reduceByKey(_+_)
(单词计数).collect
- 启动:
- <2>. 处理HDFS文件,结果保存在hdfs上
- 启动:
spark-shell --master spark://XXXX:7077
(指 - sc.textFile("hdfs://XXXX:9000/sp_wc.txt").flatMap(.split(" ")).map((,1)).reduceByKey(+).saveAsTextFile("hdfs://XXXX:9000/output/spark/WordCount")
- 启动:
- <1>. 处理本地文件,把结果打印到屏幕上
(3) 单步运行WordCount -> RDD
- 启动shell:
spark-shell
-
scala> val rdd1 = sc.textFile("/root/sp_wc.txt") rdd1: org.apache.spark.rdd.RDD[String] = /root/sp_wc.txt MapPartitionsRDD[1] at textFile at <console>:24 scala> rdd1.collect res0: Array[String] = Array(I love Scala, I love Skark, 2019/5/8) scala> val rdd2 = rdd1.flatMap(_.split(" ")) rdd2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[2] at flatMap at <console>:26 scala> rdd2.collect res1: Array[String] = Array(I, love, Scala, I, love, Skark, 2019/5/8) scala> val rdd3 = rdd2.map((_,1)) rdd3: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[3] at map at <console>:28 scala> rdd3.collect res2: Array[(String, Int)] = Array((I,1), (love,1), (Scala,1), (I,1), (love,1), (Skark,1), (2019/5/8,1)) scala> val rdd4 = rdd3.reduceByKey(_+_) rdd4: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[4] at reduceByKey at <console>:30 scala> rdd4.collect res3: Array[(String, Int)] = Array((2019/5/8,1), (love,2), (I,2), (Skark,1), (Scala,1)) 复制代码
(4) 在IDE中运行WorkCount
- <1>. scala版本
-
import org.apache.spark.SparkConf import org.apache.spark.SparkContext object WordCount { def main(args: Array[String]): Unit = { //创建一个Spark配置文件 val conf = new SparkConf().setAppName("Scala WordCount").setMaster("local") //创建Spark对象 val sc = new SparkContext(conf) val result = sc.textFile(args(0)) .flatMap(_.split(" ")) .map((_, 1)) .reduceByKey(_ + _) .saveAsTextFile(args(1)) sc.stop() } } 复制代码
-
- <2>. Java版本
-
import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaPairRDD; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; import org.apache.spark.api.java.function.FlatMapFunction; import org.apache.spark.api.java.function.Function2; import org.apache.spark.api.java.function.PairFunction; import parquet.format.PageHeader; import scala.Tuple2; public class WordCount { public static void main(String[] args) { // TODO Auto-generated method stub SparkConf conf = new SparkConf() .setAppName("JavaWordCount") .setMaster("local") ; //新建SparkContext对象 JavaSparkContext sc = new JavaSparkContext(conf) ; //读入数据 JavaRDD<String> lines = sc.textFile("hdfs://XXXX:9000/WordCount.txt") ; //分词 第一个参数表示读进来的话 第二个参数表示 返回值 JavaRDD<String> words = lines.flatMap(new FlatMapFunction<String, String>() { @Override public Iterator<String> call(String input) throws Exception { return Arrays.asList(input.split(" ")).iterator() ; } }) ; //每个单词记一次数 /* * String, String, Integer * input <key value> */ JavaPairRDD<String, Integer> ones = words.mapToPair(new PairFunction<String, String, Integer>() { @Override public Tuple2<String, Integer> call(String input) throws Exception { return new Tuple2<String, Integer>(input, 1) ; } }) ; //执行reduce操作 /* * Integer, Integer, Integer * nteger arg0, Integer arg1 返回值 */ JavaPairRDD<String,Integer> counts = ones.reduceByKey(new Function2<Integer, Integer, Integer>() { @Override public Integer call(Integer arg0, Integer arg1) throws Exception { // TODO Auto-generated method stub return arg0 + arg1 ; } }) ; //打印结果 List<Tuple2<String, Integer>> output = counts.collect() ; for (Tuple2<String, Integer> tuple :output) { System.out.println(tuple._1 + " : " + tuple._2) ; } sc.stop() ; } } 复制代码
-
(5) WordCount程序处理过程
(6) Spark提交任务的流程
6. Spark的算子
(1) RDD基础
- <1>. 什么是RDD
- RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。
- <2>. RDD的属性(源码中的一段话)
- **一组分片(Partition)。**即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。
- **一个计算每个分区的函数。**Spark中RDD的计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。
- **RDD之间的依赖关系。**RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。
- **一个Partitioner,即RDD的分片函数。**当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量。
- **一个列表。**存储存取每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
- <3>. RDD的创建方式
- 通过外部的数据文件创建,如HDFS:
val rdd1 = sc.textFile(“hdfs://XXXX:9000/data.txt”)
- 通过sc.parallelize进行创建:
val rdd1 = sc.parallelize(Array(1,2,3,4,5,6,7,8))
- DD的类型:Transformation和Action
- 通过外部的数据文件创建,如HDFS:
- <4>. RDD的基本原理
(2) Transformation
- RDD中的所有转换都是延迟加载的,也就是说,它们并不会直接计算结果。相反的,它们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当发生一个要求返回结果给Driver的动作时,这些转换才会真正运行。这种设计让Spark更加有效率地运行。
(3) Action
(4) RDD的缓存机制
- RDD通过persist方法或cache方法可以将前面的计算结果缓存,但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
- 通过查看源码发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。
- 缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。
- Demo示例:
- 通过UI进行监控:
(5) RDD的Checkpoint(检查点)机制:容错机制
- 检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage(血统)做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。
- 设置checkpoint的目录,可以是本地的文件夹、也可以是HDFS。一般是在具有容错能力,高可靠的文件系统上(比如HDFS, S3等)设置一个检查点路径,用于保存检查点数据。
- 分别举例说明:
- <1>. 本地目录
- 注意:这种模式,需要将spark-shell运行在本地模式上
- <2>. HDFS的目录
- 注意:这种模式,需要将spark-shell运行在集群模式上
(6) RDD的依赖关系和Spark任务中的Stage
-
RDD的依赖关系
-
RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。
-
窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用
- 总结:窄依赖我们形象的比喻为独生子女
-
宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition
- 总结:窄依赖我们形象的比喻为超生
-
-
Spark任务中的Stage
- DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。
(7) RDD基础练习
-
练习1:
-
//通过并行化生成rdd val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9