Spark3学习笔记

文章目录

  • 一、Spark 基础
    • 1、Spark概述
      • 1.1 Spark简介
      • 1.2 Spark VS Hadoop
      • 1.3 Spark特点
      • 1.4 Spark入门Demo
    • 2、Spark 运行模式
      • 2.1 概述
      • 2.2 Local模式
      • 2.3 Standalone 模式
      • 2.4 配置高可用( Standalone +HA)
      • 2.5 Yarn 模式
      • 2.6 K8S & Mesos 模式
      • 2.7 Windows 模式
      • 2.8 部署模式对比
      • 2.9 端口号
    • 3、Spark 运行架构
      • 3.1 运行架构
      • 3.2 核心组件
      • 3.3 核心概念
      • 3.4 提交流程
  • 二、Spark核心编程(Core)
    • 1、概述
    • 2、RDD详解
      • 2.1 RDD概述
      • 2.2 RDD主要属性
      • 2.3 执行原理
    • 3、RDD-API
      • 3.1 RDD 的创建方式
      • 3.2 RDD 并行度与分区
      • 3.3 RDD 转换算子
      • 3.4 RDD 行动算子
      • 3.5 RDD 的持久化/缓存
      • 3.6 RDD 容错机制 Checkpoint
      • 3.7 RDD 文件读取与保存
    • 4、RDD其他概念
      • 4.1 RDD序列化
      • 4.2 RDD 依赖关系
      • 4.3 DAG 的生成和划分 Stage
      • 4.4 RDD 分区器
    • 5、RDD累加器
    • 6、RDD广播变量
  • 三、Spark SQL
    • 1、SparkSQL 概述
      • 1.1 SparkSQL的发展
      • 1.2 Hive and SparkSQL
      • 1.3 SparkSQL 特点
      • 1.4 DataFrame简介
      • 1.5 DataSet
      • 1.6 RDD、DataFrame、DataSet 的区别
    • 2、SparkSQL 核心编程
      • 2.1 概述
      • 2.2 创建 DataFrame/DataSet
      • 2.3 两种查询风格:DSL 和 SQL
      • 2.4 RDD、DataFrame、DataSet 三者的关系
    • 3、Spark实战
      • 3.1 Spark SQL 完成 WordCount
      • 3.2 用户自定义函数
    • 4、数据的加载和保存
      • 4.1 通用的加载和保存方式
      • 4.2 Parquet
      • 4.3 JSON
      • 4.4 CSV
      • 4.5 MySQL
      • 4.6 Hive
  • 四、Spark Streaming
    • 1、SparkStreaming 概述
      • 1.1 简介
      • 1.2 特点
      • 1.3 整体流程
      • 1.4 数据抽象
    • 2、DStream 相关操作
      • 2.1 DStream 创建
      • 2.2 DStream 转换
      • 2.3 DStream 输出
      • 2.4 优雅关闭

一、Spark 基础

1、Spark概述

1.1 Spark简介

Spark 是一种基于内存的快速、通用、可扩展的大数据分析计算引擎。在 FullStack 理想的指引下,Spark 中的 Spark SQL 、SparkStreaming 、MLLib 、GraphX 、R 五大子框架和库之间可以无缝地共享数据和操作, 这不仅打造了 Spark 在当今大数据计算领域其他计算框架都无可匹敌的优势, 而且使得 Spark 正在加速成为大数据处理中心首选通用计算平台。

  • Spark Core:实现了 Spark 的基本功能,包含 RDD、任务调度、内存管理、错误恢复、与存储系统交互等模块
  • Spark SQL:Spark 用来操作结构化数据的程序包。通过 Spark SQL,我们可以使用 SQL 操作数据
  • Spark Streaming:Spark 提供的对实时数据进行流式计算的组件。提供了用来操作数据流的 API
  • Spark MLlib:提供常见的机器学习(ML)功能的程序库。包括分类、回归、聚类、协同过滤等,还提供了模型评估、数据导入等额外的支持功能
  • GraphX(图计算):Spark 中用于图计算的 API,性能良好,拥有丰富的功能和运算符,能在海量数据上自如地运行复杂的图算法
  • 集群管理器:Spark 设计为可以高效地在一个计算节点到数千个计算节点之间伸缩计算
  • Structured Streaming:处理结构化流,统一了离线和实时的 API

1.2 Spark VS Hadoop

Hadoop Spark
类型 分布式基础平台, 包含计算, 存储, 调度 分布式计算工具
场景 大规模数据集上的批处理 迭代计算, 交互式计算, 流计算
价格 对机器要求低, 便宜 对内存有要求, 相对较贵
编程范式 Map+Reduce, API 较为底层, 算法适应性差 RDD 组成 DAG 有向无环图, API 较为顶层, 方便使用
数据存储结构 MapReduce 中间计算结果存在 HDFS 磁盘上, 延迟大 RDD 中间运算结果存在内存中 , 延迟小
运行方式 Task 以进程方式维护, 任务启动慢 Task 以线程方式维护, 任务启动快

注意:尽管 Spark 相对于 Hadoop 而言具有较大优势,但 Spark 并不能完全替代 Hadoop,Spark 主要用于替代 Hadoop 中的 MapReduce 计算模型。存储依然可以使用 HDFS,但是中间结果可以存放在内存中;调度可以使用 Spark 内置的,也可以使用更成熟的调度系统 YARN 等。 实际上,Spark 已经很好地融入了 Hadoop 生态圈,并成为其中的重要一员,它可以借助于 YARN 实现资源调度管理,借助于 HDFS 实现分布式存储。 此外,Hadoop 可以使用廉价的、异构的机器来做分布式存储与计算,但是,Spark 对硬件的要求稍高一些,对内存与 CPU 有一定的要求。

1.3 Spark特点

  • 快。与 Hadoop 的 MapReduce 相比,Spark 基于内存的运算要快 100 倍以上,基于硬盘的运算也要快 10 倍以上。Spark 实现了高效的 DAG 执行引擎,可以通过基于内存来高效处理数据流。
  • 易用。Spark 支持 Java、Python、R 和 Scala 的 API,还支持超过 80 种高级算法,使用户可以快速构建不同的应用。而且 Spark 支持交互式的 Python 和 Scala 的 shell,可以非常方便地在这些 shell 中使用 Spark 集群来验证解决问题的方法。
  • 通用。Spark 提供了统一的解决方案。Spark 可以用于批处理、交互式查询(Spark SQL)、实时流处理(Spark Streaming)、机器学习(Spark MLlib)和图计算(GraphX)。这些不同类型的处理都可以在同一个应用中无缝使用。Spark 统一的解决方案非常具有吸引力,毕竟任何公司都想用统一的平台去处理遇到的问题,减少开发和维护的人力成本和部署平台的物力成本。
  • 兼容性。Spark 可以非常方便地与其他的开源产品进行融合。比如,Spark 可以使用 Hadoop 的 YARN 和 Apache Mesos 作为它的资源管理和调度器,并且可以处理所有 Hadoop 支持的数据,包括 HDFS、HBase 和 Cassandra 等。这对于已经部署 Hadoop 集群的用户特别重要,因为不需要做任何数据迁移就可以使用 Spark 的强大处理能力。Spark 也可以不依赖于第三方的资源管理和调度器,它实现了 Standalone 作为其内置的资源管理和调度框架,这样进一步降低了 Spark 的使用门槛,使得所有人都可以非常容易地部署和使用 Spark。此外,Spark 还提供了在 EC2 上部署 Standalone 的 Spark 集群的工具。

1.4 Spark入门Demo

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

        val sparConf = new SparkConf().setMaster("local").setAppName("WordCount")
        val sc = new SparkContext(sparConf)
        wordcount91011(sc)
        sc.stop()
    }
    // groupBy
    def wordcount1(sc : SparkContext): Unit = {

        val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
        val words = rdd.flatMap(_.split(" "))
        val group: RDD[(String, Iterable[String])] = words.groupBy(word=>word)
        val wordCount: RDD[(String, Int)] = group.mapValues(iter=>iter.size)
    }

    // groupByKey
    def wordcount2(sc : SparkContext): Unit = {
        val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
        val words = rdd.flatMap(_.split(" "))
        val wordOne = words.map((_,1))
        val group: RDD[(String, Iterable[Int])] = wordOne.groupByKey()
        val wordCount: RDD[(String, Int)] = group.mapValues(iter=>iter.size)
    }

    // reduceByKey
    def wordcount3(sc : SparkContext): Unit = {
        val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
        val words = rdd.flatMap(_.split(" "))
        val wordOne = words.map((_,1))
        val wordCount: RDD[(String, Int)] = wordOne.reduceByKey(_+_)
    }

    // aggregateByKey
    def wordcount4(sc : SparkContext): Unit = {
        val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
        val words = rdd.flatMap(_.split(" "))
        val wordOne = words.map((_,1))
        val wordCount: RDD[(String, Int)] = wordOne.aggregateByKey(0)(_+_, _+_)
    }

    // foldByKey
    def wordcount5(sc : SparkContext): Unit = {
        val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
        val words = rdd.flatMap(_.split(" "))
        val wordOne = words.map((_,1))
        val wordCount: RDD[(String, Int)] = wordOne.foldByKey(0)(_+_)
    }

    // combineByKey
    def wordcount6(sc : SparkContext): Unit = {
        val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
        val words = rdd.flatMap(_.split(" "))
        val wordOne = words.map((_,1))
        val wordCount: RDD[(String, Int)] = wordOne.combineByKey(
            v=>v,
            (x:Int, y) => x + y,
            (x:Int, y:Int) => x + y
        )
    }

    // countByKey
    def wordcount7(sc : SparkContext): Unit = {
        val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
        val words = rdd.flatMap(_.split(" "))
        val wordOne = words.map((_,1))
        val wordCount: collection.Map[String, Long] = wordOne.countByKey()
    }

    // countByValue
    def wordcount8(sc : SparkContext): Unit = {
        val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
        val words = rdd.flatMap(_.split(" "))
        val wordCount: collection.Map[String, Long] = words.countByValue()
    }

    // reduce, aggregate, fold
    def wordcount91011(sc : SparkContext): Unit = {
        val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
        val words = rdd.flatMap(_.split(" "))

        // 【(word, count),(word, count)】
        // word => Map[(word,1)]
        val mapWord = words.map(
            word => {
                mutable.Map[String, Long]((word,1))
            }
        )

       val wordCount = mapWord.reduce(
            (map1, map2) => {
                map2.foreach{
                    case (word, count) => {
                        val newCount = map1.getOrElse(word, 0L) + count
                        map1.update(word, newCount)
                    }
                }
                map1
            }
        )
        println(wordCount)
    }
}

2、Spark 运行模式

2.1 概述

  • local 本地模式(单机)–学习测试使用,分为 local 单线程和 local-cluster 多线程
  • standalone 独立集群模式–学习测试使用,典型的 Mater/slave 模式
  • standalone-HA 高可用模式–生产环境使用,基于 standalone 模式,使用 zk 搭建高可用,避免 Master 是有单点故障的
  • **on yarn 集群模式–生产环境使用,**运行在 yarn 集群之上,由 yarn 负责资源管理,Spark 负责任务调度和计算。好处:计算资源按需伸缩,集群利用率高,共享底层存储,避免数据跨集群迁移
  • on mesos 集群模式–国内使用较少,运行在 mesos 资源管理器框架之上,由 mesos 负责资源管理,Spark 负责任务调度和计算
  • on cloud 集群模式–中小公司未来会更多的使用云服务,比如 AWS 的 EC2,使用这个模式能很方便的访问 Amazon 的 S3

2.2 Local模式

Local 模式,就是不需要其他任何节点资源就可以在本地执行 Spark 代码的环境,一般用于教学,调试,演示等, 之前在 IDEA 中运行代码的环境我们称之为开发环境,不太一样

# 这里我hadoop环境为3.1.3
wget https://archive.apache.org/dist/spark/spark-3.0.0/spark-3.0.0-bin-hadoop3.2.tgz
tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz -C /opt/module
cd /opt/module
mv spark-3.0.0-bin-hadoop3.2 spark
# 然后进入spark目录
bin/spark-shell
# 启动后可以进入Web界面进行监控 http://hadoop102:4040/

