Apache Spark

文章目录

Spark诞生

spark背景介绍

Spark 是一个用来实现快速而通用的集群计算的平台。在速度方面,Spark 扩展了广泛使用的 MapReduce 计算模型,而且高效地支持更多计算模式,包括交互式查询和流处理。在处理大规模数据集时,速度是非常重要的。速度快就意味着我们可以进行交互式的数据操作,否则我们每次操作就需要等待数分钟甚至数小时。Spark 的一个主要特点就是能够在内存中进行计算,因而更快。不过即使是必须在磁盘上进行的复杂计算,Spark 依然比 MapReduce 更加高效。
总的来说,Spark 适用于各种各样原先需要多种不同的分布式平台的场景,包括批处理、迭代算法、交互式查询、流处理。通过在一个统一的框架下支持这些不同的计算,Spark使我们可以简单而低耗地把各种处理流程整合在一起。而这样的组合,在实际的数据分析过程中是很有意义的。不仅如此,Spark 的这种特性还大大减轻了原先需要对各种平台别管理的负担。Spark 所提供的接口非常丰富。除了提供基于 Python、Java、Scala 和 SQL 的简单易用的API 以及内建的丰富的程序库以外,Spark 还能和其他大数据工具密切配合使用。例如,Spark 可以运行在 Hadoop 集群上,访问包括 Cassandra 在内的任意 Hadoop 数据源。

  • Spark 是一种由 Scala 语言开发的快速、通用、可扩展的大数据分析引擎
  • Spark Core 中提供了 Spark 最基础与最核心的功能
  • Spark SQL 是 Spark 用来操作结构化数据的组件。通过 Spark SQL,用户可以使用SQL 或者 Apache Hive 版本的 SQL 方言(HQL)来查询数据。
  • Spark Streaming 是 Spark 平台上针对实时数据进行流式计算的组件,提供了丰富的处理数据流的 API
  • 总结

Spark是一个快如闪电的统一分析引擎(计算框架)用于大规模数据集的处理。Spark在做数据的批处理计算,计算性能大约是Hadoop MapReduce的10~100倍,因为Spark使用比较先进的基于DAG 任务调度,可以将一个任务拆分成若干个阶段,然后将这些阶段分批次交给集群计算节点处理。

  • MapReduce VS Spark
    在这里插入图片描述

整个MapReduce的计算实现的是基于磁盘的IO计算,随着大数据技术的不断普及,人们开始重新定义大数据的处理方式,不仅仅满足于能在合理的时间范围内完成对大数据的计算,还对计算的实效性提出了更苛刻的要求,因为人们开始探索使用Map Reduce计算框架完成一些复杂的高阶算法,往往这些算法通常不能通过1次性的Map Reduce迭代计算完成。由于Map Reduce计算模型总是把结果存储到磁盘中,每次迭代都需要将数据磁盘加载到内存,这就为后续的迭代带来了更多延长。

Spark 和Hadoop 的根本差异是多个作业之间的数据通信问题 : Spark 多个作业之间数据通信是基于内存,而Hadoop是基于磁盘。

2009年Spark在加州伯克利AMP实验室诞生,2010首次开源后该项目就受到很多开发人员的喜爱,2013年6月份开始在Apache孵化,2014年2月份正式成为Apache的顶级项目。Spark发展如此之快是因为Spark在计算层方面明显优于Hadoop的Map Reduce这磁盘迭代计算,因为Spark可以使用内存对数据做计算,而且计算的中间结果也可以缓存在内存中,这就为后续的迭代计算节省了时间,大幅度的提升了针对于海量数据的计算效率。
在这里插入图片描述
Spark也给出了在使用MapReduce和Spark做线性回归计算(算法实现需要n次迭代)上,Spark的速率几乎是MapReduce计算10~100倍这种计算速度。
在这里插入图片描述
不仅如此Spark在设计理念中也提出了One stack ruled them all战略,并且提供了基于Spark批处理至上的计算服务分支例如:实现基于Spark的交互查询、近实时流处理、机器学习、Grahx 图形关系存储等。
在这里插入图片描述
从图中不难看出Apache Spark处于计算层,Spark项目在战略上启到了承上启下的作用,并没有废弃原有以hadoop为主体的大数据解决方案。因为Spark向下可以计算来自于HDFS、HBase、Cassandra和亚马逊S3文件服务器的数据,也就意味着使用Spark作为计算层,用户原有的存储层架构无需改动。

计算流程

因为Spark计算是在MapReduce计算之后诞生,吸取了MapReduce设计经验,极大地规避了MapReduce计算过程中的诟病,先来回顾一下MapReduce计算的流程。
在这里插入图片描述
总结一下几点缺点:

1)MapReduce虽然基于矢量编程思想,但是计算状态过于简单,只是简单的将任务分为Map state和Reduce State,没有考虑到迭代计算场景。
2)在Map任务计算的中间结果存储到本地磁盘,IO调用过多,数据读写效率差。
3)MapReduce是先提交任务,然后在计算过程中申请资源。并且计算方式过于笨重。每个并行度都是由一个JVM进程来实现计算。

通过简单的罗列不难发现MapReduce计算的诟病和问题,因此Spark在计算层面上借鉴了MapReduce计算设计的经验,提出了DGASchedule和TaskSchedual概念,打破了在MapReduce任务中一个job只用Map State和Reduce State的两个阶段,并不适合一些迭代计算次数比较多的场景。因此Spark 提出了一个比较先进的设计理念,任务状态拆分,Spark在任务计算初期首先通过DGASchedule计算任务的State,将每个阶段的Sate封装成一个TaskSet,然后由TaskSchedual将TaskSet提交集群进行计算。可以尝试将Spark计算的流程使用一下的流程图描述如下:
在这里插入图片描述
相比较于MapReduce计算,Spark计算有以下优点:

1)智能DAG任务拆分,将一个复杂计算拆分成若干个State,满足迭代计算场景
2)Spark提供了计算的缓存容错策略,将计算结果存储在内存或者磁盘,加速每个state的运行,提升运行效率
3)Spark在计算初期,就已经申请好计算资源。任务并行度是通过在Executor进程中启动线程实现,相比较于MapReduce计算更加轻快。

目前Spark提供了Cluster Manager的实现由YarnStandalone、Messso、kubernates等实现。其中企业常用的有Yarn和Standalone方式的管理。

环境搭建

Standalone(资源调度由spark自己完成)

环境搭建前一些准备
  • 设置CentOS进程数和文件数(重启)
[root@CentOS ~]# vi /etc/security/limits.conf
* soft nofile 204800
* hard nofile 204800
* soft nproc 204800
* hard nproc 204800
[root@CentOS ~]# reboot

优化linux性能,修改这个最大值,需要重启

  • 配置主机名
  • 设置IP映射
  • 防火墙服务
  • 安装JDK1.8+
  • SSH配置免密
[root@CentOS ~]# ssh-keygen -t rsa
[root@CentOS ~]# ssh-copy-id 主机名
  • 重启
Hadoop环境
  • 解压hadoop压缩包
  • 配置core-site.xml
<!--nn访问入口-->
<property>
    <name>fs.defaultFS</name>
    <value>hdfs://CentOS:9000</value>
</property>
<!--hdfs工作基础目录-->
<property>
    <name>hadoop.tmp.dir</name>
    <value>/usr/hadoop-2.9.2/hadoop-${user.name}</value>
</property>
  • 配置hdfs-site.xml
<!--block副本因子-->
<property>
    <name>dfs.replication</name>
    <value>1</value>
</property>
<!--配置Sencondary namenode所在物理主机-->
<property>
    <name>dfs.namenode.secondary.http-address</name>
    <value>CentOS:50090</value>
</property>
<!--设置datanode最大文件操作数-->
<property>
    <name>dfs.datanode.max.xcievers</name>
    <value>4096</value>
</property>
<!--设置datanode并行处理能力-->
<property>
    <name>dfs.datanode.handler.count</name>
    <value>6</value>
</property>
  • 配置slaves
CentOS
  • 配置hadoop环境变量
[root@CentOS ~]# vi .bashrc
HADOOP_HOME=/usr/hadoop-2.9.2
JAVA_HOME=/usr/java/latest
PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
CLASSPATH=.
export JAVA_HOME
export PATH
export CLASSPATH
export HADOOP_HOME
[root@CentOS ~]# source .bashrc
  • 启动Hadoop服务
[root@CentOS ~]# hdfs namenode -format # 创建初始化所需的fsimage文件
[root@CentOS ~]# start-dfs.sh
Spark环境

下载spark-2.4.3-bin-without-hadoop.tgz解压到/usr目录,并且将Spark目录修改名字为spark-2.4.3然后修改spark-env.shspark-default.conf文件.
sprak下载地址:里面有许多用到的软件和版本
apache归档地址

  • 解压Spark安装包,并且修改解压文件名(原文件名太长了),配置环境变量
[root@CentOS ~]# tar -zxf spark-2.4.3-bin-without-hadoop.tgz -C /usr/
[root@CentOS ~]# mv /usr/spark-2.4.3-bin-without-hadoop/ /usr/spark-2.4.3
#配置环境变量
[root@CentOS ~]# vi .bashrc
SPARK_HOME=/usr/spark-2.4.3
HADOOP_HOME=/usr/hadoop-2.9.2
JAVA_HOME=/usr/java/latest
PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$SPARK_HOME/bin:$SPARK_HOME/sbin
CLASSPATH=.
export JAVA_HOME
export PATH
export CLASSPATH
export HADOOP_HOME
export SPARK_HOME
[root@CentOS ~]# source .bashrc
  • 配置Spark服务
[root@CentOS spark-2.4.3]# cd /usr/spark-2.4.3/conf/
[root@CentOS conf]# mv spark-env.sh.template spark-env.sh		#修改配置文件名称
[root@CentOS conf]# mv slaves.template slaves					#修改配置文件名称
#修改slaves配置文件
[root@CentOS conf]# vi slaves
CentOS
#修改spark-env.sh配置文件
[root@CentOS conf]# vi spark-env.sh
SPARK_MASTER_HOST=CentOS						#主机名
SPARK_MASTER_PORT=7077							#端口,默认7077
SPARK_WORKER_CORES=4							#配置应用程序允许使用的核数(默认是所有的core)
SPARK_WORKER_MEMORY=2g							#worker内存
LD_LIBRARY_PATH=/usr/hadoop-2.9.2/lib/native
SPARK_DIST_CLASSPATH=$(hadoop classpath)

export SPARK_MASTER_HOST
export SPARK_MASTER_PORT
export SPARK_WORKER_CORES
export SPARK_WORKER_MEMORY
export LD_LIBRARY_PATH
export SPARK_DIST_CLASSPATH
  • 启动Spark进程(目的:计算资源的分配)
[root@CentOS ~]# cd /usr/spark-2.4.3/
[root@CentOS spark-2.4.3]# ./sbin/start-all.sh
starting org.apache.spark.deploy.master.Master, logging to /usr/spark-2.4.3/logs/spark-root-org.apache.spark.deploy.master.Master-1-CentOS.out
CentOS: starting org.apache.spark.deploy.worker.Worker, logging to /usr/spark-2.4.3/logs/spark-root-org.apache.spark.deploy.worker.Worker-1-CentOS.out

spark页面地址:主机地址:8080
如果此时8080无法访问,再运行一下spark的master命令:./sbin/start-master.sh(一般不会出现情况,我之所以出现这种情况,归根结底还是spark版本和hadoop版本不太兼容,我测试后就换了新的spark版本)

  • 测试Spark
[root@CentOS spark-2.4.3]# ./bin/spark-shell --master spark://CentOS:7077 --deploy-mode client --executor-cores 2

#出现scala表示成功
scala>

executor-cores:在standalone模式表示程序每个Worker节点分配资源数。不能超过单台最大core个数,如果不清楚每台能够分配的最大core的个数,可以使用--total-executor-cores,该种分配会尽最大可能分配。

scala> sc.textFile("hdfs:///words/t_words",5)
    .flatMap(_.split(" "))
    .map((_,1))
    .reduceByKey(_+_)
    .sortBy(_._1,true,3)
    .saveAsTextFile("hdfs:///results")

在这里插入图片描述

Spark On Yarn(资源调度由Hadoop的yarn完成)

环境搭建前一些准备
  • 设置CentOS进程数和文件数(重启)
[root@CentOS ~]# vi /etc/security/limits.conf
* soft nofile 204800
* hard nofile 204800
* soft nproc 204800
* hard nproc 204800
[root@CentOS ~]# reboot
  • 配置主机名
  • 设置IP映射
  • 防火墙服务
  • 安装JDK1.8+
  • SSH配置免密
Hadoop环境
  • 解压hadoop压缩包
  • 配置core-site.xml
<!--nn访问入口-->
<property>
    <name>fs.defaultFS</name>
    <value>hdfs://CentOS:9000</value>
</property>
<!--hdfs工作基础目录-->
<property>
    <name>hadoop.tmp.dir</name>
    <value>/usr/hadoop-2.9.2/hadoop-${user.name}</value>
</property>
  • 配置hdfs-site.xml
<!--block副本因子-->
<property>
    <name>dfs.replication</name>
    <value>1</value>
</property>
<!--配置Sencondary namenode所在物理主机-->
<property>
    <name>dfs.namenode.secondary.http-address</name>
    <value>CentOS:50090</value>
</property>
<!--设置datanode最大文件操作数-->
<property>
    <name>dfs.datanode.max.xcievers</name>
    <value>4096</value>
</property>
<!--设置datanode并行处理能力-->
<property>
    <name>dfs.datanode.handler.count</name>
    <value>6</value>
</property>
  • 配置slaves
CentOS
  • 配置yarn-site.xml
<!--配置MapReduce计算框架的核心实现Shuffle-洗牌-->
<property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
</property>
<!--配置资源管理器所在的目标主机-->
<property>
    <name>yarn.resourcemanager.hostname</name>
    <value>CentOS</value>
</property>
<!--关闭物理内存检查-->
<property>
    <name>yarn.nodemanager.pmem-check-enabled</name>
    <value>false</value>
</property>
<!--关闭虚拟内存检查-->
<property>
    <name>yarn.nodemanager.vmem-check-enabled</name>
    <value>false</value>
</property>
  • 配置mapred-site.xml
<!--MapRedcue框架资源管理器的实现-->
<property>
    <name>mapreduce.framework.name</name>
    <value>yarn</value>
</property>
  • 配置hadoop环境变量
[root@CentOS ~]# vi .bashrc
HADOOP_HOME=/usr/hadoop-2.9.2
JAVA_HOME=/usr/java/latest
PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
CLASSPATH=.
export JAVA_HOME
export PATH
export CLASSPATH
export HADOOP_HOME
[root@CentOS ~]# source .bashrc
  • 启动Hadoop服务
[root@CentOS ~]# hdfs namenode -format # 创建初始化所需的fsimage文件
[root@CentOS ~]# start-dfs.sh
[root@CentOS ~]# start-yarn.sh
Spark环境

下载spark-2.4.3-bin-without-hadoop.tgz解压到/usr目录,并且将Spark目录修改名字为spark-2.4.3然后修改spark-env.shspark-default.conf文件.

  • 解压Spark安装包,并且修改解压文件名(原文件名太长了),配置环境变量
[root@CentOS ~]# tar -zxf spark-2.4.3-bin-without-hadoop.tgz -C /usr/
[root@CentOS ~]# mv /usr/spark-2.4.3-bin-without-hadoop/ /usr/spark-2.4.3
#配置环境变量
[root@CentOS ~]# tar -zxf spark-2.4.3-bin-without-hadoop.tgz -C /usr/
[root@CentOS ~]# mv /usr/spark-2.4.3-bin-without-hadoop/ /usr/spark-2.4.3
[root@CentOS ~]# vi .bashrc
SPARK_HOME=/usr/spark-2.4.3
HADOOP_HOME=/usr/hadoop-2.9.2
JAVA_HOME=/usr/java/latest
PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$SPARK_HOME/bin:$SPARK_HOME/sbin
CLASSPATH=.
export JAVA_HOME
export PATH
export CLASSPATH
export HADOOP_HOME
export SPARK_HOME
[root@CentOS ~]# source .bashrc
  • 配置Spark服务
[root@CentOS spark-2.4.3]# cd /usr/spark-2.4.3/conf/
[root@CentOS conf]# mv spark-env.sh.template spark-env.sh				#修改配置文件名称

#注意:有了yarn后,就用不到slaves了,所有的进程都由yarn运算

#修改spark-env.sh配置文件
[root@CentOS conf]# vi spark-env.sh
HADOOP_CONF_DIR=/usr/hadoop-2.9.2/etc/hadoop
YARN_CONF_DIR=/usr/hadoop-2.9.2/etc/hadoop
SPARK_EXECUTOR_CORES=4
SPARK_EXECUTOR_MEMORY=2G
SPARK_DRIVER_MEMORY=1G
LD_LIBRARY_PATH=/usr/hadoop-2.9.2/lib/native
SPARK_DIST_CLASSPATH=$(hadoop classpath):$SPARK_DIST_CLASSPATH
#配置spark执行任务的历史数据存放目录,在Standalone中也可以配置,只不过在上面忘记了
SPARK_HISTORY_OPTS="-Dspark.history.fs.logDirectory=hdfs:///spark-logs"