# 首先在data下创建文本在进入
sc.textFile("data/word.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect
:quit

# ================提交应用========
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master local[2] \
./examples/jars/spark-examples_2.12-3.0.0.jar 10
# --class 表示要执行程序的主类,此处可以更换为咱们自己写的应用程序
# --master local[2] 部署模式,默认为本地模式,数字表示分配的虚拟CPU 核数量
# spark-examples_2.12-3.0.0.jar 运行的应用类所在的 jar 包,实际使用时,可以设定为咱们自己打的 jar 包
# 数字 10 表示程序的入口参数,用于设定当前应用的任务数量

2.3 Standalone 模式

local 本地模式毕竟只是用来进行练习演示的,真实工作中还是要将应用提交到对应的集群中去执行,这里我们来看看只使用 Spark 自身节点运行的集群模式,也就是我们所谓的独立部署(Standalone)模式。Spark 的 Standalone 模式体现了经典的master-slave 模式

# 这里我准备三台机器,hadoop102为master,103,104为slave
cd /opt/module/spark/

# 1)进入解压缩后路径的 conf 目录,修改 slaves.template 文件名为 slaves
mv slaves.template slaves
# 2)修改 slaves 文件,添加work 节点
hadoop102
hadoop103
hadoop104
# 3)修改 spark-env.sh.template 文件名为 spark-env.sh
mv spark-env.sh.template spark-env.sh
# 4)修改 spark-env.sh 文件,添加 JAVA_HOME 环境变量和集群对应的 master 节点
export JAVA_HOME=/opt/module/jdk1.8.0_121
SPARK_MASTER_HOST=hadoop102
SPARK_MASTER_PORT=7077

# 注意:7077 端口,相当于 hadoop 内部通信的 8020 端口,此处的端口需要确认自己的 Hadoop配置
# 5)分发 spark-standalone 目录
xsync spark

# =======集群的启动=====
sbin/start-all.sh
# 查看是否启动,worker
jpsall
# 查看 Master 资源监控Web UI 界面: http://hadoop102:8081
# =============提交应用===============
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://hadoop102:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar 10
# master spark://hadoop102:7077 独立部署模式,连接到Spark 集群

在提交应用中,一般会同时一些提交参数

bin/spark-submit \
--class <main-class>
--master <master-url> \
... # other options
<application-jar> \ [application-arguments]
参数 解释
–class Spark 程序中包含主函数的类
–master Spark 程序运行的模式(环境)
–executor-memory 1G 指定每个 executor 可用内存为 1G
–total-executor-cores 2 指定所有executor 使用的cpu 核数为 2 个
–executor-cores 指定每个executor 使用的cpu 核数
application-jar 打包好的应用 jar,包含依赖。这个 URL 在集群中全局可见。 比如 hdfs:// 共享存储系统,如果是file:// path, 那么所有的节点的path 都包含同样的 jar
application-arguments 传给 main()方法的参数

接下来配置历史服务,由于 spark-shell 停止掉后,集群监控 Hadoop02:8081 页面就看不到历史任务的运行情况,所以开发时都配置历史服务器记录任务运行情况

# 1)修改 spark-defaults.conf.template 文件名为 spark-defaults.conf
mv spark-defaults.conf.template spark-defaults.conf
# 2)修改 spark-default.conf 文件,配置日志存储路径,如果hadoop是单体就是具体的机器名
spark.eventLog.enabled true
spark.eventLog.dir hdfs://mycluster/directory
# 注意:需要启动 hadoop 集群,HDFS 上的directory 目录需要提前存在
sbin/start-dfs.sh
hadoop fs -mkdir /directory
# 3)修改 spark-env.sh 文件, 添加日志配置
export SPARK_HISTORY_OPTS="
-Dspark.history.ui.port=18080
-Dspark.history.fs.logDirectory=hdfs://mycluster/directory
-Dspark.history.retainedApplications=30"
# 参数 1 含义:WEB UI 访问的端口号为 18080
# 参数 2 含义:指定历史服务器日志存储路径
# 参数 3 含义:指定保存Application 历史记录的个数,如果超过这个值,旧的应用程序信息将被删除,这个是内存中的应用数,而不是页面上显示的应用数
# 4)分发配置文件
xsync conf
# 重新启动集群和历史服务
sbin/start-all.sh
sbin/start-history-server.sh
# 6)重新执行任务
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://hadoop102:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar 10
# 查看历史服务:http://hadoop102:18080
# 这里我不知道为什么,使用集群就报错无法解析mycluster,所以我用了hadoop102:8020

2.4 配置高可用( Standalone +HA)

所谓的高可用是因为当前集群中的 Master 节点只有一个,所以会存在单点故障问题。所以为了解决单点故障问题,需要在集群中配置多个 Master 节点,一旦处于活动状态的 Master 发生故障时,由备用 Master 提供服务,保证作业可以继续执行。这里的高可用一般采用Zookeeper 设置

# 这里使用了zk脚本,设置两个master,三台机器都要配置好zk
zk.sh start
# 停止集群
sbin/stop-all.sh

# 修改 spark-env.sh 文件添加如下配置
# 注 释 如 下 内 容 : 
#SPARK_MASTER_HOST=hadoop102
#SPARK_MASTER_PORT=7077
# 添加如下内容:
#Master 监控页面默认访问端口为 8080,但是可能会和 Zookeeper 冲突,所以改成 8989,也可以自定义,访问 UI 监控页面时请注意
SPARK_MASTER_WEBUI_PORT=8989
export SPARK_DAEMON_JAVA_OPTS="
-Dspark.deploy.recoveryMode=ZOOKEEPER
-Dspark.deploy.zookeeper.url=hadoop102,hadoop103,hadoop104
-Dspark.deploy.zookeeper.dir=/spark"
# 分发配置文件
xsync conf/
sbin/start-all.sh

# 启动 hadoop103的单独 Master 节点,此时 hadoop103节点 Master 状态处于备用状态
sbin/start-master.sh

# 提交应用
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://hadoop102:7077,hadoop103:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar 10

2.5 Yarn 模式

独立部署(Standalone)模式由 Spark 自身提供计算资源,无需其他框架提供资源。这种方式降低了和其他第三方资源框架的耦合性,独立性非常强。但是你也要记住,Spark 主要是计算框架,而不是资源调度框架,所以本身提供的资源调度并不是它的强项,所以还是和其他专业的资源调度框架集成会更靠谱一些。所以接下来我们来学习在强大的Yarn 环境下 Spark 是如何工作的(其实是因为在国内工作中,Yarn 使用的非常多)

# 修改 hadoop 配置文件/opt/ha/hadoop-3.1.3/etc/hadoop/yarn-site.xml,  并分发
vim /opt/ha/hadoop-3.1.3/etc/hadoop/yarn-site.xml
<!--是否启动一个线程检查每个任务正使用的物理内存量,如果任务超出分配值,则直接将其杀掉,默认是 true -->
<property>
  <name>yarn.nodemanager.pmem-check-enabled</name>
  <value>false</value>
</property>
<!--是否启动一个线程检查每个任务正使用的虚拟内存量,如果任务超出分配值,则直接将其杀掉,默认是 true -->
<property>
  <name>yarn.nodemanager.vmem-check-enabled</name>
  <value>false</value>
</property>

# 修改 conf/spark-env.sh,添加 JAVA_HOME 和YARN_CONF_DIR 配置
mv spark-env.sh.template spark-env.sh
# 添加以下内容,hadoop天自己的,我这里是ha高可用地址,其他例如YARN_CONF_DIR=/opt/module/hadoop/etc/hadoop
export JAVA_HOME=/opt/module/jdk1.8.0_121
YARN_CONF_DIR=/opt/ha/hadoop-3.1.3/etc/hadoop

# 之前要启动 HDFS 以及 YARN 集群
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode cluster \
./examples/jars/spark-examples_2.12-3.0.0.jar 10

配置历史服务器,之前一样

# 修改 spark-defaults.conf.template 文件名为 spark-defaults.conf
# 其他一样,主要修改 spark-defaults.conf
spark.yarn.historyServer.address=hadoop102:18080
spark.history.ui.port=18080

# 重启history,主要在yarn调度界面就可以直接从历史记录跳转到spark的历史记录了

# 下面会把结果显示在控制台
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client \
./examples/jars/spark-examples_2.12-3.0.0.jar 10

2.6 K8S & Mesos 模式

Mesos 是Apache 下的开源分布式资源管理框架,它被称为是分布式系统的内核,在Twitter 得到广泛使用,管理着 Twitter 超过 30,0000 台服务器上的应用部署,但是在国内,依然使用着传统的Hadoop 大数据框架,所以国内使用 Mesos 框架的并不多

容器化部署是目前业界很流行的一项技术,基于Docker 镜像运行能够让用户更加方便地对应用进行管理和运维。容器管理工具中最为流行的就是Kubernetes(k8s),而 Spark 也在最近的版本中支持了k8s 部署模式。可以参考:https://spark.apache.org/docs/latest/running-on-kubernetes.html

2.7 Windows 模式

# https://archive.apache.org/dist/spark/spark-3.0.0/
# 将文件 spark-3.0.0-bin-hadoop3.2.tgz 解压缩到无中文无空格的路径中
# 执行解压缩文件路径下 bin 目录中的 spark-shell.cmd 文件,启动 Spark 本地环境

# 在 bin 目录中创建 input 目录,并添加word.txt 文件, 在命令行中输入脚本代码
# 命令行提交应用
spark-submit --class org.apache.spark.examples.SparkPi --master local[2] ../examples/jars/spark-examples_2.12-3.0.0.jar 10

2.8 部署模式对比

模式 Spark 安装机器数 需启动的进程 所属者 应用场景
Local 1 Spark 测试
Standalone 3 Master 及 Worker Spark 单独部署
Yarn 1 Yarn 及 HDFS Hadoop 混合部署

2.9 端口号

  • Spark 查看当前 Spark-shell 运行任务情况端口号:4040(计算)
  • Spark Master 内部通信服务端口号:7077
  • Standalone 模式下,Spark Master Web 端口号:8080(资源)
  • Spark 历史服务器端口号:18080
  • Hadoop YARN 任务运行情况查看端口号:8088

3、Spark 运行架构

3.1 运行架构

Spark 框架的核心是一个计算引擎,整体来说,它采用了标准 master-slave 的结构。如下图所示,它展示了一个 Spark 执行时的基本结构。图形中的Driver 表示 master,负责管理整个集群中的作业任务调度。图形中的Executor 则是 slave,负责实际执行任务

Spark3学习笔记_第1张图片

3.2 核心组件

Driver

Spark 驱动器节点,用于执行 Spark 任务中的 main 方法,负责实际代码的执行工作。Driver 在Spark 作业执行时主要负责:

  • 将用户程序转化为作业(job)
  • 在 Executor 之间调度任务(task)
  • 跟踪Executor 的执行情况
  • 通过UI 展示查询运行情况

实际上,我们无法准确地描述Driver 的定义,因为在整个的编程过程中没有看到任何有关Driver 的字眼。所以简单理解,所谓的 Driver 就是驱使整个应用运行起来的程序,也称之为Driver 类。

Executor

Spark Executor 是集群中工作节点(Worker)中的一个 JVM 进程,负责在 Spark 作业中运行具体任务(Task),任务彼此之间相互独立。Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有Executor 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点上继续运行。Executor 有两个核心功能:

  • 负责运行组成 Spark 应用的任务,并将结果返回给驱动器进程
  • 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在 Executor 进程内的,因此任务可以在运行时充分利用缓存数据加速运算

Master & Worker

Spark 集群的独立部署环境中,不需要依赖其他的资源调度框架,自身就实现了资源调度的功能,所以环境中还有其他两个核心组件:Master 和 Worker,这里的 Master 是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责,类似于 Yarn 环境中的 RM, 而Worker 呢,也是进程,一个 Worker 运行在集群中的一台服务器上,由 Master 分配资源对数据进行并行的处理和计算,类似于 Yarn 环境中 NM。

ApplicationMaster

Hadoop 用户向 YARN 集群提交应用程序时,提交程序中应该包ApplicationMaster,用于向资源调度器申请执行任务的资源容器 Container,运行用户自己的程序任务 job,监控整个任务的执行,跟踪整个任务的状态,处理任务失败等异常情况。说的简单点就是,ResourceManager(资源)和Driver(计算)之间的解耦合靠的就是ApplicationMaster

3.3 核心概念

Spark Executor 是集群中运行在工作节点(Worker)中的一个 JVM 进程,是整个集群中的专门用于计算的节点。在提交应用中,可以提供参数指定计算节点的个数,以及对应的资源。这里的资源一般指的是工作节点 Executor 的内存大小和使用的虚拟 CPU 核(Core)数量

名称 说明
–num-executors 配置 Executor 的数量
–executor-memory 配置每个 Executor 的内存大小
–executor-cores 配置每个 Executor 的虚拟 CPU core 数量

在分布式计算框架中一般都是多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正地实现多任务并行执行,记住,这里是并行,而不是并发。这里我们将整个集群并行执行任务的数量称之为并行度

针对有向无环图( DAG),是由 Spark 程序直接映射成的数据流的高级抽象模型

3.4 提交流程

开发人员根据需求写的应用程序通过 Spark 客户端提交给 Spark 运行环境执行计算的流程。在不同的部署环境中,这个提交过程基本相同,但是又有细微的区别,Spark 应用程序提交到 Yarn 环境中执行的时候,一般会有两种部署执行的方式:Client 和 Cluster。两种模式主要区别在于:Driver 程序的运行节点位置
Spark3学习笔记_第2张图片

Yarn Client 模式

Client 模式将用于监控和调度的Driver 模块在客户端执行,而不是在 Yarn 中,所以一般用于测试

  • Driver 在任务提交的本地机器上运行
  • Driver 启动后会和ResourceManager 通讯申请启动ApplicationMaster
  • ResourceManager 分配 container,在合适的NodeManager 上启动ApplicationMaster,负责向ResourceManager 申请 Executor 内存
  • ResourceManager 接到 ApplicationMaster 的资源申请后会分配 container,然后ApplicationMaster 在资源分配指定的NodeManager 上启动 Executor 进程
  • Executor 进程启动后会向Driver 反向注册,Executor 全部注册完成后Driver 开始执行main 函数
  • 之后执行到 Action 算子时,触发一个 Job,并根据宽依赖开始划分 stage,每个stage 生成对应的TaskSet,之后将 task 分发到各个Executor 上执行

Yarn Cluster 模式(重要)

Cluster 模式将用于监控和调度的 Driver 模块启动在Yarn 集群资源中执行。一般应用于实际生产环境

  • 在 YARN Cluster 模式下,任务提交后会和ResourceManager 通讯申请启动ApplicationMaster
  • 随后ResourceManager 分配 container,在合适的 NodeManager 上启动 ApplicationMaster,此时的 ApplicationMaster 就是Driver
  • Driver 启动后向 ResourceManager 申请Executor 内存,ResourceManager 接到ApplicationMaster 的资源申请后会分配container,然后在合适的NodeManager 上启动Executor 进程
  • Executor 进程启动后会向Driver 反向注册,Executor 全部注册完成后Driver 开始执行main 函数,
  • 之后执行到 Action 算子时,触发一个 Job,并根据宽依赖开始划分 stage,每个stage 生成对应的TaskSet,之后将 task 分发到各个Executor 上执行

二、Spark核心编程(Core)

1、概述

Spark 计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,用于处理不同的应用场景。三大数据结构分别是:

  • RDD : 弹性分布式数据集
  • 累加器:分布式共享只写变量
  • 广播变量:分布式共享只读变量

2、RDD详解

2.1 RDD概述

RDD(Resilient Distributed Dataset) 提供了一个抽象的数据模型,让我们不必担心底层数据的分布式特性,只需将具体的应用逻辑表达为一系列转换操作(函数),不同 RDD 之间的转换操作之间还可以形成依赖关系,进而实现管道化,从而避免了中间结果的存储,大大降低了数据复制、磁盘 IO 和序列化开销,并且还提供了更多的 API(map/reduec/filter/groupBy)

RDD叫做弹性分布式数据集,是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。

  • 弹性
    • 存储的弹性:内存与磁盘的自动切换;
    • 容错的弹性:数据丢失可以自动恢复;
    • 计算的弹性:计算出错重试机制;
    • 分片的弹性:可根据需要重新分片。
  • 分布式:数据存储在大数据集群不同节点上
  • 数据集:RDD 封装了计算逻辑,并不保存数据
  • 数据抽象:RDD 是一个抽象类,需要子类具体实现
  • 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD 里面封装计算逻辑
  • 可分区、并行计算

2.2 RDD主要属性

在源码中可以看到有对 RDD 介绍的注释

  • A list of partitions : 一组分片(Partition)/一个分区(Partition)列表,即数据集的基本组成单位。 对于 RDD 来说,每个分片都会被一个计算任务处理,分片数决定并行度。 用户可以在创建 RDD 时指定 RDD 的分片个数,如果没有指定,那么就会采用默认值
  • A function for computing each split : 一个函数会被作用在每一个分区。 Spark 中 RDD 的计算是以分片为单位的,compute 函数会被作用到每个分区上
  • A list of dependencies on other RDDs : 一个 RDD 会依赖于其他多个 RDD。 RDD 的每次转换都会生成一个新的 RDD,所以 RDD 之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark 可以通过这个依赖关系重新计算丢失的分区数据,而不是对 RDD 的所有分区进行重新计算。(Spark 的容错机制)
  • Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned): 可选项,对于 KV 类型的 RDD 会有一个 Partitioner,即 RDD 的分区函数,默认为 HashPartitioner。
  • Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file): 可选项,一个列表,存储存取每个 Partition 的优先位置(preferred location)。 对于一个 HDFS 文件来说,这个列表保存的就是每个 Partition 所在的块的位置。按照"移动数据不如移动计算"的理念,Spark 在进行任务调度的时候,会尽可能选择那些存有数据的 worker 节点来进行任务计算。

2.3 执行原理

Spark 框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 按照指定的计算模型进行数据计算。最后得到计算结果。RDD 是 Spark 框架中用于数据处理的核心模型,接下来看看在 Yarn 环境中RDD 的工作原理:

  • 启动 Yarn 集群环境
  • Spark 通过申请资源创建调度节点和计算节点
  • Spark 框架根据需求将计算逻辑根据分区划分成不同的任务
  • 调度节点将任务根据计算节点状态发送到对应的计算节点进行计算

RDD 在整个流程中主要用于将逻辑进行封装,并生成 Task 发送给Executor 节点执行计算

3、RDD-API

3.1 RDD 的创建方式

// =================1)从集合(内存)中创建 RDD========
// TODO 准备环境
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(sparkConf)

// 从内存中创建RDD,将内存中集合的数据作为处理的数据源
val seq = Seq[Int](1,2,3,4)
// parallelize : 并行
//val rdd: RDD[Int] = sc.parallelize(seq)
// makeRDD方法在底层实现时其实就是调用了rdd对象的parallelize方法。
val rdd: RDD[Int] = sc.makeRDD(seq)
rdd.collect().foreach(println)
// TODO 关闭环境
sc.stop()

// ====================2)从外部存储(文件)创建RDD=========
// TODO 准备环境
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(sparkConf)

// TODO 创建RDD
// 从文件中创建RDD,将文件中的数据作为处理的数据源
// path路径默认以当前环境的根路径为基准。可以写绝对路径,也可以写相对路径
//sc.textFile("D:\\mineworkspace\\idea\\classes\\atguigu-classes\\datas\\1.txt")
//val rdd: RDD[String] = sc.textFile("datas/1.txt")
// path路径可以是文件的具体路径,也可以目录名称
//val rdd = sc.textFile("datas")
// path路径还可以使用通配符 *
//val rdd = sc.textFile("datas/1*.txt")
// 读取的结果表示为元组,第一个元素表示文件路径,第二个元素表示文件内容
// val rdd = sc.wholeTextFiles("datas")
// path还可以是分布式存储系统路径:HDFS
val rdd = sc.textFile("hdfs://hadoop102:8020/test.txt")
rdd.collect().foreach(println)

// TODO 关闭环境
sc.stop()

// ==============3)从其他 RDD 创建=========
//主要是通过一个RDD 运算完后,再产生新的RDD
// ===============直接创建 RDD(new)=========
// 使用 new 的方式直接构造RDD,一般由Spark 框架自身使用

3.2 RDD 并行度与分区

默认情况下,Spark 可以将一个作业切分多个任务后,发送给 Executor 节点并行计算,而能够并行计算的任务数量我们称之为并行度。这个数量可以在构建RDD 时指定

// TODO 准备环境
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
sparkConf.set("spark.default.parallelism", "5")
val sc = new SparkContext(sparkConf)

// TODO 创建RDD
// RDD的并行度 & 分区
// makeRDD方法可以传递第二个参数,这个参数表示分区的数量
// 第二个参数可以不传递的,那么makeRDD方法会使用默认值 : defaultParallelism(默认并行度)
//     scheduler.conf.getInt("spark.default.parallelism", totalCores)
//    spark在默认情况下,从配置对象中获取配置参数:spark.default.parallelism
//    如果获取不到,那么使用totalCores属性,这个属性取值为当前运行环境的最大可用核数
//val rdd = sc.makeRDD(List(1,2,3,4),2)

// Spark读取文件,底层其实使用的就是Hadoop的读取方式
// 分区数量的计算方式:
//    totalSize = 7
//    goalSize =  7 / 2 = 3(byte)
//    7 / 3 = 2...1 (1.1) + 1 = 3(分区)
// 1. 数据以行为单位进行读取
//    spark读取文件,采用的是hadoop的方式读取,所以一行一行读取,和字节数没有关系
// 2. 数据读取时以偏移量为单位,偏移量不会被重复读取
/*
   1@@   => 012
   2@@   => 345
   3     => 6

 */
// 3. 数据分区的偏移量范围的计算
// 0 => [0, 3]  => 12
// 1 => [3, 6]  => 3
// 2 => [6, 7]  =>
// val rdd = sc.textFile("datas/1.txt", 2)


val rdd = sc.makeRDD(List(1,2,3,4))

// 将处理的数据保存成分区文件
rdd.saveAsTextFile("output")

// TODO 关闭环境
sc.stop()


// =======================================
// 读取内存数据时,数据可以按照并行度的设定进行数据的分区操作,数据分区规则的Spark 核心源码如下
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)
 }
}