export HADOOP_CONF_DIR
export YARN_CONF_DIR
export SPARK_EXECUTOR_CORES
export SPARK_DRIVER_MEMORY
export SPARK_EXECUTOR_MEMORY
export LD_LIBRARY_PATH
export SPARK_DIST_CLASSPATH
export SPARK_HISTORY_OPTS

[root@CentOS conf]# mv spark-defaults.conf.template spark-defaults.conf	#修改配置文件名称

#修改spark-defaults.conf配置文件
[root@CentOS conf]# vi spark-defaults.conf
spark.eventLog.enabled=true
spark.eventLog.dir=hdfs:///spark-logs
  • 在HDFS上创建spark-logs目录,用于作为Sparkhistory服务器存储数据的地方。
#注意:要和spark-env.sh中的SPARK_HISTORY_OPTS路径一致
[root@CentOS ~]# hdfs dfs -mkdir /spark-logs

注意:因为使用了yarn,所以不用再通过./sbin/start-all.sh启动spark进程,资源分配都由yarn去管理

  • 启动Spark历史服务器(启不启无所谓)
[root@CentOS spark-2.4.3]# ./sbin/start-history-server.sh
starting org.apache.spark.deploy.history.HistoryServer, logging to /usr/spark-2.4.3/logs/spark-root-org.apache.spark.deploy.history.HistoryServer-1-CentOS.out

该进程启动一个内嵌的web ui端口是18080,用户可以访问改页面查看任务执行计划、历史。

  • 测试Spark
./bin/spark-shell  --master yarn  --deploy-mode client  --num-executors 2  --executor-cores 3

--num-executors:在Yarn模式下,表示向NodeManager申请的资源数进程,--executor-cores表示每个进程所能运行线程数。

整个任务计算资源= num-executors * executor-core

scala> sc.textFile("hdfs:///words/t_words",5)
    .flatMap(_.split(" "))
    .map((_,1))
    .reduceByKey(_+_)
    .sortBy(_._1,true,3)
    .saveAsTextFile("hdfs:///results")

在这里插入图片描述

本地仿真

在该种模式下,无需安装yarn、无需启动Stanalone,一切都是模拟。

# 5:计算资源
[root@CentOS spark-2.4.3]# ./bin/spark-shell --master local[5]
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Spark context Web UI available at http://CentOS:4040
Spark context available as 'sc' (master = local[5], app id = local-1561742649329).
Spark session available as 'spark'.
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 2.4.3
      /_/

Using Scala version 2.11.12 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_191)
Type in expressions to have them evaluated.
Type :help for more information.

scala>

Spark-core 开发环境构建

  • pom依赖
<dependencies>
    <!-- https://mvnrepository.com/artifact/org.apache.spark/spark-core -->
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-core_2.11</artifactId>
        <version>2.4.3</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <!--scala编译插件-->
        <plugin>
            <groupId>net.alchim31.maven</groupId>
            <artifactId>scala-maven-plugin</artifactId>
            <version>4.0.1</version>
            <executions>
                <execution>
                    <id>scala-compile-first</id>
                    <phase>process-resources</phase>
                    <goals>
                        <goal>add-source</goal>
                        <goal>compile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

SparkRDDWordCount(本地)

//1.创建SparkContext,setMaster("local[10]") local:本地运行,10:计算资源
val conf = new SparkConf().setMaster("local[10]").setAppName("wordcount")
val sc = new SparkContext(conf)
//本地文件路径
val lineRDD: RDD[String] = sc.textFile("file:///E:/demo/words/t_word.txt")
lineRDD.flatMap(line=>line.split(" "))
    .map(word=>(word,1))
    .groupByKey()
    .map(tuple=>(tuple._1,tuple._2.sum))
    .sortBy(tuple=>tuple._2,false,1)
    .collect()
    .foreach(tuple=>println(tuple._1+"->"+tuple._2))

//3.关闭sc
sc.stop()

集群(yarn)

  1. 代码
//1.创建SparkContext
val conf = new SparkConf().setMaster("yarn").setAppName("wordcount")
val sc = new SparkContext(conf)
//hdfs上的文件路径
val lineRDD: RDD[String] = sc.textFile("hdfs:///words/t_words")
lineRDD.flatMap(line=>line.split(" "))
.map(word=>(word,1))
.groupByKey()
.map(tuple=>(tuple._1,tuple._2.sum))
.sortBy(tuple=>tuple._2,false,1)
.collect()
.foreach(tuple=>println(tuple._1+"->"+tuple._2))

//3.关闭sc
sc.stop()
  1. 打包上传到linux
  2. 发布
[root@CentOS spark-2.4.3]# ./bin/spark-submit --master yarn --deploy-mode client --class com.demo02.SparkRDDWordCount --num-executors 3 --executor-cores 4 /root/sparkrdd-1.0-SNAPSHOT.jar

集群(standalone)

  1. 代码
//1.创建SparkContext
val conf = new SparkConf().setMaster("spark://CentOS:7077").setAppName("wordcount")
val sc = new SparkContext(conf)

val lineRDD: RDD[String] = sc.textFile("hdfs:///words/t_words")
lineRDD.flatMap(line=>line.split(" "))
.map(word=>(word,1))
.groupByKey()
.map(tuple=>(tuple._1,tuple._2.sum))
.sortBy(tuple=>tuple._2,false,1)
.collect()
.foreach(tuple=>println(tuple._1+"->"+tuple._2))

//3.关闭sc
sc.stop()
  1. 打包上传到linux
  2. 发布
[root@CentOS spark-2.4.3]# ./bin/spark-submit --master spark://CentOS:7077 --deploy-mode client --class com.demo02.SparkRDDWordCount --num-executors 3 --total-executor-cores 4 /root/sparkrdd-1.0-SNAPSHOT.jar

Spark-submit提交方式参数说明

--master     master 的地址,提交任务到哪里执行,例如 spark://host:port,  yarn,  local
--deploy-mode    在本地 (client) 启动 driver 或在 cluster 上启动,默认是 client,最本质的区别:Driver程序运行在哪里
--class  应用程序的主类,仅针对 java 或 scala 应用
--name   应用程序的名称
--jars   用逗号分隔的本地 jar 包,设置后,这些 jar 将包含在 driver 和 executor 的 classpath 下
--packages   包含在driver 和executor 的 classpath 中的 jar 的 maven 坐标
--exclude-packages   为了避免冲突 而指定不包含的 package
--repositories   远程 repository
--conf PROP=VALUE   指定 spark 配置属性的值,例如 -conf spark.executor.extraJavaOptions="-XX:MaxPermSize=256m"
--properties-file    加载的配置文件,默认为 conf/spark-defaults.conf
--driver-memory  Driver内存,默认 1G
--driver-java-options    传给 driver 的额外的 Java 选项
--driver-library-path    传给 driver 的额外的库路径
--driver-class-path  传给 driver 的额外的类路径
--driver-cores   Driver 的核数,默认是1。在 yarn 或者 standalone 下使用
--executor-memory    每个 executor 的内存,默认是1G
--total-executor-cores   所有 executor 总共的核数。仅仅在 mesos 或者 standalone 下使用
--num-executors  启动的 executor 数量。默认为2。在 yarn 下使用
--executor-core  每个 executor 的核数。在yarn或者standalone下使用

deploy-mode的client和cluster 说明
以Spark Application运行到Standalone集群为例:

client:
学习测试时使用,开发不用,了解即可
Driver运行在Client上的SparkSubmit进程中
应用程序运行结果会在客户端显示
在这里插入图片描述
cluster:
生产环境中使用该模式
Driver程序在YARN集群中
应用的运行结果不能在客户端显示
在这里插入图片描述

RDD详解 (理论-面试)

Spark计算中一个重要的概念就是可以跨越多个节点的可伸缩分布式数据集 RDD(resilient distributed dataset),Spark的内存计算的核心就是RDD的并行计算。
RDD可以理解是一个弹性的,分布式、不可变的、带有分区的数据集合,所谓的Spark的批处理,实际上就是正对RDD的集合操作,RDD有以下特点:

  • 任意一个RDD都包含分区数(决定程序某个阶段计算并行度)
  • RDD所谓的分布式计算是在分区内部计算的
  • 因为RDD是只读的,RDD之间的变换存着依赖关系(宽依赖、窄依赖)
  • 针对于k-v类型的RDD,一般可以指定分区策略(一般系统提供)
  • 针对于存储在HDFS上的文件,系统可以计算最优位置,计算每个切片。(了解)

如下案例:
在这里插入图片描述
通过上诉的代码中不难发现,Spark的整个任务的计算无外乎围绕RDD的三种类型操作RDD创建RDD转换RDD Action.通常习惯性的将flatMap/map/reduceByKey称为RDD的转换算子collect触发任务执行,因此被人们称为动作算子。在Spark中所有的Transform算子都是lazy执行的,只有在Action算子的时候,Spark才会真正的运行任务,也就是说只有遇到Action算子的时候,SparkContext才会对任务做DAG状态拆分,系统才会计算每个状态下任务的TaskSet,继而TaskSchedule才会将任务提交给Executors执行。现将以上字符统计计算流程描述如下:
在这里插入图片描述
textFile(“路径”,分区数) -> flatMap -> map -> reduceByKey -> sortBy在这些转换中其中flatMap/map、reduceByKey、sortBy都是转换算子,所有的转换算子都是Lazy执行的。程序在遇到collect(Action 算子)系统会触发job执行。Spark底层会按照RDD的依赖关系将整个计算拆分成若干个阶段,我们通常将RDD的依赖关系称为RDD的血统-lineage血统的依赖通常包含:宽依赖、窄依赖

分区数据的分配

数据源是集合
sc.makeRDD(List(1,2,3,4,5),3).saveAsTextFile("output")
//结果:分区一存的1;分区二存的2,3;分区三存的4,5
  • ParallelCollectionRDD
//getPartitions方法
val slices = ParallelCollectionRDD.slice(data, numSlices).toArray

//slice方法
def slice[T: ClassTag](seq: Seq[T], numSlices: Int): Seq[Seq[T]] = {
    if (numSlices < 1) {
      throw new IllegalArgumentException("Positive number of partitions required")
    }
    // Sequences need to be sliced at the same set of index positions for operations
    // like RDD.zip() to behave as expected
    //对数据进行分区切割
    def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
      (0 until numSlices).iterator.map { i =>
        val start = ((i * length) / numSlices).toInt
        val end = (((i + 1) * length) / numSlices).toInt
        (start, end)
      }
    }
    //模式匹配
    seq match {
      case r: Range =>
        positions(r.length, numSlices).zipWithIndex.map { case ((start, end), index) =>
          // If the range is inclusive, use inclusive range for the last slice
          if (r.isInclusive && index == numSlices - 1) {
            new Range.Inclusive(r.start + start * r.step, r.end, r.step)
          }
          else {
            new Range(r.start + start * r.step, r.start + end * r.step, r.step)
          }
        }.toSeq.asInstanceOf[Seq[Seq[T]]]
      case nr: NumericRange[_] =>
        // For ranges of Long, Double, BigInteger, etc
        val slices = new ArrayBuffer[Seq[T]](numSlices)
        var r = nr
        for ((start, end) <- positions(nr.length, numSlices)) {
          val sliceSize = end - start
          slices += r.take(sliceSize).asInstanceOf[Seq[T]]
          r = r.drop(sliceSize)
        }
        slices
      case _ =>
      	//集合走此case
        val array = seq.toArray // To prevent O(n^2) operations for List etc
        positions(array.length, numSlices).map { case (start, end) =>
            array.slice(start, end).toSeq	//左闭右开,不包含end
        }.toSeq
    }
  }
数据源是文件

数据文件

1
2
3
//虽然这里分区写的是2,但不一定就是2个分区,原因如下:
sc.textFile("path",2).saveAsTextFile("output")
//点击textFile可知道调用hadoopFile读取文件,所以spark读取文件底层使用的就是hadoop的读取方式
//1.文件的分区数量计算:
totalSize:文件总大小(单位:字节)
totalSize += file.getLen();
goalSize:每个分区存储的字节大小
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
假如totalSize /numSplits 有余数,那么此时分区数就会+1,
比如这里就是7个字节(有换行符)/2个分区=每个分区3个字节(第二步数据分配会用到)
7/2=31,那么分区就是2+1=3个分区

//2.数据的分配
spark读取文件采用的是hadoop的方式,所以数据是一行一行读取的
数据读取时以偏移量为单位,偏移量不会被重复读取
把上面的数据拿过来,@表示换行符占的字节数
/*
	1@@		偏移量=> 0,1,2
	2@@		偏移量=> 3,4,5
	3		偏移量=> 6
*/
/*
分区偏移量预估范围:
分区一   [0,3]
分区二   [3,6]
*/
分区一读取3个字节  [0,3]  此时偏移量3,4,5都会被读,因为数据以行为单位读取,所以分区一的数据是12
分区二读取3个字节,但偏移量45都已经被读过了,所以只有偏移量6,分区数据只有3
//再比如数据如下,@代表换行符占的字节
数据:14字节			偏移量:
1234567@@			012345678
89@@				9,10,11,12
0					13

14字节/2个分区 =每个分区7字节
分区一	[0,7]
分区二	[7,14]
分区一读到7时也会把偏移量8读取,所以分区数据是1234567   (spark读取文件采用的是hadoop的方式,所以数据是一行一行读取的)
分区二从偏移量9开始读取,所以分区二数据890
  • SparkContext
def textFile(
      path: String,
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
    assertNotStopped()
    hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
      minPartitions).map(pair => pair._2.toString).setName(path)
  }
  • FileInputFormat
public InputSplit[] getSplits(JobConf job, int numSplits)
    throws IOException {
    Stopwatch sw = new Stopwatch().start();
    FileStatus[] files = listStatus(job);
    
    // Save the number of input files for metrics/loadgen
    job.setLong(NUM_INPUT_FILES, files.length);
    long totalSize = 0;                           // compute total size
    for (FileStatus file: files) {                // check we have valid files
      if (file.isDirectory()) {
        throw new IOException("Not a file: "+ file.getPath());
      }
      totalSize += file.getLen();
    }

    long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
    long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
      FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);

    // generate splits
    ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
    NetworkTopology clusterMap = new NetworkTopology();
    for (FileStatus file: files) {
      Path path = file.getPath();
      long length = file.getLen();
      if (length != 0) {
        FileSystem fs = path.getFileSystem(job);
        BlockLocation[] blkLocations;
        if (file instanceof LocatedFileStatus) {
          blkLocations = ((LocatedFileStatus) file).getBlockLocations();
        } else {
          blkLocations = fs.getFileBlockLocations(file, 0, length);
        }
        if (isSplitable(fs, path)) {
          long blockSize = file.getBlockSize();
          long splitSize = computeSplitSize(goalSize, minSize, blockSize);

          long bytesRemaining = length;
          while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
            String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,
                length-bytesRemaining, splitSize, clusterMap);
            splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                splitHosts[0], splitHosts[1]));
            bytesRemaining -= splitSize;
          }

          if (bytesRemaining != 0) {
            String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length
                - bytesRemaining, bytesRemaining, clusterMap);
            splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
                splitHosts[0], splitHosts[1]));
          }
        } else {
          String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,0,length,clusterMap);
          splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1]));
        }
      } else { 
        //Create empty hosts array for zero length files
        splits.add(makeSplit(path, 0, length, new String[0]));
      }
    }
    sw.stop();
    if (LOG.isDebugEnabled()) {
      LOG.debug("Total # of splits generated by getSplits: " + splits.size()
          + ", TimeTaken: " + sw.elapsedMillis());
    }
    return splits.toArray(new FileSplit[splits.size()]);
  }

RDD血统

在理解DAGSchedule如何做状态划分的前提是需要大家了解一个专业术语lineage通常被人们称为RDD的血统。在了解什么是RDD的血统之前,先来看看程序猿进化过程。

在这里插入图片描述

上图中描述了一个程序猿起源变化的过程,我们可以近似的理解类似于RDD的转换也是一样的,Spark的计算本质就是对RDD做各种转换,因为RDD是一个不可变只读的集合,因此每次的转换都需要上一次的RDD作为本次转换的输入,因此RDD的lineage描述的是RDD间的相互依赖关系。为了保证RDD中数据的健壮性,RDD数据集通过所谓的血统关系(Lineage)记住了它是如何从其它RDD中演变过来的。Spark将RDD之间的关系归类为宽依赖窄依赖。Spark会根据Lineage存储的RDD的依赖关系对RDD计算做故障容错,目前Saprk的容错策略更具RDD依赖关系重新计算对RDD做Cache对RDD做Checkpoint手段完成RDD计算的故障容错。

简单来说,相邻两个RDD之间的关系称之为依赖关系,多个连续的RDD的依赖关系,称之为血缘关系
每个RDD都会保存血缘关系

在这里插入图片描述

宽依赖|窄依赖

RDD在Lineage依赖方面分为两种Narrow Dependencies(窄依赖)Wide Dependencies(宽依赖)用来解决数据容错的高效性。Narrow Dependencies是指父RDD的每一个分区最多被一个子RDD的分区所用,表现为一个父RDD的分区对应于一个子RDD的分区或多个父RDD的分区对应于子RDD的一个分区,也就是说一个父RDD的一个分区不可能对应一个子RDD的多个分区。Wide Dependencies父RDD的一个分区对应一个子RDD的多个分区。
在这里插入图片描述