// ====================================
// 读取文本时的分类规则,和内存有所不同
public InputSplit[] getSplits(JobConf job, int numSplits)
    throws IOException {

    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);
    ...
    for (FileStatus file: files) {
      ...
        if (isSplitable(fs, path)) {
          long blockSize = file.getBlockSize();
          long splitSize = computeSplitSize(goalSize, minSize, blockSize);
      ...
  }

protected long computeSplitSize(long goalSize, long minSize,long blockSize) {
  return Math.max(minSize, Math.min(goalSize, blockSize));
}

3.3 RDD 转换算子

RDD 的算子分为两类:

  • Transformation_转换操作:返回一个新的 RDD
  • Action动作操作:返回值不是 RDD(无返回值或返回其他的)

❣️ 注意:
1、RDD 不实际存储真正要计算的数据,而是记录了数据的位置在哪里,数据的转换关系(调用了什么方法,传入什么函数)。
2、RDD 中的所有转换都是惰性求值/延迟执行的,也就是说并不会直接计算。只有当发生一个要求返回结果给 Driver 的 Action 动作时,这些转换才会真正运行。
3、之所以使用惰性求值/延迟执行,是因为这样可以在 Action 时对 RDD 操作形成 DAG 有向无环图进行 Stage 的划分和并行优化,这种设计让 Spark 更加有效率地运行。

Transformation 转换算子

转换算子 含义
map(func) 返回一个新的 RDD,该 RDD 由每一个输入元素经过 func 函数转换后组成
filter(func) 返回一个新的 RDD,该 RDD 由经过 func 函数计算后返回值为 true 的输入元素组成;;当数据进行筛选过滤后,分区不变,但是分区内的数据可能不均衡,生产环境下,可能会出现数据倾斜
flatMap(func) 类似于 map,但是每一个输入元素可以被映射为 0 或多个输出元素(所以 func 应该返回一个序列,而不是单一元素,)
mapPartitions(func) 类似于 map,但独立地在 RDD 的每一个分片上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是 Iterator[T] => Iterator[U],性能高但容易内存溢出,内存少用map
mapPartitionsWithIndex(func) 类似于 mapPartitions,但 func 带有一个整数参数表示分片的索引值,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是(Int, Interator[T]) => Iterator[U]
glom(func) 将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变
groupBy(func) 将数据根据指定的规则进行分组, 分区默认不变,但是数据会被打乱重新组合,我们将这样的操作称之为 shuffle。 groupBy[K](f: T => K)
sample(withReplacement, fraction, seed) 根据 fraction 指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed 用于指定随机数生成器种子
union(otherDataset) 对源 RDD 和参数 RDD 求并集后返回一个新的 RDD
intersection(otherDataset) 对源 RDD 和参数 RDD 求交集后返回一个新的 RDD;subtract为差集;zip为拉链;sliding为滑窗
distinct([numTasks])) 对源 RDD 进行去重后返回一个新的 RDD
partitionBy(partitioner) 将数据按照指定 Partitioner 重新进行分区。Spark 默认的分区器是 HashPartitioner
groupByKey([numTasks]) 在一个(K,V)的 RDD 上调用,将数据源的数据根据 key 对 value 进行分组(相同的key把value放一起,group by的话还是会包括key,value),返回一个(K, Iterator[V])的 RDD,推荐使用
reduceByKey(func, [numTasks]) 在一个(K,V)的 RDD 上调用,返回一个(K,V)的 RDD,使用指定的 reduce 函数,将相同 key 的值聚合到一起,与 groupByKey 类似,reduce 任务的个数可以通过第二个可选的参数来设置(有效减少落盘)
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) 对 PairRDD 中相同的 Key 值进行聚合操作,在聚合过程中同样使用了一个中立的初始值。和 aggregate 函数类似,aggregateByKey 返回值的类型不需要和 RDD 中 value 的类型一致(第一个函数是分区内,第二个是分区间)
foldByKey(zeroValue)(func) 当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为 foldByKey
combineByKey(func1,func2,func3) 最通用的对 key-value 型 rdd 进行聚集操作的聚集函(aggregation function)。类似于aggregate(),combineByKey()允许用户返回值的类型与输入不一致。参数一将相同key的第一个数据进行结构的转换实现操作,参数二表示分区内的计算规则,参数三表示分区间的计算规则;上面几个底层调用都是这个函数
sortByKey([ascending], [numTasks]) 在一个(K,V)的 RDD 上调用,K 必须实现 Ordered 接口,返回一个按照 key 进行排序的(K,V)的 RDD
sortBy(func,[ascending], [numTasks]) 与 sortByKey 类似,但是更灵活,存在shuffle,默认为升序排列。排序后新产生的 RDD 的分区数与原 RDD 的分区数一致
join(otherDataset, [numTasks]) 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素对在一起的(K,(V,W))的 RDD,还有leftOuterJoin函数
cogroup(otherDataset, [numTasks]) 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
cartesian(otherDataset) 笛卡尔积
pipe(command, [envVars]) 对 rdd 进行管道操作
coalesce(numPartitions,isShuffle) 减少 RDD 的分区数到指定值。在过滤大量数据之后,可以执行此操作,默认是不shuffle
repartition(numPartitions) 重新给 RDD 分区,一般用来扩大分区
// 一个举例,统计出每一个省份每个广告被点击数量排行的 Top3
def main(args: Array[String]): Unit = {

    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // TODO 案例实操
    // 1. 获取原始数据:时间戳,省份,城市,用户,广告
    val dataRDD = sc.textFile("datas/agent.log")

    // 2. 将原始数据进行结构的转换。方便统计
    //    时间戳,省份,城市,用户,广告
    //    =>
    //    ( ( 省份,广告 ), 1 )
    val mapRDD = dataRDD.map(
        line => {
            val datas = line.split(" ")
            (( datas(1), datas(4) ), 1)
        }
    )

    // 3. 将转换结构后的数据,进行分组聚合
    //    ( ( 省份,广告 ), 1 ) => ( ( 省份,广告 ), sum )
    val reduceRDD: RDD[((String, String), Int)] = mapRDD.reduceByKey(_+_)

    // 4. 将聚合的结果进行结构的转换
    //    ( ( 省份,广告 ), sum ) => ( 省份, ( 广告, sum ) )
    val newMapRDD = reduceRDD.map{
        case ( (prv, ad), sum ) => {
            (prv, (ad, sum))
        }
    }

    // 5. 将转换结构后的数据根据省份进行分组
    //    ( 省份, 【( 广告A, sumA ),( 广告B, sumB )】 )
    val groupRDD: RDD[(String, Iterable[(String, Int)])] = newMapRDD.groupByKey()

    // 6. 将分组后的数据组内排序(降序),取前3名
    val resultRDD = groupRDD.mapValues(
        iter => {
            iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
        }
    )

    // 7. 采集数据打印在控制台
    resultRDD.collect().foreach(println)
    sc.stop()
}

3.4 RDD 行动算子

动作算子

动作算子 含义
reduce(func) 通过 func 函数聚集 RDD 中的所有元素,这个功能必须是可交换且可并联的;聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据
collect() 在驱动程序中,以数组的形式返回数据集的所有元素
count() 返回 RDD 的元素个数
first() 返回 RDD 的第一个元素(类似于 take(1))
take(n) 返回一个由数据集的前 n 个元素组成的数组
takeSample(withReplacement,num, [seed]) 返回一个数组,该数组由从数据集中随机采样的 num 个元素组成,可以选择是否用随机数替换不足的部分,seed 用于指定随机数生成器种子
takeOrdered(n, [ordering]) 返回自然顺序或者自定义顺序的前 n 个元素
aggregate(zeroValue)(U,T) 分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合
fold(zeroValue)(U,T) 折叠操作,aggregate 的简化版操作
countByKey() 针对(K,V)类型的 RDD,返回一个(K,Int)的 map,表示每一个 key 对应的元素个数
saveAsTextFile(path) 将数据集的元素以 textfile 的形式保存到 HDFS 文件系统或者其他支持的文件系统,对于每个元素,Spark 将会调用 toString 方法,将它装换为文件中的文本
saveAsSequenceFile(path) 将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以使 HDFS 或者其他 Hadoop 支持的文件系统,必须键值对
saveAsObjectFile(path) 将数据集的元素,以 Java 序列化的方式保存到指定的目录下
foreach(func) 在数据集的每一个元素上,运行函数 func 进行更新;rdd.collect().foreach(println)是driver端执行,而rdd.foreach(println)是在excutor端执行
foreachPartition(func) 在数据集的每一个分区上,运行函数 func

统计操作

算子 含义
count 个数
mean 均值
sum 求和
max 最大值
min 最小值
variance 方差
sampleVariance 从采样中计算方差
stdev 标准差:衡量数据的离散程度
sampleStdev 采样的标准差
stats 查看统计结果

3.5 RDD 的持久化/缓存

RDD 通过 Cache 或者 Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算子时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用

// cache 操作会增加血缘关系,不改变原有的血缘关系
println(wordToOneRdd.toDebugString)
// 数据缓存
wordToOneRdd.cache()
// 可以更改存储级别,持久化操作必须在行动算子执行时完成的
//mapRdd.persist(StorageLevel.MEMORY_AND_DISK_2)


// 默认的存储级别都是仅在内存存储一份,Spark 的存储级别还有好多种,存储级别在 object StorageLevel 中定义的
object StorageLevel {
  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)

持久化级别 说明
MORY_ONLY(默认) 将 RDD 以非序列化的 Java 对象存储在 JVM 中。 如果没有足够的内存存储 RDD,则某些分区将不会被缓存,每次需要时都会重新计算。 这是默认级别
MORY_AND_DISK(开发中可以使用这个) 将 RDD 以非序列化的 Java 对象存储在 JVM 中。如果数据在内存中放不下,则溢写到磁盘上.需要时则会从磁盘上读取
MEMORY_ONLY_SER (Java and Scala) 将 RDD 以序列化的 Java 对象(每个分区一个字节数组)的方式存储.这通常比非序列化对象(deserialized objects)更具空间效率,特别是在使用快速序列化的情况下,但是这种方式读取数据会消耗更多的 CPU
MEMORY_AND_DISK_SER (Java and Scala) 与 MEMORY_ONLY_SER 类似,但如果数据在内存中放不下,则溢写到磁盘上,而不是每次需要重新计算它们
DISK_ONLY 将 RDD 分区存储在磁盘上
MEMORY_ONLY_2, MEMORY_AND_DISK_2 等 与上面的储存级别相同,只不过将持久化数据存为两份,备份每个分区存储在两个集群节点上
OFF_HEAP(实验中) 与 MEMORY_ONLY_SER 类似,但将数据存储在堆外内存中。 (即不是直接存储在 JVM 内存中)

RDD 持久化/缓存的目的是为了提高后续操作的速度;缓存的级别有很多,默认只存在内存中,开发中使用 memory_and_disk;只有执行 action 操作的时候才会真正将 RDD 数据进行持久化/缓存;实际开发中如果某一个 RDD 后续会被频繁的使用,可以将该 RDD 进行持久化/缓存

3.6 RDD 容错机制 Checkpoint

所谓的检查点其实就是通过将 RDD 中间结果写入磁盘由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发

// cache : 将数据临时存储在内存中进行数据重用
//         会在血缘关系中添加新的依赖。一旦,出现问题,可以重头读取数据
// persist : 将数据临时存储在磁盘文件中进行数据重用
//           涉及到磁盘IO,性能较低,但是数据安全
//           如果作业执行完毕,临时保存的数据文件就会丢失
// checkpoint : 将数据长久地保存在磁盘文件中进行数据重用
//           涉及到磁盘IO,性能较低,但是数据安全
//           为了保证数据安全,所以一般情况下,会独立执行作业
//           为了能够提高效率,一般情况下,是需要和cache联合使用
//           执行过程中,会切断血缘关系。重新建立新的血缘关系
//           checkpoint等同于改变数据源
        
// 设置检查点路径
sc.setCheckpointDir("./checkpoint1")
//SparkContext.setCheckpointDir("目录") //HDFS的目录
// 创建一个 RDD,读取指定位置文件:hello atguigu atguigu
val lineRdd: RDD[String] = sc.textFile("input/1.txt")
// 业务逻辑
val wordRdd: RDD[String] = lineRdd.flatMap(line => line.split(" "))
val wordToOneRdd: RDD[(String, Long)] = wordRdd.map {
  word => {
    (word, System.currentTimeMillis())
    }
}
// 增加缓存,避免再重新跑一个 job 做 checkpoint
wordToOneRdd.cache()
// 数据检查点:针对 wordToOneRdd 做检查点计算
wordToOneRdd.checkpoint()
// 触发执行逻辑
wordToOneRdd.collect().foreach(println)

缓存和检查点区别

  • Cache 缓存只是将数据保存起来,不切断血缘依赖。Checkpoint 检查点切断血缘依赖
  • Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint 的数据通常存储在 HDFS 等容错、高可用的文件系统,可靠性高
  • 建议对 checkpoint()的 RDD 使用 Cache 缓存,这样 checkpoint 的 job 只需从 Cache 缓存中读取数据即可,否则需要再从头计算一次 RDD

3.7 RDD 文件读取与保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。文件格式分为:text 文件、csv 文件、sequence 文件以及 Object 文件;文件系统分为:本地文件系统、HDFS、HBASE 以及数据库

// 读取输入文件
val inputRDD: RDD[String] = sc.textFile("input/1.txt")
// 保存数据
inputRDD.saveAsTextFile("output")

//quenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件(Flat File)
// 保存数据为 SequenceFile
dataRDD.saveAsSequenceFile("output")
// 读取 SequenceFile 文件
sc.sequenceFile[Int,Int]("output").collect().foreach(println)

//对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制
dataRDD.saveAsObjectFile("output")
// 读取数据
sc.objectFile[Int]("output").collect().foreach(println)

4、RDD其他概念

4.1 RDD序列化

闭包检查

从计算的角度, 算子以外的代码都是在Driver 端执行, 算子里面的代码都是在Executor 端执行。那么在 scala 的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给Executor 端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列化,这个操作我们称之为闭包检测。Scala2.12 版本后闭包编译方式发生了改变

序列化方法和属性

从计算的角度, 算子以外的代码都是在Driver 端执行, 算子里面的代码都是在Executor

Kryo 序列化框架

参考地址: https://github.com/EsotericSoftware/kryo

Java 的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也比较大。Spark 出于性能的考虑,Spark2.0 开始支持另外一种Kryo 序列化机制。Kryo 速度是 Serializable 的 10 倍。当 RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型已经在 Spark 内部使用 Kryo 来序列化。注意:即使使用Kryo 序列化,也要继承Serializable 接口

4.2 RDD 依赖关系

RDD 的Lineage 会记录RDD 的元数据信息和转换行为,当该RDD 的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区,RDD存在两种依赖关系类型宽依赖(wide dependency/shuffle dependency) 窄依赖(narrow dependency)

  • 窄依赖:父 RDD 的一个分区只会被子 RDD 的一个分区依赖;窄依赖的多个分区可以并行计算; 窄依赖的一个分区的数据如果丢失只需要重新计算对应的分区的数据就可以了
  • 宽依赖:父 RDD 的一个分区会被子 RDD 的多个分区依赖(涉及到 shuffle);对于宽依赖,必须等到上一阶段计算完成才能计算下一阶段
    Spark3学习笔记_第3张图片
// 查看血缘关系
println(fileRDD.toDebugString)
//查看依赖
println(wordRDD.dependencies)

4.3 DAG 的生成和划分 Stage