对于Wide Dependencies这种计算的输入和输出在不同的节点上,一般需要跨节点做Shuffle,因此如果是RDD在做宽依赖恢复的时候需要多个节点重新计算成本较高。相对于Narrow Dependencies RDD间的计算是在同一个Task当中实现的是线程内部的的计算,因此在RDD分区数据丢失的的时候,也非常容易恢复。

宽依赖:有分区的操作,有shuffle,会落地磁盘。
窄依赖:无分区的操作,无shuffle,基于内存计算。
总结:在内存足够的前提下,减少使用有shuffle的算子,会提高代码运行效率。

Stage划分(重点)

stage的数量 =shuffle依赖的数量+1

Spark任务阶段的划分是按照RDD的lineage关系逆向生成的这么一个过程,Spark任务提交的流程大致如下图所示:
在这里插入图片描述
这里可以分析一下DAGScheduel中对State拆分的逻辑代码片段如下所示:

  • DAGScheduler.scala 第719
def runJob[T, U](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      callSite: CallSite,
      resultHandler: (Int, U) => Unit,
      properties: Properties): Unit = {
    val start = System.nanoTime
    val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
    //...
  }
  • DAGScheduler - 675行
def submitJob[T, U](
    rdd: RDD[T],
    func: (TaskContext, Iterator[T]) => U,
    partitions: Seq[Int],
    callSite: CallSite,
    resultHandler: (Int, U) => Unit,
    properties: Properties): JobWaiter[U] = {
  //eventProcessLoop 实现的是一个队列,系统底层会调用 doOnReceive -> case JobSubmitted -> dagScheduler.handleJobSubmitted(951行)
  eventProcessLoop.post(JobSubmitted(
    jobId, rdd, func2, partitions.toArray, callSite, waiter,
    SerializationUtils.clone(properties)))
  waiter
}
  • DAGScheduler - 951行
private[scheduler] def handleJobSubmitted(jobId: Int,
     finalRDD: RDD[_],
     func: (TaskContext, Iterator[_]) => _,
     partitions: Array[Int],
     callSite: CallSite,
     listener: JobListener,
     properties: Properties) {
   var finalStage: ResultStage = null
   try {
     //...
     finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
   } catch {
     //...
   }
   submitStage(finalStage)
}
  • DAGScheduler - 1060行
private def submitStage(stage: Stage) {
   val jobId = activeJobForStage(stage)
   if (jobId.isDefined) {
     logDebug("submitStage(" + stage + ")")
     if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
        //计算当前State的父Stage
       val missing = getMissingParentStages(stage).sortBy(_.id)
       logDebug("missing: " + missing)
       if (missing.isEmpty) {
         logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
          //如果当前的State没有父Stage,就提交当前Stage中的Task
         submitMissingTasks(stage, jobId.get)
       } else {
         for (parent <- missing) {
           //递归查找当前父Stage的父Stage
           submitStage(parent)
         }
         waitingStages += stage
       }
     }
   } else {
     abortStage(stage, "No active job for stage " + stage.id, None)
   }
 }
  • DAGScheduler - 549行 (获取当前State的父State)
private def getMissingParentStages(stage: Stage): List[Stage] = {
   val missing = new HashSet[Stage]
   val visited = new HashSet[RDD[_]]
   // We are manually maintaining a stack here to prevent StackOverflowError
   // caused by recursively visiting
   val waitingForVisit = new ArrayStack[RDD[_]]//栈
   def visit(rdd: RDD[_]) {
     if (!visited(rdd)) {
       visited += rdd
       val rddHasUncachedPartitions = getCacheLocs(rdd).contains(Nil)
       if (rddHasUncachedPartitions) {
         for (dep <- rdd.dependencies) {
           dep match {
               //如果是宽依赖ShuffleDependency,就添加一个Stage
             case shufDep: ShuffleDependency[_, _, _] =>
               val mapStage = getOrCreateShuffleMapStage(shufDep, stage.firstJobId)
               if (!mapStage.isAvailable) {
                 missing += mapStage
               }
               //如果是窄依赖NarrowDependency,将当前的父RDD添加到栈中
             case narrowDep: NarrowDependency[_] =>
               waitingForVisit.push(narrowDep.rdd)
           }
         }
       }
     }
   }
   waitingForVisit.push(stage.rdd)
   while (waitingForVisit.nonEmpty) {//循环遍历栈,计算 stage
     visit(waitingForVisit.pop())
   }
   missing.toList
 }
  • DAGScheduler - 1083行 (提交当前Stage的TaskSet)
private def submitMissingTasks(stage: Stage, jobId: Int) {
    logDebug("submitMissingTasks(" + stage + ")")

    // First figure out the indexes of partition ids to compute.
    val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()

    // Use the scheduling pool, job group, description, etc. from an ActiveJob associated
    // with this Stage
    val properties = jobIdToActiveJob(jobId).properties

    runningStages += stage
    // SparkListenerStageSubmitted should be posted before testing whether tasks are
    // serializable. If tasks are not serializable, a SparkListenerStageCompleted event
    // will be posted, which should always come after a corresponding SparkListenerStageSubmitted
    // event.
    stage match {
      case s: ShuffleMapStage =>
        outputCommitCoordinator.stageStart(stage = s.id, maxPartitionId = s.numPartitions - 1)
      case s: ResultStage =>
        outputCommitCoordinator.stageStart(
          stage = s.id, maxPartitionId = s.rdd.partitions.length - 1)
    }
    val taskIdToLocations: Map[Int, Seq[TaskLocation]] = try {
      stage match {
        case s: ShuffleMapStage =>
          partitionsToCompute.map { id => (id, getPreferredLocs(stage.rdd, id))}.toMap
        case s: ResultStage =>
          partitionsToCompute.map { id =>
            val p = s.partitions(id)
            (id, getPreferredLocs(stage.rdd, p))
          }.toMap
      }
    } catch {
      case NonFatal(e) =>
        stage.makeNewStageAttempt(partitionsToCompute.size)
        listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))
        abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
        runningStages -= stage
        return
    }

    stage.makeNewStageAttempt(partitionsToCompute.size, taskIdToLocations.values.toSeq)

    // If there are tasks to execute, record the submission time of the stage. Otherwise,
    // post the even without the submission time, which indicates that this stage was
    // skipped.
    if (partitionsToCompute.nonEmpty) {
      stage.latestInfo.submissionTime = Some(clock.getTimeMillis())
    }
    listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))

    // TODO: Maybe we can keep the taskBinary in Stage to avoid serializing it multiple times.
    // Broadcasted binary for the task, used to dispatch tasks to executors. Note that we broadcast
    // the serialized copy of the RDD and for each task we will deserialize it, which means each
    // task gets a different copy of the RDD. This provides stronger isolation between tasks that
    // might modify state of objects referenced in their closures. This is necessary in Hadoop
    // where the JobConf/Configuration object is not thread-safe.
    var taskBinary: Broadcast[Array[Byte]] = null
    var partitions: Array[Partition] = null
    try {
      // For ShuffleMapTask, serialize and broadcast (rdd, shuffleDep).
      // For ResultTask, serialize and broadcast (rdd, func).
      var taskBinaryBytes: Array[Byte] = null
      // taskBinaryBytes and partitions are both effected by the checkpoint status. We need
      // this synchronization in case another concurrent job is checkpointing this RDD, so we get a
      // consistent view of both variables.
      RDDCheckpointData.synchronized {
        taskBinaryBytes = stage match {
          case stage: ShuffleMapStage =>
            JavaUtils.bufferToArray(
              closureSerializer.serialize((stage.rdd, stage.shuffleDep): AnyRef))
          case stage: ResultStage =>
            JavaUtils.bufferToArray(closureSerializer.serialize((stage.rdd, stage.func): AnyRef))
        }

        partitions = stage.rdd.partitions
      }

      taskBinary = sc.broadcast(taskBinaryBytes)
    } catch {
      // In the case of a failure during serialization, abort the stage.
      case e: NotSerializableException =>
        abortStage(stage, "Task not serializable: " + e.toString, Some(e))
        runningStages -= stage

        // Abort execution
        return
      case e: Throwable =>
        abortStage(stage, s"Task serialization failed: $e\n${Utils.exceptionString(e)}", Some(e))
        runningStages -= stage

        // Abort execution
        return
    }

    val tasks: Seq[Task[_]] = try {
      val serializedTaskMetrics = closureSerializer.serialize(stage.latestInfo.taskMetrics).array()
      stage match {
        case stage: ShuffleMapStage =>
          stage.pendingPartitions.clear()
          partitionsToCompute.map { id =>
            val locs = taskIdToLocations(id)
            val part = partitions(id)
            stage.pendingPartitions += id
            new ShuffleMapTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, properties, serializedTaskMetrics, Option(jobId),
              Option(sc.applicationId), sc.applicationAttemptId, stage.rdd.isBarrier())
          }

        case stage: ResultStage =>
          partitionsToCompute.map { id =>
            val p: Int = stage.partitions(id)
            val part = partitions(p)
            val locs = taskIdToLocations(id)
            new ResultTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, id, properties, serializedTaskMetrics,
              Option(jobId), Option(sc.applicationId), sc.applicationAttemptId,
              stage.rdd.isBarrier())
          }
      }
    } catch {
      case NonFatal(e) =>
        abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
        runningStages -= stage
        return
    }

    if (tasks.size > 0) {
      logInfo(s"Submitting ${tasks.size} missing tasks from $stage (${stage.rdd}) (first 15 " +
        s"tasks are for partitions ${tasks.take(15).map(_.partitionId)})")
      taskScheduler.submitTasks(new TaskSet(
        tasks.toArray, stage.id, stage.latestInfo.attemptNumber, jobId, properties))
    } else {
      // Because we posted SparkListenerStageSubmitted earlier, we should mark
      // the stage as completed here in case there are no tasks to run
      markStageAsFinished(stage, None)

      stage match {
        case stage: ShuffleMapStage =>
          logDebug(s"Stage ${stage} is actually done; " +
              s"(available: ${stage.isAvailable}," +
              s"available outputs: ${stage.numAvailableOutputs}," +
              s"partitions: ${stage.numPartitions})")
          markMapStageJobsAsFinished(stage)
        case stage : ResultStage =>
          logDebug(s"Stage ${stage} is actually done; (partitions: ${stage.numPartitions})")
      }
      submitWaitingChildStages(stage)
    }
  }
总结:

通过以上源码分析,可以得出Spark所谓宽窄依赖事实上指的是ShuffleDependency或者是NarrowDependency。如果是ShuffleDependency系统会生成一个ShuffeMapStage,如果是NarrowDependency则忽略,归为当前Stage。当系统回推到起始RDD的时候因为发现当前RDD或者ShuffleMapStage没有父Stage的时候,当前系统会将当前State下的Task封装成ShuffleMapTask(如果是ResultStage就是ResultTask),当前Task的数目等于当前state分区的分区数。然后将Task封装成TaskSet通过调用taskScheduler.submitTasks将任务提交给集群。

shuffle和宽窄依赖的关系

1.发生宽依赖就一定会伴随着shuffle。
2.发生shuffle不一定产生宽依赖

RDD任务划分

RDD任务切分分为:Application,Job、Stage、Task

  • Application:初始化一个SparkContext即生成一个Application
  • Job:一个Action算子就会生成一个Job
  • Stage:Stage等于宽依赖(ShuffleDependency)的个数+1
  • Task:一个Stage阶段中,最后一个RDD的分区个数就是Task的个数

RDD持久化

RDD缓存(内存)

缓存是一种RDD计算容错的一种手段,程序在RDD数据丢失的时候,可以通过缓存快速计算当前RDD的值,而不需要反推出所有的RDD重新计算,因此Spark在需要对某个RDD多次使用的时候,为了提高程序的执行效率用户可以考虑使用RDD的cache缓存到内存中。如下测试:

val conf = new SparkConf()
	.setAppName("word-count")
	.setMaster("local[2]")
val sc = new SparkContext(conf)
val value: RDD[String] = sc.textFile("file:///D:/demo/words/")
   .cache()
//value.count()

var begin=System.currentTimeMillis()
value.count()
var end=System.currentTimeMillis()
println("耗时:"+ (end-begin))//耗时:253

//失效缓存
value.unpersist()
begin=System.currentTimeMillis()
value.count()
end=System.currentTimeMillis()
println("不使用缓存耗时:"+ (end-begin))//2029
sc.stop()

除了调用cache之外,Spark提供了更细粒度的RDD缓存方案,用户可以根据集群的内存状态选择合适的缓存策略。用户可以使用persist方法指定缓存级别。缓存级别有如下可选项:

val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)

其中:

MEMORY_ONLY:表示数据完全不经过序列化存储在内存中,效率高,但是有可能导致内存溢出.

MEMORY_ONLY_SER和MEMORY_ONLY一样,只不过需要对RDD的数据做序列化,牺牲CPU节省内存,同样会导致内存溢出可能。

其中_2表示缓存结果有备份,如果大家不确定该使用哪种级别,一般推荐MEMORY_AND_DISK_SER_2(将RDD 作为反序列化的的对象存储在JVM 中。如果RDD不能被内存装下,超出的分区将被保存在硬盘上,并且在需要时被读取)

Check Point 机制(保存到文件中)

除了使用缓存机制可以有效的保证RDD的故障恢复,但是如果缓存失效还是会在导致系统重新计算RDD的结果,所以对于一些RDD的lineage较长的场景,计算比较耗时,用户可以尝试使用checkpoint机制存储RDD的计算结果,该种机制和缓存最大的不同在于,使用checkpoint之后被checkpoint的RDD数据直接持久化在文件系统中,一般推荐将结果写在hdfs中,这种checpoint并不会自动清空。

注意checkpoint在计算的过程中先是对RDD做mark,在任务执行结束后,再对mark的RDD实行checkpoint,也就是要重新计算被Mark之后的rdd的依赖和结果,因此为了避免Mark RDD重复计算,推荐使用策略

val conf = new SparkConf().setMaster("yarn").setAppName("wordcount")
val sc = new SparkContext(conf)
sc.setCheckpointDir("hdfs:///checkpoints")	//checkpoints的目录,存放到hdfs上

val lineRDD: RDD[String] = sc.textFile("hdfs:///words/t_word.txt")

val cacheRdd = lineRDD.flatMap(line => line.split(" "))
.map(word => (word, 1))
.groupByKey()
.map(tuple => (tuple._1, tuple._2.sum))
.sortBy(tuple => tuple._2, false, 1)
.cache()
cacheRdd.checkpoint()	//对缓存的cacheRdd做checkpoint

cacheRdd.collect().foreach(tuple=>println(tuple._1+"->"+tuple._2))
cacheRdd.unpersist()
//3.关闭sc
sc.stop()

cache、persist、check point的区别

  • cache:将数据临时存储在内存中进行数据重用,会在血缘关系中添加新的依赖(rdd.toDebugString方法可查看血缘关系),一旦出现问题,可以重头读取数据
  • persist:将数据临时存储到磁盘文件中进行数据重用,涉及到磁盘IO,性能较低,但是数据安全,如果作业执行完毕,临时保存的数据文件就会丢失。
  • check point:将数据长久的保存到磁盘文件中进行数据重用,涉及到磁盘IO,性能较低,但是数据安全,为了提高效率,一般会和cache联合使用;执行过程中,会切断血缘关系,重新建立新的血缘关系

RDD算子实战

转换算子

map(func)

传入的集合元素进行RDD[T]转换 `def map(f: T => U): org.apache.spark.rdd.RDD[U]

//parallelize将scala集合转换为并行集合,可以指定分区数
scala>  sc.parallelize(List(1,2,3,4,5),3).map(item => item*2+" " )
res1: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[2] at map at <console>:25

scala>  sc.parallelize(List(1,2,3,4,5),3).map(item => item*2+" " ).collect
res2: Array[String] = Array("2 ", "4 ", "6 ", "8 ", "10 ")
filter(func)

将满足条件结果记录 def filter(f: T=> Boolean): org.apache.spark.rdd.RDD[T]
为true的保留,过滤掉不满足条件的数据

scala>  sc.parallelize(List(1,2,3,4,5),3).filter(item=> item%2==0).collect
res3: Array[Int] = Array(2, 4)
flatMap(func)

将一个元素转换成元素的数组,然后对数组展开。def flatMap[U](f: T=> TraversableOnce[U]): org.apache.spark.rdd.RDD[U]

scala>  sc.parallelize(List("ni hao","hello spark"),3).flatMap(line=>line.split("\\s+")).collect
res4: Array[String] = Array(ni, hao, hello, spark)
mapPartitions(func)

与map类似,但在RDD的每个分区(块)上单独运行,因此当在类型T的RDD上运行时,func必须是Iterator <T> => Iterator <U>类型
def mapPartitions[U](f: Iterator[Int] => Iterator[U],preservesPartitioning: Boolean): org.apache.spark.rdd.RDD[U]
以分区为单位进行数据转换操作,但是会将整个分区的数据加载到内存进行引用,如果处理完的数据是不会被释放掉,存在对象的引用。在内存较小,数据量较大的场合下,容易出现内存溢出。

scala>  sc.parallelize(List(1,2,3,4,5),3).mapPartitions(items=> for(i<-items;if(i%2==0)) yield i*2 ).collect()
res7: Array[Int] = Array(4, 8)

map和mapPartitions的区别:
数据处理角度:
      Map算子是分区内一个数据一个数据的执行,类似于串行操作。而mapPartitions算子是以分区为单位进行批处理操作(一次性处理一个分区内的数据)
功能的角度
      Map算子主要目的是将数据源中的数据进行转换和改变,不会减少或增多数据;而mapPartitions算子需要传递一个迭代器,返回一个迭代器,没有要求元素的个数保持不变,所以可以增加或减少数据
性能角度
      Map算子因为类似于串行操作,所以性能较低,而mapPartitions算子类似于批处理操作,所以性能较高。但mapPartitions算子会长时间占用内存,有可能导致内存不够用,导致内存溢出。所以内存有限的情况下,不推荐使用mapPartitions

mapPartitionsWithIndex(func)

与mapPartitions类似,但也为func提供了表示分区索引的整数值,因此当在类型T的RDD上运行时,func必须是类型(Int,Iterator <T>)=> Iterator <U>
def mapPartitionsWithIndex[U](f: (Int, Iterator[T]) => Iterator[U],preservesPartitioning: Boolean): org.apache.spark.rdd.RDD[U]

scala>  sc.parallelize(List(1,2,3,4,5),3).mapPartitionsWithIndex((p,items)=> for(i<-items) yield (p,i)).collect
//p:分区号;items:分区数据
res11: Array[(Int, Int)] = Array((0,1), (1,2), (1,3), (2,4), (2,5))
sample(withReplacement, fraction, seed)

对数据进行一定比例的采样,使用withReplacement参数控制是否允许重复采样。
def sample(withReplacement: Boolean,fraction: Double,seed: Long): org.apache.spark.rdd.RDD[T]

scala>  sc.parallelize(List(1,2,3,4,5,6,7),3).sample(false,0.7,1L).collect
res13: Array[Int] = Array(1, 4, 6, 7)
union(otherDataset)

返回一个新数据集,其中包含源数据集和参数中元素的并集

scala> var rdd1=sc.parallelize(Array(("张三",1000),("李四",100),("赵六",300)))
scala> var rdd2=sc.parallelize(Array(("张三",1000),("王五",100),("温七",300)))
scala> rdd1.union(rdd2).collect
res16: Array[(String, Int)] = Array((张三,1000), (李四,100), (赵六,300), (张三,1000), (王五,100), (温七,300))
intersection(otherDataset)

返回包含源数据集和参数中元素交集的新RDD。

def intersection(other: org.apache.spark.rdd.RDD[T],numPartitions: Int): org.apache.spark.rdd.RDD[T]

scala> var rdd1=sc.parallelize(Array(("张三",1000),("李四",100),("赵六",300)))
scala> var rdd2=sc.parallelize(Array(("张三",1000),("王五",100),("温七",300)))
scala> rdd1.intersection(rdd2).collect
res17: Array[(String, Int)] = Array((张三,1000))
subtract

返回数据集A在数据集B的差集

val rdd1 = sc.makeRDD(List(1,2,3,4));
val rdd2 = sc.makeRDD(List(3,4,5,6));
println(rdd1.subtract(rdd2).collect().mkString(","))
//结果:1,2
zip(拉链)

要求每个分区内的数据量必须相同

val rdd1 = sc.makeRDD(List(1,2,3,4));
val rdd2 = sc.makeRDD(List(3,4,5,6));
rdd1.zip(rdd2).collect().foreach(print)
//结果:(1,3)(2,4)(3,5)(4,6)

//分区内数量不同(1个分区)
val rdd1 = sc.makeRDD(List(1,2,3,4));
val rdd2 = sc.makeRDD(List(3,4,5,6,7));

//多个分区
val rdd3 = sc.makeRDD(List(1,2,3,4),2);
val rdd4= sc.makeRDD(List(3,4,5,6),4);

rdd1.zip(rdd2).collect().foreach(print)//在scala中是可以的,只是会丢弃7而已,spark中不可以
rdd3.zip(rdd4).collect().foreach(print)//报错:Can only zip RDDs with same number of elements in each partition

distinct([numPartitions]))

返回包含源数据集的不同元素的新数据集(个人理解,就是sql中的去重)。

scala>  sc.parallelize(List(1,2,3,3,5,7,2),3).distinct.collect
res19: Array[Int] = Array(3, 1, 7, 5, 2)
groupBy

根据某个值进行分组

val rdd3 = sc.makeRDD(List(("a",1),("a",2),("a",3),("b",4))).groupBy(_.1)
//结果:(a,CompactBuffer((a,1), (a,2), (a,3))),(b,CompactBuffer((b,4)))
groupByKey([numPartitions])和groupBy区别在于分组后产生的value

在(K,V)对的数据集上调用时,返回(K,Iterable <V>)对的数据集。 注意:如果要对每个键执行聚合(例如总和或平均值)进行分组,则使用reduceByKey或aggregateByKey将产生更好的性能。 注意:默认情况下,输出中的并行级别取决于父RDD的分区数。您可以传递可选的numPartitions参数来设置不同数量的任务。
在这里插入图片描述

scala>  sc.parallelize(List("ni hao","hello spark"),3).flatMap(line=>line.split("\\s+")).map(word=>(word,1)).groupByKey(3).map(tuple=>(tuple._1,tuple._2.sum)).collect

val rdd4 = sc.makeRDD(List(("a",1),("a",2),("a",3),("b",4)));
//结果:(a,CompactBuffer(1, 2, 3)),(b,CompactBuffer(4))
reduceByKey(func, [numPartitions])

当调用(K,V)对的数据集时,返回(K,V)对的数据集,其中使用给定的reduce函数func聚合每个键的值,该函数必须是类型(V,V)=> V。
在这里插入图片描述

scala>  sc.parallelize(List("ni hao","hello spark"),3).flatMap(line=>line.split("\\s+")).map(word=>(word,1)).reduceByKey((v1,v2)=>v1+v2).collect()
res33: Array[(String, Int)] = Array((hao,1), (hello,1), (spark,1), (ni,1))

scala>  sc.parallelize(List("ni hao","hello spark"),3).flatMap(line=>line.split("\\s+")).map(word=>(word,1)).reduceByKey(_+_).collect()
res34: Array[(String, Int)] = Array((hao,1), (hello,1), (spark,1), (ni,1))

reduceByKey和groupByKey的区别
功能上的区别
      reduceByKey包含分组和聚合,groupByKey只能分组,不能聚合
性能上的区别
      都存在shuffle操作,但reduceByKey可以预聚合,可以减少落盘的数据量

aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions])

当调用(K,V)对的数据集时,返回(K,U)对的数据集,其中使用给定的组合函数和中性“零”值聚合每个键的值。允许与输入值类型不同的聚合值类型,同时避免不必要的分配。

可以将数据根据不同的规则进行分区内计算和分区间计算

/*
第一个参数:初始值,主要用于当碰见第一个ke的时候,和value进行分区内计算
第二个参数列表(2个参数):
	第1个参数:表示分区内计算规则
	第2个参数:表示分区间计算规则
*/

//分区间和分区内计算规则一样:求和
scala>  sc.parallelize(List("ni hao","hello spark"),3).flatMap(line=>line.split("\\s+")).map(word=>(word,1)).aggregateByKey(0L)((z,v)=>z+v,(u1,u2)=>u1+u2).collect
res35: Array[(String, Long)] = Array((hao,1), (hello,1), (spark,1), (ni,1))



//分区间和分区内计算规则不一样:分区内取最大值,分区间将最大值求和
//(a,1)(a,2)在一个分区,(a,3)(a,4)在一个分区,分区内的最大值分别为2和4,最后就是2+4=6
sc.makeRDD(List(("a",1),("a",2),("a",3),("a",4)),2)
  .aggregateByKey(0L)((z,v)=>math.max(z,v),(u1,u2)=>u1+u2)
//结果:(a,6)
foldByKey

aggregateByKey的简化版,如果aggregateByKey的分区内和分区间计算规则一样,可以使用此算子少传一个参数

//如果分区内和分区间计算规则一样,还可以使用foldByKey算子,可以少传入一个参数,如下:
sc.makeRDD(List(("a",1),("a",2),("b",4),("b",5)),2).foldByKey(1)(_+_).foreach(println)
//结果:(a,4) (b,10)
sortByKey([ascending], [numPartitions])

当调用K实现Ordered的(K,V)对数据集时,返回按键升序或降序排序的(K,V)对数据集,如布尔升序参数中所指定。

scala>  sc.parallelize(List("ni hao","hello spark"),3).flatMap(line=>line.split("\\s+")).map(word=>(word,1)).aggregateByKey(0L)((z,v)=>z+v,(u1,u2)=>u1+u2).sortByKey(false).collect()
res37: Array[(String, Long)] = Array((spark,1), (ni,1), (hello,1), (hao,1))
sortBy(func,[ascending], [numPartitions])

对(K,V)数据集调用sortBy时,用户可以通过指定func指定排序规则,T => U 要求U必须实现Ordered接口

scala>  sc.parallelize(List("ni hao","hello spark"),3).flatMap(line=>line.split("\\s+")).map(word=>(word,1)).aggregateByKey(0L)((z,v)=>z+v,(u1,u2)=>u1+u2).sortBy(_._2,true,2).collect
res42: Array[(String, Long)] = Array((hao,1), (hello,1), (spark,1), (ni,1))
join

当调用类型(K,V)和(K,W)的数据集时,返回(K,(V,W))对的数据集以及每个键的所有元素对。通过leftOuterJoin,rightOuterJoin和fullOuterJoin支持外连接。

scala> var rdd1=sc.parallelize(Array(("001","张三"),("002","李四"),("003","王五")))
scala> var rdd2=sc.parallelize(Array(("001",("apple",18.0)),("001",("orange",18.0))))
scala> rdd1.join(rdd2).collect
res43: Array[(String, (String, (String, Double)))] = Array((001,(张三,(apple,18.0))), (001,(张三,(orange,18.0))))
cogroup

当调用类型(K,V)和(K,W)的数据集时,返回(K,(Iterable ,Iterable ))元组的数据集。此操作也称为groupWith。

scala> var rdd1=sc.parallelize(Array(("001","张三"),("002","李四"),("003","王五")))
scala> var rdd2=sc.parallelize(Array(("001","apple"),("001","orange"),("002","book")))
scala> rdd1.cogroup(rdd2).collect()
res46: Array[(String, (Iterable[String], Iterable[String]))] = Array((001,(CompactBuffer(张三),CompactBuffer(apple, orange))), (002,(CompactBuffer(李四),CompactBuffer(book))), (003,(CompactBuffer(王五),CompactBuffer())))
cartesian

当调用类型为T和U的数据集时,返回(T,U)对的数据集(所有元素对)。

scala> var rdd1=sc.parallelize(List("a","b","c"))
scala> var rdd2=sc.parallelize(List(1,2,3,4))
scala> rdd1.cartesian(rdd2).collect()
res47: Array[(String, Int)] = Array((a,1), (a,2), (a,3), (a,4), (b,1), (b,2), (b,3), (b,4), (c,1), (c,2), (c,3), (c,4))
coalesce(numPartitions)

将RDD中的分区数减少为numPartitions。过滤大型数据集后,可以使用该算子减少分区数。

默认情况下,不会将分区中的数据打乱重新组合(A、B、C3个分区合成2个,有可能AB重新分到一个分区,C在一个分区,而不会把B中的数据打乱分给A和C),这种情况下可能导致数据不均衡,出现数据倾斜;如果想要数据均衡,可以将第二个参数设置为true,就会调用shuffle
也可以扩大分区,但扩大分区时如果不进行shuffle,实际是没意义的

scala>  sc.parallelize(List("ni hao","hello spark"),3).coalesce(1).partitions.length
res50: Int = 1

scala>  sc.parallelize(List("ni hao","hello spark"),3).coalesce(1).getNumPartitions
res51: Int = 1
repartition

随机重新调整RDD中的数据以创建更多或更少的分区,底层调用的就是 coalesce(numPartitions, shuffle = true)

scala> sc.parallelize(List("a","b","c"),3).mapPartitionsWithIndex((index,values)=>for(i<-values) yield (index,i) ).collect
res52: Array[(Int, String)] = Array((0,a), (1,b), (2,c))

scala> sc.parallelize(List("a","b","c"),3).repartition(2).mapPartitionsWithIndex((index,values)=>for(i<-values) yield (index,i) ).collect
res53: Array[(Int, String)] = Array((0,a), (0,c), (1,b))

动作算子(只列举几个,其他的参考官方文档)

reduce

聚合

println(sc.makeRDD(List(1, 2, 3, 4, 5)).reduce(_ + _))
//结果:15
aggregate
println(sc.makeRDD(List(1, 2, 3, 4, 5),2).aggregate(1)((_ + _),(_+_)))
//结果:18
/*原因:
aggregateByKey:初始值只会参与分区内计算
aggregate:初始值会参与分区内计算和分区间计算
上面结果为18的原因:1,2在一个分区;3,4,5在一个分区;
1+2+1=4,分区一加上初始值1
3+4+5+1=13,分区2加上初始值1
4+13+1=18,分区间计算加上初始值1
*/
fold

aggregate的简化版,如果aggregate的分区内和分区间计算规则一样,可以少传入一个参数

println(sc.makeRDD(List(1, 2, 3, 4, 5),2).fold(1)(_+_))
//结果:18
collect

用在测试环境下,通常使用collect算子将远程计算的结果以分区为单位拿到Drvier端,注意一般数据量比较小,用于测试。

scala> var rdd1=sc.parallelize(List(1,2,3,4,5),3).collect().foreach(println)
countByKey

统计key出现的次数

println(sc.makeRDD(List(("a", 1), ("a", 2), ("a", 4), ("b", 2))).countByKey())
//结果:Map(a -> 3, b -> 1)
//如果数据源只是value类型,可以使用countByValue
println(sc.makeRDD(List(1, 2, 3, 4, 5,1),2).countByValue())
//结果:Map(5 -> 1, 1 -> 2, 2 -> 1, 3 -> 1, 4 -> 1)
saveAsTextFile

将计算结果存储在文件系统中,一般存储在HDFS上

scala>  sc.parallelize(List("ni hao","hello spark"),3).flatMap(_.split("\\s+")).map((_,1)).reduceByKey(_+_).sortBy(_._2,false,3).saveAsTextFile("hdfs:///wordcounts")
foreach

迭代遍历所有的RDD中的元素,通常是将foreach传递的数据写到外围系统中,比如说可以将数据写入到Hbase中。

scala>  sc.parallelize(List("ni hao","hello spark"),3).flatMap(_.split("\\s+")).map((_,1)).reduceByKey(_+_).sortBy(_._2,false,3).foreach(println)
(hao,1)
(hello,1)
(spark,1)
(ni,1)

//带collect再调用foreach算子和直接调用foreach算子的区别
val conf = new SparkConf().setMaster("local[2]").setAppName("Test")
val sc = new SparkContext(conf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))

rdd.collect().foreach(print)		//其实是Driver端内存集合的循环遍历
println("**************")
rdd.foreach(print)					//其实是Executor端内存数据的遍历
/*结果:1234
	**********
	3412(也有可能是3142,1324...等情况),具体原因见下图
*/

在这里插入图片描述
在这里插入图片描述

注意:RDD的方法和Scala集合对象的方法不一样
          集合对象的方法都是在同一个节点的内存中完成的。
          RDD的方法可以将计算逻辑发送到Executor端(分布式节点)执行
RDD的方法外部的操作都是在Driver端执行的,而方法内部的逻辑代码是在Executor端执行。

注意如果使用以上代码写数据到外围系统,会因为不断创建和关闭连接影响写入效率,一般推荐使用foreachPartition

val lineRDD: RDD[String] = sc.textFile("file:///E:/demo/words/t_word.txt")
lineRDD.flatMap(line=>line.split(" "))
    .map(word=>(word,1))
    .groupByKey()
    .map(tuple=>(tuple._1,tuple._2.sum))
    .sortBy(tuple=>tuple._2,false,3)
    .foreachPartition(items=>{
        //创建连接
        items.foreach(t=>println("存储到数据库"+t))
        //关闭连接
    })

shuffle算子

分区
repartition():执行时需要进行shuffle操作
coalesce(numPartitions,shuffle=false):默认不需要shuffle
使用场景:将设RDD/DataFrame/DataSetN个分区,若重新划分M个分区
1.N<M:一般情况下N个分区有数据分布不均匀的情况,利用HashPartitioner函数将数据重新划分M个,此时将shuffule设置true.
**重分区前后相当于宽依赖,会发生shuffle过程**,此时可以使用coalesce(shuffle=true),或reparation
2.N>M:N,M相差不多(N1000M100),那么就可以将N个分区中的若干个分区合并成一个新分区,最终为M个分区,前后是**窄依赖关系**
可以使用coalesce(shuffle=false)
3.N>MN,M相差悬殊,此时将shuffule设置false,父子RDD是窄依赖关系,他们同处一个stage中,就可能造成spark并行度不够,从而影响性能
如果M1的时候,为了是coalesce之前操作有更好的并行度,可以将shuffle设置为true
注:如果传入分区数大于当前分区数,需要将shuffle设置为true,才能使RDD分区数改变。
去重
distinct():使用spark默认并行度(分区数)对筛选数据进行去重
distinct([numPartitions])):手动指定spark并行度(分区数)对筛选数据进行去重
排序
//对key,value类型RDD,根据key进行排序,指定是否升序排列,分区数来进行排序
def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length): RDD[(K, V)]
//指定是否升序排列,分区数,排序函数进行排序
def sortBy[K](f: (T) => K, ascending: Boolean = true, numPartitions: Int = this.partitions.length):RDD[(k,v)]
聚合
//指定spark并行度,使用在map预聚合,减少reduce端数据shuffle量级,进行高效处理
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)] 
//指定spark分区类(可以是默认hashPartitioner,RangePartioner,
//也可以根据业务自己继承Partitioner自己实现分区规则),
//使用在map预聚合,减少reduce端数据shuffle量级,进行高效处理
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)]
//根据业务方法和指定分区类对key,value对的rdd进行分组,进而得到新的RDD
def groupBy[K](f: T => K, p: Partitioner):RDD[(K, Iterable[V])]
//指定分区类进行RDD数据分组,得到新的RDD
def groupByKey(partitioner: Partitioner):RDD[(K, Iterable[V])]
//根据传入参数zeroValue,分区类对数据进行聚合
def aggregateByKey[U: ClassTag](zeroValue: U, partitioner: Partitioner): RDD[(K, U)]
//根据传入参数zeroValue,分区数量对数据进行聚合
def aggregateByKey[U: ClassTag](zeroValue: U, numPartitions: Int): RDD[(K, U)]
//使用预聚合函数,在map端进行预聚合,减少在reduce时shuffle时的数据量
def combineByKey[C](createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C): RDD[(K, C)]

集合操作
//与其他RDD去交集
def intersection(other: RDD[T]): RDD[T]
//指定取交集时的分区类,与其他RDD去交集
def intersection(other: RDD[T], partitioner: Partitioner)(implicit ord: Ordering[T] = null): RDD[T]
//指定取交集时的分区数量,与其他RDD取交集
def intersection(other: RDD[T], numPartitions: Int): RDD[T] 
//指定取差集时的分区数量,与其他RDD取差集
def subtract(other: RDD[T], numPartitions: Int): RDD[T] 
//指定取差集时的分区类,与其他RDD取差集
def subtract(other: RDD[T], p: Partitioner)(implicit ord: Ordering[T] = null): RDD[T]
//指定key,vaule pair的key作为取差集差的依据,与其他RDD取差集
def subtractByKey[W: ClassTag](other: RDD[(K, W)]): RDD[(K, V)]
//指定key,vaule pair的key作为取差集差的依据,并设置分区数,与其他RDD取差集
def subtractByKey[W: ClassTag](other: RDD[(K, W)], numPartitions: Int): RDD[(K, V)]
//指定key,vaule pair的key作为取差集差的依据,并设置分区类,与其他RDD取差集
def subtractByKey[W: ClassTag](other: RDD[(K, W)], p: Partitioner): RDD[(K, V)]
//设置分区类,当前RDD与其他RDD取差集
def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))]
//设置分区类,当前RDD与其他RDD取差集
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
//设置分区数,当前RDD与其他RDD取并集
def join[W](other: RDD[(K, W)], numPartitions: Int): RDD[(K, (V, W))]
//以左侧RDD依据,与其他RDD取并集,保留左侧RDD元素,将右侧RDD不在左侧RDD元素与左侧并在一起作为并集
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]

共享变量

通常,当在远程集群节点上执行传递给Spark操作(例如map或者reduce)的函数,如果这些function需要用到Drive中定义的变量,Spark会将这些定义在Driver中的变量拷贝到所有的worker节点,并且这些变量的修改的值并不会传递回给Driver定义的变量。这样看来通常跨任务的读写共享变量效率不高,但是,Spark确实为两种常见的使用模式提供了两种有限类型的共享变量:广播变量累加器

变量广播

通常情况下,当一个RDD的很多操作都需要使用driver中定义的变量时,每次操作,driver都要把变量发送给worker节点一次,如果这个变量中的数据很大的话,会产生很高的传输负载,导致执行效率降低。使用广播变量可以使程序高效地将一个很大的只读数据发送给多个worker节点,而且对每个worker节点只需要传输一次,每次操作时executor可以直接获取本地保存的数据副本,不需要多次传输。

//本案例中,数据(userList )中用0和1表示的性别,要改为汉字,
//通过广播genderMap获取value即可,这里genderMap只是用来读取,未对其做修改
val conf = new SparkConf().setAppName("demo").setMaster("local[2]")
val sc = new SparkContext(conf)

val userList = List(
    "001,张三,28,0",
    "002,李四,18,1",
    "003,王五,38,0",
    "004,zhaoliu,38,-1"
)
val genderMap = Map("0" -> "女", "1" -> "男")
val bcMap = sc.broadcast(genderMap)

sc.parallelize(userList,3)
.map(info=>{
    val prefix = info.substring(0, info.lastIndexOf(","))
    val gender = info.substring(info.lastIndexOf(",") + 1)
    val genderMapValue = bcMap.value
    val newGender = genderMapValue.getOrElse(gender, "未知")
    prefix + "," + newGender
}).collect().foreach(println)

sc.stop() 

累加器

Spark提供的Accumulator,主要用于多个节点对一个变量进行共享性的操作。Accumulator只提供了累加的功能。但是确给我们提供了多个task对一个变量并行操作的功能。但是task只能对Accumulator进行累加操作,不能读取它的值。只有Driver程序可以读取Accumulator的值。

scala> var count=sc.longAccumulator("count")
scala> sc.parallelize(List(1,2,3,4,5,6),3).foreach(item=> count.add(item))
scala> count.value
res1: Long = 21

注意:转换算子中调用累加器,如果没有动作算子,是不会执行的
自定义累加器
//继承AccumulatorV2并定义泛型
	IN:累加器输入的数据类型
	OUT:累加器返回的数据类型
	重写方法
									//这里IN和OUT需要根据自己需求改为对应类型,不能直接使用
  class MyAccumulator extends AccumulatorV2[IN,OUT]{
    //判断是否是初始状态
    override def isZero: Boolean = ???

    override def copy(): AccumulatorV2[Nothing, Nothing] = ???

    override def reset(): Unit = ???

    // 获取累加器需要计算的值
    override def add(v: Nothing): Unit = ???

    // Driver端合并多个累加器
    override def merge(other: AccumulatorV2[Nothing, Nothing]): Unit = ???

    override def value: Nothing = ???
  }

//注册自定义的累加器
sc.register(new MyAccumulator())

Spark SQL

Spark SQL 是 Spark 用于结构化数据(structured data)处理的Spark模块。SparkSQL 可以简化 RDD 的开发,提高开发效率,且执行效率非常快,所以实际工作中,基本上采用的就是SparkSQL。Spark SQL 为了简化 RDD 的开发,提高开发效率,提供了 2 个编程抽象,类似 Spark Core 中的 RDD

DataFrame
DataSet

DataFrame和DataSet名词解析

Data Frame命名列的数据集,它在概念是等价于关系型数据库。DataFrames可以从很多地方构建,比如说结构化数据文件、hive中的表或者外部数据库,使用Dataset[row]的数据集,可以理解DataFrame其实就是DataSet的一个特例
在这里插入图片描述

上图直观地体现了 DataFrame 和 RDD 的区别。
左侧的 RDD[Person]虽然以 Person 为类型参数,但 Spark 框架本身不了解 Person 类的内部结构。而右侧的 DataFrame 却提供了详细的结构信息,使得 Spark SQL 可以清楚地知道该数据集中包含哪些列,每列的名称和类型各是什么。

DataFrame 是为数据提供了 Schema 的视图。可以把它当做数据库中的一张表来对待
DataFrame 也是懒执行的,但性能上比 RDD 要高,主要原因:优化的执行计划,即查询计划通过 Spark catalyst optimiser 进行优化。

DataSet分布式数据集合。DataSet 是 Spark 1.6 中添加的一个新抽象,是 DataFrame的一个扩展。它提供了 RDD 的优势(强类型,使用强大的 lambda 函数的能力)以及 Spark SQL 优化执行引擎的优点。DataSet 也可以使用功能性的转换(操作 map,flatMap,filter等等)。

  • DataSet 是 DataFrame API 的一个扩展,是 SparkSQL 最新的数据抽象
  • 用户友好的 API 风格,既具有类型安全检查也具有 DataFrame 的查询优化特性;
  • 用样例类来对 DataSet 中定义数据的结构信息,样例类中每个属性的名称直接映射到DataSet 中的字段名称
  • DataSet 是强类型的。比如可以有 DataSet[Car],DataSet[Person]
  • DataFrame 是 DataSet 的特列,DataFrame=DataSet[Row] ,所以可以通过 as 方法将DataFrame 转换为 DataSet。Row 是一个类型,跟 Car、Person 这些的类型一样,所有的表结构信息都用 Row 来表示。获取数据时需要指定顺序

RDD、DataFrame、DataSet 三者的关系

Spark1.0 => RDD
Spark1.3 => DataFrame
Spark1.6 => Dataset

共性
  • RDD、DataFrame、DataSet 全都是 spark 平台下的分布式弹性数据集,为处理超大型数据提供便利;
  • 三者都有惰性机制,在进行创建、转换,如 map 方法时,不会立即执行,只有在遇到Action 如 foreach 时,三者才会开始遍历运算;
  • 三者有许多共同的函数,如 filter,排序等;
  • 在对 DataFrame 和 Dataset 进行操作许多操作都需要这个包:import spark.implicits._(在创建好 SparkSession 对象后尽量直接导入)
  • 三者都会根据 Spark 的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出
  • 三者都有 partition 的概念
  • DataFrame 和 DataSet 均可使用模式匹配获取各个字段的值和类型
区别

RDD

  • RDD 一般和 spark mllib 同时使用
  • RDD 不支持 sparksql 操作

DataFrame

  • 与 RDD 和 Dataset 不同,DataFrame 每一行的类型固定为 Row,每一列的值没法直接访问,只有通过解析才能获取各个字段的值
  • DataFrame 与 DataSet 一般不与 spark mllib 同时使用
  • DataFrame 与 DataSet 均支持 SparkSQL 的操作,比如 select,groupby 之类,还能注册临时表/视窗,进行 sql 语句操作
  • DataFrame 与 DataSet 支持一些特别方便的保存方式,比如保存成 csv,可以带上表头,这样每一列的字段名一目了然

DataSet

  • Dataset 和 DataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同。DataFrame 其实就是 DataSet 的一个特例 type DataFrame = Dataset[Row]
  • DataFrame 也可以叫 Dataset[Row],每一行的类型是 Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的 getAS 方法或者共性中的第七条提到的模式匹配拿出特定字段。而 Dataset 中,每一行是什么类型是不一定的,在自定义了 case class 之后可以很自由的获得每一行的信息

三者的互相转换

在这里插入图片描述

快速入门

  • pom依赖
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-sql_2.11</artifactId>
    <version>2.4.3</version>
</dependency>

Spark中所有功能的入口点是SparkSession类。要创建基本的SparkSession,只需使用SparkSession.builder():

注意: RDD=>DataFrame=>DataSet 转换需要引入隐式转换规则,否则无法转换
spark 不是包名,是上下文环境对象名
import spark.implicits._       这里的spark不是包名!!!而是创建的sparkSession的对象名

//SparkSession的两种创建方式
//1.创建上下文环境配置对象,在SparkSession.builder()后最为config方法参数传进去
 val conf= new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
 //创建 SparkSession 对象
 val spark = SparkSession.builder().config(conf).getOrCreate()
 //RDD=>DataFrame=>DataSet 转换需要引入隐式转换规则,否则无法转换
 //spark 不是包名,是上下文环境对象名
 import spark.implicits._

//2.直接在SparkSession.builder()后设置master和appName
val spark = SparkSession.builder()
          .appName("hellosql")
          .master("local[10]")
           .getOrCreate()
import spark.implicits._

//关闭Spark日志
spark.sparkContext.setLogLevel("FATAL")
spark.stop()

DSL语法

在说DataFrame和Dataset数据集创建前,先说一下DSL语法,下面会用到
DataFrame 提供一个特定领域语言(domain-specific language, DSL)去管理结构化的数据。可以在 Scala, Java, Python 和 R 中使用 DSL,使用 DSL 语法风格不必去创建临时视图

注意:涉及到运算的时候, 每列都必须使用$, 或者采用引号表达式:单引号+字段名
对age+1的两种不同写法,推荐使用$
dataFrame/dataSet数据.select($“username”,$“age” + 1).show
dataFrame/dataSet数据.select('username, 'age + 1).show()

创建Dataset数据集

  • 集合(Case-Class:常规类,数据内容不可变)
case class Person(id:Int,name:String,age:Int,sex:Boolean)
def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
    .appName("hellosql")
    .master("local[10]")
    .getOrCreate()
    import spark.implicits._

    val dataset: Dataset[Person] = List(Person(1,"zhangsan",18,true),Person(2,"wangwu",28,true)).toDS()
    //这里就是DSL语法,通过$"字段名"查询,后面不用跟创建的临时视图名
    //注意:使用$必须引入import spark.implicits._
    dataset.select($"id",$"name").show()

    //关闭Spark日志
    spark.sparkContext.setLogLevel("FATAL")
    spark.stop()
}
  • 元组
 case class Person(id:Int,name:String,age:Int,sex:Boolean)
  def main(args: Array[String]): Unit = {
      val spark = SparkSession.builder()
          .appName("hellosql")
          .master("local[10]")
          .getOrCreate()
      import spark.implicits._

    val dataset: Dataset[(Int,String,Int,Boolean)] = List((1,"zhangsan",18,true),(2,"wangwu",28,true)).toDS()
      dataset.select($"_1",$"_2").show()

    //关闭Spark日志
    spark.sparkContext.setLogLevel("FATAL")
    spark.stop()
  }
  • 加载json数据
case class Person(name: String, age: Long)
def main(args: Array[String]): Unit = {
  val spark = SparkSession.builder()
  .master("local[5]")
  .appName("spark session")
  .getOrCreate()
  spark.sparkContext.setLogLevel("FATAL")
  import spark.implicits._

  val dataset = spark.read.json("D:///Persion.json").as[Person]
  dataset.show()

  spark.stop()
}

创建Data Frame数据集

  • 加载json文件
val spark = SparkSession.builder()
.appName("hellosql")
.master("local[10]")
.getOrCreate()
import spark.implicits._

val frame = spark.read.json("file:///f:/person.json")
frame.show()

//关闭Spark日志
spark.sparkContext.setLogLevel("FATAL")
spark.stop()
  • 集合
case class Person(name:String,age:Long)
def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
    .appName("hellosql")
    .master("local[10]")
    .getOrCreate()
    import spark.implicits._

    List(Person("zhangsan",18),Person("王五",20)).toDF("uname","uage").show()
    //关闭Spark日志
    spark.sparkContext.setLogLevel("FATAL")
    spark.stop()
}
  • 元祖
case class Person(name:String,age:Long)
  def main(args: Array[String]): Unit = {
      val spark = SparkSession.builder()
          .appName("hellosql")
          .master("local[10]")
          .getOrCreate()
      import spark.implicits._

      List(("zhangsan",18),("王五",20)).toDF("name","age").show()
    //关闭Spark日志
    spark.sparkContext.setLogLevel("FATAL")
    spark.stop()
  }
  • 通过 RDD 转换(灵活)
case class Person(name:String,age:Long)
def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
    .appName("hellosql")
    .master("local[10]")
    .getOrCreate()
    import spark.implicits._
	/*RDD转为dataFrame有两种方式
		1.在前面把数据对应的字段名和类型创建好,使用createDataFrame函数,createDataFrame对Rdd数据的类型要求更高一些,不太推荐使用这种
		2.调用toDF函数后只跟列名,更推荐使用这种
	*/
	//第一种:RDD泛型是ROW
    val lines:RDD[Row] = spark.sparkContext.parallelize(List("zhangsan,20", "lisi,30"))
    .map(line => Row(line.split(",")(0), line.split(",")(1).toInt))
	//定义数据对应的字段名和数据类型
    val structType = new StructType(Array(StructField("name",StringType,true),StructField("age",IntegerType,true)))
    //转为dataFrame
    val frame = spark.createDataFrame(lines,structType)
    frame.show()
	
	//第二种:
    val lines2: RDD[(Int, String, Int)] = spark.sparkContext.makeRDD(List((1, "zhangsan", 30), (2, "lisi", 28), (3, "wangwu", 20)))
    val frame2 = lines2.toDF("id","name", "age");
    frame2.show()

    //关闭Spark日志
    spark.sparkContext.setLogLevel("FATAL")
    spark.stop()
}

DataFrame 算子操作

如下格式数据

Michael,29,2000,true
Andy,30,5000,true
Justin,19,1000,true
Kaine,20,5000,true
Lisa,19,1000,false

select
//Michael,29,2000,true
var rdd=  spark.sparkContext.textFile("file:///D:/people.txt")
.map(_.split(","))
.map(arr=>Row(arr(0),arr(1).trim().toInt,arr(2).trim().toDouble,arr(3).trim().toBoolean))

var fields=new StructField("name",StringType,true)::new StructField("age",IntegerType,true)::new StructField("salary",DoubleType,true):: new StructField("sex",BooleanType,true)::Nil
spark.createDataFrame(rdd,StructType(fields))
.select($"name",$"age",$"sex",$"salary",$"salary" * 12 as "年薪")
.show()

结果如下:

+-------+---+-----+------+-------+	
|   name|age|  sex|salary|   年薪|
+-------+---+-----+------+-------+
|Michael| 29| true|2000.0|24000.0|
|   Andy| 30| true|5000.0|60000.0|
| Justin| 19| true|1000.0|12000.0|
|  Kaine| 20| true|5000.0|60000.0|
|   Lisa| 19|false|1000.0|12000.0|
+-------+---+-----+------+-------+
filter
var rdd=  spark.sparkContext.textFile("file:///D:/people.txt")
.map(_.split(","))
.map(arr=>Row(arr(0),arr(1).trim().toInt,arr(2).trim().toDouble,arr(3).trim().toBoolean))

var fields=new StructField("name",StringType,true)::new StructField("age",IntegerType,true)::new StructField("salary",DoubleType,true):: new StructField("sex",BooleanType,true)::Nil
spark.createDataFrame(rdd,StructType(fields))
    .select($"name",$"age",$"sex",$"salary",$"salary" * 12 as "年薪")
    .filter($"name" === "Michael" or $"年薪" <  60000)
.show()

结果如下:

+-------+---+-----+------+-------+
|   name|age|  sex|salary|   年薪|
+-------+---+-----+------+-------+
|Michael| 29| true|2000.0|24000.0|
| Justin| 19| true|1000.0|12000.0|
|   Lisa| 19|false|1000.0|12000.0|
+-------+---+-----+------+-------+
where
//Michael,29,2000,true
var rdd=  spark.sparkContext.textFile("file:///D:/people.txt")
.map(_.split(","))
.map(arr=>Row(arr(0),arr(1).trim().toInt,arr(2).trim().toDouble,arr(3).trim().toBoolean))

var fields=new StructField("name",StringType,true)::new StructField("age",IntegerType,true)::new StructField("salary",DoubleType,true):: new StructField("sex",BooleanType,true)::Nil
spark.createDataFrame(rdd,StructType(fields))
.select($"name",$"age",$"sex",$"salary",$"salary" * 12 as "year_salary") //不允许别名中有 中文 bug
.where("(name = 'Michael') or ( year_salary <= 24000) ")
.show()
var rdd=  spark.sparkContext.textFile("file:///D:/people.txt")
  .map(_.split(","))
  .map(arr=>Row(arr(0),arr(1).trim().toInt,arr(2).trim().toDouble,arr(3).trim().toBoolean))

var fields=new StructField("name",StringType,true)::new StructField("age",IntegerType,true)::new StructField("salary",DoubleType,true):: new StructField("sex",BooleanType,true)::Nil
spark.createDataFrame(rdd,StructType(fields))
    .select($"name",$"age",$"sex",$"salary",$"salary" * 12 as "年薪")
    .where($"name" === "Michael" or $"年薪" <= 24000)
    .show()
withColumn
//Michael,29,2000,true
var rdd=  spark.sparkContext.textFile("file:///D:/people.txt")
.map(_.split(","))
.map(arr=>Row(arr(0),arr(1).trim().toInt,arr(2).trim().toDouble,arr(3).trim().toBoolean))

var fields=new StructField("name",StringType,true)::new StructField("age",IntegerType,true)::new StructField("salary",DoubleType,true):: new StructField("sex",BooleanType,true)::Nil
spark.createDataFrame(rdd,StructType(fields))
.select($"name",$"age",$"sex",$"salary",$"salary" * 12 as "年薪")
.where($"name" === "Michael" or $"年薪" <= 24000)
.withColumn("年终奖",$"年薪" * 0.8)
.show()

结果如下:

+-------+---+-----+------+-------+-------+
|   name|age|  sex|salary|   年薪| 年终奖|
+-------+---+-----+------+-------+-------+
|Michael| 29| true|2000.0|24000.0|19200.0|
| Justin| 19| true|1000.0|12000.0| 9600.0|
|   Lisa| 19|false|1000.0|12000.0| 9600.0|
+-------+---+-----+------+-------+-------+
groupBy
var rdd=  spark.sparkContext.textFile("file:///D:/people.txt")
.map(_.split(","))
.map(arr=>Row(arr(0),arr(1).trim().toInt,arr(2).trim().toDouble,arr(3).trim().toBoolean))

var fields=new StructField("name",StringType,true)::new StructField("age",IntegerType,true)::new StructField("salary",DoubleType,true):: new StructField("sex",BooleanType,true)::Nil
spark.createDataFrame(rdd,StructType(fields))
    .select($"age",$"sex")
    .groupBy($"sex")
    .avg("age")
.show()

结果如下:

+-----+--------+
|  sex|avg(age)|
+-----+--------+
| true|    24.5|
|false|    19.0|
+-----+--------+
agg:在整体DataFrame不分组聚合
var rdd=  spark.sparkContext.textFile("file:///D:/people.txt")
.map(_.split(","))
.map(arr=>Row(arr(0),arr(1).trim().toInt,arr(2).trim().toDouble,arr(3).trim().toBoolean))

var fields=new StructField("name",StringType,true)::new StructField("age",IntegerType,true)::new StructField("salary",DoubleType,true):: new StructField("sex",BooleanType,true)::Nil
import org.apache.spark.sql.functions._ //引包,不然sum函数无法使用
spark.createDataFrame(rdd,StructType(fields))
.select($"age",$"sex",$"salary")
.groupBy($"sex")
.agg(sum($"salary") as "toatalSalary",avg("age") as "avgAge",max($"salary"))
.show()

结果如下:

+-----+------------+------+-----------+
|  sex|toatalSalary|avgAge|max(salary)|
+-----+------------+------+-----------+
| true|     13000.0|  24.5|     5000.0|
|false|      1000.0|  19.0|     1000.0|
+-----+------------+------+-----------+
join

准备数据dept.txt

1,销售部门
2,研发部门
3,媒体运营
4,后勤部门

people.txt

Michael,29,2000,true,1
Andy,30,5000,true,1
Justin,19,1000,true,2
Kaine,20,5000,true,2
Lisa,19,1000,false,3

//Michael,29,2000,true,1
var rdd=  spark.sparkContext.textFile("file:///D:/people.txt")
.map(_.split(","))
.map(arr=>Row(arr(0),arr(1).trim().toInt,arr(2).trim().toDouble,arr(3).trim().toBoolean,arr(4).trim().toInt))

var fields=new StructField("name",StringType,true)::new StructField("age",IntegerType,true)::new StructField("salary",DoubleType,true):: new StructField("sex",BooleanType,true)::
new StructField("deptno",IntegerType,true)::Nil

val user = spark.createDataFrame(rdd,StructType(fields)).as("user")

var dept =  spark.sparkContext.textFile("file:///D:/dept.txt")
.map(line =>(line.split(",")(0).toInt,line.split(",")(1)))
.toDF("deptno","deptname").as("dept")

user.select($"name",$"user.deptno")
.join(dept,$"dept.deptno" === $"user.deptno")
.show()

结果如下:

+-------+------+------+--------+
|   name|deptno|deptno|deptname|
+-------+------+------+--------+
|Michael|     1|     1|销售部门|
|   Andy|     1|     1|销售部门|
|   Lisa|     3|     3|媒体运营|
| Justin|     2|     2|研发部门|
|  Kaine|     2|     2|研发部门|
+-------+------+------+--------+
drop
userDF.select($"deptno",$"salary" )
.groupBy($"deptno")
.agg(sum($"salary") as "总薪资",avg($"salary") as "平均值",max($"salary") as "最大值")
.join(deptDF,$"dept.deptno" === $"user.deptno")
.drop($"dept.deptno")
.show()

结果如下:

+------+-------+------------------+-------+--------+
|deptno| 总薪资|            平均值| 最大值|deptname|
+------+-------+------------------+-------+--------+
|     1|43000.0|14333.333333333334|20000.0|销售部门|
|     2|38000.0|           19000.0|20000.0|研发部门|
+------+-------+------------------+-------+--------+
orderBy
userDF.select($"deptno",$"salary" )
.groupBy($"deptno")
.agg(sum($"salary") as "总薪资",avg($"salary") as "平均值",max($"salary") as "最大值")
.join(deptDF,$"dept.deptno" === $"user.deptno")
.drop($"dept.deptno")
.orderBy($"总薪资" asc)
.show()

结果如下:

+------+-------+------------------+-------+--------+
|deptno| 总薪资|            平均值| 最大值|deptname|
+------+-------+------------------+-------+--------+
|     2|38000.0|           19000.0|20000.0|研发部门|
|     1|43000.0|14333.333333333334|20000.0|销售部门|
+------+-------+------------------+-------+--------+
map
userDF.map(row => (row.getString(0),row.getInt(1))).show()

结果如下:

+--------+---+
|    name|age|
+--------+---+
|zhangsan| 28|
+--------+---+

默认情况下SparkSQL会在执行SQL的时候将序列化里面的参数数值,一般情况下系统提供了常见类型的Encoder,如果出现了没有的Encoder,用户需要声明 隐式转换Encoder

row.getValuesMap()方法,获取指定几列的值,返回的是个map 进行筛选重组RDD

implicit val mapEncoder = org.apache.spark.sql.Encoders.kryo[Map[String, Any]]
userDF.map(row => row.getValuesMap[Any](List("name","age","salary")))
.foreach(map=>{
  var name=map.getOrElse("name","")
  var age=map.getOrElse("age",0)
  var salary=map.getOrElse("salary",0.0)
  println(name+" "+age+" "+salary)
})
flatMap
implicit val mapEncoder = org.apache.spark.sql.Encoders.kryo[Map[String, Any]]
userDF.flatMap(row => row.getValuesMap(List("name","age")))
    .map(item => item._1 +" -> "+item._2)
    .show()

结果如下:

+---------------+
|          value|
+---------------+
|name -> Michael|
|      age -> 29|
|   name -> Andy|
|      age -> 30|
| name -> Justin|
|      age -> 19|
|  name -> Kaine|
|      age -> 20|
|   name -> Lisa|
|      age -> 19|
+---------------+
limit (take(n))
var rdd=  spark.sparkContext.textFile("file:///D:/people.txt")
.map(_.split(","))
.map(arr=>Row(arr(0),arr(1).trim().toInt,arr(2).trim().toDouble,arr(3).trim().toBoolean,arr(4).trim().toInt))

var fields=new StructField("name",StringType,true)::new StructField("age",IntegerType,true)::new StructField("salary",DoubleType,true):: new StructField("sex",BooleanType,true)::
new StructField("deptno",IntegerType,true)::Nil

val user = spark.createDataFrame(rdd,StructType(fields)).as("user")

var dept =  spark.sparkContext.textFile("file:///D:/dept.txt")
.map(line =>(line.split(",")(0).toInt,line.split(",")(1)))
.toDF("deptno","deptname").as("dept")

user.select($"name",$"deptno" as "u_dept")
.join(dept,$"dept.deptno" === $"u_dept")
.drop("u_dept")
.orderBy($"deptno" desc)

.limit(3)
.show()

结果如下:

|  name|deptno|deptname|
+------+------+--------+
|  Lisa|     3|媒体运营|
|Justin|     2|研发部门|
| Kaine|     2|研发部门|
+------+------+--------+

SQL获取DataFrame

准备数据people.txt

Michael,29,20000,true,MANAGER,1
Andy,30,15000,true,SALESMAN,1
Justin,19,8000,true,CLERK,1
Kaine,20,20000,true,MANAGER,2
Lisa,19,18000,false,SALESMAN,2
简单的sql查询
val rdd = spark.sparkContext.textFile("file:///D:/people.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0), tokens(1).toInt, tokens(2).toDouble, tokens(3).toBoolean, tokens(4), tokens(5).toInt)
})
var fields=new StructField("name",StringType,true)::
new StructField("age",IntegerType,true)::
new StructField("salary",DoubleType,true)::
new StructField("sex",BooleanType,true)::
new StructField("job",StringType,true)::
new StructField("deptno",IntegerType,true)::Nil		//Nil是空List

val userDF = spark.createDataFrame(rdd,StructType(fields))

//创建一个视图
userDF.createTempView("t_user")

spark.sql("select * from t_user where name like '%M%' or salary between 10000 and 20000").show()

//关闭Spark日志
spark.sparkContext.setLogLevel("FATAL")
spark.stop()
group by
val rdd = spark.sparkContext.textFile("file:///D:/people.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0), tokens(1).toInt, tokens(2).toDouble, tokens(3).toBoolean, tokens(4), tokens(5).toInt)
})
var fields=new StructField("name",StringType,true)::
new StructField("age",IntegerType,true)::
new StructField("salary",DoubleType,true)::
new StructField("sex",BooleanType,true)::
new StructField("job",StringType,true)::
new StructField("deptno",IntegerType,true)::Nil

val userDF = spark.createDataFrame(rdd,StructType(fields))

//创建一个视图
userDF.createTempView("t_user")

spark.sql("select deptno,max(salary),avg(salary),sum(salary),count(1) from t_user group by deptno").show()

//关闭Spark日志
spark.sparkContext.setLogLevel("FATAL")
spark.stop()
having 过滤
val rdd = spark.sparkContext.textFile("file:///D:/people.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0), tokens(1).toInt, tokens(2).toDouble, tokens(3).toBoolean, tokens(4), tokens(5).toInt)
})
var fields=new StructField("name",StringType,true)::
new StructField("age",IntegerType,true)::
new StructField("salary",DoubleType,true)::
new StructField("sex",BooleanType,true)::
new StructField("job",StringType,true)::
new StructField("deptno",IntegerType,true)::Nil

val userDF = spark.createDataFrame(rdd,StructType(fields))

//创建一个视图
userDF.createTempView("t_user")

spark.sql("select deptno,max(salary),avg(salary),sum(salary),count(1) total from t_user group by deptno having total > 2 ")
.show()

//关闭Spark日志
spark.sparkContext.setLogLevel("FATAL")
spark.stop()
表连接 join
val userRdd = spark.sparkContext.textFile("file:///D:/people.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0), tokens(1).toInt, tokens(2).toDouble, tokens(3).toBoolean, tokens(4), tokens(5).toInt)
})
val deptRdd = spark.sparkContext.textFile("file:///D:/dept.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0).toInt, tokens(1))
})
var userFields=new StructField("name",StringType,true)::
new StructField("age",IntegerType,true)::
new StructField("salary",DoubleType,true)::
new StructField("sex",BooleanType,true)::
new StructField("job",StringType,true)::
new StructField("deptno",IntegerType,true)::Nil

var deptFields=new StructField("deptno",IntegerType,true)::
new StructField("name",StringType,true)::Nil

spark.createDataFrame(userRdd,StructType(userFields)).createTempView("t_user")
spark.createDataFrame(deptRdd,StructType(deptFields)).createTempView("t_dept")

spark.sql("select u.*,d.name from t_user u left join t_dept d on u.deptno=d.deptno")
.show()

//关闭Spark日志
spark.sparkContext.setLogLevel("FATAL")
spark.stop()
limit
val userRdd = spark.sparkContext.textFile("file:///D:/people.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0), tokens(1).toInt, tokens(2).toDouble, tokens(3).toBoolean, tokens(4), tokens(5).toInt)
})
val deptRdd = spark.sparkContext.textFile("file:///D:/dept.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0).toInt, tokens(1))
})
var userFields=new StructField("name",StringType,true)::
new StructField("age",IntegerType,true)::
new StructField("salary",DoubleType,true)::
new StructField("sex",BooleanType,true)::
new StructField("job",StringType,true)::
new StructField("deptno",IntegerType,true)::Nil

var deptFields=new StructField("deptno",IntegerType,true)::
new StructField("name",StringType,true)::Nil

spark.createDataFrame(userRdd,StructType(userFields)).createTempView("t_user")
spark.createDataFrame(deptRdd,StructType(deptFields)).createTempView("t_dept")

spark.sql("select u.*,d.name from t_user u left join t_dept d on u.deptno=d.deptno order by u.age asc limit 8")
.show()

//关闭Spark日志
spark.sparkContext.setLogLevel("FATAL")
spark.stop()
子查询
val userRdd = spark.sparkContext.textFile("file:///D:/people.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0), tokens(1).toInt, tokens(2).toDouble, tokens(3).toBoolean, tokens(4), tokens(5).toInt)
})
val deptRdd = spark.sparkContext.textFile("file:///D:/dept.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0).toInt, tokens(1))
})
var userFields=new StructField("name",StringType,true)::
new StructField("age",IntegerType,true)::
new StructField("salary",DoubleType,true)::
new StructField("sex",BooleanType,true)::
new StructField("job",StringType,true)::
new StructField("deptno",IntegerType,true)::Nil

var deptFields=new StructField("deptno",IntegerType,true)::
new StructField("name",StringType,true)::Nil

spark.createDataFrame(userRdd,StructType(userFields)).createTempView("t_user")
spark.createDataFrame(deptRdd,StructType(deptFields)).createTempView("t_dept")

spark.sql("select * from (select name,age,salary from t_user)")
.show()

//关闭Spark日志
spark.sparkContext.setLogLevel("FATAL")
spark.stop()
开窗函数

在正常的统计分析中 ,通常使用聚合函数作为分析,聚合分析函数的特点是将n行记录合并成一行,在数据库的统计当中还有一种统计称为开窗统计,开窗函数可以实现将一行变成多行。可以将数据库查询的每一条记录比作是一幢高楼的一层, 开窗函数就是在每一层开一扇窗, 让每一层能看到整装楼的全貌或一部分。

准备数据

Michael,29,20000,true,MANAGER,1
Andy,30,15000,true,SALESMAN,1
Justin,19,8000,true,CLERK,1
Kaine,20,20000,true,MANAGER,2
Lisa,19,18000,false,SALESMAN,2

+-------+---+-------+-----+--------+------+
|   name|age| salary|  sex|     job|deptno|
+-------+---+-------+-----+--------+------+
|Michael| 29|20000.0| true| MANAGER|     1|
|   Jimi| 25|20000.0| true|SALESMAN|     1|
|   Andy| 30|15000.0| true|SALESMAN|     1|
| Justin| 19| 8000.0| true|   CLERK|     1|
|  Kaine| 20|20000.0| true| MANAGER|     2|
|   Lisa| 19|18000.0|false|SALESMAN|     2|
+-------+---+-------+-----+--------+------+
over()
  • 查询每个部门员工信息,并返回本部门的平均薪资
val userRdd = spark.sparkContext.textFile("file:///D:/people.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0), tokens(1).toInt, tokens(2).toDouble, tokens(3).toBoolean, tokens(4), tokens(5).toInt)
})

var userFields=new StructField("name",StringType,true)::
new StructField("age",IntegerType,true)::
new StructField("salary",DoubleType,true)::
new StructField("sex",BooleanType,true)::
new StructField("job",StringType,true)::
new StructField("deptno",IntegerType,true)::Nil


spark.createDataFrame(userRdd,StructType(userFields)).createTempView("t_user")

spark.sql("select *, avg(salary) over(partition by deptno) as avgSalary from t_user")
.show()

//关闭Spark日志
spark.sparkContext.setLogLevel("FATAL")
spark.stop()

结果如下:

+-------+---+-------+-----+--------+------+------------------+
|   name|age| salary|  sex|     job|deptno|         avgSalary|
+-------+---+-------+-----+--------+------+------------------+
|Michael| 29|20000.0| true| MANAGER|     1|14333.333333333334|
|   Andy| 30|15000.0| true|SALESMAN|     1|14333.333333333334|
| Justin| 19| 8000.0| true|   CLERK|     1|14333.333333333334|
|  Kaine| 20|20000.0| true| MANAGER|     2|           19000.0|
|   Lisa| 19|18000.0|false|SALESMAN|     2|           19000.0|
+-------+---+-------+-----+--------+------+------------------+
ROW_NUMBER() (按顺序排名,无重复)

1,2,3 3条数据,有并列第一(1,2一样),排名不重复且不减少

  • 统计员工在部门内部薪资排名
spark.sql("select * , ROW_NUMBER() over(partition by deptno order by salary DESC) as rank from t_user")
      .show()

结果如下:

+-------+---+-------+-----+--------+------+----+
|   name|age| salary|  sex|     job|deptno|rank|
+-------+---+-------+-----+--------+------+----+
|Michael| 29|20000.0| true| MANAGER|     1|   1|
|   Andy| 30|15000.0| true|SALESMAN|     1|   2|
| Justin| 19| 8000.0| true|   CLERK|     1|   3|
|  Kaine| 20|20000.0| true| MANAGER|     2|   1|
|   Lisa| 19|18000.0|false|SALESMAN|     2|   2|
+-------+---+-------+-----+--------+------+----+
  • 统计员工在公司所有员工的薪资排名
spark.sql("select * , ROW_NUMBER() over(order by salary DESC) as rank from t_user")
      .show()

结果如下:

+-------+---+-------+-----+--------+------+----+
|   name|age| salary|  sex|     job|deptno|rank|
+-------+---+-------+-----+--------+------+----+
|Michael| 29|20000.0| true| MANAGER|     1|   1|
|  Kaine| 20|20000.0| true| MANAGER|     2|   2|
|   Lisa| 19|18000.0|false|SALESMAN|     2|   3|
|   Andy| 30|15000.0| true|SALESMAN|     1|   4|
| Justin| 19| 8000.0| true|   CLERK|     1|   5|
+-------+---+-------+-----+--------+------+----+

可以看出ROW_NUMBER()函数只能计算结果在当前开窗函数中的顺序。并不能计算排名。(参考部门内部薪资排名,部门为1的排序有1、2、3;部门为2的又从1开始排,而不是从4)

DENSE_RANK() (排名有重复,总数会减少)

1,1,2 3条数据,有并列第一,排名减少

  • 计算员工在公司薪资排名
val sql="select * , DENSE_RANK() over(order by salary DESC)  rank  from t_emp"
spark.sql(sql).show()

结果如下:

+-------+---+-------+-----+--------+------+----+
|   name|age| salary|  sex|     job|deptno|rank|
+-------+---+-------+-----+--------+------+----+
|Michael| 29|20000.0| true| MANAGER|     1|   1|
|   Jimi| 25|20000.0| true|SALESMAN|     1|   1|
|  Kaine| 20|20000.0| true| MANAGER|     2|   1|
|   Lisa| 19|18000.0|false|SALESMAN|     2|   2|
|   Andy| 30|15000.0| true|SALESMAN|     1|   3|
| Justin| 19| 8000.0| true|   CLERK|     1|   4|
+-------+---+-------+-----+--------+------+----+
  • 计算员工在公司部门薪资排名
val sql="select * , DENSE_RANK() over(partition by deptno order by salary DESC)  rank  from t_emp"
spark.sql(sql).show()

结果如下:

+-------+---+-------+-----+--------+------+----+
|   name|age| salary|  sex|     job|deptno|rank|
+-------+---+-------+-----+--------+------+----+
|Michael| 29|20000.0| true| MANAGER|     1|   1|
|  Kaine| 20|20000.0| true| MANAGER|     2|   1|
|   Lisa| 19|18000.0|false|SALESMAN|     2|   2|
|   Andy| 30|15000.0| true|SALESMAN|     1|   3|
| Justin| 19| 8000.0| true|   CLERK|     1|   4|
+-------+---+-------+-----+--------+------+----+
RANK() (排名有重复,总数不会变)

1,1,3 3条数据,有并列第一,但排名还是到3
该函数和DENSE_RANK()类似,不同的是RANK计算的排名顺序不连续。

  • 计算员工在公司部门薪资排名
val sql="select * , RANK() over(partition by deptno order by salary DESC)  rank  from t_user"
spark.sql(sql).show()

结果如下:

+-------+---+-------+-----+--------+------+----+
|   name|age| salary|  sex|     job|deptno|rank|
+-------+---+-------+-----+--------+------+----+
|Michael| 29|20000.0| true| MANAGER|     1|   1|
|  Kaine| 20|20000.0| true| MANAGER|     2|   1|
|   Lisa| 19|18000.0|false|SALESMAN|     2|   3|
|   Andy| 30|15000.0| true|SALESMAN|     1|   4|
| Justin| 19| 8000.0| true|   CLERK|     1|   5|
+-------+---+-------+-----+--------+------+----+
自定义函数(spark.udf)
单行函数
val rdd = spark.sparkContext.textFile("file:///D:/people.txt")
.map(line => {
    val tokens = line.split(",")
    Row(tokens(0), tokens(1).toInt, tokens(2).toDouble, tokens(3).toBoolean, tokens(4), tokens(5).toInt)
})
var fields=new StructField("name",StringType,true)::
new StructField("age",IntegerType,true)::
new StructField("salary",DoubleType,true)::
new StructField("sex",BooleanType,true)::
new StructField("job",StringType,true)::
new StructField("deptno",IntegerType,true)::Nil		//Nil是空List

val userDF = spark.createDataFrame(rdd,StructType(fields))

//创建一个视图
userDF.createTempView("t_user")
//自定义函数,根据job去将对应的salary*不同的数字
spark.udf.register("yearSalary",(job:String,salary:Double)=> {
    job match {
        case "MANAGER" => salary * 14
        case "SALESMAN" => salary * 16
        case "CLERK" => salary * 13
        case _ => salary*12
    }
})
//这里的yearSalary就是上面的自定义函数的名字
spark.sql("select name,salary, yearSalary(job,salary) as yearSalary from t_user")
.show()
聚合函数

强类型的 Dataset 和弱类型的 DataFrame 都提供了相关的聚合函数, 如 count(),countDistinct(),avg(),max(),min()。除此之外,用户可以设定自己的自定义聚合函数。通过继承 UserDefinedAggregateFunction 来实现用户自定义弱类型聚合函数。从 Spark3.0 版本后,UserDefinedAggregateFunction 已经不推荐使用了。可以统一采用强类型聚合函数

准备数据order.txt

1,苹果,4.5,2,001
2,橘子,2.5,5,001
3,机械键盘,800,1,002

  • 无类型聚合( Spark SQL)

MySumAggregateFunction

/*
定义类继承 UserDefinedAggregateFunction,并重写其中方法
*/

import org.apache.spark.sql.Row
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types.{DataType, DoubleType, IntegerType, StructType}

class MySumAggregateFunction extends UserDefinedAggregateFunction{
  //聚合函数输入参数的数据类型
  override def inputSchema: StructType = {
    new StructType().add("price",DoubleType).add("count",IntegerType)
  }
  //聚合函数缓冲区中值的数据类型
  override def bufferSchema: StructType = {
      new StructType().add("totalCost",DoubleType)
  }
 // 统计结果值类型
  override def dataType: DataType = DoubleType
  //函数的稳定性,一般不需要做额外实现,直接返回true
  override def deterministic: Boolean = true
  // 函数缓冲区初始化
  override def initialize(buffer: MutableAggregationBuffer): Unit = {
    //初始化第一个参数的值是0
    buffer.update(0,0.0)
  }
  //更新缓冲区中的数据
  override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
    val price = input.getAs[Double](0)
    val count = input.getAs[Int](1)
    val historyCost = buffer.getDouble(0)
    buffer.update(0,historyCost+(price*count))
  }
  //合并缓冲区
  override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
    var totalCost=buffer1.getDouble(0)+buffer2.getDouble(0)
    buffer1.update(0,totalCost)
  }
  //执行最终的返回结果
  override def evaluate(buffer: Row): Any = {
    buffer.getDouble(0)
  }
}

按照userid统计每个用户的总消费

 case class OrderLog(price: Double, count: Int,userid:String)
  def main(args: Array[String]): Unit = {
      val spark = SparkSession.builder()
          .appName("hellosql")
          .master("local[10]")
          .getOrCreate()
    import spark.implicits._
    

    var orderDF=  spark.sparkContext.textFile("file:///D:/order.txt")
      .map(_.split(","))
      .map(arr=> OrderLog(arr(2).toDouble,arr(3).toInt,arr(4)))
      .toDF().createTempView("t_order")

    spark.udf.register("customsum",new MySumAggregateFunction())
    spark.sql("select userid , customsum(price,count) as totalCost from t_order group by userid").show()

    //关闭Spark日志
    spark.sparkContext.setLogLevel("FATAL")
    spark.stop()
  }

结果:

+------+---------+
|userid|totalCost|
+------+---------+
|   001|     21.5|
|   002|    800.0|
+------+---------+
  • 有类型聚合|强类型聚合** (DataFrame API )
    AverageState
case class AverageState(var sum: Double, var total: Int)

MyAggregator

/**
 * 定义类继承 org.apache.spark.sql.expressions.Aggregator
 * 重写类中的方法
 * Aggregator需要3个泛型:Aggregator[IN,BUF,OUT]
 * IN:输入的数据类型
 * BUF:缓冲区数据类型
 * OUT:输出的数据类型
 * 
 */

import org.apache.spark.sql.{Encoder, Encoders}
import org.apache.spark.sql.catalyst.expressions.GenericRowWithSchema
import org.apache.spark.sql.expressions.Aggregator

									//		IN					BUF		OUT
class MyAggregator extends Aggregator[GenericRowWithSchema,AverageState,Double] {
  //缓冲区的初始值
  override def zero: AverageState = AverageState(0.0,0)

  //局部合并,根据是输入的数据更新缓冲区的数据
  override def reduce(b: AverageState, a: GenericRowWithSchema): AverageState ={
      var sum=b.sum + a.getAs[Int]("count") * a.getAs[Double]("price")
      var count=b.total+1
      b.copy(sum,count)
  }
  //合并缓冲区
  override def merge(b1: AverageState, b2: AverageState): AverageState = {
    b1.copy(b1.sum+b2.sum,b1.total+b2.total)
  }
  //计算结果
  override def finish(reduction: AverageState): Double = {
    reduction.sum/reduction.total
  }
  //缓冲区的编码操作
  override def bufferEncoder: Encoder[AverageState] = {
    Encoders.product[AverageState]
  }
  //输出的编码操作
  override def outputEncoder: Encoder[Double] = {
    Encoders.scalaDouble
  }
}

使用

 var orderDF=  spark.sparkContext.textFile("file:///D:/order.txt")
      .map(_.split(","))
      .map(arr=> OrderLog(arr(2).toDouble,arr(3).toInt,arr(4)))
      .toDF()
    val avg = new MyAggregator().toColumn.name("avgCost")

    orderDF.groupBy($"userid").agg(avg).show()

Load & save 函数:数据的导入和导出

Load
  • MySQL集成(引入MysQL驱动jar)
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
//连接mysql数据库
spark.read
    .format("jdbc")
    .option("url", "jdbc:mysql://CentOS:3306/test")
    .option("dbtable", "t_user")
    .option("user", "root")
    .option("password", "root")
    .load().createTempView("t_user")

spark.sql("select * from t_user").show()

通过jdbc连接数据库,option可填的部分参数:

属性名称默认值含义
url要连接的JDBC URL,可以在URL中指定特定于源的连接属性
dbtable应该读取或写入的JDBC表
query将数据读入Spark的查询语句
drivercom.mysql.jdbc.Driver用于连接到此URL的JDBC驱动程序的类名
numPartitions表读取和写入中可用于并行的最大分区数,这同时确定了最大并发的JDBC连接数
partitionColumn, lowerBound, upperBound如果指定了任一选项,则必须指定全部选项。此外,还必须指定numPartitionspartitionColumn必须是表中的数字,日期或时间戳列。注意:lowerBoundupperBound 仅用于决定分区步幅,而不是用于过滤表中的行。因此,表中的所有行都将被分区并返回。 这些选项仅适用于读操作。
queryTimeout0超时时间(单位:秒),零意味着没有限制
fetchsize用于确定每次往返要获取的行数(例如,Oracle是10行),这 可以 用于 提升 JDBC 驱动程序的性能。此选项仅适用于读
batchsize1000JDBC批处理大小,用于确定每次往返要插入的行数。 这可以用于提升JDBC驱动程序的性能。此选项仅适用于写
isolationLevelREAD_UNCOMMITTED事务隔离级别,适用于当前连接。它可以是NONE,READ_COMMITTED,READ_UNCOMMITTED,REPEATABLE_READ 或 SERIALIZABLE之一,对应于 JDBC的Connection 对象定义的标准事务隔离级别,默认值为 READ_UNCOMMITTED。此选项仅适用于写
sessionInitStatement在向远程数据库打开每个数据库会话之后,在开始读取数据之前,此选项将执行自定义SQL语句(或PL / SQL块)。 使用它来实现会话初始化,例如:option("sessionInitStatement", """BEGIN execute immediate 'alter session set "_serial_direct_read"=true'; END;""")
truncatefalse当启用SaveMode.Overwrite时,此选项会导致 Spark 截断现有表,而不是删除并重新创建它。这样更高效,并且防止删除表元数据(例如,索引)。但是,在某些情况下,例如新数据具有不同的 schema 时,它将无法工作。此选项仅适用于写
cascadeTruncatefalse如果JDBC数据库(目前为 PostgreSQL和Oracle)启用并支持,则此选项允许执行TRUNCATE TABLE t CASCADE(在PostgreSQL的情况下,仅执行TRUNCATE TABLE t CASCADE以防止无意中截断表)。这将影响其他表,因此应谨慎使用。此选项仅适用于写
createTableOptions此选项允许在创建表时设置特定于数据库的表和分区选项(例如,CREATE TABLE t (name string) ENGINE=InnoDB)。此选项仅适用于写
createTableColumnTypes创建表时要使用的数据库列数据类型而不是默认值。(例如:name CHAR(64),comments VARCHAR(1024))。指定的类型应该是有效的 spark sql 数据类型。 此选项仅适用于写
customSchema用于从JDBC连接器读取数据的自定义 schema。例如,id DECIMAL(38, 0), name STRING。您还可以指定部分字段,其他字段使用默认类型映射。 例如,id DECIMAL(38,0)。列名应与JDBC表的相应列名相同。用户可以指定Spark SQL的相应数据类型,而不是使用默认值。 此选项仅适用于读
pushDownPredicate用于 启用或禁用 谓词下推 到 JDBC数据源的选项。默认值为 true,在这种情况下,Spark会尽可能地将过滤器下推到JDBC数据源。否则,如果设置为 false,则不会将过滤器下推到JDBC数据源,此时所有过滤器都将由Spark处理
  • 读取CSV格式的数据
spark.read.format("csv")
    .option("sep", ",")		//分隔符
    .option("inferSchema", "true")		//模式推断
    .option("header", "true")	//是否有表头
    .load("file:///D:/t_user.csv").createTempView("t_user")

读取/保存csv,option可填的部分参数:

属性名称默认值含义
sep/delimiter,字段和值之间的分隔符
encodingUTF-8对于读取,根据给定的编码类型解码CSV文件。写入时,指定保存的CSV文件的编码(字符集)
headerfalse对于读取,使用第一行作为列的名称。对于写入,将列的名称写入第一行。注意,如果给定的路径是一个字符串的RDD,这个头选项将删除所有与头相同的行(如果存在的话)
inferSchemafalse从数据自动推断输入模式。它需要一次额外的数据传递
dateFormatyyyy-MM-dd设置表示日期格式的字符串。自定义日期格式遵循日期时间模式的格式。这适用于日期类型

csv参数更详细博客链接

  • 读取json数据
 spark.read.format("json")
     .load("file:///D:/Person.json")
     .createTempView("t_user")

读取/保存json,option可填的部分参数:

属性名称默认值含义
primitivesAsStringfalse将所有原始类型推断为字符串类型
prefersDecimalfalse将所有浮点类型推断为decimal类型,如果不适合,则推断为double类型
allowCommentsfalse忽略JSON记录中的Java / C ++样式注释
allowUnquotedFieldNamesfalse允许不带引号的JSON字段名称
allowSingleQuotestrue除双引号外,还允许使用单引号
allowNumericLeadingZerosfalse允许数字前有零
allowBackslashEscapingAnyCharacterfalse允许反斜杠转义任何字符
allowUnquotedControlCharsfalse允许JSON字符串包含不带引号的控制字符(值小于32的ASCII字符,包括制表符和换行符)或不包含
modePERMISSIVEPERMISSIVE:允许在解析过程中处理损坏记录; DROPMALFORMED:忽略整个损坏的记录;FAILFAST:遇到损坏的记录时抛出异常
columnNameOfCorruptRecordcolumnNameOfCorruptRecord(默认值是spark.sql.columnNameOfCorruptRecord中指定的值):允许重命名由PERMISSIVE 模式创建的新字段(存储格式错误的字符串)。这会覆盖spark.sql.columnNameOfCorruptRecord
dateFormatyyyy-MM-dd设置表示日期格式的字符串。自定义日期格式遵循java.text.SimpleDateFormat中的格式
timestampFormatyyyy-MM-dd’T’HH:mm:ss[.SSS][XXX]设置表示时间戳格式的字符串。 自定义日期格式遵循java.text.SimpleDateFormat中的格式
multiLinefalse解析可能跨越多行的一条记录

option其他参数,可以参考文档,点击跳转

Save:写到其他系统
  • 数据导入MysQL
val personRDD = spark.sparkContext.parallelize(Array("14 tom 19", "15 jerry 18", "16 kitty 20"))
.map(_.split(" "))
.map(tokens=>Row(tokens(0).toInt,tokens(1),tokens(2).toInt))
//通过StrutType直接指定每个字段的schema
val schema = StructType(
    List(
        StructField("id",IntegerType,true),
        StructField("name",StringType,true),
        StructField("age",IntegerType,true)
    )
)
val props = new Properties()
props.put("user", "root")
props.put("password", "root")

spark.createDataFrame(personRDD,schema)
.write.mode("append")
.jdbc("jdbc:mysql://CentOS:3306/test",
      "t_user",props)
  • 存储为Json
val personRDD = spark.sparkContext.parallelize(Array("14 tom 19", "15 jerry 18", "16 kitty 20"))
.map(_.split(" "))
.map(tokens=>Row(tokens(0).toInt,tokens(1),tokens(2).toInt))
//通过StrutType直接指定每个字段的schema
val schema = StructType(
    List(
        StructField("id",IntegerType,true),
        StructField("name",StringType,true),
        StructField("age",IntegerType,true)
    )
)

spark.createDataFrame(personRDD,schema)
.write.mode("append")
.format("json")
.save("file:///D:/aa.json")
  • 存储为CSV格式
val personRDD = spark.sparkContext.parallelize(Array("14 tom 19", "15 jerry 18", "16 kitty 20"))
.map(_.split(" "))
.map(tokens=>Row(tokens(0).toInt,tokens(1),tokens(2).toInt))
//通过StrutType直接指定每个字段的schema
val schema = StructType(
    List(
        StructField("id",IntegerType,true),
        StructField("name",StringType,true),
        StructField("age",IntegerType,true)
    )
)

spark.createDataFrame(personRDD,schema)
.write.mode("append")
.format("csv")
.option("header", "true")//存储表头
.save("file:///D:/csv")

csv参数更详细博客链接

  • 生成分区文件:partitionBy
val spark = SparkSession.builder()
.appName("hellosql")
.master("local[10]")
.getOrCreate()

spark.read.format("csv")
.option("sep", ",")
.option("inferSchema", "true")
.option("header", "true")
.load("file:///D:/t_user.csv").createTempView("t_user")

spark.sql("select * from t_user")
.write.format("json")
.mode(SaveMode.Overwrite)
.partitionBy("id")
.save("file:///D:/partitions")

spark.stop()

Standalone集群构建

物理资源:CentOSA/B/C-6.5 64bit 内存2GB

主机名IP角色
CentOSA192.168.111.134Zookepper、NameNode、DataNode、journalnode、zkfc、master(spark)、Worker(Spark)、Kafka
CentOSB192.168.111.135Zookepper、NameNode、DataNode、journalnode、zkfc、master(spark)、Worker(Spark)、Kafka
CentOSC192.168.111.136Zookepper、DataNode、journalnode、master(spark)、Worker(Spark)、Kafka

在这里插入图片描述

环境准备

  • 主机名和IP映射关系
[root@CentOSX ~]# vi /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.111.134 CentOSA
192.168.111.135 CentOSB
192.168.111.136 CentOSC
  • 设置CentOS进程数和文件数(需要重启)
[root@CentOS ~]# vi /etc/security/limits.conf
* soft nofile 204800
* hard nofile 204800
* soft nproc 204800
* hard nproc 204800
[root@CentOS ~]# reboot
  • 关闭防火墙(略)
[root@CentOSX ~]# service iptables stop
iptables: Setting chains to policy ACCEPT: filter          [  OK  ]
iptables: Flushing firewall rules:                         [  OK  ]
iptables: Unloading modules:                               [  OK  ]
[root@CentOSX ~]# chkconfig iptables off
  • 同步时钟
[root@CentOSX ~]# yum install -y ntp
[root@CentOSX ~]# ntpdate ntp.ubuntu.com
[root@CentOSX ~]# clock -w
  • SSH免密码认证
[root@CentOSX ~]# ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
2c:4b:ba:46:66:27:18:74:11:a7:96:f5:e5:96:97:d5 root@CentOSA
The key's randomart image is:
+--[ RSA 2048]----+
|    ooo   .   .. |
|  . .= . o . o  E|
| . .+   . + o    |
|  ..   . . .     |
|   o  o S        |
|  . =o.o         |
|   +.o.          |
|    ..           |
|   ..            |
+-----------------+
[root@CentOSX ~]# ssh-copy-id CentOSA
[root@CentOSX ~]# ssh-copy-id CentOSB
[root@CentOSX ~]# ssh-copy-id CentOSC
  • 安装JDKjdk-8u191-linux-x64.rpm,配置环境变量
[root@CentOSX ~]# rpm -ivh jdk-8u191-linux-x64.rpm
[root@CentOSX ~]# vi .bashrc
JAVA_HOME=/usr/java/latest
PATH=$PATH:$/bin
CLASSPATH=.
export JAVA_HOME
export PATH
export CLASSPATH
[root@CentOSX ~]# source .bashrc
[root@CentOSX ~]# java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)

Zookeeper集群

[root@CentOSX ~]# tar -zxf zookeeper-3.4.6.tar.gz -C /usr/
[root@CentOSX ~]# mkdir /root/zkdata

[root@CentOSA ~]# echo 1 >> /root/zkdata/myid
[root@CentOSB ~]# echo 2 >> /root/zkdata/myid
[root@CentOSC ~]# echo 3 >> /root/zkdata/myid
[root@CentOSX ~]# vi /usr/zookeeper-3.4.6/conf/zoo.cfg
tickTime=2000
dataDir=/root/zkdata
clientPort=2181
initLimit=5
syncLimit=2

server.1=CentOSA:2887:3887
server.2=CentOSB:2887:3887
server.3=CentOSC:2887:3887

[root@CentOSX ~]# /usr/zookeeper-3.4.6/bin/zkServer.sh start zoo.cfg
[root@CentOSX ~]# /usr/zookeeper-3.4.6/bin/zkServer.sh status zoo.cfg
JMX enabled by default
Using config: /usr/zookeeper-3.4.6/bin/../conf/zoo.cfg
Mode: `follower|leader`

Kafka集群

[root@hadoopX ~]# tar -zxf kafka_2.11-2.2.0.tgz -C /usr
[root@hadoop2 ~]# vi /usr/kafka_2.11-2.2.0/config/server.properties
############################# Server Basics #############################
broker.id=[0|1|2]
############################# Socket Server Settings #############################
listeners=PLAINTEXT://CentOS[A|B|C]:9092
############################# Log Basics #############################
# A comma separated list of directories under which to store log files
log.dirs=/usr/kafka-logs
############################# Zookeeper #############################
zookeeper.connect=CentOSA:2181,CentOSB:2181,CentOSC:2181

启动kafka

[root@CentOSX ~]# cd /usr/kafka_2.11-2.2.0/
[root@CentOSX kafka_2.11-2.2.0]# ./bin/kafka-server-start.sh -daemon config/server.properties

测试kafka是否可用

  • 创建topic01
[root@CentOSA kafka_2.11-2.2.0]# ./bin/kafka-topics.sh 
                                --zookeeper CentOSA:2181,CentOSB:2181,CentOSC:2181 
                                --create 
                                --topic topic01 
                                --partitions 3 
                                --replication-factor 3
Created topic topic01.

这里partitions指定日志分区数目,replication-factor指定分区日志的副本因子。

  • 消费者订阅topic01
[root@CentOSA kafka_2.11-2.2.0]# ./bin/kafka-console-consumer.sh 
                                --bootstrap-server CentOSA:9092,CentOSB:9092,CentOSC:9092 
                                --topic topic01
  • 生产消息
[root@CentOSB kafka_2.11-2.2.0]# ./bin/kafka-console-producer.sh --broker-list CentOSA:9092,CentOSB:9092,CentOSC:9092 --topic topic01
> Hello Kafka

修改kakfa关闭脚本(自带的脚本有问题)
修改kafka-server-stop.sh

#!/bin/sh
SIGNAL=${SIGNAL:-TERM}
#PIDS=$(ps ax | grep -i 'kafka\.Kafka' | grep java | grep -v grep | awk '{print $1}')
PIDS=$(jps | grep 'Kafka' | awk '{print $1}')
if [ -z "$PIDS" ]; then
  echo "No kafka server to stop"
  exit 1
else
  kill -s $SIGNAL $PIDS
fi

Hadoop NameNode HA(sprak自己管理资源,不使用yarn)

  • 解压并配置HADOOP_HOME
[root@CentOSX ~]# tar -zxf hadoop-2.9.2.tar.gz -C /usr/
[root@CentOSX ~]# vi .bashrc
HADOOP_HOME=/usr/hadoop-2.9.2
JAVA_HOME=/usr/java/latest
PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
CLASSPATH=.
export JAVA_HOME
export PATH
export CLASSPATH
export HADOOP_HOME
[root@CentOSX ~]# source .bashrc
  • 配置core-site.xml
<!--配置Namenode服务ID-->
<property>		
      <name>fs.defaultFS</name>		
      <value>hdfs://mycluster</value>	
</property>
<property>		
     <name>hadoop.tmp.dir</name>		
     <value>/usr/hadoop-2.9.2/hadoop-${user.name}</value>    
</property>
<property>		
     <name>fs.trash.interval</name>		
     <value>30</value>    
</property>
<!--配置机架脚本,作用:优化,不配也行-->
<property>		
     <name>net.topology.script.file.name</name>		
     <value>/usr/hadoop-2.9.2/etc/hadoop/rack.sh</value>    
</property>
<!--配置ZK服务信息-->
<property>   
	<name>ha.zookeeper.quorum</name>
	<value>CentOSA:2181,CentOSB:2181,CentOSC:2181</value> 
</property>
<!--配置SSH秘钥位置-->
<property>
     <name>dfs.ha.fencing.methods</name>
     <value>sshfence</value>
</property>
<property>
     <name>dfs.ha.fencing.ssh.private-key-files</name>
     <value>/root/.ssh/id_rsa</value>
</property>

  • 配置机架脚本
[root@CentOSX ~]# touch /usr/hadoop-2.9.2/etc/hadoop/rack.sh
[root@CentOSX ~]# chmod u+x /usr/hadoop-2.9.2/etc/hadoop/rack.sh
[root@CentOSX ~]# vi /usr/hadoop-2.9.2/etc/hadoop/rack.sh
while [ $# -gt 0 ] ; do
	  nodeArg=$1
	  exec</usr/hadoop-2.9.2/etc/hadoop/topology.data
	  result="" 
	  while read line ; do
		ar=( $line ) 
		if [ "${ar[0]}" = "$nodeArg" ] ; then
		  result="${ar[1]}"
		fi
	  done 
	  shift 
	  if [ -z "$result" ] ; then
		echo -n "/default-rack"
	  else
		echo -n "$result "
	  fi
done
[root@CentOSX ~]# vi /usr/hadoop-2.9.2/etc/hadoop/topology.data
192.168.111.134 /rack01
192.168.111.135 /rack02
192.168.111.136 /rack03

#测试机架
[root@CentOSX ~]#  /usr/hadoop-2.9.2/etc/hadoop/rack.sh 192.168.111.134
/rack01 [root@CentOSX ~]
  • 配置hdfs-site.xml
<property>
	<name>dfs.replication</name>
	<value>3</value>
</property> 
<!--开启自动故障转移-->
<property>
	<name>dfs.ha.automatic-failover.enabled</name>
	<value>true</value>
</property>
<!--解释core-site.xml内容-->
<property>
	<name>dfs.nameservices</name>
	<value>mycluster</value>
</property>
<property>
	<name>dfs.ha.namenodes.mycluster</name>
	<value>nn1,nn2</value>
</property>
<property>
	<name>dfs.namenode.rpc-address.mycluster.nn1</name>
	<value>CentOSA:9000</value>
</property>
<property>
	 <name>dfs.namenode.rpc-address.mycluster.nn2</name>
	 <value>CentOSB:9000</value>
</property>
<!--配置日志服务器的信息-->
<property>
  <name>dfs.namenode.shared.edits.dir</name>
  <value>qjournal://CentOSA:8485;CentOSB:8485;CentOSC:8485/mycluster</value>
</property>
<!--实现故障转切换的实现类-->
<property>
	<name>dfs.client.failover.proxy.provider.mycluster</name>
	<value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
</property>
  • 配置slaves
CentOSA
CentOSB
CentOSC
  • 启动HDFS(集群初始化启动)
[root@CentOSX ~]# hadoop-daemon.sh start journalnode 	#(等待10s钟)
[root@CentOSA ~]# hdfs namenode -format			#主节点执行即可
[root@CentOSA ~]# hadoop-daemon.sh start namenode		#主节点执行即可
[root@CentOSB ~]# hdfs namenode -bootstrapStandby		#从节点执行
[root@CentOSB ~]# hadoop-daemon.sh start namenode		#从节点执行
#注册Namenode信息到zookeeper中,只需要在CentOSA或者B上任意一台执行一下指令
[root@CentOSA|B ~]# hdfs zkfc -formatZK
[root@CentOSA ~]# hadoop-daemon.sh start zkfc
[root@CentOSB ~]# hadoop-daemon.sh start zkfc
[root@CentOSX ~]# hadoop-daemon.sh start datanode

Spark Standalone集群

  • 安装配置Spark
[root@CentOSX ~]# tar -zxf spark-2.4.3-bin-without-hadoop.tgz -C /usr/
[root@CentOSX ~]# mv /usr/spark-2.4.3-bin-without-hadoop/ /usr/spark-2.4.3
[root@CentOSX ~]# cd /usr/spark-2.4.3/conf/
[root@CentOS conf]# mv spark-env.sh.template spark-env.sh
[root@CentOS conf]# mv slaves.template slaves
[root@CentOS conf]# vi slaves
CentOSA
CentOSB
CentOSC
[root@CentOS conf]# vi spark-env.sh
SPARK_WORKER_CORES=4
SPARK_WORKER_MEMORY=2g
LD_LIBRARY_PATH=/usr/hadoop-2.9.2/lib/native
SPARK_DIST_CLASSPATH=$(hadoop classpath)
#集群新增配置,需要在zookeeper创建spark目录
SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=CentOSA:2181,CentOSB:2181,CentOSC:2181 -Dspark.deploy.zookeeper.dir=/spark"

export SPARK_WORKER_CORES
export SPARK_WORKER_MEMORY
export LD_LIBRARY_PATH
export SPARK_DIST_CLASSPATH
export SPARK_DAEMON_JAVA_OPTS
  • 启动Spark服务
[root@CentOSX ~]# cd /usr/spark-2.4.3/
[root@CentOSA spark-2.4.3]# ./sbin/start-all.sh
[root@CentOSB spark-2.4.3]# ./sbin/start-master.sh
[root@CentOSC spark-2.4.3]# ./sbin/start-master.sh
  • 测试Spark环境
[root@CentOSA spark-2.4.3]# ./bin/spark-shell --master spark://CentOSA:7077,CentOSB:7077,CentOSC:7077 --total-executor-cores 6
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Spark context Web UI available at http://CentOSA:4040
Spark context available as 'sc' (master = spark://CentOSA:7077,CentOSB:7077,CentOSC:7077, app id = app-20190708162400-0000).
Spark session available as 'spark'.
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 2.4.3
      /_/

Using Scala version 2.11.12 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_191)
Type in expressions to have them evaluated.
Type :help for more information.

scala>


  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值