DAG(Directed Acyclic Graph 有向无环图)指的是数据转换执行的过程,有方向,无闭环(其实就是 RDD 执行的流程); 原始的 RDD 通过一系列的转换操作就形成了 DAG 有向无环图,任务执行时,可以按照 DAG 的描述,执行真正的计算(数据被操作的一个过程)

对于DAG的边界,开始通过 SparkContext 创建的 RDD;触发 Action结束,一旦触发 Action 就形成了一个完整的 DAG。RDD 任务切分中间分为:Application、Job、Stage 和 Task,注意:Application->Job->Stage->Task 每一层都是 1 对 n 的关系

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

Spark3学习笔记_第4张图片

一个 Spark 程序可以有多个 DAG(有几个 Action,就有几个 DAG,图最后只有一个 Action那么就是一个 DAG)。一个 DAG 可以有多个 Stage(根据宽依赖/shuffle 进行划分)。同一个 Stage 可以有多个 Task 并行执行(task 数=分区数,如图Stage1 中有三个分区 P1、P2、P3,对应的也有三个 Task)。可以看到这个 DAG 中只 reduceByKey 操作是一个宽依赖,Spark 内核会以此为边界将其前后划分成不同的 Stage。同时我们可以注意到,在图中 Stage1 中,从 textFile 到 flatMap 到 map 都是窄依赖,这几步操作可以形成一个流水线操作,通过 flatMap 操作生成的 partition 可以不用等待整个 RDD 计算结束,而是继续进行 map 操作,这样大大提高了计算的效率

Spark3学习笔记_第5张图片

  • 为什么要划分 Stage? --并行计算

一个复杂的业务逻辑如果有 shuffle,那么就意味着前面阶段产生结果后,才能执行下一个阶段,即下一个阶段的计算要依赖上一个阶段的数据。那么我们按照 shuffle 进行划分(也就是按照宽依赖就行划分),就可以将一个 DAG 划分成多个 Stage/阶段,在同一个 Stage 中,会有多个算子操作,可以形成一个 pipeline 流水线,流水线内的多个平行的分区可以并行执行

  • 如何划分 DAG 的 stage?

对于窄依赖,partition 的转换处理在 stage 中完成计算,不划分(将窄依赖尽量放在在同一个 stage 中,可以实现流水线计算)。对于宽依赖,由于有 shuffle 的存在,只能在父 RDD 处理完成后,才能开始接下来的计算,也就是说需要要划分 stage

**总结:**Spark 会根据 shuffle/宽依赖使用回溯算法来对 DAG 进行 Stage 划分,从后往前,遇到宽依赖就断开,遇到窄依赖就把当前的 RDD 加入到当前的 stage/阶段中

4.4 RDD 分区器

Spark 目前支持 Hash 分区和 Range 分区,和用户自定义分区。Hash 分区为当前的默认分区。分区器直接决定了 RDD 中分区的个数、RDD 中每条数据经过 Shuffle 后进入哪个分区,进而决定了 Reduce 的个数。

  • Hash 分区:对于给定的 key,计算其 hashCode,并除以分区个数取余
  • Range 分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
// 自定义分区
object Spark01_RDD_Part {

    def main(args: Array[String]): Unit = {
        val sparConf = new SparkConf().setMaster("local").setAppName("WordCount")
        val sc = new SparkContext(sparConf)

        val rdd = sc.makeRDD(List(
            ("nba", "xxxxxxxxx"),
            ("cba", "xxxxxxxxx"),
            ("wnba", "xxxxxxxxx"),
            ("nba", "xxxxxxxxx"),
        ),3)
        val partRDD: RDD[(String, String)] = rdd.partitionBy( new MyPartitioner )

        partRDD.saveAsTextFile("output")

        sc.stop()
    }

    /**
      * 自定义分区器
      * 1. 继承Partitioner
      * 2. 重写方法
      */
    class MyPartitioner extends Partitioner{
        // 分区数量
        override def numPartitions: Int = 3

        // 根据数据的key值返回数据所在的分区索引(从0开始)
        override def getPartition(key: Any): Int = {
            key match {
                case "nba" => 0
                case "wnba" => 1
                case _ => 2
            }
        }
    }
}

5、RDD累加器

累加器用来把 Executor 端变量信息聚合到 Driver 端。在 Driver 程序中定义的变量,在Executor 端的每个 Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后,传回 Driver 端进行 merge

系统累加器

// 系统累加器,不用累加器会发现连累加都无法正常使用
// 获取系统累加器
// Spark默认就提供了简单数据聚合的累加器
val sumAcc = sc.longAccumulator("sum")

//sc.doubleAccumulator
//sc.collectionAccumulator

val mapRDD = rdd.map(
    num => {
        // 使用累加器
        sumAcc.add(num)
        num
    }
)

// 获取累加器的值
// 少加:转换算子中调用累加器,如果没有行动算子的话,那么不会执行
// 多加:转换算子中调用累加器,如果没有行动算子的话,那么不会执行
// 一般情况下,累加器会放置在行动算子进行操作
mapRDD.collect()
mapRDD.collect()
println(sumAcc.value)
sc.stop()

自定义累加器

object Spark04_Acc_WordCount {

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

        val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
        val sc = new SparkContext(sparConf)

        val rdd = sc.makeRDD(List("hello", "spark", "hello"))

        // 累加器 : WordCount
        // 创建累加器对象
        val wcAcc = new MyAccumulator()
        // 向Spark进行注册
        sc.register(wcAcc, "wordCountAcc")

        rdd.foreach(
            word => {
                // 数据的累加(使用累加器)
                wcAcc.add(word)
            }
        )

        // 获取累加器累加的结果
        println(wcAcc.value)

        sc.stop()

    }
    /*
      自定义数据累加器:WordCount

      1. 继承AccumulatorV2, 定义泛型
         IN : 累加器输入的数据类型 String
         OUT : 累加器返回的数据类型 mutable.Map[String, Long]

      2. 重写方法(6)
     */
    class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Long]] {

        private var wcMap = mutable.Map[String, Long]()

        // 判断是否初始状态
        override def isZero: Boolean = {
            wcMap.isEmpty
        }

        override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = {
            new MyAccumulator()
        }

        override def reset(): Unit = {
            wcMap.clear()
        }

        // 获取累加器需要计算的值
        override def add(word: String): Unit = {
            val newCnt = wcMap.getOrElse(word, 0L) + 1
            wcMap.update(word, newCnt)
        }

        // Driver合并多个累加器
        override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = {

            val map1 = this.wcMap
            val map2 = other.value

            map2.foreach{
                case ( word, count ) => {
                    val newCount = map1.getOrElse(word, 0L) + count
                    map1.update(word, newCount)
                }
            }
        }

        // 累加器结果
        override def value: mutable.Map[String, Long] = {
            wcMap
        }
    }
}

6、RDD广播变量

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark 会为每个任务分别发送

object BroadcastVariablesTest {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")

    //不使用广播变量
    val kvFruit: RDD[(Int, String)] = sc.parallelize(List((1,"apple"),(2,"orange"),(3,"banana"),(4,"grape")))
    val fruitMap: collection.Map[Int, String] =kvFruit.collectAsMap
    //scala.collection.Map[Int,String] = Map(2 -> orange, 4 -> grape, 1 -> apple, 3 -> banana)
    val fruitIds: RDD[Int] = sc.parallelize(List(2,4,1,3))
    //根据水果编号取水果名称
    val fruitNames: RDD[String] = fruitIds.map(x=>fruitMap(x))
    fruitNames.foreach(println)
    //注意:以上代码看似一点问题没有,但是考虑到数据量如果较大,且Task数较多,
    //那么会导致,被各个Task共用到的fruitMap会被多次传输
    //应该要减少fruitMap的传输,一台机器上一个,被该台机器中的Task共用即可
    //如何做到?---使用广播变量
    //注意:广播变量的值不能被修改,如需修改可以将数据存到外部数据源,如MySQL、Redis
    println("=====================")
    val BroadcastFruitMap: Broadcast[collection.Map[Int, String]] = sc.broadcast(fruitMap)
    val fruitNames2: RDD[String] = fruitIds.map(x=>BroadcastFruitMap.value(x))
    fruitNames2.foreach(println)

  }
}

三、Spark SQL

1、SparkSQL 概述

Spark SQL 是Spark 用于结构化数据(structured data)处理的 Spark 模块

1.1 SparkSQL的发展

Spark3学习笔记_第6张图片

  • Hive

    Hive 实现了 SQL on Hadoop,使用 MapReduce 执行任务 简化了 MapReduce 任务。但Hive 的查询延迟比较高,原因是使用 MapReduce 做计算。

  • Shark

    Shark 改写 Hive 的物理执行计划, 使用 Spark 代替 MapReduce 物理引擎 使用列式内存存储。以上两点使得 Shark 的查询效率很高。但是Shark 执行计划的生成严重依赖 Hive,想要增加新的优化非常困难;Hive 是进程级别的并行,Spark 是线程级别的并行,所以 Hive 中很多线程不安全的代码不适用于 Spark;由于以上问题,Shark 维护了 Hive 的一个分支,并且无法合并进主线,难以为继;在 2014 年 7 月 1 日的 Spark Summit 上,Databricks 宣布终止对 Shark 的开发,将重点放到 Spark SQL 上,但也因此发展出两个支线:SparkSQL 和 Hive on Spark

    其中 SparkSQL 作为 Spark 生态的一员继续发展,而不再受限于 Hive,只是兼容 Hive;而Hive on Spark 是一个Hive 的发展计划,该计划将 Spark 作为Hive 的底层引擎之一,也就是说,Hive 将不再受限于一个引擎,可以采用 Map-Reduce、Tez、Spark 等引擎

  • SparkSQL-DataFrame

    Spark SQL 执行计划和优化交给优化器 Catalyst;内建了一套简单的 SQL 解析器,可以不使用 HQL;还引入和 DataFrame 这样的 DSL API,完全可以不依赖任何 Hive 的组件。但是对于初期版本的 SparkSQL,依然有挺多问题,例如只能支持 SQL 的使用,不能很好的兼容命令式,入口不够统一等。

  • SparkSQL-Dataset

    SparkSQL 在 1.6 时代,增加了一个新的 API,叫做 Dataset,Dataset 统一和结合了 SQL 的访问和命令式 API 的使用,这是一个划时代的进步。在 Dataset 中可以轻易的做到使用 SQL 查询并且筛选数据,然后使用命令式 API 进行探索式分析

1.2 Hive and SparkSQL

  • Hive 是将 SQL 转为 MapReduce。
  • SparkSQL 可以理解成是将 SQL 解析成:“RDD + 优化” 再执行

1.3 SparkSQL 特点

  • 易整合,无缝的整合了 SQL 查询和 Spark 编程
  • 统一的数据访问,使用相同的方式连接不同的数据源
  • 兼容 Hive,在已有的仓库上直接运行 SQL 或者 HiveQL
  • 标准数据连接,通过 JDBC 或者 ODBC 来连接

1.4 DataFrame简介

在 Spark 中,DataFrame 是一种以 RDD 为基础的分布式数据集,类似于传统数据库中的二维表格。DataFrame 与 RDD 的主要区别在于,前者带有 schema 元信息,即 DataFrame 所表示的二维表数据集的每一列都带有名称和类型。这使得 Spark SQL 得以洞察更多的结构信息,从而对藏于 DataFrame 背后的数据源以及作用于 DataFrame 之上的变换进行了针对性的优化,最终达到大幅提升运行时效率的目标。反观 RDD,由于无从得知所存数据元素的具体内部结构,Spark Core 只能在 stage 层面进行简单、通用的流水线优化。

DataFrame 的前身是 SchemaRDD,从 Spark 1.3.0 开始 SchemaRDD 更名为 DataFrame,并不再直接继承自 RDD,而是自己实现了 RDD 的绝大多数功能。同时,与Hive 类似,DataFrame 也支持嵌套数据类型(struct、array 和 map)。从 API 易用性的角度上看,DataFrame API 提供的是一套高层的关系操作,比函数式的 RDD API 要更加友好,门槛更低

总结:DataFrame 就是一个分布式的表DataFrame = RDD - 泛型 + SQL 的操作 + 优化

1.5 DataSet

DataSet 是分布式数据集合。DataSet 是Spark 1.6 中添加的一个新抽象,是DataFrame的一个扩展。它提供了RDD 的优势(强类型,使用强大的 lambda 函数的能力)以及SparkSQL 优化执行引擎的优点。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 来表示。获取数据时需要指定顺序

1.6 RDD、DataFrame、DataSet 的区别

Spark3学习笔记_第7张图片

  • RDD[Person]:以 Person 为类型参数,但不了解 其内部结构
  • DataFrame:提供了详细的结构信息 schema 列的名称和类型。这样看起来就像一张表了
  • DataSet[Person]:不光有 schema 信息,还有类型信息

总结

  • DataFrame = RDD - 泛型 + Schema + SQL + 优化
  • DataSet = DataFrame + 泛型
  • DataSet = RDD + Schema + SQL + 优化

2、SparkSQL 核心编程

2.1 概述

  • 在 spark2.0 版本之前

    SQLContext 是创建 DataFrame 和执行 SQL 的入口。HiveContext 通过 hive sql 语句操作 hive 表数据,兼容 hive 操作,hiveContext 继承自 SQLContext。

  • 在 spark2.0 之后

    这些都统一于 SparkSession,SparkSession 封装了 SqlContext 及 HiveContext;实现了 SQLContext 及 HiveContext 所有功能;通过 SparkSession 还可以获取到 SparkConetxt

2.2 创建 DataFrame/DataSet

读取文本文件

# =====读取文本文件
# 在本地创建一个文件,有 id、name、age 三列,用空格分隔,然后上传到 hdfs 上
vim /root/person.txt
1 zhangsan 20
2 lisi 29
3 wangwu 25
4 zhaoliu 30
5 tianqi 35
6 kobe 40

# 打开 spark-shell
# 创建 RDD
spark/bin/spark-shell
# RDD[Array[String]]
val lineRDD= sc.textFile("hdfs://node1:8020/person.txt").map(_.split(" ")) 
# 定义 case class(相当于表的 schema)
case class Person(id:Int, name:String, age:Int)
# 将 RDD 和 case class 关联
# RDD[Person]
val personRDD = lineRDD.map(x => Person(x(0).toInt, x(1), x(2).toInt))
# 将 RDD 转换成 DataFrame
# DataFrame
val personDF = personRDD.toDF
# 查看数据和 schema
personDF.show
personDF.printSchema
# 注册表
personDF.createOrReplaceTempView("t_person")
# 执行 SQL
spark.sql("select id,name from t_person where id > 3").show
# 也可以通过 SparkSession 构建 DataFrame
val dataFrame=spark.read.text("hdfs://node1:8020/person.txt")
#注意:直接读取的文本文件没有完整schema信息
dataFrame.show
dataFrame.printSchema

读取 json 文件读取 parquet 文件

val jsonDF= spark.read.json("file:///resources/people.json")
# 接下来就可以使用 DataFrame 的函数操作
jsonDF.show
# 注意:直接读取 json 文件有 schema 信息,因为 json 文件本身含有 Schema 信息,SparkSQL 可以自动解析

# 读取 parquet 文件
val parquetDF=spark.read.parquet("file:///resources/users.parquet")
parquetDF.show
# 注意:直接读取 parquet 文件有 schema 信息,因为 parquet 文件中保存了列的信息

2.3 两种查询风格:DSL 和 SQL

首先进行准备工作,先读取文件并转换为 DataFrame 或 DataSet

val lineRDD= sc.textFile("hdfs://node1:8020/person.txt").map(_.split(" "))
case class Person(id:Int, name:String, age:Int)
val personRDD = lineRDD.map(x => Person(x(0).toInt, x(1), x(2).toInt))
val personDF = personRDD.toDF
personDF.show
//val personDS = personRDD.toDS
//personDS.show

DSL 风格:SparkSQL 提供了一个领域特定语言(DSL)以方便操作结构化数据

// 查看 name 字段的数据
personDF.select(personDF.col("name")).show
personDF.select(personDF("name")).show
personDF.select(col("name")).show
personDF.select("name").show

// 查看 name 和 age 字段数据
personDF.select("name", "age").show

// 查询所有的 name 和 age,并将 age+1
personDF.select(personDF.col("name"), personDF.col("age") + 1).show
personDF.select(personDF("name"), personDF("age") + 1).show
personDF.select(col("name"), col("age") + 1).show
personDF.select("name","age").show
//personDF.select("name", "age"+1).show
personDF.select($"name",$"age",$"age"+1).show

// 过滤 age 大于等于 25 的,使用 filter 方法过滤
personDF.filter(col("age") >= 25).show
personDF.filter($"age" >25).show

// 统计年龄大于 30 的人数
personDF.filter(col("age")>30).count()
personDF.filter($"age" >30).count()

// 按年龄进行分组并统计相同年龄的人数
personDF.groupBy("age").count().show

SQL 风格:DataFrame 的一个强大之处就是我们可以将它看作是一个关系型数据表,然后可以通过在程序中使用 spark.sql() 来执行 SQL 查询,结果将作为一个 DataFrame 返回

// 如果想使用 SQL 风格的语法,需要将 DataFrame 注册成表,采用如下的方式
personDF.createOrReplaceTempView("t_person")
spark.sql("select * from t_person").show

// 显示表的描述信息
spark.sql("desc t_person").show

// 查询年龄最大的前两名
spark.sql("select * from t_person order by age desc limit 2").show

// 查询年龄大于 30 的人的信息
spark.sql("select * from t_person where age > 30 ").show

// 使用 SQL 风格完成 DSL 中的需求
spark.sql("select name, age + 1 from t_person").show
spark.sql("select name, age from t_person where age > 25").show
spark.sql("select count(age) from t_person where age > 30").show
spark.sql("select age, count(age) from t_person group by age").show

总结

  • DataFrame 和 DataSet 都可以通过 RDD 来进行创建
  • 也可以通过读取普通文本创建–注意:直接读取没有完整的约束,需要通过 RDD+Schema
  • 通过 josn/parquet 会有完整的约束
  • 不管是 DataFrame 还是 DataSet 都可以注册成表,之后就可以使用 SQL 进行查询了! 也可以使用 DSL!

2.4 RDD、DataFrame、DataSet 三者的关系

Spark3学习笔记_第8张图片

共性:

  • 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 之后可以很自由的获得每一行的信息
object Spark01_SparkSQL_Basic {

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

        // TODO 创建SparkSQL的运行环境
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("sparkSQL")
        val spark = SparkSession.builder().config(sparkConf).getOrCreate()
        import spark.implicits._

        // RDD <=> DataFrame
        val rdd = spark.sparkContext.makeRDD(List((1, "zhangsan", 30), (2, "lisi", 40)))
        val df: DataFrame = rdd.toDF("id", "name", "age")
        val rowRDD: RDD[Row] = df.rdd

        // DataFrame <=> DataSet
        val ds: Dataset[User] = df.as[User]
        val df1: DataFrame = ds.toDF()

        // RDD <=> DataSet
        val ds1: Dataset[User] = rdd.map {
            case (id, name, age) => {
                User(id, name, age)
            }
        }.toDS()
        val userRDD: RDD[User] = ds1.rdd


        // TODO 关闭环境
        spark.close()
    }
    case class User( id:Int, name:String, age:Int )
}

3、Spark实战

3.1 Spark SQL 完成 WordCount

引入依赖

<dependency>
  <groupId>org.apache.sparkgroupId>
  <artifactId>spark-sql_2.12artifactId>
  <version>3.0.0version>
dependency>

SQL风格

object WordCount {
  def main(args: Array[String]): Unit = {
    //1.创建SparkSession
    val spark: SparkSession = SparkSession.builder().master("local[*]").appName("SparkSQL").getOrCreate()
    val sc: SparkContext = spark.sparkContext
    sc.setLogLevel("WARN")
    //2.读取文件
    val fileDF: DataFrame = spark.read.text("D:\\data\\words.txt")
    val fileDS: Dataset[String] = spark.read.textFile("D:\\data\\words.txt")
    //fileDF.show()
    //fileDS.show()
    //3.对每一行按照空格进行切分并压平
    //fileDF.flatMap(_.split(" ")) //注意:错误,因为DF没有泛型,不知道_是String
    import spark.implicits._
    val wordDS: Dataset[String] = fileDS.flatMap(_.split(" "))//注意:正确,因为DS有泛型,知道_是String
    //wordDS.show()
    /*
    +-----+
    |value|
    +-----+
    |hello|
    |   me|
    |hello|
    |  you|
      ...
     */
    //4.对上面的数据进行WordCount
    wordDS.createOrReplaceTempView("t_word")
    val sql =
      """
        |select value ,count(value) as count
        |from t_word
        |group by value
        |order by count desc
      """.stripMargin
    spark.sql(sql).show()

    sc.stop()
    spark.stop()
  }
}

DSL 风格

object WordCount2 {
  def main(args: Array[String]): Unit = {
    //1.创建SparkSession
    val spark: SparkSession = SparkSession.builder().master("local[*]").appName("SparkSQL").getOrCreate()
    val sc: SparkContext = spark.sparkContext
    sc.setLogLevel("WARN")
    //2.读取文件
    val fileDF: DataFrame = spark.read.text("D:\\data\\words.txt")
    val fileDS: Dataset[String] = spark.read.textFile("D:\\data\\words.txt")
    //fileDF.show()
    //fileDS.show()
    //3.对每一行按照空格进行切分并压平
    //fileDF.flatMap(_.split(" ")) //注意:错误,因为DF没有泛型,不知道_是String
    import spark.implicits._
    val wordDS: Dataset[String] = fileDS.flatMap(_.split(" "))//注意:正确,因为DS有泛型,知道_是String
    //wordDS.show()
    /*
    +-----+
    |value|
    +-----+
    |hello|
    |   me|
    |hello|
    |  you|
      ...
     */
    //4.对上面的数据进行WordCount
    wordDS.groupBy("value").count().orderBy($"count".desc).show()

    sc.stop()
    spark.stop()
  }
}

3.2 用户自定义函数

用户可以通过 spark.udf 功能添加自定义函数,实现自定义功能

对于UDF,见如下基础功能

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

    // TODO 创建SparkSQL的运行环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("sparkSQL")
    val spark = SparkSession.builder().config(sparkConf).getOrCreate()
    import spark.implicits._
    // 1)创建DataFrame
    val df = spark.read.json("datas/user.json")
    df.createOrReplaceTempView("user")
    //2)注册UDF
    spark.udf.register("prefixName", (name:String) => {
        "Name: " + name
    })
    //应用UDF
    spark.sql("select age, prefixName(username) from user").show

    // TODO 关闭环境
    spark.close()
}

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

object Spark03_SparkSQL_UDAF1 {

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

        // TODO 创建SparkSQL的运行环境
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("sparkSQL")
        val spark = SparkSession.builder().config(sparkConf).getOrCreate()

        val df = spark.read.json("datas/user.json")
        df.createOrReplaceTempView("user")
        spark.udf.register("ageAvg", functions.udaf(new MyAvgUDAF()))
        spark.sql("select ageAvg(age) from user").show

        // 早期版本中,spark不能在sql中使用强类型UDAF操作
        // SQL & DSL
        // 早期的UDAF强类型聚合函数使用DSL语法操作
        // val ds: Dataset[User] = df.as[User]

        // 将UDAF函数转换为查询的列对象,注意下面的in要修改成User
        // val udafCol: TypedColumn[User, Long] = new MyAvgUDAF().toColumn
        // ds.select(udafCol).show
        
        // TODO 关闭环境
        spark.close()
    }
    /*
     自定义聚合函数类:计算年龄的平均值
     1. 继承org.apache.spark.sql.expressions.Aggregator, 定义泛型
         IN : 输入的数据类型 Long
         BUF : 缓冲区的数据类型 Buff
         OUT : 输出的数据类型 Long
     2. 重写方法(6)
     */
    case class Buff( var total:Long, var count:Long )
    class MyAvgUDAF extends Aggregator[Long, Buff, Long]{
        // z & zero : 初始值或零值
        // 缓冲区的初始化
        override def zero: Buff = {
            Buff(0L,0L)
        }

        // 根据输入的数据更新缓冲区的数据
        override def reduce(buff: Buff, in: Long): Buff = {
            buff.total = buff.total + in
            buff.count = buff.count + 1
            buff
        }

        // 合并缓冲区
        override def merge(buff1: Buff, buff2: Buff): Buff = {
            buff1.total = buff1.total + buff2.total
            buff1.count = buff1.count + buff2.count
            buff1
        }

        //计算结果
        override def finish(buff: Buff): Long = {
            buff.total / buff.count
        }

        // 缓冲区的编码操作
        override def bufferEncoder: Encoder[Buff] = Encoders.product

        // 输出的编码操作
        override def outputEncoder: Encoder[Long] = Encoders.scalaLong
    }
}

4、数据的加载和保存

4.1 通用的加载和保存方式

# ===============加载数据============
# spark.read.load 是加载数据的通用方法
# 如果读取不同格式的数据,可以对不同的数据格式进行设定
spark.read.format("…")[.option("…")].load("…")
# format("…"):指定加载的数据类型,包括"csv"、"jdbc"、"json"、"orc"、"parquet"和"textFile"
# load("…"):在"csv"、"jdbc"、"json"、"orc"、"parquet"和"textFile"格式下需要传入加载数据的路径
# option("…"):在"jdbc"格式下需要传入 JDBC 相应参数,url、user、password 和 dbtable
# 接在文件上进行查询: 文件格式.`文件路径`
spark.sql("select * from json.`/opt/module/data/user.json`").show


# ==============保存数据==========
# df.write.save 是保存数据的通用方法
df.write.format("…")[.option("…")].save("…")
# format("…"):指定保存的数据类型,包括"csv"、"jdbc"、"json"、"orc"、"parquet"和"textFile"
# save ("…"):在"csv"、"orc"、"parquet"和"textFile"格式下需要传入保存数据的路径
# option("…"):在"jdbc"格式下需要传入 JDBC 相应参数,url、user、password 和 dbtable保存操作可以使用 SaveMode, 用来指明如何处理数据,使用 mode()方法来设置。
# 有一点很重要: 这些 SaveMode 都是没有加锁的, 也不是原子操作
df.write.mode("append").json("/opt/module/data/output")

Scala/Java Any Language Meaning
SaveMode.ErrorIfExists(default) “error”(default) 如果文件已经存在则抛出异常
SaveMode.Append “append” 如果文件已经存在则追加
SaveMode.Overwrite “overwrite” 如果文件已经存在则覆盖
SaveMode.Ignore “ignore” 如果文件已经存在则忽略

4.2 Parquet

Spark SQL 的默认数据源为 Parquet 格式。Parquet 是一种能够有效存储嵌套数据的列式存储格式。数据源为 Parquet 文件时,Spark SQL 可以方便的执行所有的操作,不需要使用 format。修改配置项spark.sql.sources.default,可修改默认数据源格式

# 1)加载数据
val df = spark.read.load("examples/src/main/resources/users.parquet")
df.show
# 2)保存数据
var df = spark.read.json("/opt/module/data/input/people.json")
# 保存为 parquet 格式
df.write.mode("append").save("/opt/module/data/output")

4.3 JSON

Spark SQL 能够自动推测 JSON 数据集的结构,并将它加载为一个Dataset[Row]. 可以通过 SparkSession.read.json()去加载 JSON 文件。注意:Spark 读取的 JSON 文件不是传统的JSON 文件,每一行都应该是一个 JSON 串

#json格式如下
{"name":"Michael"}
{"name":"Andy""age":30}
[{"name":"Justin""age":19},{"name":"Justin""age":19}]

# 导入隐式转换
import spark.implicits._
# 加载 JSON 文件
val path = "/opt/module/spark-local/people.json"
val peopleDF = spark.read.json(path)
# 创建临时表
peopleDF.createOrReplaceTempView("people")
# 查询
val teenagerNamesDF = spark.sql("SELECT  name  FROM  people  WHERE  age  BETWEEN  13 AND 19")
teenagerNamesDF.show()

4.4 CSV

Spark SQL 可以配置 CSV 文件的列表信息,读取CSV 文件,CSV 文件的第一行设置为数据列

spark.read.format("csv").option("sep", ";").option("inferSchema","true").option("header", "true").load("data/user.csv")

4.5 MySQL

Spark SQL 可以通过 JDBC 从关系型数据库中读取数据的方式创建DataFrame,通过对DataFrame 一系列的计算后,还可以将数据再写回关系型数据库中。如果使用spark-shell 操作,可在启动shell 时指定相关的数据库驱动路径或者将相关的数据库驱动放到 spark 的类路径下

bin/spark-shell --jars mysql-connector-java-5.1.27-bin.jar

演示在Idea 中通过 JDBC 对 Mysql 进行操作,导入依赖关系

<dependency>
  <groupId>mysqlgroupId>
  <artifactId>mysql-connector-javaartifactId>
  <version>5.1.27version>
dependency>
object Spark04_SparkSQL_JDBC {

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

        // TODO 创建SparkSQL的运行环境
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("sparkSQL")
        val spark = SparkSession.builder().config(sparkConf).getOrCreate()
        import spark.implicits._

        // 读取MySQL数据
        val df = spark.read
                .format("jdbc")
                .option("url", "jdbc:mysql://hadoop102:3306/spark-sql")
                .option("driver", "com.mysql.jdbc.Driver")
                .option("user", "root")
                .option("password", "123123")
                .option("dbtable", "user")
                .load()
        //df.show

        // 保存数据
        df.write
                .format("jdbc")
                .option("url", "jdbc:mysql://hadoop102:3306/spark-sql")
                .option("driver", "com.mysql.jdbc.Driver")
                .option("user", "root")
                .option("password", "123123")
                .option("dbtable", "user1")
                .mode(SaveMode.Append)
                .save()

        // TODO 关闭环境
        spark.close()
    }
}

4.6 Hive

Apache Hive 是 Hadoop 上的 SQL 引擎,Spark SQL 编译时可以包含 Hive 支持,也可以不包含。包含 Hive 支持的 Spark SQL 可以支持 Hive 表访问、UDF (用户自定义函数)以及 Hive 查询语言(HiveQL/HQL)等。

若要把 Spark SQL 连接到一个部署好的 Hive 上,必须把 hive-site.xml 复制到Spark 的配置文件目录中($SPARK_HOME/conf)。即使没有部署好 Hive,Spark SQL 也可以运行。 需要注意的是,如果你没有部署好 Hive,Spark SQL 会在当前的工作目录中创建出自己的 Hive 元数据仓库,叫作 metastore_db。此外,如果你尝试使用 HiveQL 中的CREATE TABLE (并非 CREATE EXTERNAL TABLE)语句来创建表,这些表会被放在你默认的文件系统中的 /user/hive/warehouse 目录中(如果你的 classpath 中有配好的hdfs-site.xml默认的文件系统就是 HDFS,否则就是本地文件系统)。spark-shell 默认是Hive 支持的;代码中是默认不支持的,需要手动指定(加一个参数即可)

内嵌的 HIVE,如果使用 Spark 内嵌的 Hive, 则什么都不用做, 直接使用即可。Hive 的元数据存储在 derby 中, 默认仓库地址:$SPARK_HOME/spark-warehouse,在实际使用中, 几乎没有任何人会使用内置的 Hive

spark.sql("show tables").show
spark.sql("create table aa(id int)")
spark.sql("load data local inpath 'input/ids.txt' into table aa")
spark.sql("select * from aa").show

外部的 HIVE,如果想连接外部已经部署好的Hive,需要通过以下几个步骤:

  • Spark 要接管 Hive 需要把hive-site.xml 拷贝到conf/目录下
  • 把 Mysql 的驱动 copy 到 jars/目录下
  • 如果访问不到 hdfs,则需要把core-site.xmlhdfs-site.xml 拷贝到 conf/目录下
  • 重启 spark-shell
spark.sql("show tables").show
# Spark SQL CLI 可以很方便的在本地运行Hive 元数据服务以及从命令行执行查询任务。在
# Spark 目录下执行如下命令启动 Spark SQL CLI,直接执行 SQL 语句,类似一Hive 窗口
bin/spark-sql

# Spark Thrift Server 是Spark 社区基于HiveServer2 实现的一个Thrift 服务。旨在无缝兼容HiveServer2。
# 因为 Spark Thrift Server 的接口和协议都和HiveServer2 完全一致,因此我们部署好 Spark Thrift Server 后,可以直接使用hive 的 beeline 访问Spark Thrift Server 执行相关语句。
# Spark Thrift Server 的目的也只是取代HiveServer2,因此它依旧可以和 Hive Metastore 进行交互,获取到hive 的元数据
sbin/start-thriftserver.sh
# 使用 beeline 连接 Thrift Server
bin/beeline -u jdbc:hive2://Hadoop102:10000 -n root

最后是IDEA操作Hive,首先导入依赖

<dependency>
  <groupId>org.apache.sparkgroupId>
  <artifactId>spark-hive_2.12artifactId>
  <version>3.0.0version>
dependency>

<dependency>
  <groupId>org.apache.hivegroupId>
  <artifactId>hive-execartifactId>
  <version>1.2.1version>
dependency>
<dependency>
  <groupId>mysqlgroupId>
  <artifactId>mysql-connector-javaartifactId>
  <version>5.1.27version>
dependency>

然后将hive-site.xml 文件拷贝到项目的 resources 目录中,代码实现,注意:在开发工具中创建数据库默认是在本地仓库,通过参数修改数据库仓库的地址:config("spark.sql.warehouse.dir", "hdfs://hadoop102:8020/user/hive/warehouse")

object Spark05_SparkSQL_Hive {

    def main(args: Array[String]): Unit = {
        // 权限问题,此处的 root 改为你们自己的 hadoop 用户名称
        System.setProperty("HADOOP_USER_NAME", "atguigu")
        // TODO 创建SparkSQL的运行环境
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("sparkSQL")
        val spark = SparkSession.builder().enableHiveSupport().config(sparkConf).getOrCreate()

        // 使用SparkSQL连接外置的Hive
        // 1. 拷贝Hive-size.xml文件到classpath下
        // 2. 启用Hive的支持
        // 3. 增加对应的依赖关系(包含MySQL驱动)
        spark.sql("show tables").show

        // TODO 关闭环境
        spark.close()
    }
}

四、Spark Streaming

1、SparkStreaming 概述

1.1 简介

Spark Streaming 是一个基于 Spark Core 之上的实时计算框架,可以从很多数据源消费数据并对数据进行实时的处理,具有高吞吐量和容错能力强等特点。

Spark3学习笔记_第9张图片

和 Spark 基于 RDD 的概念很相似,Spark Streaming 使用离散化流(discretized stream)作为抽象表示,叫作 DStream。DStream 是随时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为 RDD 存在,而 DStream 是由这些 RDD 所组成的序列(因此得名“离散化”)。所以简单来将,DStream 就是对 RDD 在实时数据处理场景的一种封装

1.2 特点

  • 易用,可以像编写离线批处理一样去编写流式程序,支持 java/scala/python 语言
  • 容错,SparkStreaming 在没有额外代码和配置的情况下可以恢复丢失的工作
  • 易整合到 Spark 体系,流式处理与批处理和交互式查询相结合

1.3 整体流程

Spark3学习笔记_第10张图片

Spark Streaming 中,会有一个接收器组件 Receiver,作为一个长期运行的 task 跑在一个 Executor 上。Receiver 接收外部的数据流形成 input DStream。

DStream 会被按照时间间隔划分成一批一批的 RDD,当批处理间隔缩短到秒级时,便可以用于处理实时数据流。时间间隔的大小可以由参数指定,一般设在 500 毫秒到几秒之间。对 DStream 进行操作就是对 RDD 进行操作,计算处理的结果可以传给外部系统。Spark Streaming 接受到实时数据后,给数据分批次,然后传给 Spark Engine 处理最后生成该批次的结果。

1.4 数据抽象

这里使用 netcat 工具向 9999 端口不断的发送数据,通过 SparkStreaming 读取端口数据并统计不同单词出现的次数,首先添加依赖

<dependency>
  <groupId>org.apache.sparkgroupId>
  <artifactId>spark-streaming_2.12artifactId>
  <version>3.0.0version>
dependency>

编写demo代码

object SparkStreaming01_WordCount {

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

        // TODO 创建环境对象
        // StreamingContext创建时,需要传递两个参数
        // 第一个参数表示环境配置
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        // 第二个参数表示批量处理的周期(采集周期)
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        // TODO 逻辑处理
        // 获取端口数据
        val lines: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 9999)

        val words = lines.flatMap(_.split(" "))

        val wordToOne = words.map((_,1))

        val wordToCount: DStream[(String, Int)] = wordToOne.reduceByKey(_+_)

        wordToCount.print()

        // 由于SparkStreaming采集器是长期执行的任务,所以不能直接关闭
        // 如果main方法执行完毕,应用程序也会自动结束。所以不能让main执行完毕
        //ssc.stop()
        // 1. 启动采集器
        ssc.start()
        // 2. 等待采集器的关闭
        ssc.awaitTermination()
    }
}

// 启动程序并通过netcat 发送数据
// nc -lk 9999 
// hello spark

Spark Streaming 的基础抽象是 DStream(Discretized Stream,离散化数据流,连续不断的数据流),代表持续性的数据流和经过各种 Spark 算子操作后的结果数据流

  • DStream 本质上就是一系列时间上连续的 RDD
  • 对 DStream 的数据的进行操作也是按照 RDD 为单位来进行的

Spark3学习笔记_第11张图片

  • 容错性,底层 RDD 之间存在依赖关系,DStream 直接也有依赖关系,RDD 具有容错性,那么 DStream 也具有容错性
  • 准实时性/近实时性,Spark Streaming 将流式计算分解成多个 Spark Job,对于每一时间段数据的处理都会经过 Spark DAG 图分解以及 Spark 的任务集的调度过程。对于目前版本的 Spark Streaming 而言,其最小的 Batch Size 的选取在 0.5~5 秒钟之间

2、DStream 相关操作

2.1 DStream 创建

RDD队列

测试过程中,可以通过使用 ssc.queueStream(queueOfRDDs)来创建 DStream,每一个推送到这个队列中的RDD,都会作为一个DStream 处理

object SparkStreaming02_Queue {

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

        // TODO 创建环境对象
        // StreamingContext创建时,需要传递两个参数
        // 第一个参数表示环境配置
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        // 第二个参数表示批量处理的周期(采集周期)
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val rddQueue = new mutable.Queue[RDD[Int]]()

        val inputStream = ssc.queueStream(rddQueue,oneAtATime = false)
        val mappedStream = inputStream.map((_,1))
        val reducedStream = mappedStream.reduceByKey(_ + _)
        reducedStream.print()

        ssc.start()

        for (i <- 1 to 5) {
            rddQueue += ssc.sparkContext.makeRDD(1 to 300, 10)
            Thread.sleep(2000)
        }

        ssc.awaitTermination()
    }
}

自定义数据源

需要继承Receiver,并实现 onStart、onStop 方法来自定义数据源采集

object SparkStreaming03_DIY {

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

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val messageDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver())
        messageDS.print()

        ssc.start()
        ssc.awaitTermination()
    }
    /*
    自定义数据采集器
    1. 继承Receiver,定义泛型, 传递参数
    2. 重写方法
     */
    class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY) {
        private var flg = true
        override def onStart(): Unit = {
            new Thread(new Runnable {
                override def run(): Unit = {
                    while ( flg ) {
                        val message = "采集的数据为:" + new Random().nextInt(10).toString
                        store(message)
                        Thread.sleep(500)
                    }
                }
            }).start()
        }

        override def onStop(): Unit = {
            flg = false;
        }
    }
}

Kafka 数据源(面试、开发重点)

ReceiverAPI:需要一个专门的Executor 去接收数据,然后发送给其他的 Executor 做计算。存在的问题,接收数据的Executor 和计算的Executor 速度会有所不同,特别在接收数据的Executor 速度大于计算的Executor 速度,会导致计算数据的节点内存溢出。早期版本中提供此方式,当前版本不适用

DirectAPI:是由计算的Executor 来主动消费Kafka 的数据,速度由自身控制,这里只讲这个模式,首先导入依赖

<dependency>
    <groupId>org.apache.sparkgroupId>
    <artifactId>spark-streaming-kafka-0-10_2.12artifactId>
    <version>3.0.0version>
dependency>
<dependency>
    <groupId>com.fasterxml.jackson.coregroupId>
    <artifactId>jackson-coreartifactId>
    <version>2.10.1version>
dependency>

编写代码

object SparkStreaming04_Kafka {

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

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val kafkaPara: Map[String, Object] = Map[String, Object](
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104:9092",
            ConsumerConfig.GROUP_ID_CONFIG -> "atguigu",
            "key.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer",
            "value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer"
        )

        val kafkaDataDS: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
            ssc,
            LocationStrategies.PreferConsistent,
            ConsumerStrategies.Subscribe[String, String](Set("atguiguNew"), kafkaPara)
        )
        kafkaDataDS.map(_.value()).print()


        ssc.start()
        ssc.awaitTermination()
    }
}

// 查看Kafka 消费进度
// bin/kafka-consumer-groups.sh --describe --bootstrap-server hadoop102:9092 --group atguigu

2.2 DStream 转换

无状态转化操作

无状态转化操作就是把简单的RDD 转化操作应用到每个批次上,也就是转化DStream 中的每一个RDD,即每个批次的处理不依赖于之前批次的数据

Transformation 含义
map(func) 对 DStream 中的各个元素进行 func 函数操作,然后返回一个新的 DStream
flatMap(func) 与 map 方法类似,只不过各个输入项可以被输出为零个或多个输出项
filter(func) 过滤出所有函数 func 返回值为 true 的 DStream 元素并返回一个新的 DStream
union(otherStream) 将源 DStream 和输入参数为 otherDStream 的元素合并,并返回一个新的 DStream
reduceByKey(func, [numTasks]) 利用 func 函数对源 DStream 中的 key 进行聚合操作,然后返回新的(K,V)对构成的 DStream
join(otherStream, [numTasks]) 输入为(K,V)、(K,W)类型的 DStream,返回一个新的(K,(V,W)类型的 DStream
transform(func) 通过 RDD-to-RDD 函数作用于 DStream 中的各个 RDD,可以是任意的 RDD 操作,从而返回一个新的 RDD

Transform示例

object SparkStreaming06_State_Transform {

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

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val lines = ssc.socketTextStream("localhost", 9999)

        // transform方法可以将底层RDD获取到后进行操作
        // 1. DStream功能不完善
        // 2. 需要代码周期性的执行

        // Code : Driver端
        val newDS: DStream[String] = lines.transform(
            rdd => {
                // Code : Driver端,(周期性执行)
                rdd.map(
                    str => {
                        // Code : Executor端
                        str
                    }
                )
            }
        )
        // Code : Driver端
        val newDS1: DStream[String] = lines.map(
            data => {
                // Code : Executor端
                data
            }
        )
        ssc.start()
        ssc.awaitTermination()
    }

}

有状态转化操作

有状态转换包括基于追踪状态变化的转换(updateStateByKey)和滑动窗口的转换

object SparkStreaming05_State {

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

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))
        ssc.checkpoint("cp")

        // 无状态数据操作,只对当前的采集周期内的数据进行处理
        // 在某些场合下,需要保留数据统计结果(状态),实现数据的汇总
        // 使用有状态操作时,需要设定检查点路径
        val datas = ssc.socketTextStream("localhost", 9999)

        val wordToOne = datas.map((_,1))

        //val wordToCount = wordToOne.reduceByKey(_+_)

        // updateStateByKey:根据key对数据的状态进行更新
        // 传递的参数中含有两个值
        // 第一个值表示相同的key的value数据
        // 第二个值表示缓存区相同key的value数据
        val state = wordToOne.updateStateByKey(
            ( seq:Seq[Int], buff:Option[Int] ) => {
                val newCount = buff.getOrElse(0) + seq.sum
                Option(newCount)
            }
        )
        state.print()

        ssc.start()
        ssc.awaitTermination()
    }
}

Window Operations 可以设置窗口的大小和滑动窗口的间隔来动态的获取当前Steaming 的允许状态。所有基于窗口的操作都需要两个参数,分别为窗口时长以及滑动步长。注意:这两者都必须为采集周期大小的整数倍

object SparkStreaming06_State_Window {

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

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val lines = ssc.socketTextStream("localhost", 9999)
        val wordToOne = lines.map((_,1))

        // 窗口的范围应该是采集周期的整数倍
        // 窗口可以滑动的,但是默认情况下,一个采集周期进行滑动
        // 这样的话,可能会出现重复数据的计算,为了避免这种情况,可以改变滑动的滑动(步长)
        val windowDS: DStream[(String, Int)] = wordToOne.window(Seconds(6), Seconds(6))

        val wordToCount = windowDS.reduceByKey(_+_)

        wordToCount.print()

        ssc.start()
        ssc.awaitTermination()
    }
}

关于Window 的操作还有如下方法:

  • window(windowLength, slideInterval): 基于对源DStream 窗化的批次进行计算返回一个新的Dstream;
  • countByWindow(windowLength, slideInterval): 返回一个滑动窗口计数流中的元素个数;
  • reduceByWindow(func, windowLength, slideInterval): 通过使用自定义函数整合滑动区间流元素来创建一个新的单元素流;
  • reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]): 当在一个(K,V) 对的DStream 上调用此函数,会返回一个新(K,V)对的 DStream,此处通过对滑动窗口中批次数据使用 reduce 函数来整合每个 key 的 value 值。
  • reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]): 这个函数是上述函数的变化版本,每个窗口的 reduce 值都是通过用前一个窗的 reduce 值来递增计算。通过 reduce 进入到滑动窗口数据并"反向 reduce"离开窗口的旧数据来实现这个操作。一个例子是随着窗口滑动对keys 的"加"“减"计数。通过前边介绍可以想到,这个函数只适用于"可逆的 reduce 函数”,也就是这些 reduce 函数有相应的"反 reduce"函数(以参数 invFunc 形式传入)。如前述函数,reduce 任务的数量通过可选参数来配置
sc.checkpoint("cp")

val lines = ssc.socketTextStream("localhost", 9999)
val wordToOne = lines.map((_,1))

// reduceByKeyAndWindow : 当窗口范围比较大,但是滑动幅度比较小,那么可以采用增加数据和删除数据的方式
// 无需重复计算,提升性能。
val windowDS: DStream[(String, Int)] =
    wordToOne.reduceByKeyAndWindow(
        (x:Int, y:Int) => { x + y},
        (x:Int, y:Int) => {x - y},
        Seconds(9), Seconds(3))

2.3 DStream 输出

Output Operations 可以将 DStream 的数据输出到外部的数据库或文件系统。当某个 Output Operations 被调用时,spark streaming 程序才会开始真正的计算过程(与 RDD 的 Action 类似)

Output Operation 含义
print() 打印到控制台
saveAsTextFiles(prefix, [suffix]) 保存流的内容为文本文件,文件名为"prefix-TIME_IN_MS[.suffix]"
saveAsObjectFiles(prefix,[suffix]) 保存流的内容为 SequenceFile,文件名为 “prefix-TIME_IN_MS[.suffix]”
saveAsHadoopFiles(prefix,[suffix]) 保存流的内容为 hadoop 文件,文件名为"prefix-TIME_IN_MS[.suffix]"
foreachRDD(func) 对 Dstream 里面的每个 RDD 执行 func;注意连接不能写在 driver 层面(序列化); 如果写在 foreach 则每个 RDD 中的每一条数据都创建,得不偿失;增加 foreachPartition,在分区创建(获取)

2.4 优雅关闭

流式任务需要 7*24 小时执行,但是有时涉及到升级代码需要主动停止程序,但是分布式程序,没办法做到一个个进程去杀死,所有配置优雅的关闭就显得至关重要了。使用外部文件系统来控制内部程序关闭

object SparkStreaming08_Close {

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

        /*
           线程的关闭:
           val thread = new Thread()
           thread.start()

           thread.stop(); // 强制关闭

         */

        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        val lines = ssc.socketTextStream("localhost", 9999)
        val wordToOne = lines.map((_,1))

        wordToOne.print()

        ssc.start()

        // 如果想要关闭采集器,那么需要创建新的线程
        // 而且需要在第三方程序中增加关闭状态
        new Thread(
            new Runnable {
                override def run(): Unit = {
                    // 优雅地关闭
                    // 计算节点不在接收新的数据,而是将现有的数据处理完毕,然后关闭
                    // Mysql : Table(stopSpark) => Row => data
                    // Redis : Data(K-V)
                    // ZK    : /stopSpark
                    // HDFS  : /stopSpark
                    /*
                    while ( true ) {
                        if (true) {
                            // 获取SparkStreaming状态
                            val state: StreamingContextState = ssc.getState()
                            if ( state == StreamingContextState.ACTIVE ) {
                                ssc.stop(true, true)
                            }
                        }
                        Thread.sleep(5000)
                    }
                     */

                    Thread.sleep(5000)
                    val state: StreamingContextState = ssc.getState()
                    if ( state == StreamingContextState.ACTIVE ) {
                        ssc.stop(true, true)
                    }
                    System.exit(0)
                }
            }
        ).start()

        ssc.awaitTermination() // block 阻塞main线程
    }
}

对于优雅恢复,停止前可以保存检查点

object SparkStreaming09_Resume {

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

        val ssc = StreamingContext.getActiveOrCreate("cp", ()=>{
            val sparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
            val ssc = new StreamingContext(sparkConf, Seconds(3))

            val lines = ssc.socketTextStream("localhost", 9999)
            val wordToOne = lines.map((_,1))
            wordToOne.print()
            ssc
        })

        ssc.checkpoint("cp")

        ssc.start()
        ssc.awaitTermination() // block 阻塞main线程

    }
}

你可能感兴趣的:(大数据,spark)