Spark

1.1 什么是 Spark :传输=》处理 =》储存
Spark 是一个快速(基于内存), 通用, 可扩展的集群计算引擎
并且 Spark 目前已经成为 Apache 最活跃的开源项目, 有超过 1000 个活跃的贡献者.
历史 (mr慢,也不能迭代计算)

  1. 2009 年,Spark 诞生于 UC Berkeley(加州大学伯克利分校, CAL) 的 AMP 实验室, 项目采用 Scala 编程语言编写.
  2. 2010 年, Spark 正式对外开源
  3. 2013 年 6 月, 进入 Apache 孵化器
  4. 2014 年, 成为 Apache 的顶级项目.
  5. 目前最新的版本是 2.4.4 ==》2.1.1
    参考: http://spark.apache.org/history.html

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

易用
Spark 支持 Scala, Java, Python, R 和 SQL 脚本, 并提供了超过 80 种高性能的算法, 非常容易创建并行 App
而且 Spark 支持交互式的 Python 和 Scala 的 shell, 这意味着可以非常方便地在这些 shell 中使用 Spark 集群来验证解决问题的方法, 而不是像以前一样 需要打包, 上传集群, 验证等. 这对于原型开发非常重要.

通用
Spark 结合了SQL, Streaming和复杂分析.
Spark 提供了大量的类库, 包括 SQL 和 DataFrames, 机器学习(MLlib), 图计算(GraphicX), 实时流处理(Spark Streaming) .
可以把这些类库无缝的柔和在一个 App 中.
减少了开发和维护的人力成本以及部署平台的物力成本.

可融合性
Spark 可以非常方便的与其他开源产品进行融合.
比如, Spark 可以使用 Hadoop 的 YARN 和 Appache Mesos 作为它的资源管理和调度器, 并且可以处理所有 Hadoop 支持的数据, 包括 HDFS, HBase等.


1.3 Spark 内置模块介绍

独立调度器: 自己搭建
多用yarn:资源调度
Spark core: 基础和核心
集群管理器(Cluster Manager)
Spark 设计为可以高效地在一个计算节点到数千个计算节点之间伸缩计算。
为了实现这样的要求,同时获得最大灵活性,Spark 支持在各种集群管理器(Cluster Manager)上运行,目前 Spark 支持 3 种集群管理器:

  1. Hadoop YARN(在国内使用最广泛)
  2. Apache Mesos(国内使用较少, 国外使用较多)
  3. Standalone(Spark 自带的资源调度器, 需要在集群中的每台节点上配置 Spark)
    SparkCore
    实现了 Spark 的基本功能,包含任务调度、内存管理、错误恢复、与存储系统交互等模块。SparkCore 中还包含了对弹性分布式数据集(Resilient Distributed DataSet,简称RDD)的API定义。
    Spark SQL
    是 Spark 用来操作结构化数据的程序包。通过SparkSql,我们可以使用 SQL或者Apache Hive 版本的 SQL 方言(HQL)来查询数据。Spark SQL 支持多种数据源,比如 Hive 表、Parquet 以及 JSON 等。
    Spark Streaming
    是 Spark 提供的对实时数据进行流式计算的组件。提供了用来操作数据流的 API,并且与 Spark Core 中的 RDD API 高度对应。
    Spark MLlib
    提供常见的机器学习 (ML) 功能的程序库。包括分类、回归、聚类、协同过滤等,还提供了模型评估、数据导入等额外的支持功能。

Spark 得到了众多大数据公司的支持,这些公司包括 Hortonworks、IBM、Intel、Cloudera、MapR、Pivotal、百度、阿里、腾讯、京东、携程、优酷土豆。
当前百度的 Spark 已应用于大搜索、直达号、百度大数据等业务;
阿里利用 GraphX 构建了大规模的图计算和图挖掘系统,实现了很多生产系统的推荐算法;
腾讯Spark集群达到 8000 台的规模,是当前已知的世界上最大的 Spark 集群。


第 2 章 Spark 运行模式
本章介绍在各种运行模式如何运行 Spark 应用.
首先需要下载 Spark
下载 Spark
1.官网地址 http://spark.apache.org/
2.文档查看地址 https://spark.apache.org/docs/2.1.1/
3.下载地址 https://archive.apache.org/dist/spark/
目前最新版本为 2.4.4, 考虑到国内企业使用情况我们仍然选择 2.1.1 来学习. 不过2.x.x 的版本差别都不大.
集群角色 Driver
Master 和 Worker

  1. Master ==》resourcemanger 1个master多个worker
    Spark 特有资源调度系统的 Leader。掌管着整个集群的资源信息,类似于 Yarn 框架中的 ResourceManager,主要功能:
  2. 监听 Worker,看 Worker 是否正常工作;
  3. Master 对 Worker、Application 等的管理(接收 Worker 的注册并管理所有的Worker,接收 Client 提交的 Application,调度等待的 Application 并向Worker 提交)。
  4. Worker ==》nodemanger
    Spark 特有资源调度系统的 Slave,有多个。每个 Slave 掌管着所在节点的资源信息,类似于 Yarn 框架中的 NodeManager,主要功能:
  5. 通过 RegisterWorker 注册到 Master;
  6. 定时发送心跳给 Master;
  7. 根据 Master 发送的 Application 配置进程环境,并启动 ExecutorBackend(执行 Task 所需的临时进程) 》进程Executor --》线程Task
    Driver和Executor
    》提交任务之后才有的角色
  8. Driver(驱动器) ==》工作流程在驱动器
    Spark 的驱动器是执行开发程序中的 main 方法的线程。
    它负责开发人员编写的用来创建SparkContext、创建RDD,以及进行RDD的转化操作和行动操作代码的执行。如果你是用Spark Shell,那么当你启动Spark shell的时候,系统后台自启了一个Spark驱动器程序,就是在Spark shell中预加载的一个叫作 sc 的SparkContext对象。如果驱动器程序终止,那么Spark应用也就结束了。
    主要负责: 1. 将用户程序转化为作业(Job); 2. 在Executor之间调度任务(Task); 3. 跟踪Executor的执行情况; 4. 通过UI展示查询运行情况。
    ==》源码中Executor是一个对象,平时说的Executor是一个进程
  9. Executor(执行器) //具体执行
    Spark Executor是一个工作节点,负责在 Spark 作业中运行任务,任务间相互独立。Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有Executor节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他Executor节点上继续运行。主要负责:
  10. 负责运行组成 Spark 应用的任务,并将状态信息返回给驱动器程序;
  11. 通过自身的块管理器(Block Manager)为用户程序中要求缓存的RDD提供内存式存储。RDD是直接缓存在Executor内的,因此任务可以在运行时充分利用缓存数据加速运算。
    总结:Master 和 Worker 是 Spark 的守护进程,即 Spark 在特定模式下正常运行所必须的进程。Driver 和 Executor 是临时程序,当有具体任务提交到 Spark 集群才会开启的程序。
    扩展
    Spark的驱动器是执行开发程序中的 main方法的进程。它负责开发人员编写的用来创建SparkContext、创建 RDD,以及进行 RDD 的转化操作和行动操作代码的执行。如果你是用spark shell,那么当你启动 Spark shell的时候,系统后台自启了一个 Spark 驱动器程序,就是在Spark shell 中预加载的一个叫作 sc 的 SparkContext 对象。如果驱动器程序终止,那么Spark 应用也就结束了。

Driver在spark作业执行时主要负责以下操作:

1)把用户程序转为任务

Driver程序负责把用户程序转为多个物理执行的单元,这些单元也被称为任务(task)。从上层来看,spark程序的流程是这样的:读取或者转化数据创建一系列 RDD,然后使用转化操作生成新的RDD,最后使用行动操作得到结果或者将数据存储到文件存储系统中。Spark 程序其实是隐式地创建出了一个由上述操作组成的逻辑上的有向无环图。当Driver序运行时,它会把这个逻辑图转为物理执行计划。

Spark 会对逻辑执行计划作一些优化,比如将连续的映射转为流水线化执行,将多个操作合并到一个步骤中等。这样 Spark 就把逻辑计划转为一系列步骤(stage)。而每个stage又由多个task组成。这些task会被打包并送到集群中。task是 Spark 中最小的执行单元,用户程序通常要启动成百上千的独立任务。

2)跟踪Executor的运行状况

有了物理执行计划之后,Driver程序必须在各个Executor进程间协调任务的调度。Executor进程启动后,会向Driver进程注册自己。因此,Driver进程就可以跟踪应用中所有的Executor节点的运行信息。

3)为执行器节点调度任务

Driver程序会根据当前的Executor节点集合,尝试把所有Task基于数据所在位置分配给合适的Executor进程。当Task执行时,Executor进程会把缓存数据存储起来,而Driver进程同样会跟踪这些缓存数据的位置,并且利用这些位置信息来调度以后的任务,以尽量减少数据的网络传输。

4)UI展示应用运行状况

Driver程序会将一些 Spark 应用的运行时的信息通过网页界面呈现出来,默认在端口4040 上。比如,在本地模式下,访问 http://localhost:4040 就可以看到这个网页了。

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

执行器进程有两大作用:

1、它们负责运行组成 Spark 应用的任务,并将结果返回给驱动器进程;

2、它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存数据加速运算。

执行器程序通常都运行在专用的进程中。


2.1 Local 模式
Local 模式就是指的只在一台计算机上来运行 Spark.
通常用于测试的目的来使用 Local 模式, 实际的生产环境中不会使用 Local 模式.
2.1.1 解压 Spark 安装包
把安装包上传到/opt/software/下, 并解压到/opt/module/目录下
tar -zxvf spark-2.1.1-bin-hadoop2.7.tgz -C /opt/module
然后复制刚刚解压得到的目录, 并命名为spark-local:
cp -r spark-2.1.1-bin-hadoop2.7 spark-local
2.1.2 运行官方求PI的案例
bin/spark-submit
–class org.apache.spark.examples.SparkPi
–master local[2]
./examples/jars/spark-examples_2.11-2.1.1.jar 100
注意:
• 如果你的shell是使用的zsh, 则需要把local[2]加上引号:‘local[2]’
说明: ==》这里的local[2]:用2个线程
• 使用spark-submit来发布应用程序.
• 语法:
./bin/spark-submit
–class
–master
–deploy-mode
–conf =
… # other options

[application-arguments]
– --master 指定 master 的地址,默认为local. 表示在本机运行.
– --class 你的应用的启动类 (如 org.apache.spark.examples.SparkPi)
• --deploy-mode 是否发布你的驱动到 worker节点(cluster 模式) 或者作为一个本地客户端 (client 模式) (default: client)
• --conf: 任意的 Spark 配置属性, 格式key=value. 如果值包含空格,可以加引号"key=value"
• application-jar: 打包好的应用 jar,包含依赖. 这个 URL 在集群中全局可见。 比如hdfs:// 共享存储系统, 如果是 file:// path, 那么所有的节点的path都包含同样的jar
• application-arguments: 传给main()方法的参数
• --executor-memory 1G 指定每个executor可用内存为1G
• --total-executor-cores 6 指定所有executor使用的cpu核数为6个
• --executor-cores 表示每个executor使用的 cpu 的核数
• 关于 Master URL 的说明

Master URL Meaning
local Run Spark locally with one worker thread (i.e. no parallelism at all).
local[K] Run Spark locally with K worker threads (ideally, set this to the number of cores on your machine).
使用K个工作线程在本地运行Spark(理想情况下,将其设置为计算机上的内核数量)
local[*] Run Spark locally with as many worker threads as logical cores on your machine. ==>尽可能多的
spark://HOST:PORT Connect to the given Spark standalone cluster master. The port must be whichever one your master is configured to use, which is 7077 by default.
mesos://HOST:PORT Connect to the given Mesos cluster. The port must be whichever one your is configured to use, which is 5050 by default. Or, for a Mesos cluster using ZooKeeper, use mesos://zk://… To submit with --deploy-mode cluster, the HOST:PORT should be configured to connect to the MesosClusterDispatcher.

yarn Connect to a YARNcluster in client or cluster mode depending on the value of --deploy-mode. The cluster location will be found based on the HADOOP_CONF_DIR or YARN_CONF_DIR variable.
根据——deploy-mode的值,在客户机或集群模式下连接到YARNcluster
结果展示
该算法是利用蒙特·卡罗算法求PI

备注: 也可以使用run-examples来运行
bin/run-example SparkPi 100
2.1.3 使用 Spark-shell
Spark-shell 是 Spark 给我们提供的交互式命令窗口(类似于 Scala 的 REPL)
本案例在 Spark-shell 中使用 Spark 来统计文件中各个单词的数量.
步骤1: 创建 2 个文本文件
mkdir input
cd input
touch 1.txt
touch 2.txt
分别在 1.txt 和 2.txt 内输入一些单词.
步骤2: 打开 Spark-shell
bin/spark-shell

步骤3: 查看进程和通过 web 查看应用程序运行情况

地址: http://hadoop102:4040

步骤4: 运行 wordcount 程序 ==》可以用一个变量来接
sc.textFile(“input/”).flatMap(.split(" ")).map((, 1)).reduceByKey(_ + _).collect

步骤5: 登录hadoop102:4040查看程序运行

2.1.4 提交流程
Spark 通用运行简易流程
查看 ppt

2.1.5 wordcount 数据流程分析:
查看 ppt

  1. textFile(“input”):读取本地文件input文件夹数据;
  2. flatMap(_.split(" ")):压平操作,按照空格分割符将一行数据映射成一个个单词;
  3. map((_,1)):对每一个元素操作,将单词映射为元组;
  4. reduceByKey(+):按照key将值进行聚合,相加;
  5. collect:将数据收集到Driver端展示。

2.2 Spark 核心概念介绍
driver program(驱动程序)
每个 Spark 应用程序都包含一个驱动程序, 驱动程序负责把并行操作发布到集群上.
驱动程序包含 Spark 应用程序中的主函数, 定义了分布式数据集以应用在集群中.
在前面的wordcount案例集中, spark-shell 就是我们的驱动程序, 所以我们可以在其中键入我们任何想要的操作, 然后由他负责发布.
驱动程序通过SparkContext对象来访问 Spark, SparkContext对象相当于一个到 Spark 集群的连接.
在 spark-shell 中, 会自动创建一个SparkContext对象, 并把这个对象命名为sc.

RDDs(Resilient Distributed Dataset) 弹性分布式数据集
一旦拥有了SparkContext对象, 就可以使用它来创建 RDD 了. 在前面的例子中, 我们调用sc.textFile(…)来创建了一个 RDD, 表示文件中的每一行文本. 我们可以对这些文本行运行各种各样的操作.
在第二部分的SparkCore中, 我们重点就是学习 RDD.
cluster managers(集群管理器)
为了在一个 Spark 集群上运行计算, SparkContext对象可以连接到几种集群管理器(Spark’s own standalone cluster manager, Mesos or YARN).
集群管理器负责跨应用程序分配资源.
executor(执行器)
SparkContext对象一旦成功连接到集群管理器, 就可以获取到集群中每个节点上的执行器(executor).
执行器是一个进程(进程名: ExecutorBackend, 运行在 Worker 上), 用来执行计算和为应用程序存储数据.
然后, Spark 会发送应用程序代码(比如:jar包)到每个执行器. 最后, SparkContext对象发送任务到执行器开始执行程序.

专业术语
Term Meaning
Application User program built on Spark. Consists of a driver program and executors on the cluster. (构建于 Spark 之上的应用程序. 包含驱动程序和运行在集群上的执行器)
Application jar A jar containing the user’s Spark application. In some cases users will want to create an “uber jar” containing their application along with its dependencies. The user’s jar should never include Hadoop or Spark libraries, however, these will be added at runtime.
Driver program The process running the main() function of the application and creating the SparkContext
Cluster manager An external service for acquiring resources on the cluster (e.g. standalone manager, Mesos, YARN)
Deploy mode Distinguishes where the driver process runs. In “cluster” mode, the framework launches the driver inside of the cluster. In “client” mode, the submitter launches the driver outside of the cluster.
Worker node Any node that can run application code in the cluster
Executor A process launched for an application on a worker node, that runs tasks and keeps data in memory or disk storage across them. Each application has its own executors.
Task A unit of work that will be sent to one executor
Job A parallel computation consisting of multiple tasks that gets spawned in response to a Spark action (e.g. save, collect); you’ll see this term used in the driver’s logs.
Stage Each job gets divided into smaller sets of tasks called stages that depend on each other (similar to the map and reduce stages in MapReduce); you’ll see this term used in the driver’s logs.


2.3 Standalone 模式
构建一个由 Master + Slave 构成的 Spark 集群,Spark 运行在集群中。
这个要和 Hadoop 中的 Standalone 区别开来. 这里的 Standalone 是指只用 Spark 来搭建一个集群, 不需要借助其他的框架.是相对于 Yarn 和 Mesos 来说的.


2.3.1 配置 Standalone 模式
步骤1: 复制 spark, 并命名为spark-standalone
cp -r spark-2.1.1-bin-hadoop2.7 spark-standalone
步骤2: 进入配置文件目录conf, 配置spark-evn.sh
cd conf/
cp spark-env.sh.template spark-env.sh
在spark-env.sh文件中配置如下内容:
SPARK_MASTER_HOST=hadoop102
SPARK_MASTER_PORT=7077 # 默认端口就是7077, 可以省略不配
步骤3: 修改 slaves 文件, 添加 worker 节点
cp slaves.template slaves
在slaves文件中配置如下内容:
hadoop102
hadoop103
hadoop104
步骤4: 分发spark-standalone
步骤5: 启动 Spark 集群
sbin/start-all.sh

可能碰到的问题
• 如果启动的时候报:JAVA_HOME is not set, 则在sbin/spark-config.sh中添加入JAVA_HOME变量即可. 不要忘记分发修改的文件

步骤6: 在网页中查看 Spark 集群情况
地址: http://hadoop102:8080


2.3.2 使用 Standalone 模式运行计算 PI 的程序 ==》可以在运行过程中进入4040,不过要足够快 f
bin/spark-submit
–class org.apache.spark.examples.SparkPi
–master spark://hadoop102:7077
–executor-memory 1G
–total-executor-cores 6 \ ==》总的executor
–executor-cores 2 \ ==》2个core
./examples/jars/spark-examples_2.11-2.1.1.jar 100

bin/spark-submit
–class org.apache.spark.examples.SparkPi
–master spark://hadoop102:7077
–executor-memory 1G
–total-executor-cores 6 --executor-cores 2 ./examples/jars/spark-examples_2.11-2.1.1.jar 100
bin/spark-submit --class org.apache.spark.examples.SparkPi --master spark://hadoop102:7077 --executor-memory 1G --total-executor-cores 6 --executor-cores 2 ./examples/jars/spark-examples_2.11-2.1.1.jar 100


2.3.3 在 Standalone 模式下启动 Spark-shell
bin/spark-shell
–master spark://hadoop102:7077

集群模式下,启动的shell

说明:
• --master spark://hadoop102 :7077指定要连接的集群的master

执行wordcount程序
sc.textFile(“input/”).flatMap(.split(" ")).map((,1)).reduceByKey(+).collect
res4: Array[(String, Int)] = Array((are,2), (how,2), (hello,4), (spark,2), (world,2), (you,2))
注意:
• 每个worker节点上要有相同的文件夹:input/, 否则会报文件不存在的异常


2.3.4 配置 Spark 任务历史服务器(为 Standalone 模式配置)
在 Spark-shell 没有退出之前, 我们是可以看到正在执行的任务的日志情况:http://hadoop102:4040. 但是退出 Spark-shell 之后, 执行的所有任务记录全部丢失.

Driver: 默认运行在客户端
Deploy: 部署
端口号: 8081 worker (在hadoop102,103,104都可以启动) 8080 master(master启动)

Cluster: 运行在集群
Client:运行在客户端

所以需要配置任务的历史服务器, 方便在任何需要的时候去查看日志.
步骤1: 配置spark-default.conf文件, 开启 Log
cp spark-defaults.conf.template spark-defaults.conf
在spark-defaults.conf文件中, 添加如下内容:
spark.master spark://hadoop102:7077
spark.eventLog.enabled true
spark.eventLog.dir hdfs://hadoop102:9000/spark-job-log
注意:
hdfs://hadoop102:9000/spark-job-log 目录必须提前存在, 名字随意
步骤2: 修改spark-env.sh文件,添加如下配置.
export SPARK_HISTORY_OPTS="-Dspark.history.ui.port=18080 -Dspark.history.retainedApplications=30 -Dspark.history.fs.logDirectory=hdfs://hadoop102:9000/spark-job-log"
步骤3: 分发配置文件
步骤4: 启动历史服务
需要先启动 HDFS
启动spark得histroy: sbin/start-history-server.sh

ui 地址: http://hadoop102:18080

步骤5: 启动任务, 查看历史服务器
bin/spark-submit
–class org.apache.spark.examples.SparkPi
–master spark://hadoop102:7077
–executor-memory 1G
–total-executor-cores 6
./examples/jars/spark-examples_2.11-2.1.1.jar 100

Standalone-client提交模式总结:

执行流程

  1. client模式提交任务后,会在客户端启动Driver进程。

  2. Driver会向Master申请启动Application启动的资源。

  3. 资源申请成功,Driver端将task发送到worker端执行。

  4. worker将task执行结果返回到Driver端。

    client模式适用于测试调试程序。Driver进程是在客户端启动的,这里的客户端就是指提交应用程序的当前节点。在Driver端可以看到task执行的情况。生产环境下不能使用client模式,是因为:假设要提交100个application到集群运行,Driver每次都会在client端启动,那么就会导致客户端100次网卡流量暴增的问题
    

Standalone-cluster
Standalone-cluster提交方式,应用程序使用的所有jar包和文件,必须保证所有的worker节点都要有,因为此种方式,spark不会自动上传包
解决方式:

  1. 将所有的依赖包和文件打到同一个包中,然后放在hdfs上。
  2. 将所有的依赖包和文件各放一份在worker节点上。

Standalone-cluster执行流程

cluster模式提交应用程序后,会向Master请求启动Driver
Master接受请求,随机在集群一台节点启动Driver进程
Driver启动后为当前的应用程序申请资源
Driver端发送task到worker节点上执行
worker将执行情况和执行结果返回给Driver端


2.3.5 HA 配置(为 Mater 配置)
由于 master 只有一个, 所以也有单点故障问题.

可以启动多个 master, 先启动的处于 Active 状态, 其他的都处于 Standby 状态
步骤1: 给 spark-env.sh 添加如下配置

注释掉如下内容:

#SPARK_MASTER_HOST=hadoop102
#SPARK_MASTER_PORT=7077

添加上如下内容:

export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=hadoop102:2181,hadoop103:2181,hadoop104:2181 -Dspark.deploy.zookeeper.dir=/spark"
步骤2: 分发配置文件
步骤3: 启动 Zookeeper
步骤4: 在 hadoop102 启动全部节点 ==》重新启动
sbin/start-all.sh
会在当前节点启动一个 master
步骤5: 在 hadoop103 启动一个 master
sbin/start-master.sh
步骤6: 查看 master 的状态

步骤7: 杀死 hadoop102 的 master 进程
hadoop103 的 master 会自动切换成 Active ==》会有延迟


2.3.6 Standalone 工作模式图解


2.4 Yarn 模式
2.4.1 Yarn 模式概述
Spark 客户端可以直接连接 Yarn,不需要额外构建Spark集群。
有 yarn-client 和 yarn-cluster 两种模式,主要区别在于:Driver 程序的运行节点不同。
• yarn-client:Driver程序运行在客户端,适用于交互、调试,希望立即看到app的输出
• yarn-cluster:Driver程序运行在由 RM(ResourceManager)启动的 AM(AplicationMaster)上, 适用于生产环境。
工作模式介绍:
查看 ppt

2.4.2 Yarn 模式配置
步骤1: 修改 hadoop 配置文件 yarn-site.xml, 添加如下内容:
由于咱们的测试环境的虚拟机内存太少, 防止将来任务被意外杀死, 配置所以做如下配置.

yarn.nodemanager.pmem-check-enabled false yarn.nodemanager.vmem-check-enabled false 修改后分发配置文件. 步骤2: 复制 spark, 并命名为spark-yarn cp -r spark-standalone spark-yarn 步骤3: 修改spark-evn.sh文件 去掉 master 的 HA 配置, 日志服务的配置保留着. 并添加如下配置: 告诉 spark 客户端 yarn 相关配置 YARN_CONF_DIR=/opt/module/hadoop-2.7.2/etc/hadoop 步骤4: 执行一段程序 bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master yarn \ --deploy-mode client \ ./examples/jars/spark-examples_2.11-2.1.1.jar 100 s

2.4.3 日志服务
在前面的页面中点击 history 无法直接连接到 spark 的日志.
可以在spark-default.conf中添加如下配置达到上述目的
spark.yarn.historyServer.address=hadoop102:18080
spark.history.ui.port=18080

可能碰到的问题:
如果在 yarn 日志端无法查看到具体的日志, 则在yarn-site.xml中添加如下配置

yarn.log.server.url http://hadoop102:19888/jobhistory/logs ________________________________________

2.5 Mesos 模式
Spark客户端直接连接 Mesos;不需要额外构建 Spark 集群。
国内应用比较少,更多的是运用yarn调度。


2.6 几种运行模式的对比
模式 Spark安装机器数 需启动的进程 所属者
Local 1 无 Spark
Standalone 多台 Master及Worker Spark
Yarn 1 Yarn及HDFS Hadoop


第 3 章 案例实操
Spark Shell 仅在测试和验证我们的程序时使用的较多,在生产环境中,通常会在 IDE 中编制程序,然后打成 jar 包,然后提交到集群,最常用的是创建一个 Maven 项目,利用 Maven 来管理 jar 包的依赖。


3.1 编写 WordCount 程序
步骤1:创建 maven 项目, 导入依赖


org.apache.spark
spark-core_2.11
2.1.1






net.alchim31.maven
scala-maven-plugin
3.4.6



compile
testCompile





步骤2: 创建WordCount.scala
package day01

object WordCount {
def main(args: Array[String]): Unit = {
// 1. 创建 SparkConf对象, 并设置 App名字
val conf: SparkConf = new SparkConf().setAppName(“WordCount”)
// 2. 创建SparkContext对象
val sc = new SparkContext(conf)
// 3. 使用sc创建RDD并执行相应的transformation和action
sc.textFile("/input")
.flatMap(.split(" "))
.map((
, 1))
.reduceByKey(_ + _)
.saveAsTextFile("/result")
// 4. 关闭连接
sc.stop()
}
}


3.2 测试
3.2.1 测试1: 打包到 Linux 测试
使用 maven 的打包命令打包. 然后测试
bin/spark-submit --class day01.WordCount --master yarn input/spark_test-1.0-SNAPSHOT.jar
3.2.2 测试2: idea 本地直接提交应用
使用 local 模式执行
相当于代码是在 window 下执行的.
package day01

import org.apache.spark.{SparkConf, SparkContext}

object WordCount {
def main(args: Array[String]): Unit = {
// 1. 创建 SparkConf对象, 并设置 App名字, 并设置为 local 模式
val conf: SparkConf = new SparkConf().setAppName(“WordCount”).setMaster(“local[*]”)
// 2. 创建SparkContext对象
val sc = new SparkContext(conf)
// 3. 使用sc创建RDD并执行相应的transformation和action
val wordAndCount: Array[(String, Int)] = sc.textFile(ClassLoader.getSystemResource(“words.txt”).getPath)
.flatMap(.split(" "))
.map((
, 1))
.reduceByKey(_ + _)
.collect()
wordAndCount.foreach(println)
// 4. 关闭连接
sc.stop()
}
}


第 2 部分 Spark Core 3d
第 4 章 RDD 概述
4.1 什么是 RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象。==>灵活,支持分布式,是基础
在代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
4.2 RDD 的 5 个主要属性(property)
• A list of partitions
多个分区. 分区可以看成是数据集的基本组成单位.
对于 RDD 来说, 每个分区都会被一个计算任务处理, 并决定了并行计算的粒度.
用户可以在创建 RDD 时指定 RDD 的分区数, 如果没有指定, 那么就会采用默认值. 默认值就是程序所分配到的 CPU Coure 的数目.
每个分配的存储是由BlockManager 实现的. 每个分区都会被逻辑映射成 BlockManager 的一个 Block, 而这个 Block 会被一个 Task 负责计算.

• A function for computing each split
计算每个切片(分区)的函数.
Spark 中 RDD 的计算是以分片为单位的, 每个 RDD 都会实现 compute 函数以达到这个目的.
• A list of dependencies on other RDDs
与其他 RDD 之间的依赖关系
RDD 的每次转换都会生成一个新的 RDD, 所以 RDD 之间会形成类似于流水线一样的前后依赖关系. 在部分分区数据丢失时, Spark 可以通过这个依赖关系重新计算丢失的分区数据, 而不是对 RDD 的所有分区进行重新计算.

• Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned) ==》可选的Optionally
==》对存储键值对的 RDD, 才有一个可选的分区器.
只有对于 key-value的 RDD, 才会有 Partitioner, 非key-value的 RDD 的 Partitioner 的值是 None. Partitiner 不但决定了 RDD 的本区数量, 也决定了 parent RDD Shuffle 输出时的分区数量.
• Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file) ==》preferred locations 偏好设置,用于计算的时候降级使用
存储每个切片优先(preferred location)位置的列表. 比如对于一个 HDFS 文件来说, 这个列表保存的就是每个 Partition 所在文件块的位置. 按照“移动数据不如移动计算”的理念, Spark 在进行任务调度的
时候, 会尽可能地将计算任务分配到其所要处理数据块的存储位置.

4.3 理解 RDD
一个 RDD 可以简单的理解为一个分布式的元素集合.
RDD 表示只读的分区的数据集,对 RDD 进行改动,只能通过 RDD 的转换操作, 然后得到新的 RDD, 并不会对原 RDD 有任何的影响
在 Spark 中, 所有的工作要么是创建 RDD, 要么是转换已经存在 RDD 成为新的 RDD, 要么在 RDD 上去执行一些操作来得到一些计算结果.
每个 RDD 被切分成多个分区(partition), 每个分区可能会在集群中不同的节点上进行计算. ==》一个切片一个分区?
RDD 特点

  1. 弹性
    • 存储的弹性:内存与磁盘的自动切换; ==》内存 不够时,放入磁盘
    • 容错的弹性:数据丢失可以自动恢复; ==》计算过程中,数据丢失,容错
    • 计算的弹性:计算出错重试机制; ==》从切片的源头重新计算
    • 分片的弹性:可根据需要重新分片。
  2. 分区
    RDD 逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分区的数据。
    如果 RDD 是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果 RDD 是通过其他 RDD 转换而来,则 compute函数是执行转换逻辑将其他 RDD 的数据进行转换。
  3. 只读 ==》安全
    RDD 是只读的,要想改变 RDD 中的数据,只能在现有 RDD 基础上创建新的 RDD。
    由一个 RDD 转换到另一个 RDD,可以通过丰富的转换算子实现,不再像 MapReduce 那样只能写map和reduce了。
    RDD 的操作算子包括两类,
    • 一类叫做transformation,它是用来将 RDD 进行转化,构建 RDD 的血缘关系; 》返回值是RDD时,是转换算子; 否则为行动算子
    • 另一类叫做action(
    》行动算子,触发行动),它是用来触发 RDD 进行计算,得到 RDD 的相关计算结果或者 保存 RDD 数据到文件系统中. ==》DAG
  4. 依赖(血缘) 》保证容错
    RDDs 通过操作算子进行转换,转换得到的新 RDD 包含了从其他 RDDs 衍生所必需的信息,RDDs 之间维护着这种血缘关系,也称之为依赖。
    如下图所示,依赖包括两种,
    • 一种是窄依赖,RDDs 之间分区是一一对应的,
    • 另一种是宽依赖,下游 RDD 的每个分区与上游 RDD(也称之为父RDD)的每个分区都有关,是多对多的关系。
    》决定了starg阶段

宽依赖:进行shuffle
窄依赖:不进行shuflle

  1. 缓存 ==》保证执行效率
    如果在应用程序中多次使用同一个 RDD,可以将该 RDD 缓存起来,该 RDD 只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该 RDD 的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。
    如下图所示,RDD-1 经过一系列的转换后得到 RDD-n 并保存到 hdfs,RDD-1 在这一过程中会有个中间结果,如果将其缓存到内存,那么在随后的 RDD-1 转换到 RDD-m 这一过程中,就不会计算其之前的 RDD-0 了。

  2. checkpoint ==》可以理解:缓存的另外一个版本,可以存储到外部(非内存)
    虽然 RDD 的血缘关系天然地可以实现容错,当 RDD 的某个分区数据计算失败或丢失,可以通过血缘关系重建。
    但是对于长时间迭代型应用来说,随着迭代的进行,RDDs 之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。
    为此,RDD 支持checkpoint 将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint 后的 RDD 不需要知道它的父 RDDs 了,它可以从 checkpoint 处拿到数据。


第 5 章 RDD 编程
本章介绍 RDD 的编程技术


5.1 RDD 编程模型 ==》最核心的就是各种转换
在 Spark 中,RDD 被表示为对象,通过对象上的方法调用来对 RDD 进行转换。
经过一系列的transformations定义 RDD 之后,就可以调用 actions 触发 RDD 的计算
action可以是向应用程序返回结果(count, collect等),或者是向存储系统保存数据(saveAsTextFile等)。
在Spark中,只有遇到action,才会执行 RDD 的计算(即延迟计算),这样在运行时可以通过管道的方式传输多个转换。
要使用 Spark,开发者需要编写一个 Driver 程序,它被提交到集群以调度运行 Worker
Driver 中定义了一个或多个 RDD,并调用 RDD 上的 action,Worker 则执行 RDD 分区计算任务。


5.2 RDD 的创建
在 Spark 中创建 RDD 的方式可以分为 3 种:
• 从集合中创建 RDD
• 从外部存储创建 RDD
• 从其他 RDD 转换得到新的 RDD。

5.2.1 从集合中创建 RDD

  1. 使用parallelize函数创建
    scala> val arr = Array(10,20,30,40,50,60)
    arr: Array[Int] = Array(10, 20, 30, 40, 50, 60)

scala> val rdd1 = sc.parallelize(arr)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :26
2. 使用makeRDD函数创建
makeRDD和parallelize是一样的.
scala> val rdd1 = sc.makeRDD(Array(10,20,30,40,50,60))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at makeRDD at :24
说明:
• 一旦 RDD 创建成功, 就可以通过并行的方式去操作这个分布式的数据集了.
• parallelize和makeRDD还有一个重要的参数就是把数据集切分成的分区数.
• Spark 会为每个分区运行一个任务(task). 正常情况下, Spark 会自动的根据你的集群来设置分区数
5.2.2 从外部存储创建 RDD
Spark 也可以从任意 Hadoop 支持的存储数据源来创建分布式数据集.
可以是本地文件系统, HDFS, Cassandra, HVase, Amazon S3 等等.
Spark 支持 文本文件, SequenceFiles, 和其他所有的 Hadoop InputFormat.
scala> var distFile = sc.textFile(“words.txt”)
distFile: org.apache.spark.rdd.RDD[String] = words.txt MapPartitionsRDD[1] at textFile at :24

scala> distFile.collect
res0: Array[String] = Array(spark hello, hello world, how are you, abc efg)
说明:
• url可以是本地文件系统文件, hdfs://…, s3n://…等等
• 如果是使用的本地文件系统的路径, 则必须每个节点都要存在这个路径
• 所有基于文件的方法, 都支持目录, 压缩文件, 和通配符(). 例如: textFile("/my/directory"), textFile("/my/directory/.txt"), and textFile("/my/directory/*.gz").
• textFile还可以有第二个参数, 表示分区数. 默认情况下, 每个块对应一个分区.(对 HDFS 来说, 块大小默认是 128M). 可以传递一个大于块数的分区数, 但是不能传递一个比块数小的分区数.
• 关于读取文件和保存文件的其他知识, 后面专门的章节介绍.
5.2.3 从其他 RDD 转换得到新的 RDD
就是通过 RDD 的各种转换算子来得到新的 RDD.
详见 5.3 节


5.3 RDD 的转换(transformation)
在 RDD 上支持 2 种操作:

  1. transformation
    从一个已知的 RDD 中创建出来一个新的 RDD 例如: map就是一个transformation.
  2. action
    在数据集上计算结束之后, 给驱动程序返回一个值. 例如: reduce就是一个action.
    本节学习 RDD 的转换操作, Action操作下节再学习.
    在 Spark 中几乎所有的transformation操作都是懒执行的(lazy), 也就是说transformation操作并不会立即计算他们的结果, 而是记住了这个操作.
    只有当通过一个action来获取结果返回给驱动程序的时候这些转换操作才开始计算.
    这种设计可以使 Spark 运行起来更加的高效.
    默认情况下, 你每次在一个 RDD 上运行一个action的时候, 前面的每个transformed RDD 都会被重新计算.
    但是我们可以通过persist (or cache)方法来持久化一个 RDD 在内存中, 也可以持久化到磁盘上, 来加快访问速度. 后面有专门的章节学习这种持久化技术.
    根据 RDD 中数据类型的不同, 整体分为 2 种 RDD:
    • Value类型
    • Key-Value类型(其实就是存一个二维的元组)

5.3.1 Value 类型—1

  1. map(func)
    作用: 返回一个新的 RDD, 该 RDD 是由原 RDD 的每个元素经过函数转换后的值而组成. 就是对 RDD 中的数据做转换.

案例:
创建一个包含1-10的的 RDD,然后将每个元素*2形成新的 RDD
scala > val rdd1 = sc.parallelize(1 to 10)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24
// 得到一个新的 RDD, 但是这个 RDD 中的元素并不是立即计算出来的
scala> val rdd2 = rdd1.map(_ * 2)
rdd2: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[1] at map at
:26

// 开始计算 rdd2 中的元素, 并把计算后的结果传递给驱动程序
scala> rdd2.collect
res0: Array[Int] = Array(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
2. mapPartitions(func)
作用: 类似于map(func), 但是是独立在每个分区上运行.所以:Iterator => Iterator
假设有N个元素,有M个分区,那么map的函数的将被调用N次,而mapPartitions被调用M次,一个函数一次处理所有分区。
scala> val source = sc.parallelize(1 to 10)
source: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[5] at parallelize at :24

scala> source.mapPartitions(it => it.map(_ * 2))
res7: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[6] at mapPartitions at :27

scala> res7.collect
res8: Array[Int] = Array(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)


  1. mapPartitionsWithIndex(func)
    作用: 和mapPartitions(func)类似. 但是会给func多提供一个Int值来表示分区的索引. 所以func的类型是:(Int, Iterator) => Iterator

scala> val rdd1 = sc.parallelize(Array(10,20,30,40,50,60))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24

scala> rdd1.mapPartitionsWithIndex((index, items) => items.map((index, _)))
res8: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[3] at mapPartitionsWithIndex at :27

scala> res8.collect
res9: Array[(Int, Int)] = Array((0,10), (0,20), (0,30), (1,40), (1,50), (1,60))
分区数的确定, 和对数组中的元素如何进行分区

  1. 确定分区数:
    override def defaultParallelism(): Int =
    scheduler.conf.getInt(“spark.default.parallelism”, totalCores)
  2. 对元素进行分区
    // length: RDD 中数据的长度 numSlices: 分区数
    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 =>

case nr: NumericRange[_] =>

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
}.toSeq
}
4. flatMap(func)
作用: 类似于map,但是每一个输入元素可以被映射为 0 或多个输出元素(所以func应该返回一个序列,而不是单一元素 T => TraversableOnce[U])

案例:
创建一个元素为 1-5 的RDD,运用 flatMap创建一个新的 RDD,新的 RDD 为原 RDD 每个元素的 平方和三次方 来组成 1,1,4,8,9,27…
scala> val rdd1 = sc.parallelize(Array(1,2,3,4,5))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[5] at parallelize at :24

scala> rdd1.flatMap(x => Array(x * x, x * x * x))
res13: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[6] at flatMap at :27

scala> res13.collect
res14: Array[Int] = Array(1, 1, 4, 8, 9, 27, 16, 64, 25, 125)
5. map()和mapPartition()的区别

  1. map():每次处理一条数据。
  2. mapPartition():每次处理一个分区的数据,这个分区的数据处理完后,原 RDD 中该分区的数据才能释放,可能导致 OOM。
  3. 开发指导:当内存空间较大的时候建议使用mapPartition(),以提高处理效率。
  4. glom()
    作用: 将每一个分区的元素合并成一个数组,形成新的 RDD 类型是RDD[Array[T]]
    案例
    创建一个 4 个分区的 RDD,并将每个分区的数据放到一个数组
    scala> var rdd1 = sc.parallelize(Array(10,20,30,40,50,60), 4)
    rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24

scala> rdd1.glom.collect
res2: Array[Array[Int]] = Array(Array(10), Array(20, 30), Array(40), Array(50, 60))
7. groupBy(func)
作用: ==》效率低,影响分区,有shuffle过程, 没有预聚合
按照func的返回值进行分组.
func返回值作为 key, 对应的值放入一个迭代器中. 返回的 RDD: RDD[(K, Iterable[T])
每组内元素的顺序不能保证, 并且甚至每次调用得到的顺序也有可能不同.
案例:
创建一个 RDD,按照元素的奇偶性进行分组
scala> val rdd1 = sc.makeRDD(Array(1, 3, 4, 20, 4, 5, 8))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at makeRDD at :24

scala> rdd1.groupBy(x => if(x % 2 == 1) “odd” else “even”)
res4: org.apache.spark.rdd.RDD[(String, Iterable[Int])] = ShuffledRDD[4] at groupBy at :27

scala> res4.collect
res5: Array[(String, Iterable[Int])] = Array((even,CompactBuffer(4, 20, 4, 8)), (odd,CompactBuffer(1, 3, 5)))
8. filter(func)
作用: 过滤. 返回一个新的 RDD 是由func的返回值为true的那些元素组成
案例
创建一个 RDD(由字符串组成),过滤出一个新 RDD(包含“xiao”子串)
scala> val names = sc.parallelize(Array(“xiaoli”, “laoli”, “laowang”, “xiaocang”, “xiaojing”, “xiaokong”))
names: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[0] at parallelize at :24

scala> names.filter(_.contains(“xiao”))
res3: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at filter at :27

scala> res3.collect
res4: Array[String] = Array(xiaoli, xiaocang, xiaojing, xiaokong)
9. sample(withReplacement, fraction, seed)
作用:

  1. 以指定的随机种子随机抽样出比例为fraction的数据,(抽取到的数量是: size * fraction). 需要注意的是得到的结果并不能保证准确的比例.
  2. withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样. 放回表示数据有可能会被重复抽取到, false 则不可能重复抽取到. 如果是false, 则fraction必须是:[0,1], 是 true 则大于等于0就可以了.
  3. seed用于指定随机数生成器种子。 一般用默认的, 或者传入当前的时间戳
    不放回抽样
    scala> val rdd1 = sc.parallelize(1 to 10)
    rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[15] at parallelize at :24

scala> rdd1.sample(false, 0.5).collect
res15: Array[Int] = Array(1, 3, 4, 7)
放回抽样
scala> rdd1.sample(true, 2).collect
res25: Array[Int] = Array(1, 1, 2, 3, 3, 4, 4, 5, 5, 5, 5, 5, 6, 6, 7, 7, 8, 8, 9)
10. distinct([numTasks]))
作用:
对 RDD 中元素执行去重操作. 参数表示任务的数量.默认值和分区数保持一致.
scala> val rdd1 = sc.parallelize(Array(10,10,2,5,3,5,3,6,9,1))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[28] at parallelize at :24

scala> rdd1.distinct().collect
res29: Array[Int] = Array(6, 10, 2, 1, 3, 9, 5)
5.3.1 Value 类型—2
11. coalesce(numPartitions)
作用: 缩减分区数到指定的数量,用于大数据集过滤后,提高小数据集的执行效率。
scala> val rdd1 = sc.parallelize(0 to 100, 5)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[45] at parallelize at :24

scala> rdd1.partitions.length
res39: Int = 5

// 减少分区的数量至 2
scala> rdd1.coalesce(2)
res40: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[46] at coalesce at :27

scala> res40.partitions.length
res41: Int = 2
注意:
• 第二个参数表示是否shuffle, 如果不传或者传入的为false, 则表示不进行shuffer, 此时分区数减少有效, 增加分区数无效.
12. repartition(numPartitions)
作用: 根据新的分区数, 重新 shuffle 所有的数据, 这个操作总会通过网络.
新的分区数相比以前可以多, 也可以少
scala> val rdd1 = sc.parallelize(0 to 100, 5)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[45] at parallelize at :24

scala> rdd1.repartition(3)
res44: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[51] at repartition at :27

scala> res44.partitions.length
res45: Int = 3

scala> rdd1.repartition(10)
res46: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[55] at repartition at :27

scala> res46.partitions.length
res47: Int = 10
13. coalasce和repartition的区别

  1. coalesce重新分区,可以选择是否进行shuffle过程。由参数shuffle: Boolean = false/true决定。
  2. repartition实际上是调用的coalesce,进行shuffle。源码如下:
    def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    coalesce(numPartitions, shuffle = true)
    }
  3. 如果是减少分区, 尽量避免 shuffle
  4. sortBy(func,[ascending], [numTasks])
    作用: 使用func先对数据进行处理,按照处理后结果排序,默认为正序。

scala> val rdd1 = sc.parallelize(Array(1,3,4,10,4,6,9,20,30,16))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[46] at parallelize at :24

scala> rdd1.sortBy(x => x).collect
res17: Array[Int] = Array(1, 3, 4, 4, 6, 9, 10, 16, 20, 30)

scala> rdd1.sortBy(x => x, true).collect
res18: Array[Int] = Array(1, 3, 4, 4, 6, 9, 10, 16, 20, 30)

// 不用正序
scala> rdd1.sortBy(x => x, false).collect
res19: Array[Int] = Array(30, 20, 16, 10, 9, 6, 4, 4, 3, 1)
15. pipe(command, [envVars])
作用: 管道,针对每个分区,把 RDD 中的每个数据通过管道传递给shell命令或脚本,返回输出的RDD。一个分区执行一次这个命令. 如果只有一个分区, 则执行一次命令.
注意:
• 脚本要放在 worker 节点可以访问到的位置
步骤1: 创建一个脚本文件pipe.sh 脚本
文件内容如下: ==》read line: 一次读一个元素
echo “hello”
while read line;do
echo “>>>”$line ==》打印到新的RDD中,非控制台
done
步骤2: 创建只有 1 个分区的RDD
scala> val rdd1 = sc.parallelize(Array(10,20,30,40), 1)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24

scala> rdd1.pipe("./pipe.sh").collect
res1: Array[String] = Array(hello, >>>10, >>>20, >>>30, >>>40) ==》5个
步骤3: 创建有 2 个分区的 RDD
scala> val rdd1 = sc.parallelize(Array(10,20,30,40), 2)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[3] at parallelize at :24

scala> rdd1.pipe("./pipe.sh").collect
res2: Array[String] = Array(hello, >>>10, >>>20, hello, >>>30, >>>40)
总结: 每个分区执行一次脚本, 但是每个元素算是标准输入中的一行


5.3.2 双 Value 类型交互
这里的“双 Value 类型交互”是指的两个 RDD[V] 进行交互.

  1. union(otherDataset)
    作用:求并集. 对源 RDD 和参数 RDD 求并集后返回一个新的 RDD
    案例
    需求: 创建两个RDD,求并集
    scala> val rdd1 = sc.parallelize(1 to 6)
    rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at :24

scala> val rdd2 = sc.parallelize(4 to 10)
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[3] at parallelize at :24

scala> rdd1.union(rdd2)
res0: org.apache.spark.rdd.RDD[Int] = UnionRDD[4] at union at :29

scala> res0.collect
res1: Array[Int] = Array(1, 2, 3, 4, 5, 6, 4, 5, 6, 7, 8, 9, 10)
注意:
• union和++是等价的
2. subtract (otherDataset)l
作用: 计算差集. 从原 RDD 中减去 原 RDD 和 otherDataset 中的共同的部分.
scala> rdd1.subtract(rdd2).collect
res4: Array[Int] = Array(2, 1, 3)

scala> rdd2.subtract(rdd1).collect
res5: Array[Int] = Array(8, 10, 7, 9)
3. intersection(otherDataset)
作用: 计算交集. 对源 RDD 和参数 RDD 求交集后返回一个新的 RDD
scala> rdd1.intersection(rdd2).collect
res8: Array[Int] = Array(4, 6, 5)
4. cartesian(otherDataset)
作用: 计算 2 个 RDD 的笛卡尔积. 尽量避免使用
scala> rdd1.cartesian(rdd2).collect
res11: Array[(Int, Int)] = Array((1,4), (1,5), (1,6), (2,4), (2,5), (2,6), (3,4), (3,5), (3,6), (1,7), (1,8), (1,9), (1,10), (2,7), (2,8), (2,9), (2,10), (3,7), (3,8), (3,9), (3,10), (4,4), (4,5), (4,6), (5,4), (5,5), (5,6), (6,4), (6,5), (6,6), (4,7), (4,8), (4,9), (4,10), (5,7), (5,8), (5,9), (5,10), (6,7), (6,8), (6,9), (6,10))
5. zip(otherDataset)
作用: 拉链操作. 需要注意的是, 在 Spark 中, 两个 RDD 的元素的数量和分区数都必须相同, 否则会抛出异常.(在 scala 中, 两个集合的长度可以不同)
其实本质就是要求的每个分区的元素的数量相同.
scala> val rdd1 = sc.parallelize(1 to 5)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[34] at parallelize at :24

scala> val rdd2 = sc.parallelize(11 to 15)
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[35] at parallelize at :24

scala> rdd1.zip(rdd2).collect
res17: Array[(Int, Int)] = Array((1,11), (2,12), (3,13), (4,14), (5,15))


5.3.3 Key-Value 类型–1 ==》特殊形式的RDD
大多数的 Spark 操作可以用在任意类型的 RDD 上, 但是有一些比较特殊的操作只能用在key-value类型的 RDD 上.
这些特殊操作大多都涉及到 shuffle 操作, 比如: 按照 key 分组(group), 聚集(aggregate)等.
在 Spark 中, 这些操作在包含对偶类型(Tuple2)的 RDD 上自动可用(通过隐式转换).
object RDD {
implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
(implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null): PairRDDFunctions[K, V] = {
new PairRDDFunctions(rdd)
}
键值对的操作是定义在PairRDDFunctions类上, 这个类是对RDD[(K, V)]的装饰.

  1. partitionBy
    Hash分区器 ==>按照key的hash值进行分区 、 hash % partitionNum
    缺点: 容易造成数据倾斜

RangPartitoner : 边界数组(长度未知),水塘抽样

com.spark.sparkcore.day01.Wordcountcom.spark.sparkcore.day01.Wordcount
作用: 对 pairRDD 进行分区操作,如果原有的 partionRDD 的分区器和传入的分区器相同, 则返回原 pairRDD,否则会生成 ShuffleRDD,即会产生 shuffle 过程。
partitionBy 算子源码
def partitionBy(partitioner: Partitioner): RDD[(K, V)] = self.withScope {

if (self.partitioner == Some(partitioner)) {
self
} else {
new ShuffledRDD[K, V, V](self, partitioner)
}
}
scala> val rdd1 = sc.parallelize(Array((1, “a”), (2, “b”), (3, “c”), (4, “d”)))
rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[0] at parallelize at :24

scala> rdd1.partitions.length
res1: Int = 2

scala> rdd1.partitionBy(new org.apache.spark.HashPartitioner(3)).partitions.length
res3: Int = 3
2. reduceByKey(func, [numTasks]) ==》有预聚合的特点:不改变聚合值得属性,原来是int,聚合后还是int
作用: 在一个(K,V)的 RDD 上调用,返回一个(K,V)的 RDD,使用指定的reduce函数,将相同key的value聚合到一起,reduce任务的个数可以通过第二个可选的参数来设置。

scala> val rdd1 = sc.parallelize(List((“female”,1),(“male”,5),(“female”,5),(“male”,2)))
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at :24

scala> rdd1.reduceByKey(_ + _)
res1: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[1] at reduceByKey at :27

scala> res1.collect
res2: Array[(String, Int)] = Array((female,6), (male,7))
3. groupByKey()
作用: 按照key进行分组.

scala> val rdd1 = sc.parallelize(Array(“hello”, “world”, “spark”, “hello”, “are”, “go”))
rdd1: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[2] at parallelize at :24

scala> val rdd2 = rdd1.map((_, 1))
rdd2: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[3] at map at :26

scala> rdd2.groupByKey()
res3: org.apache.spark.rdd.RDD[(String, Iterable[Int])] = ShuffledRDD[4] at groupByKey at :29

scala> res3.collect
res4: Array[(String, Iterable[Int])] = Array((are,CompactBuffer(1)), (hello,CompactBuffer(1, 1)), (go,CompactBuffer(1)), (spark,CompactBuffer(1)), (world,CompactBuffer(1)))

scala> res3.map(t => (t._1, t._2.sum))
res5: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[5] at map at :31

scala> res5.collect
res7: Array[(String, Int)] = Array((are,1), (hello,2), (go,1), (spark,1), (world,1))
注意:
• 基于当前的实现, groupByKey必须在内存中持有所有的键值对. 如果一个key有太多的value, 则会导致内存溢出(OutOfMemoryError)
• 所以这操作非常耗资源, 如果分组的目的是为了在每个key上执行聚合操作(比如: sum 和 average), 则应该使用PairRDDFunctions.aggregateByKey 或者PairRDDFunctions.reduceByKey, 因为他们有更好的性能(会先在分区进行预聚合)
4. reduceByKey和groupByKey的区别

==>优先选:reducerByKey

  1. reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。
  2. groupByKey:按照key进行分组,直接进行shuffle。
  3. 开发指导:reduceByKey比groupByKey性能更好,建议使用。但是需要注意是否会影响业务逻辑。
  4. aggregateByKey(zeroValue)(seqOp, combOp, [numTasks])
    ==>如果分区内和分区间得逻辑聚合一致,可以用foldbykey替换aggregateByKey
    ==》foldBkey不需要零值得时候, 可以用reduceByKey来替换foldBykey
    reducerBykey < foldBykey < aggregateByKey < combineByKey(零值得函数)

==》零值参与分区内的运算,并决定类型 (k,v)=>(k,u), 零值只参与分区内得计算,不同的key都会参与计算

函数声明:
def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,
combOp: (U, U) => U): RDD[(K, U)] = self.withScope {
aggregateByKey(zeroValue, defaultPartitioner(self))(seqOp, combOp)
}
使用给定的 combine 函数和一个初始化的zero value, 对每个key的value进行聚合.
这个函数返回的类型U不同于源 RDD 中的V类型. U的类型是由初始化的zero value来定的. 所以, 我们需要两个操作: - 一个操作(seqOp)去把 1 个v变成 1 个U - 另外一个操作(combOp)来合并 2 个U
第一个操作用于在一个分区进行合并, 第二个操作用在两个分区间进行合并.
为了避免内存分配, 这两个操作函数都允许返回第一个参数, 而不用创建一个新的U
参数描述:

  1. zeroValue:给每一个分区中的每一个key一个初始值;
  2. seqOp:函数用于在每一个分区中用初始值逐步迭代value;
  3. combOp:函数用于合并每个分区中的结果。

案例:
需求: 创建一个 pairRDD,取出每个分区相同key对应值的最大值,然后相加
查看 ppt

scala> val rdd = sc.parallelize(List((“a”,3),(“a”,2),(“c”,4),(“b”,3),(“c”,6),(“c”,8)),2)
rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at :24

scala> rdd.aggregateByKey(Int.MinValue)(math.max(_, ), _ +)
res0: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[1] at aggregateByKey at :27

scala> res0.collect
res1: Array[(String, Int)] = Array((b,3), (a,3), (c,12))
练习: 计算每个 key 的平均值
6. foldByKey
参数: (zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
作用:aggregateByKey的简化操作,seqop和combop相同
scala> val rdd = sc.parallelize(Array((“a”,3), (“a”,2), (“c”,4), (“b”,3), (“c”,6), (“c”,8)))
rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[2] at parallelize at :24

scala> rdd.foldByKey(0)(_ + _).collect
res5: Array[(String, Int)] = Array((b,3), (a,5), (c,18))
思考: workcount 可以使用那些算子?
思考: reduceByKey, aggregateByKey, foldByKey 的区别和联系?


5.3.3 Key-Value 类型—2
7. combineByKey[C]
函数声明:
def combineByKey[C](
createCombiner: V => C, ==》在每个分区只执行一次

                   mergeValue: (C, V) => C, ==》分区内聚合
                   mergeCombiners: (C, C) => C): RDD[(K, C)] = self.withScope {
combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners,
    partitioner, mapSideCombine, serializer)(null)

}

  1. 作用: 针对每个K, 将V进行合并成C, 得到RDD[(K,C)]
  2. 参数描述:
    • createCombiner: combineByKey会遍历分区中的每个key-value对. 如果第一次碰到这个key, 则调用createCombiner函数,传入value, 得到一个C类型的值.(如果不是第一次碰到这个 key, 则不会调用这个方法)
    • mergeValue: 如果不是第一个遇到这个key, 则调用这个函数进行合并操作. 分区内合并
    • mergeCombiners 跨分区合并相同的key的值©. 跨分区合并
  3. workcount
    案例
    需求1: 创建一个 pairRDD,根据 key 计算每种 key 的value的平均值。(先计算每个key出现的次数以及可以对应值的总和,再相除得到结果)
    查看 ppt

scala> val input = sc.parallelize(Array((“a”, 88), (“b”, 95), (“a”, 91), (“b”, 93), (“a”, 95), (“b”, 98)),2)
input: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[5] at parallelize at :24
// acc 累加器, 用来记录分区内的值的和这个 key 出现的次数
// acc1, acc2 跨分区的累加器
scala> input.combineByKey((_, 1), (acc:(Int, Int), v) => (acc._1 + v, acc._2 + 1), (acc1:(Int, Int), acc2: (Int, Int))=> (acc1._1 + acc2._1, acc1._2 + acc2._2))
res10: org.apache.spark.rdd.RDD[(String, (Int, Int))] = ShuffledRDD[7] at combineByKey at :27

scala> res10.collect
res11: Array[(String, (Int, Int))] = Array((b,(286,3)), (a,(274,3)))

scala> res10.map(t => (t._1, t._2._1.toDouble / t._2._2)).collect
res12: Array[(String, Double)] = Array((b,95.33333333333333), (a,91.33333333333333))
对比几个按照 key 聚集的函数的区别和联系
8. sortByKey
是一个整体排序

作用: 在一个(K,V)的 RDD 上调用, K必须实现 Ordered[K] 接口(或者有一个隐式值: Ordering[K]), 返回一个按照key进行排序的(K,V)的 RDD
scala> val rdd = sc.parallelize(Array((1, “a”), (10, “b”), (11, “c”), (4, “d”), (20, “d”), (10, “e”)))
rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[11] at parallelize at :24

scala> rdd.sortByKey()
res25: org.apache.spark.rdd.RDD[(Int, String)] = ShuffledRDD[14] at sortByKey at :27

scala> res25.collect
res26: Array[(Int, String)] = Array((1,a), (4,d), (10,b), (10,e), (11,c), (20,d))

scala> rdd.sortByKey(true).collect
res27: Array[(Int, String)] = Array((1,a), (4,d), (10,b), (10,e), (11,c), (20,d))

// 倒序
scala> rdd.sortByKey(false).collect
res28: Array[(Int, String)] = Array((20,d), (11,c), (10,b), (10,e), (4,d), (1,a))
9. mapValues
作用: 针对(K,V)形式的类型只对V进行操作
scala> val rdd = sc.parallelize(Array((1, “a”), (10, “b”), (11, “c”), (4, “d”), (20, “d”), (10, “e”)))
rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[21] at parallelize at :24

scala> rdd.mapValues("<" + _ + “>”).collect
res29: Array[(Int, String)] = Array((1,), (10,), (11,), (4,), (20,), (10,))
10. join(otherDataset, [numTasks])
内连接: 与sql种的类似
在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素对在一起的(K,(V,W))的RDD
scala> var rdd1 = sc.parallelize(Array((1, “a”), (1, “b”), (2, “c”)))
rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[6] at parallelize at :24

scala> var rdd2 = sc.parallelize(Array((1, “aa”), (3, “bb”), (2, “cc”)))
rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[7] at parallelize at :24

scala> rdd1.join(rdd2).collect
res2: Array[(Int, (String, String))] = Array((2,(c,cc)), (1,(a,aa)), (1,(b,aa)))
注意:
• 如果某一个 RDD 有重复的 Key, 则会分别与另外一个 RDD 的相同的 Key进行组合.
• 也支持外连接: leftOuterJoin, rightOuterJoin, and fullOuterJoin.
11. cogroup(otherDataset, [numTasks])
作用:在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
scala> val rdd1 = sc.parallelize(Array((1, 10),(2, 20),(1, 100),(3, 30)),1)
rdd1: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[23] at parallelize at :24

scala> val rdd2 = sc.parallelize(Array((1, “a”),(2, “b”),(1, “aa”),(3, “c”)),1)
rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[24] at parallelize at :24

scala> rdd1.cogroup(rdd2).collect
res9: Array[(Int, (Iterable[Int], Iterable[String]))] = Array((1,(CompactBuffer(10, 100),CompactBuffer(a, aa))), (3,(CompactBuffer(30),CompactBuffer©)), (2,(CompactBuffer(20),CompactBuffer(b))))


5.3.4 案例实操
需求

  1. 数据结构:时间戳,省份,城市,用户,广告,字段使用空格分割。
    1516609143867 6 7 64 16
    1516609143869 9 4 75 18
    1516609143869 1 7 87 12
    下载数据
  2. 需求: 统计出每一个省份广告被点击次数的 TOP3
    具体实现
    步骤1: 把文件放入到 resources目录下
    步骤2: 具体代码
    package day03

/*
数据结构:时间戳,省份,城市,用户,广告 字段使用空格分割。

样本如下:
1516609143867 6 7 64 16
1516609143869 9 4 75 18
1516609143869 1 7 87 12

统计出每一个省份广告被点击次数的 TOP3
/
object Practice {
def main(args: Array[String]): Unit = {
// 1. 初始化spark配置信息, 并建立到spark的连接
val conf = new SparkConf().setAppName(“Practice”).setMaster("local[
]")
val sc = new SparkContext(conf)
// 2. 从文件中读取数据, 得到 RDD. RDD中存储的是文件的中的每行数据
val lines: RDD[String] = sc.textFile(“file://” + ClassLoader.getSystemResource(“agent.log”))

    // 3. ((provice, ad), 1)
    val provinceADAndOne: RDD[((String, String), Int)] = lines.map(line => {
        val splits: Array[String] = line.split(" ")
        ((splits(1), splits(4)), 1)
    })
    // 4. 计算每个省份每个广告被点击的总次数
    val provinceADSum: RDD[((String, String), Int)] = provinceADAndOne.reduceByKey(_ + _)

    // 5. 将省份作为key,广告加点击数为value: (Province,(AD,sum))
    val provinceToAdSum: RDD[(String, (String, Int))] = provinceADSum.map(x => (x._1._1, (x._1._2, x._2)))

    // 6. 按照省份进行分组
    val provinceGroup: RDD[(String, Iterable[(String, Int)])] = provinceToAdSum.groupByKey()

    //7. 对同一个省份的广告进行排序, 按照点击数的降序
    val result: RDD[(String, List[(String, Int)])] = provinceGroup.mapValues {
        x => x.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
    }

    //8. 按照省份的升序展示最终结果
    result.sortBy(_._1).collect.foreach(println)

    // 9. 关闭连接
    sc.stop()

}

}

使用倒推法:

=> [((pid, cid), 1), ((pid, cid), 1)] reuceByKey
=> [(pid, cid), count), (pid, cid), count] map
=> [pid, (cid, count), (cid, count)] groupByKey
=> [pid, Iterable((cid, count), (cid, count), …)]


5.4 RDD的 Action 操作

  1. reduce(func)
    通过func函数聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据。
    scala> val rdd1 = sc.parallelize(1 to 100)
    rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24

scala> rdd1.reduce(_ + _)
res0: Int = 5050

scala> val rdd2 = sc.parallelize(Array((“a”, 1), (“b”, 2), (“c”, 3)))
rdd2: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[1] at parallelize at :24

scala> rdd2.reduce((x, y) => (x._1 + y._1, x._2 + y._2))
res2: (String, Int) = (abc,6)

  1. collect ==》不要太大,太大容易内存溢出
    以数组的形式返回 RDD 中的所有元素.
    所有的数据都会被拉到 driver 端, 所以要慎用
  2. count()
    返回 RDD 中元素的个数.
  3. take(n)
    返回 RDD 中前 n 个元素组成的数组.
    take 的数据也会拉到 driver 端, 应该只对小数据集使用
    scala> val rdd1 = sc.makeRDD(Array(10, 20, 30, 50, 60))
    rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at makeRDD at :24

scala> rdd1.take(2)
res3: Array[Int] = Array(10, 20)
5. first
返回 RDD 中的第一个元素. 类似于take(1).
6. takeOrdered(n, [ordering]) ==》全局排序的缺点
返回排序后的前 n 个元素, 默认是升序排列.
数据也会拉到 driver 端
scala> val rdd1 = sc.makeRDD(Array(100, 20, 130, 500, 60))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[4] at makeRDD at :24

scala> rdd1.takeOrdered(2)
res6: Array[Int] = Array(20, 60)

scala> rdd1.takeOrdered(2)(Ordering.Int.reverse)
res7: Array[Int] = Array(500, 130)
7. aggregate
def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U
aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致
注意:
• zeroValue 分区内聚合和分区间聚合的时候各会使用:每个分区使用一次,分区间进行合并时再进行一次
scala> val rdd1 = sc.makeRDD(Array(100, 30, 10, 30, 1, 50, 1, 60, 1), 2)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[8] at makeRDD at :24

scala> rdd1.aggregate(0)(_ + _, _ + _)
res12: Int = 283

scala> val rdd1 = sc.makeRDD(Array(“a”, “b”, “c”, “d”), 2)
rdd1: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[9] at makeRDD at :24

scala> rdd1.aggregate(“x”)(_ + _, _ + _)
res13: String = xxabxcd
8. fold
折叠操作,aggregate的简化操作,seqop和combop一样的时候,可以使用fold
scala> val rdd1 = sc.makeRDD(Array(100, 30, 10, 30, 1, 50, 1, 60, 1), 2)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[10] at makeRDD at :24

scala> rdd1.fold(0)(_ + _)
res16: Int = 283

scala> val rdd1 = sc.makeRDD(Array(“a”, “b”, “c”, “d”), 2)
rdd1: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[11] at makeRDD at :24

scala> rdd1.fold(“x”)(_ + _)
res17: String = xxabxcd

  1. saveAsTextFile(path)
    作用:将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark 将会调用toString方法,将它装换为文件中的文本
  2. saveAsSequenceFile(path)
    作用:将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以使 HDFS 或者其他 Hadoop 支持的文件系统。
  3. saveAsObjectFile(path)
    作用:用于将 RDD 中的元素序列化成对象,存储到文件中。
  4. countByKey() ==>按key的不同记数 ,这是一个行动算子
    作用:针对(K,V)类型的 RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。
    应用: 可以用来查看数据是否倾斜
    scala> val rdd1 = sc.parallelize(Array((“a”, 10), (“a”, 20), (“b”, 100), (“c”, 200)))
    rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[15] at parallelize at :24

scala> rdd1.countByKey()
res19: scala.collection.Map[String,Long] = Map(b -> 1, a -> 2, c -> 1)

  1. foreach(func) ==》作用: 向外部存储写数据:jdbc / hive / hbase
    //缺点: 每次都需要进行一次连接; foreachPartiton这个一个分区执行一次好点

作用: 针对 RDD 中的每个元素都执行一次func
每个函数是在 Executor 上执行的, 不是在 driver 端执行的.


5.5 RDD 中函数的传递
我们进行 Spark 进行编程的时候, 初始化工作是在 driver端完成的, 而实际的运行程序是在executor端进行的. 所以就涉及到了进程间的通讯, 数据是需要序列化的.

Driver: main线程中的一些操作 (main函数的中线程+ .filter方法)
Executor: 实际运行的程序 (RDD的数据在这里) 进程间通讯
传递函数 ==>进程间通讯,数据需要序列化
package day04

说明:
• 直接运行程序会发现报错: 没有初始化. 因为rdd.filter(isMatch) 用到了对象this的方法isMatch, 所以对象this需要序列化,才能把对象从driver发送到executor.

• 解决方案: 让 Searcher 类实现序列化接口:Serializable (采用动态序列化时不可行的)

传递变量

说明:
• 这次没有传递函数, 而是传递了一个属性过去. 仍然会报错没有序列化. 因为this仍然没有序列化.
• 解决方案有 2 种:
• (1)让类实现序列化接口:Serializable
(2)传递局部变量而不是属性. ==》这个与对象没有关系,这个要注意;除了这个底部不需要继承,其他地方都要有继承,即使时setset(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)

kryo 序列化框架E
在Spark的架构中,在网络中传递的或者缓存在内存、硬盘中的对象需要进行序列化操作,序列化的作用主要是利用时间换空间:
参考地址: https://github.com/EsotericSoftware/kryo
Java 的序列化比较重, 能够序列化任何的类. 比较灵活,但是相当的慢, 并且序列化后对象的提交也比较大.
Spark 处于性能的考虑, 支持另外一种序列化机制: kryo (2.0开始支持). kryo 比较快和简洁.(速度是Serializable的10倍). 想获取更好的性能应该使用 kryo 来序列化.
从2.0开始, Spark 内部已经在使用 kryo 序列化机制: 当 RDD 在 Shuffle数据的时候, 简单数据类型, 简单数据类型的数组和字符串类型已经在使用 kryo 来序列化.
有一点需要注意的是: 即使使用 kryo 序列化, 也要继承 Serializable 接口
Extends Serializable.
==》默认可以省略,源码中已经声明了


5.6 RDD 的依赖关系
RDD Lineage
scala> var rdd1 = sc.textFile("./LICENSE")rdd1: org.apache.spark.rdd.RDD[String] = ./words.txt MapPartitionsRDD[16] at textFile at :24

scala> val rdd2 = rdd1.flatMap(_.split(" "))
rdd2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[17] at flatMap at :26

scala> val rdd3 = rdd2.map((_, 1))
rdd3: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[18] at map at :28

scala> val rdd4 = rdd3.reduceByKey(_ + _)
rdd4: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[19] at reduceByKey at :30
查看 RDD 的血缘关系
scala> rdd1.toDebugString
res1: String = ==》切片数,并行度(2) 2个
(2) ./words.txt MapPartitionsRDD[1] at textFile at :24 []
| ./words.txt HadoopRDD[0] at textFile at :24 []

scala> rdd2.toDebugString
res2: String = 3个
(2) MapPartitionsRDD[2] at flatMap at :26 []
| ./words.txt MapPartitionsRDD[1] at textFile at :24 []
| ./words.txt HadoopRDD[0] at textFile at :24 []

scala> rdd3.toDebugString
res3: String = 4个 同一个stage中
(2) MapPartitionsRDD[3] at map at :28 []
| MapPartitionsRDD[2] at flatMap at :26 []
| ./words.txt MapPartitionsRDD[1] at textFile at :24 []
| ./words.txt HadoopRDD[0] at textFile at :24 []

scala> rdd4.toDebugString
res4: String = reduce中,新的stage
(2) ShuffledRDD[4] at reduceByKey at :30 []
±(2) MapPartitionsRDD[3] at map at :28 []
| MapPartitionsRDD[2] at flatMap at :26 []
| ./words.txt MapPartitionsRDD[1] at textFile at :24 []
| ./words.txt HadoopRDD[0] at textFile at :24 []
说明:
• 圆括号中的数字表示 RDD 的并行度. 也就是有几个分区.
查看 RDD 的依赖关系

scala> rdd1.dependencies
res28: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@70dbde75)

scala> rdd2.dependencies
res29: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@21a87972)

scala> rdd3.dependencies
res30: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@4776f6af)

scala> rdd4.dependencies ==>shuffle依赖
res31: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@4809035f)
想理解 RDDs 是如何工作的, 最重要的事情就是了解 transformations.
RDD 之间的关系可以从两个维度来理解: 一个是 RDD 是从哪些 RDD 转换而来, 也就是 RDD 的 parent RDD(s)是什么; 另一个就是 RDD 依赖于 parent RDD(s)的哪些 Partition(s). 这种关系就是 RDD 之间的依赖.
==》需要知道RDD的转换和依赖是什么
依赖 有 2 种策略:
• 窄依赖(transformations with narrow dependencies)
• 宽依赖(transformations with wide dependencies)
宽依赖对 Spark 去评估一个 transformations 有更加重要的影响, 比如对性能的影响.
窄依赖 ==>转换算子多数是窄依赖
如果 B RDD 是由 A RDD 计算得到的, 则 B RDD 就是 Child RDD, A RDD 就是 parent RDD.
如果依赖关系在设计的时候就可以确定, 而不需要考虑父 RDD 分区中的记录, 并且如果父 RDD 中的每个分区最多只有一个子分区, 这样的依赖就叫窄依赖
一句话总结: 父 RDD 的每个分区最多被一个 RDD 的分区使用

一个子可以有多个父
具体来说, 窄依赖的时候, 子 RDD 中的分区要么只依赖一个父 RDD 中的一个分区(比如map, filter操作), 要么在设计时候就能确定子 RDD 是父 RDD 的一个子集(比如: coalesce).
所以, 窄依赖的转换可以在任何的的一个分区上单独执行, 而不需要其他分区的任何信息. ==>只要不是窄依赖就是shuffle依赖
宽依赖 ==》主要看运算需不需要shuffle判断是宽窄依赖
如果 父 RDD 的分区被不止一个子 RDD 的分区依赖, 就是宽依赖. ==>1个父多个子

宽依赖工作的时候, 不能随意在某些记录上运行, 而是需要使用特殊的方式(比如按照 key)来获取分区中的所有数据.
例如: 在排序(sort)的时候, 数据必须被分区, 同样范围的 key 必须在同一个分区内. 具有宽依赖的 transformations 包括: sort, reduceByKey, groupByKey, join, 和调用rePartition函数的任何操作.


5.7 Spark 中的 Job 划分
一个 Spark 应用包含一个驱动进程(driver process, 在这个进程中写 spark 的逻辑代码)和多个执行器进程(executor process, 跨越集群中的多个节点).
Spark 程序自己是运行在驱动节点, 然后发送指令到执行器节点.
一个 Spark 集群可以同时运行多个 Spark 应用, 这些应用是由集群管理器(cluster manager)来调度
Spark 应用可以并发的运行多个 job. job 对应着给定的应用内的在 RDD 上的每个 action 操作.
Spark 应用
一个 Spark 应用可以包含多个 Spark job, Spark job 是在驱动程序中由 SparkContext 来定义的.
当启动一个 SparkContext 的时候, 就开启了一个 Spark 应用. 一个驱动程序被启动了, 多个执行器在集群中的多个工作节点(worker nodes)也被启动了. 一个执行器就是一个 JVM, 一个执行器不能跨越多个节点, 但是一个节点可以包括多个执行器.
一个 RDD 会跨多个执行器被并行计算. 每个执行器可以有这个 RDD 的多个分区, 但是一个分区不能跨越多个执行器.


5.8 Spark Job 的划分
由于 Spark 的懒执行, 在驱动程序调用一个action之前, Spark 应用不会做任何事情.
针对每个 action, Spark 调度器就创建一个执行图(execution graph)和启动一个 Spark job
每个 job 由多个stages 组成, 这些 stages 就是实现最终的 RDD 所需的数据转换的步骤. 一个宽依赖划分一个 stage.
每个 stage 由多个 tasks 来组成, 这些 tasks 就表示每个并行计算, 并且会在多个执行器上执行.
==》划分:
Application:
表示一个应用 SparkContext
一个应用(4040)里面有多个job
Job: 在一个应用中,每调用一次行动算子(collect),就会启动一个job
Stage:(多个分区内 并行执行)
阶段,有一个shuffle算子,就一个阶段处理(宽依赖,就有shuffle)
阶段与阶段之间串行

Task
任务
一个阶段内会包含多个task, 是由RDD的分区数(切片数)
一个分区一个task,可以多个task并行
以上几点,从上到下依次,一个应用可以由多个job。。。

DAG(Directed Acyclic Graph) 有向无环图
Spark 的顶层调度层使用 RDD 的依赖为每个 job 创建一个由 stages 组成的 DAG(有向无环图). 在 Spark API 中, 这被称作 DAG 调度器(DAG Scheduler).
我们已经注意到, 有些错误, 比如: 连接集群的错误, 配置参数错误, 启动一个 Spark job 的错误, 这些错误必须处理, 并且都表现为 DAG Scheduler 错误. 这是因为一个 Spark job 的执行是被 DAG 来处理.
DAG 为每个 job 构建一个 stages 组成的图表, 从而确定运行每个 task 的位置, 然后传递这些信息给 TaskSheduler. TaskSheduler 负责在集群中运行任务.

Jobs
Spark job 处于 Spark 执行层级结构中的最高层. 每个 Spark job 对应一个 action, 每个 action 被 Spark 应用中的驱动所程序调用.
可以把 Action 理解成把数据从 RDD 的数据带到其他存储系统的组件(通常是带到驱动程序所在的位置或者写到稳定的存储系统中)
只要一个 action 被调用, Spark 就不会再向这个 job 增加新的东西.
stages
前面说过, RDD 的转换是懒执行的, 直到调用一个 action 才开始执行 RDD 的转换.
正如前面所提到的, 一个 job 是由调用一个 action 来定义的. 一个 action 可能会包含一个或多个转换( transformation ), Spark 根据宽依赖把 job 分解成 stage.
从整体来看, 一个 stage 任务是“计算(task)”的集合, 每个“计算”在各自的 Executor 中进行运算, 而不需要同其他的执行器或者驱动进行网络通讯. 换句话说, 当任何两个 workers 之间开始需要网络通讯的时候, 这时候一个新的 stage 就产生了, 例如: shuffle 的时候.
这些创建 stage 边界的依赖称为 ShuffleDependencies. shuffle 是由宽依赖所引起的, 比如: sort, groupBy, 因为他们需要在分区中重新分发数据. 那些窄依赖的转换会被分到同一个 stage 中.
想想我们以前学习的 “worldcount 案例”

Spark 会把 flatMap, map 合并到一个 stage 中, 因为这些转换不需要 shuffle. 所以, 数据只需要传递一次, 每个执行器就可以顺序的执行这些操作.
因为边界 stage 需要同驱动进行通讯, 所以与 job 有关的 stage 通常必须顺序执行而不能并行执行.
如果这个 stage 是用来计算不同的 RDDs, 被用来合并成一个下游的转换(比如: join), 也是有可能并行执行的. 但是仅需要计算一个 RDD 的宽依赖转换必须顺序计算.
所以, 设计程序的时候, 尽量少用 shuffle.
Tasks ==>调度最小的单位
stage 由 tasks 组成. 在执行层级中, task 是最小的执行单位. 每一个 task 表现为一个本地计算.
一个 stage 中的所有 tasks 会对不同的数据执行相同的代码.(程序代码一样, 只是作用在了不同的数据上)
一个 task 不能被多个执行器来执行, 但是, 每个执行器会动态的分配多个 slots 来执行 tasks, 并且在整个生命周期内会并行的运行多个 task. 每个 stage 的 task 的数量对应着分区的数量, 即每个 Partition 都被分配一个 Task

在大多数情况下, 每个 stage 的所有 task 在下一个 stage 开启之前必须全部完成.


5.0 RDD 的持久化
每碰到一个 Action 就会产生一个 job, 每个 job 开始计算的时候总是从这个 job 最开始的 RDD 开始计算.
先看一段代码
package day04

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object CacheDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
val sc = new SparkContext(conf)

    val rdd1 = sc.parallelize(Array("ab", "bc"))
    val rdd2 = rdd1.flatMap(x => {
        println("flatMap...")
        x.split("")
    })
    val rdd3: RDD[(String, Int)] = rdd2.map(x => {
        (x, 1)
    })

    rdd3.collect.foreach(println)
    println("-----------")
    rdd3.collect.foreach(println)

}

}
执行结果:

说明:
• 每调用一次 collect, 都会创建一个新的 job, 每个 job 总是从它血缘的起始开始计算. 所以, 会发现中间的这些计算过程都会重复的执行.
• 原因是因为 rdd记录了整个计算过程. 如果计算的过程中出现哪个分区的数据损坏或丢失, 则可以从头开始计算来达到容错的目的.
RDD 数据的持久化
每个 job 都会重新进行计算, 在有些情况下是没有必要, 如何解决这个问题呢?
Spark 一个重要能力就是可以持久化数据集在内存中. 当我们持久化一个 RDD 时, 每个节点都会存储他在内存中计算的那些分区, 然后在其他的 action 中可以重用这些数据. 这个特性会让将来的 action 计算起来更快(通常块 10 倍). 对于迭代算法和快速交互式查询来说, 缓存(Caching)是一个关键工具.
可以使用方法persist()或者cache()来持久化一个 RDD. 在第一个 action 会计算这个 RDD, 然后把结果的存储到他的节点的内存中. Spark 的 Cache 也是容错: 如果 RDD 的任何一个分区的数据丢失了, Spark 会自动的重新计算.
RDD 的各个 Partition 是相对独立的, 因此只需要计算丢失的部分即可, 并不需要重算全部 Partition
另外, 允许我们对持久化的 RDD 使用不同的存储级别.
例如: 可以存在磁盘上, 存储在内存中(堆内存中), 跨节点做复本.
可以给persist()来传递存储级别. cache()方法是使用默认存储级别(StorageLevel.MEMORY_ONLY)的简写方法.
Storage Level Meaning
MEMORY_ONLY Store RDD as deserialized Java objects in the JVM. If the RDD does not fit in memory, some partitions will not be cached and will be recomputed on the fly each time they’re needed. This is the default level.
MEMORY_AND_DISK Store RDD as deserialized Java objects in the JVM. If the RDD does not fit in memory, store the partitions that don’t fit on disk, and read them from there when they’re needed.
MEMORY_ONLY_SER (Java and Scala) Store RDD as serialized Java objects (one byte array per partition). This is generally more space-efficient than deserialized objects, especially when using a fast serializer, but more CPU-intensive to read.

MEMORY_AND_DISK_SER (Java and Scala) Similar to MEMORY_ONLY_SER, but spill partitions that don’t fit in memory to disk instead of recomputing them on the fly each time they’re needed.
DISK_ONLY Store the RDD partitions only on disk.
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. Same as the levels above, but replicate each partition on two cluster nodes.
OFF_HEAP (experimental) Similar to MEMORY_ONLY_SER, but store the data in off-heap memory. This requires off-heap memory to be enabled.
// rdd2.cache() // 等价于 rdd2.persist(StorageLevel.MEMORY_ONLY)
rdd2.persist(StorageLevel.MEMORY_ONLY)

说明:
• 第一个 job 会计算 RDD2, 以后的 job 就不用再计算了.
• 有一点需要说明的是, 即使我们不手动设置持久化, Spark 也会自动的对一些 shuffle 操作的中间数据做持久化操作(比如: reduceByKey). 这样做的目的是为了当一个节点 shuffle 失败了避免重新计算整个输入. 当时, 在实际使用的时候, 如果想重用数据, 仍然建议调用persist 或 cache


5.10 设置检查点
Spark 中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过 Lineage 做容错的辅助
Lineage 过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的 RDD 开始重做 Lineage,就会减少开销。
检查点通过将数据写入到 HDFS 文件系统实现了 RDD 的检查点功能。
为当前 RDD 设置检查点。该函数将会创建一个二进制的文件,并存储到 checkpoint 目录中,该目录是用 SparkContext.setCheckpointDir()设置的。在 checkpoint 的过程中,该RDD 的所有依赖于父 RDD中 的信息将全部被移除。
对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发, 在触发的时候需要对这个 RDD 重新计算.

package day04

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object CheckPointDemo {
def main(args: Array[String]): Unit = {
// 要在SparkContext初始化之前设置, 都在无效 (动态设置方式)
//在sc之前就可以,还可以放在main方法外面
System.setProperty(“HADOOP_USER_NAME”, “spark”)
val conf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
val sc = new SparkContext(conf)
// 设置 checkpoint的目录. 如果spark运行在集群上, 则必须是 hdfs 目录
sc.setCheckpointDir(“hdfs://hadoop102:9000/checkpoint”)
val rdd1 = sc.parallelize(Array(“abc”))
val rdd2: RDD[String] = rdd1.map(_ + " : " + System.currentTimeMillis())

    /*
    标记 RDD2的 checkpoint.
    RDD2会被保存到文件中(文件位于前面设置的目录中), 并且会切断到父RDD的引用, 也就是切断了它向上的血缘关系
    该函数必须在job被执行之前调用.
    强烈建议把这个RDD序列化到内存中, 否则, 把他保存到文件的时候需要重新计算.
     */
    rdd2.checkpoint()
    rdd2.collect().foreach(println)
    rdd2.collect().foreach(println)
    rdd2.collect().foreach(println)
}

}

存到HDFS需要设置:
(1) System.setProperty(“HADOOP_USER_NAME”, “spark”)
(2) 直接设置环境变化

说明上图重新启用了一个新的job’

持久化和checkpoint的区别
==》checkpoint会存到一个目录里面,下次可以从目录中读取
Checkpoint的执行机制: 针对rdd4做 Checkpoint,执行完后,会启动一个job专门来做checkpoint

  1. 持久化只是将数据保存在 BlockManager 中,而 RDD 的 Lineage 是不变的。但是checkpoint 执行完后,RDD 已经没有之前所谓的依赖 RDD 了,而只有一个强行为其设置的checkpointRDD,RDD 的 Lineage 改变了。
  2. 持久化的数据丢失可能性更大,磁盘、内存都可能会存在数据丢失的情况。但是 checkpoint 的数据通常是存储在如 HDFS 等容错、高可用的文件系统,数据丢失可能性较小。
  3. 注意: 默认情况下,如果某个 RDD 没有持久化,但是设置了checkpoint,会存在问题. 本来这个 job 都执行结束了,但是由于中间 RDD 没有持久化,checkpoint job 想要将 RDD 的数据写入外部文件系统的话,需要全部重新计算一次,再将计算出来的 RDD 数据 checkpoint到外部文件系统。 所以,建议对 checkpoint()的 RDD 使用持久化, 这样 RDD 只需要计算一次就可以了.
    直接用cache的结果

第 6 章 Key-Value 类型 RDD 的数据分区器
对于只存储 value的 RDD, 不需要分区器.
只有存储Key-Value类型的才会需要分区器.
Spark 目前支持 Hash 分区和 Range 分区,用户也可以自定义分区.
Hash 分区为当前的默认分区,Spark 中分区器直接决定了 RDD 中分区的个数、RDD 中每条数据经过 Shuffle 过程属于哪个分区和 Reduce 的个数.
查看 RDD 的分区
value RDD 的分区器
scala> val rdd1 = sc.parallelize(Array(10))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at :25

scala> rdd1.partitioner
res8: Option[org.apache.spark.Partitioner] = None
key-value RDD 的分区器
scala> val rdd1 = sc.parallelize(Array((“hello”, 1), (“world”, 1)))
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[3] at parallelize at :25

scala> rdd1.partitioner
res11: Option[org.apache.spark.Partitioner] = None

// 导入HashPartitioner
import org.apache.spark.HashPartitioner
// 对 rdd1 重新分区, 得到分区后的 RDD, 分区器使用 HashPartitioner
scala> val rdd2 = rdd1.partitionBy(new HashPartitioner(3))
rdd2: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[5] at partitionBy at :27

scala> rdd2.partitioner
res14: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@3)
HashPartitioner
HashPartitioner分区的原理:对于给定的key,计算其hashCode,并除以分区的个数取余,如果余数小于 0,则用余数+分区的个数(否则加0),最后返回的值就是这个key所属的分区ID。

package day01

import org.apache.spark.rdd.RDD
import org.apache.spark.{HashPartitioner, SparkConf, SparkContext}

object Test {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
val sc = new SparkContext(conf)
val rdd1 = sc.parallelize(Array((10, “a”), (20, “b”), (30, “c”), (40, “d”), (50, “e”), (60, “f”)))
// 把分区号取出来, 检查元素的分区情况
val rdd2: RDD[(Int, String)] = rdd1.mapPartitionsWithIndex((index, it) => it.map(x => (index, x._1 + " : " + x._2)))

    println(rdd2.collect.mkString(","))

    // 把 RDD1使用 HashPartitioner重新分区
    val rdd3 = rdd1.partitionBy(new HashPartitioner(5))
    // 检测RDD3的分区情况
    val rdd4: RDD[(Int, String)] = rdd3.mapPartitionsWithIndex((index, it) => it.map(x => (index, x._1 + " : " + x._2)))
    println(rdd4.collect.mkString(","))


}

}

RangePartitioner
HashPartitioner 分区弊端: 可能导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有 RDD 的全部数据。比如我们前面的例子就是一个极端, 他们都进入了 0 分区.
RangePartitioner 作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。实现过程为:
第一步:先从整个 RDD 中抽取出样本数据,将样本数据排序,计算出每个分区的最大 key 值,形成一个Array[KEY]类型的数组变量 rangeBounds;(边界数组).
第二步:判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求 RDD 中的 KEY 类型必须是可以排序的.
比如[1,100,200,300,400],然后对比传进来的key,返回对应的分区id。
自定义分区器
要实现自定义的分区器,你需要继承 org.apache.spark.Partitioner, 并且需要实现下面的方法:

  1. numPartitions
    该方法需要返回分区数, 必须要大于0.
  2. getPartition(key)
    返回指定键的分区编号(0到numPartitions-1)。
  3. equals
    Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同
  4. hashCode
    如果你覆写了equals, 则也应该覆写这个方法.
    MyPartitioner
    package day04

import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}
/*
使用自定义的 Partitioner 是很容易的 :只要把它传给 partitionBy() 方法即可。

Spark 中有许多依赖于数据混洗的方法,比如 join() 和 groupByKey(),
它们也可以接收一个可选的 Partitioner 对象来控制输出数据的分区方式。
/
object MyPartitionerDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(“Practice”).setMaster("local[
]")
val sc = new SparkContext(conf)

    val rdd1 = sc.parallelize(
        Array((10, "a"), (20, "b"), (30, "c"), (40, "d"), (50, "e"), (60, "f")),
        3)
    val rdd2: RDD[(Int, String)] = rdd1.partitionBy(new MyPartitioner(4))
    val rdd3: RDD[(Int, String)] = rdd2.mapPartitionsWithIndex((index, items) => items.map(x => (index, x._1 + " : " + x._2)))
    println(rdd3.collect.mkString(" "))

}

}

class MyPartitioner(numPars: Int) extends Partitioner {
override def numPartitions: Int = numPars

override def getPartition(key: Any): Int = {
    1
}

}


第 7 章 文件中数据的读取和保存
本章专门学习如何从文件中读取数据和保存数据到文件中.
从文件中读取数据是创建 RDD 的一种方式.
把数据保存的文件中的操作是一种 Action.
Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为:Text文件、Json文件、Csv文件( ,号隔开/Tsv)、Sequence文件以及Object文件;
文件系统分为:本地文件系统、HDFS、Hbase 以及 数据库。
平时用的比较多的就是: 从 HDFS 读取和保存 Text 文件.


7.1 读写 Text 文件
// 读取本地文件
scala> val rdd1 = sc.textFile("./words.txt")
rdd1: org.apache.spark.rdd.RDD[String] = ./words.txt MapPartitionsRDD[5] at textFile at :24

scala> val rdd2 = rdd1.flatMap(.split(" ")).map((, 1)).reduceByKey(_ +_)
rdd2: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[8] at reduceByKey at :26
// 保存数据到 hdfs 上.
scala> rdd2.saveAsTextFile(“hdfs://hadoop102:9000/words_output”)


为什么集群模式下,文件的输入路径会报不存在?

7.2 读取 Json 文件
如果 JSON 文件中每一行就是一个 JSON 记录,那么可以通过将 JSON 文件当做文本文件来读取,然后利用相关的 JSON 库对每一条数据进行 JSON 解析。
注意:使用 RDD 读取 JSON 文件处理很复杂,同时 SparkSQL 集成了很好的处理 JSON 文件的方式,所以实际应用中多是采用SparkSQL处理JSON文件。
关于 SparkSQL 后面的章节专门去讲
// 读取 json 数据的文件, 每行是一个 json 对象
scala> val rdd1 = sc.textFile("/opt/module/spark-local/examples/src/main/resources/people.json")
rdd1: org.apache.spark.rdd.RDD[String] = /opt/module/spark-local/examples/src/main/resources/people.json MapPartitionsRDD[11] at textFile at :24
// 导入 scala 提供的可以解析 json 的工具类
scala> import scala.util.parsing.json.JSON
import scala.util.parsing.json.JSON

// 使用 map 来解析 Json, 需要传入 JSON.parseFull
scala> val rdd2 = rdd1.map(JSON.parseFull) ==》解析
rdd2: org.apache.spark.rdd.RDD[Option[Any]] = MapPartitionsRDD[12] at map at :27

// 解析到的结果其实就是 Option 组成的数组, Option 存储的就是 Map 对象
scala> rdd2.collect
res2: Array[Option[Any]] = Array(Some(Map(name -> Michael)), Some(Map(name -> Andy, age -> 30.0)), Some(Map(name -> Justin, age -> 19.0)))


7.3 读写 SequenceFile 文件
SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件(Flat File)。
Spark 有专门用来读取 SequenceFile 的接口。在 SparkContext 中,可以调用 sequenceFile keyClass, valueClass
注意:SequenceFile 文件只针对 PairRDD(键值对)
先保存一个 SequenceFile 文件
scala> val rdd1 = sc.parallelize(Array((“a”, 1),(“b”, 2),(“c”, 3)))
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[13] at parallelize at :25

scala> rdd1.saveAsSequenceFile(“hdfs://hadoop102:9000/seqFiles”)

这里要写一个key-value: sequenceFile[String,Int]

读取 SequenceFile 文件

scala> val rdd1 = sc.sequenceFileString, Int
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[18] at sequenceFile at :25 ===》泛型要写一个元组

scala> rdd1.collect
res4: Array[(String, Int)] = Array((a,1), (b,2), (c,3))
注意: 需要指定泛型的类型 sc.sequenceFile[String, Int]


7.4 读写 objectFile 文件
对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。
可以通过objectFilek,v 函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile() 实现对对象文件的输出
把 RDD 保存为objectFile
scala> val rdd1 = sc.parallelize(Array((“a”, 1),(“b”, 2),(“c”, 3)))
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[19] at parallelize at :25

scala> rdd1.saveAsObjectFile(“hdfs://hadoop102:9000/obj_file”)

读取 objectFile
scala> val rdd1 = sc.objectFile(String, Int)
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[25] at objectFile at :25

scala> rdd1.collect
res8: Array[(String, Int)] = Array((a,1), (b,2), (c,3))


7.5 从 HDFS 读写文件
Spark 的整个生态系统与 Hadoop 完全兼容的,所以对于 Hadoop 所支持的文件类型或者数据库类型,Spark 也同样支持.
另外,由于 Hadoop 的 API 有新旧两个版本,所以 Spark 为了能够兼容 Hadoop 所有的版本,也提供了两套创建操作接口.
对于外部存储创建操作而言,HadoopRDD 和 newHadoopRDD 是最为抽象的两个函数接口,主要包含以下四个参数.
1)输入格式(InputFormat): 制定数据输入的类型,如 TextInputFormat 等,新旧两个版本所引用的版本分别是 org.apache.hadoop.mapred.InputFormat 和org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
2)键类型: 指定[K,V]键值对中K的类型
3)值类型: 指定[K,V]键值对中V的类型
4)分区值: 指定由外部存储生成的RDD的partition数量的最小值,如果没有指定,系统会使用默认值defaultMinSplits
注意:其他创建操作的API接口都是为了方便最终的Spark程序开发者而设置的,是这两个接口的高效实现版本.例如,对于textFile而言,只有path这个指定文件路径的参数,其他参数在系统内部指定了默认值。
1.在Hadoop中以压缩形式存储的数据,不需要指定解压方式就能够进行读取,因为Hadoop本身有一个解压器会根据压缩文件的后缀推断解压算法进行解压.
2.如果用Spark从Hadoop中读取某种类型的数据不知道怎么读取的时候,上网查找一个使用map-reduce的时候是怎么读取这种这种数据的,然后再将对应的读取方式改写成上面的hadoopRDD和newAPIHadoopRDD两个类就行了

可以根据源码走一遍,这个很有必要
==》关于在HDFS的分区:


7.6 从 Mysql 数据读写文件
引入 Mysql 依赖:

mysql
mysql-connector-java
5.1.27

从 Mysql 读取数据
package day04

import java.sql.DriverManager

import org.apache.spark.rdd.JdbcRDD
import org.apache.spark.{SparkConf, SparkContext}

object JDBCDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
val sc = new SparkContext(conf)
//定义连接mysql的参数
val driver = “com.mysql.jdbc.Driver”
val url = “jdbc:mysql://hadoop102:3306/rdd”
val userName = “root”
val passWd = “aaa”

    val rdd = new JdbcRDD(
        sc,
        () => {
            Class.forName(driver)
            DriverManager.getConnection(url, userName, passWd)
        },
        "select id, name from user where id >= ? and id <= ?",
        1,
        20,
        2,
        result => (result.getInt(1), result.getString(2))
    )
    rdd.collect.foreach(println)

}

}

向 Mysql 写入数据
package day04

import java.sql.{Connection, DriverManager, PreparedStatement}

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object JDBCDemo2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
val sc = new SparkContext(conf)
//定义连接mysql的参数
val driver = “com.mysql.jdbc.Driver”
val url = “jdbc:mysql://hadoop102:3306/rdd”
val userName = “root”
val passWd = “aaa”

    val rdd: RDD[(Int, String)] = sc.parallelize(Array((110, "police"), (119, "fire")))
    // 对每个分区执行 参数函数
    rdd.foreachPartition(it => {
        Class.forName(driver)
        val conn: Connection = DriverManager.getConnection(url, userName, passWd)
        it.foreach(x => {
            val statement: PreparedStatement = conn.prepareStatement("insert into user values(?, ?)")
            statement.setInt(1, x._1)
            statement.setString(2, x._2)
            statement.executeUpdate()
        })
    })


}

}
方法1: 第一种方法 : 连接不能写在外面,这样连接写在driver端;
而foreach是行动算子,在exteror端,需要把连接序列化过去,不靠谱

方法2: 用foreachPartiton不用每次执行

方法3:批处理的方式


7.7 从 Hbase 读写文件
由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现,Spark 可以通过Hadoop输入格式访问 HBase。
这个输入格式会返回键值对数据,其中键的类型为org. apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为org.apache.hadoop.hbase.client.Result。
导入依赖

org.apache.hbase
hbase-server
1.3.1

org.apache.hbase hbase-client 1.3.1 从 HBase 读取数据 package day04

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.Result
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object HBaseDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
val sc = new SparkContext(conf)

    val hbaseConf: Configuration = HBaseConfiguration.create()
    hbaseConf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104")
    hbaseConf.set(TableInputFormat.INPUT_TABLE, "student")

    val rdd: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
        hbaseConf,
        classOf[TableInputFormat],
        classOf[ImmutableBytesWritable],
        classOf[Result])

    val rdd2: RDD[String] = rdd.map {
         (_, result) => Bytes.toString(result.getRow)
    }
    rdd2.collect.foreach(println)
    sc.stop()
}

}

向 HBase 写入数据
package day04

import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.Put
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableOutputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.mapreduce.Job
import org.apache.spark.{SparkConf, SparkContext}

object HBaseDemo2 {

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

    val hbaseConf = HBaseConfiguration.create()
    hbaseConf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104")
    hbaseConf.set(TableOutputFormat.OUTPUT_TABLE, "student")
    // 通过job来设置输出的格式的类
    val job = Job.getInstance(hbaseConf)
    job.setOutputFormatClass(classOf[TableOutputFormat[ImmutableBytesWritable]])
    job.setOutputKeyClass(classOf[ImmutableBytesWritable])
    job.setOutputValueClass(classOf[Put])

    val initialRDD = sc.parallelize(List(("100", "apple", "11"), ("200", "banana", "12"), ("300", "pear", "13")))
    val hbaseRDD = initialRDD.map(x => {
        val put = new Put(Bytes.toBytes(x._1))
        put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(x._2))
        put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("weight"), Bytes.toBytes(x._3))
        (new ImmutableBytesWritable(), put)
    })
    hbaseRDD.saveAsNewAPIHadoopDataset(job.getConfiguration)
}

}


第 8 章 RDD 编程进阶
共享变量问题
看下面的代码:
package day04

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object AccDemo1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
val sc = new SparkContext(conf)
val p1 = Person(10)
// 将来会把对象序列化之后传递到每个节点上
val rdd1 = sc.parallelize(Array(p1))
val rdd2: RDD[Person] = rdd1.map(p => {p.age = 100; p})

    rdd2.count()
    // 仍然是 10
    println(p1.age)
}

}

case class Person(var age:Int)
正常情况下, 传递给 Spark 算子(比如: map, reduce 等)的函数都是在远程的集群节点上执行, 函数中用到的所有变量都是独立的拷贝.
这些变量被拷贝到集群上的每个节点上, 都这些变量的更改不会传递回驱动程序.
支持跨 task 之间共享变量通常是低效的, 但是 Spark 对共享变量也提供了两种支持:

  1. 累加器
  2. 广播变量

8.1 累加器(Accumulator)(写)
累加器用来对信息进行聚合,通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,所以更新这些副本的值不会影响驱动器中的对应变量。
如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。
累加器是一种变量, 仅仅支持“add”, 支持并发. 累加器用于去实现计数器或者求和. Spark 内部已经支持数字类型的累加器, 开发者可以添加其他类型的支持.
内置累加器
需求:计算文件中空行的数量
package day04

import org.apache.spark.rdd.RDD
import org.apache.spark.util.LongAccumulator
import org.apache.spark.{SparkConf, SparkContext}

object AccDemo1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
val sc = new SparkContext(conf)
val rdd: RDD[String] = sc.textFile(“file://” + ClassLoader.getSystemResource(“words.txt”).getPath)
// 得到一个 Long 类型的累加器. 将从 0 开始累加
val emptyLineCount: LongAccumulator = sc.longAccumulator
rdd.foreach(s => if (s.trim.length == 0) emptyLineCount.add(1))
println(emptyLineCount.value)
}
}
说明:
• 在驱动程序中通过sc.longAccumulator得到Long类型的累加器, 还有Double类型的
• 可以通过value来访问累加器的值.(与sum等价). avg得到平均值
• 只能通过add来添加值.
• 累加器的更新操作最好放在action中, Spark 可以保证每个 task 只执行一次. 如果放在 transformations 操作中则不能保证只更新一次.有可能会被重复执行.
自定义累加器
下面这个累加器可以用于在程序运行过程中收集一些文本类信息,最终以List[String]的形式返回。

自定义累加器

package day04

import java.util
import java.util.{ArrayList, Collections}

import org.apache.spark.util.AccumulatorV2

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

}

}

class MyAcc extends AccumulatorV2[String, java.util.List[String]] {
private val _list: java.util.List[String] = Collections.synchronizedList(new ArrayListString)
override def isZero: Boolean = _list.isEmpty

override def copy(): AccumulatorV2[String, util.List[String]] = {
    val newAcc = new MyAcc
    _list.synchronized {
        newAcc._list.addAll(_list)
    }
    newAcc
}

override def reset(): Unit = _list.clear()

override def add(v: String): Unit = _list.add(v)

override def merge(other: AccumulatorV2[String, util.List[String]]): Unit =other match {
    case o: MyAcc => _list.addAll(o.value)
    case _ => throw new UnsupportedOperationException(
        s"Cannot merge ${this.getClass.getName} with ${other.getClass.getName}")
}

override def value: util.List[String] = java.util.Collections.unmodifiableList(new util.ArrayList[String](_list))

}
测试:
object MyAccDemo {
def main(args: Array[String]): Unit = {
val pattern = “”"^\d+$"""
val conf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
val sc = new SparkContext(conf)
// 统计出来非纯数字, 并计算纯数字元素的和
val rdd1 = sc.parallelize(Array(“abc”, “a30b”, “aaabb2”, “60”, “20”))

    val acc = new MyAcc
    sc.register(acc)
    val rdd2: RDD[Int] = rdd1.filter(x => {
        val flag: Boolean = x.matches(pattern)
        if (!flag) acc.add(x)
        flag
    }).map(_.toInt)
    println(rdd2.reduce(_ + _))
    println(acc.value)
}

}
求max,vag,min,sum

注意:
• 在使用自定义累加器的不要忘记注册sc.register(acc)


8.2 广播变量 (读)
==>多个线程处于同一个进程,一个进程中同用一个set集合
广播变量在每个节点上保存一个只读的变量的缓存, 而不用给每个 task 来传送一个 copy.
例如, 给每个节点一个比较大的输入数据集是一个比较高效的方法. Spark 也会用该对象的广播逻辑去分发广播变量来降低通讯的成本.
广播变量通过调用SparkContext.broadcast(v)来创建. 广播变量是对v的包装, 通过调用广播变量的 value方法可以访问.
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)

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


第 3 部分 Spark Core 项目实战 2d
第 1 章 准备数据
本实战项目的数据是采集自电商的用户行为数据.
主要包含用户的 4 种行为: 搜索, 点击, 下单和支付.
数据格式如下, 不同的字段使用下划线分割开_:

数据说明:

  1. 数据采用_分割字段
  2. 每一行表示用户的一个行为, 所以每一行只能是四种行为中的一种.
  3. 如果搜索关键字是 null, 表示这次不是搜索
  4. 如果点击的品类 id 和产品 id 是 -1 表示这次不是点击
  5. 下单行为来说一次可以下单多个产品, 所以品类 id 和产品 id 都是多个, id 之间使用逗号,分割. 如果本次不是下单行为, 则他们相关数据用null来表示
  6. 支付行为和下单行为类似.

第 2 章 需求 1: Top10 热门品类
2.1 需求 1 简介
品类是指的产品的的分类, 一些电商品类分多级, 咱们的项目中品类类只有一级. 不同的公司可能对热门的定义不一样. 我们按照每个品类的 点击、下单、支付 的量来统计热门品类.
2.2 需求1 思路
2.2.1 思路 1
分别统计每个品类点击的次数, 下单的次数和支付的次数.》传统方式需要遍历3次,比较耗时
缺点: 统计 3 次, 需要启动 3 个 job, 每个 job 都有对原始数据遍历一次, 非常好使
2.2.2 思路 2
》使用累加器,排序取前10
最好的办法应该是遍历一次能够计算出来上述的 3 个指标.
使用累加器可以达成我们的需求.

  1. 遍历全部日志数据, 根据品类 id 和操作类型分别累加. 需要用到累加器
    • 定义累加器
    • 当碰到订单和支付业务的时候注意拆分字段才能得到品类 id
  2. 遍历完成之后就得到每个每个品类 id 和操作类型的数量.
  3. 按照点击下单支付的顺序来排序
  4. 取出 Top10

(1)Main方法:

2.3 需求 1 具体实现

  1. 用来封装用户行为的bean类
    /**
  • 用户访问动作表
  • @param date 用户点击行为的日期
  • @param user_id 用户的ID
  • @param session_id Session的ID
  • @param page_id 某个页面的ID
  • @param action_time 动作的时间点
  • @param search_keyword 用户搜索的关键词
  • @param click_category_id 某一个商品品类的ID
  • @param click_product_id 某一个商品的ID
  • @param order_category_ids 一次订单中所有品类的ID集合
  • @param order_product_ids 一次订单中所有商品的ID集合
  • @param pay_category_ids 一次支付中所有品类的ID集合
  • @param pay_product_ids 一次支付中所有商品的ID集合
  • @param city_id 城市 id
    */
    case class UserVisitAction(date: String,
    user_id: Long,
    session_id: String,
    page_id: Long,
    action_time: String,
    search_keyword: String,
    click_category_id: Long,
    click_product_id: Long,
    order_category_ids: String,
    order_product_ids: String,
    pay_category_ids: String,
    pay_product_ids: String,
    city_id: Long)
    case class CategoryCountInfo(categoryId: String,
    clickCount: Long,
    orderCount: Long,
    payCount: Long)
  1. 定义用到的累加器
    需要统计每个品类的点击量, 下单量和支付量, 所以我们在累加器中使用 Map 来存储这些数据: Map(cid, “click”-> 100, cid, “order”-> 50, ….)
    import org.apache.spark.util.AccumulatorV2

import scala.collection.mutable

class MapAccumulator extends AccumulatorV2[(String, String), mutable.Map[(String, String), Long]] {
val map: mutable.Map[(String, String), Long] = mutable.Map(String, String), Long

override def isZero: Boolean = map.isEmpty

override def copy(): AccumulatorV2[(String, String), mutable.Map[(String, String), Long]] = {
    val newAcc = new MapAccumulator
    map.synchronized {
        newAcc.map ++= map
    }
    newAcc
}

override def reset(): Unit = map.clear


override def add(v: (String, String)): Unit = {
    map(v) = map.getOrElseUpdate(v, 0) + 1
}

// otherMap: (1, click) -> 20 this: (1, click) -> 10 thisMap: (1,2) -> 30
// otherMap: (1, order) -> 5 thisMap: (1,3) -> 5
override def merge(other: AccumulatorV2[(String, String), mutable.Map[(String, String), Long]]): Unit = {
    val otherMap: mutable.Map[(String, String), Long] = other.value
    otherMap.foreach {
        kv => map.put(kv._1, map.getOrElse(kv._1, 0L) + kv._2)
    }
}

override def value: mutable.Map[(String, String), Long] = map

}
3. 具体实现
整体入口
import com.spark.practice.app.bean.{CategoryCountInfo, UserVisitAction}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

/**

  • Author lzc

  • Date 2019/8/9 10:56 AM
    */
    object PracticeApp {
    def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setAppName(“Practice”).setMaster(“local[2]”)
    val sc = new SparkContext(conf)
    // 1. 读取文件中的数据
    val lineRDD: RDD[String] = sc.textFile("/Users/lzc/Desktop/user_visit_action.txt")
    // 2. 类型调整
    val userVisitActionRDD: RDD[UserVisitAction] = lineRDD.map(line => {
    val splits: Array[String] = line.split("_")
    UserVisitAction(
    splits(0),
    splits(1).toLong,
    splits(2),
    splits(3).toLong,
    splits(4),
    splits(5),
    splits(6).toLong,
    splits(7).toLong,
    splits(8),
    splits(9),
    splits(10),
    splits(11),
    splits(12).toLong)
    })

    // 需求 1
    val categoryTop10: List[CategoryCountInfo] = CategoryTop10App.statCategoryTop10(sc, userVisitActionRDD)
    println(CategoryCountInfoList)
    
    sc.stop()
    

    }
    }
    需求 1 的具体实现
    import com.spark.practice.app.acc.MapAccumulator
    import com.spark.practice.app.bean.{CategoryCountInfo, UserVisitAction}
    import org.apache.spark.SparkContext
    import org.apache.spark.rdd.RDD

import scala.collection.mutable

object CategoryTop10App {
def statCategoryTop10(sc: SparkContext, userVisitActionRDD: RDD[UserVisitAction]): List[CategoryCountInfo] = {
// 1. 注册累加器
val acc = new MapAccumulator
sc.register(acc, “CategoryActionAcc”)

    // 2. 遍历日志
    userVisitActionRDD.foreach {
        visitAction => {
            if (visitAction.click_category_id != -1) {
                acc.add((visitAction.click_category_id.toString, "click"))
            } else if (visitAction.order_category_ids != "null") {
                visitAction.order_category_ids.split(",").foreach {
                    oid => acc.add((oid, "order"))
                }
            } else if (visitAction.pay_category_ids != "null") {
                visitAction.pay_category_ids.split(",").foreach {
                    pid => acc.add((pid, "pay"))
                }
            }
        }
    }
    
    // 3. 遍历完成之后就得到每个每个品类 id 和操作类型的数量. 然后按照 CategoryId 进行进行分组
    val actionCountByCategoryIdMap: Map[String, mutable.Map[(String, String), Long]] = acc.value.groupBy(_._1._1)
    
    // 4. 转换成 CategoryCountInfo 类型的集合, 方便后续处理
    val categoryCountInfoList: List[CategoryCountInfo] = actionCountByCategoryIdMap.map {
        case (cid, actionMap) => CategoryCountInfo(
            cid,
            actionMap.getOrElse((cid, "click"), 0),
            actionMap.getOrElse((cid, "order"), 0),
            actionMap.getOrElse((cid, "pay"), 0)
        )
    }.toList
    
    // 5. 按照 点击 下单 支付 的顺序降序来排序
    val sortedCategoryInfoList: List[CategoryCountInfo] = categoryCountInfoList.sortBy(info => (info.clickCount, info.orderCount, info.payCount))(Ordering.Tuple3(Ordering.Long.reverse, Ordering.Long.reverse, Ordering.Long.reverse))
    
    // 6. 截取前 10
    val top10: List[CategoryCountInfo] = sortedCategoryInfoList.take(10)
    // 7. 返回 top10 品类 id
    top10
}

}

求出来的结果

最终结果


第 3 章 需求 2: Top10热门品类中每个品类的 Top10 活跃 Session 统计
3.1 需求分析 ==》点击数前10 >排序中的三种方法
对于排名前 10 的品类,分别获 取每个品类点击次数排名前 10 的 sessionId。(注意: 这里我们只关注点击次数, 不关心下单和支付次数)
这个就是说,对于 top10 的品类,每一个都要获取对它点击次数排名前 10 的 sessionId。4
这个功能,可以让我们看到,对某个用户群体最感兴趣的品类,各个品类最感兴趣最典型的用户的 session 的行为。
3.2 思路
=》倒推

  1. 过滤出来 category Top10的日志
    • 需要用到需求1的结果, 然后只需要得到categoryId就可以了
  2. 转换结果为 RDD[(categoryId, sessionId), 1] 然后统计数量 => RDD[(categoryId, sessionId), count]
  3. 统计每个品类 top10. => RDD[categoryId, (sessionId, count)] => RDD[categoryId, Iterable[(sessionId, count)]]
  4. 对每个 Iterable[(sessionId, count)]进行排序, 并取每个Iterable的前10
  5. 把数据封装到 CategorySession 中
    倒推法的思路:

3.3 具体代码实现

  1. bean类
    CategorySession 类
    封装最终写入到数据库的数据
    case class CategorySession(categoryId: String,
    sessionId: String,
    clickCount: Long)
  2. 具体实现
    package com.spark.practice.app

import com.spark.practice.app.bean.{CategoryCountInfo, CategorySession, UserVisitAction}
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD

/**

  • Author lzc

  • Date 2019/8/9 10:49 AM
    */
    object CategorySessionApp {
    def statCategoryTop10Session(sc: SparkContext, userVisitActionRDD: RDD[UserVisitAction], categoryTop10: List[CategoryCountInfo]) = {
    // 1. 得到top10的品类的id
    val categoryIdTop10: List[String] = categoryTop10.map(.categoryId)
    // 2. 过滤出来只包含 top10 品类id的那些用户行为
    val filteredUserVisitActionRDD: RDD[UserVisitAction] = userVisitActionRDD.filter(UserVisitAction => {
    categoryIdTop10.contains(UserVisitAction.click_category_id.toString)
    })
    // 3. 聚合操作
    // => RDD[(品类id, sessionId))] map
    // => RDD[(品类id, sessionId), 1)]
    val categorySessionOne: RDD[((Long, String), Int)] = filteredUserVisitActionRDD
    .map(userVisitAction => ((userVisitAction.click_category_id, userVisitAction.session_id), 1))
    // RDD[(品类id, sessionId), count)]
    val categorySessionCount: RDD[(Long, (String, Int))] =
    categorySessionOne.reduceByKey(
    + _).map {
    case ((cid, sid), count) => (cid, (sid, count))
    }
    // 4. 按照品类 id 进行分组
    // RDD[品类id, Iterator[(sessionId, count)]]
    val categorySessionCountGrouped: RDD[(Long, Iterable[(String, Int)])] = categorySessionCount.groupByKey

    // 5. 排序取前 10
    val categorySessionRDD: RDD[CategorySession] = categorySessionCountGrouped.flatMap {
        case (cid, it) => {
            val list: List[(String, Int)] = it.toList.sortBy(_._2)(Ordering.Int.reverse).take(10)
            val result: List[CategorySession] = list.map {
                case (sid, count) => CategorySession(cid.toString, sid, count)
            }
            result
        }
    }
    categorySessionRDD.collect.foreach(println)
    

    }
    }
    /*

  1. 得到top10的品类的id

  2. 过去出来只包含 top10 品类id的那些用户行为

  3. 分组计算
    => RDD[(品类id, sessionId))] map
    => RDD[(品类id, sessionId), 1)] reduceByKey
    => RDD[(品类id, sessionId), count)] map
    => RDD[品类id, (sessionId, count)] groupByKey
    RDD[品类id, Iterator[(sessionId, count)]]
    */

  4. 前面的实现存在的问题
    下面的代码可能存在的问题:
    // 5. 排序取前 10
    val categorySessionRDD: RDD[CategorySession] = categorySessionCountGrouped.flatMap {
    case (cid, it) => {
    val list: List[(String, Int)] = it.toList.sortBy(_._2)(Ordering.Int.reverse).take(10)
    val result: List[CategorySession] = list.map {
    case (sid, count) => CategorySession(cid.toString, sid, count)
    }
    result
    }
    }
    上面的操作中, 有一个操作是把迭代器中的数据转换成List之后再进行排序, 这里存在内存溢出的可能. 如果迭代器的数据足够大, 当转变成 List 的时候, 会把这个迭代器的所有数据都加载到内存中, 所以有可能造成内存的溢出.
    前面的排序是使用的 Scala 的排序操作, 由于 scala 排序的时候需要把数据全部加载到内存中才能完成排序, 所以理论上都存在内存溢出的风险.
    如果使用 RDD 提供的排序功能, 可以避免内存溢出的风险, 因为 RDD 的排序需要 shuffle, 是采用了内存+磁盘来完成的排序.

  5. 解决方案 1
    使用 RDD 的排序功能, 但是由于 RDD 排序是对所有的数据整体排序, 所以一次只能针对一个 CategoryId 进行排序操作.
    参考下面的代码:
    def statCategoryTop10Session_1(sc: SparkContext, userVisitActionRDD: RDD[UserVisitAction], categoryTop10: List[CategoryCountInfo]) = {
    // 1. 得到top10的品类的id
    val categoryIdTop10: List[String] = categoryTop10.map(.categoryId)
    // 2. 过去出来只包含 top10 品类id的那些用户行为
    val filteredUserVisitActionRDD: RDD[UserVisitAction] = userVisitActionRDD.filter(UserVisitAction => {
    categoryIdTop10.contains(UserVisitAction.click_category_id.toString)
    })
    // 3. 聚合操作
    // => RDD[(品类id, sessionId))] map
    // => RDD[(品类id, sessionId), 1)]
    val categorySessionOne: RDD[((Long, String), Int)] = filteredUserVisitActionRDD
    .map(userVisitAction => ((userVisitAction.click_category_id, userVisitAction.session_id), 1))
    // RDD[(品类id, sessionId), count)]
    val categorySessionCount: RDD[(Long, (String, Int))] =
    categorySessionOne.reduceByKey(
    + _).map {
    case ((cid, sid), count) => (cid, (sid, count))
    }

    // 4. 每个品类 id 排序取前 10的 session
    categoryIdTop10.foreach(cid => {
    // 针对某个具体的 CategoryId, 过滤出来只只包含这个CategoryId的 RDD, 然后整体j降序p排列
    val top10: Array[CategorySession] = categorySessionCount
    .filter(_.1 == cid.toLong)
    .sortBy(
    ._2._2, ascending = false)
    .take(10)
    .map {
    case (cid, (sid, count)) => CategorySession(cid.toString, sid, count)
    }
    top10.foreach(println)

    })

}
5. 解决方案 2
方案 1 解决了内存溢出的问题, 但是也有另外的问题: 提交的 job 比较多, 有一个品类 id 就有一个 job, 在本案例中就有了 10 个 job.
有没有更加好的方案呢?
可以把同一个品类的数据都进入到同一个分区内, 然后对每个分区的数据进行排序!
需要用到自定义分区器.
自定义分区器
class MyPartitioner(categoryIdTop10: List[String]) extends Partitioner {
// 给每个 cid 配一个分区号(使用他们的索引就行了)
private val cidAndIndex: Map[String, Int] = categoryIdTop10.zipWithIndex.toMap

override def numPartitions: Int = categoryIdTop10.size

override def getPartition(key: Any): Int = {
    key match {
        case (cid: Long, _) => cidAndIndex(cid.toString)
    }
}

}
CategorySession修改
case class CategorySession(categoryId: String,
sessionId: String,
clickCount: Long) extends Ordered[CategorySession] {
override def compare(that: CategorySession): Int = {
if (this.clickCount <= that.clickCount) 1
else -1
}

}
具体方法
def statCategoryTop10Session_2(sc: SparkContext, userVisitActionRDD: RDD[UserVisitAction], categoryTop10: List[CategoryCountInfo]) = {
// 1. 得到top10的品类的id
val categoryIdTop10: List[String] = categoryTop10.map(_.categoryId)
// 2. 过去出来只包含 top10 品类id的那些用户行为
val filteredUserVisitActionRDD: RDD[UserVisitAction] = userVisitActionRDD.filter(UserVisitAction => {
categoryIdTop10.contains(UserVisitAction.click_category_id.toString)
})
// 3. 聚合操作
// => RDD[(品类id, sessionId))] map
// => RDD[(品类id, sessionId), 1)]
val categorySessionOne: RDD[((Long, String), Int)] = filteredUserVisitActionRDD
.map(userVisitAction => ((userVisitAction.click_category_id, userVisitAction.session_id), 1))
// RDD[(品类id, sessionId), count)] 在 reduceByKey 的时候指定分区器
val categorySessionCount: RDD[CategorySession] = categorySessionOne
.reduceByKey(new MyPartitioner(categoryIdTop10), _ + _) // 指定分区器 (相比以前有变化)
.map {
case ((cid, sid), count) => CategorySession(cid.toString, sid, count)
}

// 4. 对每个分区内的数据排序取前 10(相比以前有变化)
val categorySessionRDD: RDD[CategorySession] = categorySessionCount.mapPartitions(it => {
    
    // 这个时候也不要把 it 变化 list 之后再排序, 否则仍然会有可能出现内存溢出.
    // 我们可以把数据存储到能够自动排序的集合中 比如 TreeSet 或者 TreeMap 中, 并且永远保持这个集合的长度为 10
    // 让TreeSet默认安装 count 的降序排列, 需要让CategorySession现在 Ordered 接口(Comparator)
    var top10: mutable.TreeSet[CategorySession] = mutable.TreeSet[CategorySession]()
    
    it.foreach(cs => {
        top10 += cs // 把 CategorySession 添加到 TreeSet 中
        if (top10.size > 10) { // 如果 TreeSet 的长度超过 10, 则移除最后一个
            top10 = top10.take(10)
        }
    })
    top10.toIterator
})
categorySessionRDD.collect.foreach(println)

}


第 4 章 需求 3: 页面单跳转化率统计
4.1 需求简介
计算页面单跳转化率,什么是页面单跳转换率,比如一个用户在一次 Session 过程中访问的页面路径 3,5,7,9,10,21,那么页面 3 跳到页面 5 叫一次单跳,7-9 也叫一次单跳,那么单跳转化率就是要统计页面点击的概率
比如:计算 3-5 的单跳转化率,先获取符合条件的 Session 对于页面 3 的访问次数(PV)为 A,然后获取符合条件的 Session 中访问了页面 3 又紧接着访问了页面 5 的次数为 B,那么 B/A 就是 3-5 的页面单跳转化率.

产品经理和运营总监,可以根据这个指标,去尝试分析,整个网站,产品,各个页面的表现怎么样,是不是需要去优化产品的布局;吸引用户最终可以进入最后的支付页面。
数据分析师,可以此数据做更深一步的计算和分析。
企业管理层,可以看到整个公司的网站,各个页面的之间的跳转的表现如何,可以适当调整公司的经营战略或策略。
在该模块中,需要根据查询对象中设置的 Session 过滤条件,先将对应得 Session 过滤出来,然后根据查询对象中设置的页面路径,计算页面单跳转化率,比如查询的页面路径为:3、5、7、8,那么就要计算 3-5、5-7、7-8 的页面单跳转化率。
需要注意的一点是,页面的访问时有先后的,要做好排序。
4.2 思路分析

  1. 读取到规定的页面
  2. 过滤出来规定页面的日志记录, 并统计出来每个页面的访问次数 countByKey 是行动算子 reduceByKey 是转换算子
  3. 明确哪些页面需要计算跳转次数 1-2, 2-3, 3-4 …
  4. 按照 session 统计所有页面的跳转次数, 并且需要按照时间升序来排序
    • 按照 session 分组, 然后并对每组内的 UserVisitAction 进行排序
    • 转换访问流水
    • 过滤出来和统计目标一致的跳转
  5. 统计跳转次数
  6. 计算跳转率

4.3 具体业务实现
package com.spark.practice.app

import java.text.DecimalFormat

import com.spark.practice.app.bean.UserVisitAction
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD

/**

  • Author lzc

  • Date 2019/8/9 2:26 PM
    */
    object PageConversionApp {
    def calcPageConversion(spark: SparkContext, userVisitActionRDD: RDD[UserVisitAction], targetPageFlow: String) = {
    // 1. 读取到规定的页面
    val pageFlowArr: Array[String] = targetPageFlow.split(",")
    val prePageFlowArr: Array[String] = pageFlowArr.slice(0, pageFlowArr.length - 1)
    val postPageFlowArr: Array[String] = pageFlowArr.slice(1, pageFlowArr.length)
    // 2. 过滤出来规定页面的日志记录, 并统计出来每个页面的访问次数 countByKey 是行动算子 reduceByKey 是转换算子
    val targetPageCount: collection.Map[Long, Long] = userVisitActionRDD
    .filter(uva => pageFlowArr.contains(uva.page_id.toString))
    .map(uva => (uva.page_id, 1L))
    .countByKey

    // 3. 明确哪些页面需要计算跳转次数 1-2  2-3 3-4 ...   (组合出来跳转流)
    val targetJumpPages: Array[String] = prePageFlowArr.zip(postPageFlowArr).map(t => t._1 + "-" + t._2)
    
    // 4. 按照 session 统计所有页面的跳转次数, 并且需要按照时间升序来排序
    // 4.1 按照 session 分组, 然后并对每组内的 UserVisitAction 进行排序
    val pageJumpRDD: RDD[String] = userVisitActionRDD.groupBy(_.session_id).flatMap {
        case (_, actions) => {
            val visitActions: List[UserVisitAction] = actions.toList.sortBy(_.action_time)
            // 4.2 转换访问流水
            val pre: List[UserVisitAction] = visitActions.slice(0, visitActions.length - 1)
            val post: List[UserVisitAction] = visitActions.slice(1, visitActions.length)
            // 4.3 过滤出来和统计目标一致的跳转
            pre.zip(post).map(t => t._1.page_id + "-" + t._2.page_id).filter(targetJumpPages.contains(_))
        }
    }
    
    // 5. 统计跳转次数  数据量已经很少了, 拉到驱动端计算
    val pageJumpCount: Array[(String, Int)] = pageJumpRDD.map((_, 1)).reduceByKey(_ + _).collect
    
    // 6. 计算跳转率
    
    val formatter = new DecimalFormat(".00%")
    // 转换成百分比
    val conversionRate: Array[(String, String)] = pageJumpCount.map {
        case (p2p, jumpCount) =>
            val visitCount: Long = targetPageCount.getOrElse(p2p.split("-").head.toLong, 0L)
            val rate: String = formatter.format(jumpCount.toDouble / visitCount)
            (p2p, rate)
    }
    conversionRate.foreach(println)
    

    }
    }

/*

  1. 读取到规定的页面
    例如: targetPageFlow:“1,2,3,4,5,6,7”

  2. 过滤出来规定页面的日志记录 并统计出来每个页面的访问次数
    例如: 只需过滤出来1,2,3,4,5,6 第7页面不需要过滤

  3. 计算页面跳转次数(肯定是按照每个 session 来统计)
    1->2 2->3 …
    3.1 统计每个页面访问次数

  4. 计算转化率
    页面跳转次数 / 页面访问次数
    1->2/1 表示页面1到页面2的转化率

*/
4.4 执行
PageConversionApp.calcPageConversion(sc, userVisitActionRDD, “1,2,3,4,5,6”)


第 4 部分 Spark Sql 1d
第 9 章 Spark SQL 概述
本章从整体来了解 Spark SQL.


9.1 什么是 Spark SQL ==》真正的逻辑点
Spark SQL 是 Spark 用于结构化数据(structured data)处理的 Spark 模块.
与基本的 Spark RDD API 不同, Spark SQL 的抽象数据类型为 Spark 提供了关于数据结构和正在执行的计算的更多信息.
在内部, Spark SQL 使用这些额外的信息去做一些额外的优化.
有多种方式与 Spark SQL 进行交互, 比如: SQL 和 Dataset API. 当计算结果的时候, 使用的是相同的执行引擎, 不依赖你正在使用哪种 API 或者语言.
这种统一也就意味着开发者可以很容易在不同的 API 之间进行切换, 这些 API 提供了最自然的方式来表达给定的转换.
我们已经学习了 Hive,它是将 Hive SQL 转换成 MapReduce 然后提交到集群上执行,大大简化了编写 MapReduc 的程序的复杂性,由于 MapReduce 这种计算模型执行效率比较慢。
所以 Spark SQL 的应运而生,它是将 Spark SQL 转换成 RDD,然后提交到集群执行,执行效率非常快!
Spark SQL 它提供了2个编程抽象, 类似 Spark Core 中的 RDD
• DataFrame (逐渐被淘汰)
• DataSet


9.2 Spark SQL 的特点

  1. Integrated(易整合) ==》混合编程
    无缝的整合了 SQL 查询和 Spark 编程.

  2. Uniform Data Access(统一的数据访问方式)
    使用相同的方式连接不同的数据源.

  3. Hive Integration(集成 Hive)
    在已有的仓库上直接运行 SQL 或者 HiveQL

  4. Standard Connectivity(标准的连接方式)
    通过 JDBC 或者 ODBC 来连接


9.3 什么是 DataFrame ==》当成一张表更合适点
与 RDD 类似,DataFrame 也是一个分布式数据容器。
然而DataFrame更像传统数据库的二维表格,除了数据以外,还记录数据的结构信息,即schema。(schema类型视图)
同时,与Hive类似,DataFrame也支持嵌套数据类型(struct、array和map)。
从 API 易用性的角度上看,DataFrame API提供的是一套高层的关系操作,比函数式的 RDD API 要更加友好,门槛更低。

上图直观地体现了DataFrame和RDD的区别。
左侧的RDD[Person]虽然以Person为类型参数,但Spark框架本身不了解Person类的内部结构。
而右侧的DataFrame却提供了详细的结构信息,使得 Spark SQL 可以清楚地知道该数据集中包含哪些列,每列的名称和类型各是什么。
DataFrame是为数据提供了Schema的视图。可以把它当做数据库中的一张表来对待, ==》没法在编译的时候进行检测(类型校验)
===》dateset也是懒执行 的
DataFrame也是懒执行的
性能上比 RDD要高,主要原因: 优化的执行计划:查询计划通过Spark catalyst optimiser进行优化。比如下面一个例子:

为了说明查询优化,我们来看上图展示的人口数据分析的示例。图中构造了两个DataFrame,将它们join之后又做了一次filter操作。
如果原封不动地执行这个执行计划,最终的执行效率是不高的。因为join是一个代价较大的操作,也可能会产生一个较大的数据集。
如果我们能将filter下推到 join下方,先对DataFrame进行过滤,再join过滤后的较小的结果集,便可以有效缩短执行时间。
而Spark SQL的查询优化器正是这样做的。简而言之,逻辑查询计划优化就是一个利用基于关系代数的等价变换,将高成本的操作替换为低成本操作的过程。


9.4 什么是 DataSet 》类型可以直接拿出
• 是DataFrame API的一个扩展,是 SparkSQL 最新的数据抽象(1.6新增)。
• 用户友好的API风格,既具有类型安全检查也具有DataFrame的查询优化特性。
• Dataset支持编解码器,当需要访问非堆上的数据时可以避免反序列化整个对象,提高了效率。
• 样例类被用来在DataSet中定义数据的结构信息,样例类中每个属性的名称直接映射到DataSet中的字段名称。
• DataFrame是DataSet的特列,DataFrame=DataSet[Row] ,所以可以通过as方法将DataFrame转换为DataSet。Row是一个类型,跟Car、Person这些的类型一样,所有的表结构信息都用Row来表示。
• DataSet是强类型的。比如可以有DataSet[Car],DataSet[Person].
• DataFrame只能存row类型
》Dataset中可以存任意类型
• DataFrame只是知道字段,但是不知道字段的类型,所以在执行这些操作的时候是没办法在编译的时候检查是否类型失败的,比如你可以对一个String进行减法操作,在执行的时候才报错,而DataSet不仅仅知道字段,而且知道字段类型,所以有更严格的错误检查。就跟JSON对象和类对象之间的类比。


第 10 章 Spark SQL 编程
本章重点学习如何使用 DataFrame和DataSet进行编程. 已经他们之间的关系和转换.
关于具体的 SQL 书写不是本章的重点.


10.1 SparkSession
在老的版本中,SparkSQL 提供两种 SQL 查询起始点:一个叫SQLContext,用于Spark 自己提供的 SQL 查询;一个叫 HiveContext,用于连接 Hive 的查询。
从2.0开始, SparkSession是 Spark 最新的 SQL 查询起始点,实质上是SQLContext和HiveContext的组合,所以在SQLContext和HiveContext上可用的 API 在SparkSession上同样是可以使用的。
SparkSession内部封装了SparkContext,所以计算实际上是由SparkContext完成的。

当我们使用 spark-shell 的时候, spark 会自动的创建一个叫做spark的SparkSession, 就像我们以前可以自动获取到一个sc来表示SparkContext


10.2 使用 DataFrame 进行编程
首先学习 DataFrame相关的知识.
Spark SQL 的 DataFrame API 允许我们使用 DataFrame 而不用必须去注册临时表或者生成 SQL 表达式.
DataFrame API 既有 transformation操作也有action操作. DataFrame的转换从本质上来说更具有关系, 而 DataSet API 提供了更加函数式的 API


10.2.1 创建 DataFrame
With a SparkSession, applications can create DataFrames from an existing RDD, from a Hive table, or from Spark data sources.
有了 SparkSession 之后, 通过 SparkSession有 3 种方式来创建DataFrame:
• 通过 Spark 的数据源创建 //json/文本文件
• 通过已知的 RDD 来创建 //
通过查询一个 Hive 表来创建. //hive查询


通过 Spark 数据源创建 ==》DF就是一个表结构,用show就可以表示出来
Spark支持的数据源:

// 读取 json 文件
scala> val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/employees.json")
df: org.apache.spark.sql.DataFrame = [name: string, salary: bigint]

// 展示结果
scala> df.show
±------±-----+
| name|salary|
±------±-----+
|Michael| 3000|
| Andy| 4500|
| Justin| 3500|
| Berta| 4000|
±------±-----+
====》扩展Scala中读取json文件的方式:

通过 RDD 进行转换
后面章节专门讨论
通过查询 Hive 表创建
后面章节专门讨论


10.2.2 DataFrame 语法风格
SQL 语法风格(主要)
SQL 语法风格是指我们查询数据的时候使用 SQL 语句来查询.
这种风格的查询必须要有临时视图或者全局视图来辅助

创建表: creatGlobalTem(全局的表), createTempViem
//createOrReplaceTempView 这个方法创建表不会报错
scala> val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> df.createOrReplaceTempView(“people”) //sql需要一个表名

scala> spark.sql(“select * from people”).show ==》通过sql查看
±—±------+
| age| name|
±—±------+
|null|Michael|
| 30| Andy|
| 19| Justin|
±—±------+
注意:
• 临时视图只能在当前 Session 有效, 在新的 Session 中无效.
• 可以创建全局视图. 访问全局视图需要全路径:如global_temp.xxx
scala> val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> df.createGlobalTempView(“people”)

scala> spark.sql(“select * from global_temp.people”)
res31: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> res31.show
±—±------+
| age| name|
±—±------+
|null|Michael|
| 30| Andy|
| 19| Justin|
±—±------+

scala> spark.newSession.sql(“select * from global_temp.people”)
res33: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> res33.show
±—±------+
| age| name|
±—±------+
|null|Michael|
| 30| Andy|
| 19| Justin|
±—±------+
Sql风格的步骤:

DSL 语法风格(了解)
DataFrame提供一个特定领域语言(domain-specific language, DSL)去管理结构化的数据. 可以在 Scala, Java, Python 和 R 中使用 DSL
使用 DSL 语法风格不必去创建临时视图了.

查看 Schema 信息 ==》Schema: 模式
scala> val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> df.printSchema

root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
使用 DSL 查询
只查询name列数据
scala> df.select($“name”).show
±------+
| name|
±------+
|Michael|
| Andy|
| Justin|
±------+

scala> df.select(“name”).show ==》只有一个字段时, 可 以 省 略 + − − − − − − − + ∣ n a m e ∣ + − − − − − − − + ∣ M i c h a e l ∣ ∣ A n d y ∣ ∣ J u s t i n ∣ + − − − − − − − + 查 询 n a m e 和 a g e s c a l a > d f . s e l e c t ( " n a m e " , " a g e " ) . s h o w + − − − − − − − + − − − − + ∣ n a m e ∣ a g e ∣ + − − − − − − − + − − − − + ∣ M i c h a e l ∣ n u l l ∣ ∣ A n d y ∣ 30 ∣ ∣ J u s t i n ∣ 19 ∣ + − − − − − − − + − − − − + 查 询 n a m e 和 a g e + 1 s c a l a > d f . s e l e c t ( 可以省略 +-------+ | name| +-------+ |Michael| | Andy| | Justin| +-------+ 查询name和age scala> df.select("name", "age").show +-------+----+ | name| age| +-------+----+ |Michael|null| | Andy| 30| | Justin| 19| +-------+----+ 查询name和age + 1 scala> df.select( ++name++MichaelAndyJustin++nameagescala>df.select("name","age").show+++nameage+++MichaelnullAndy30Justin19+++nameage+1scala>df.select(“name”, " a g e " + 1 ) . s h o w + − − − − − − − + − − − − − − − − − + ∣ n a m e ∣ ( a g e + 1 ) ∣ + − − − − − − − + − − − − − − − − − + ∣ M i c h a e l ∣ n u l l ∣ ∣ A n d y ∣ 31 ∣ ∣ J u s t i n ∣ 20 ∣ + − − − − − − − + − − − − − − − − − + 注 意 : • 设 计 到 运 算 的 时 候 , 每 列 都 必 须 使 用 "age" + 1).show +-------+---------+ | name|(age + 1)| +-------+---------+ |Michael| null| | Andy| 31| | Justin| 20| +-------+---------+ 注意: • 设计到运算的时候, 每列都必须使用 "age"+1).show+++name(age+1)+++MichaelnullAndy31Justin20+++:,使
查询age大于20的数据
scala> df.filter($“age” > 21).show
±–±—+
|age|name|
±–±—+
| 30|Andy|
±–±—+
按照age分组,查看数据条数
scala> df.groupBy(“age”).count.show
±—±----+
| age|count|
±—±----+
| 19| 1|
|null| 1|
| 30| 1|
±—±----+


10.2.3 RDD 和 DataFrame 的交互

  1. 从 RDD 到 DataFrame
    涉及到RDD, DataFrame, DataSet之间的操作时, 需要导入:import spark.implicits._ 这里的spark不是包名, 而是表示SparkSession 的那个对象. 所以必须先创建SparkSession对象再导入. implicits是一个内部object
    首先创建一个RDD
    scala> val rdd1 = sc.textFile("/opt/module/spark-local/examples/src/main/resources/people.txt")
    rdd1: org.apache.spark.rdd.RDD[String] = /opt/module/spark-local/examples/src/main/resources/people.txt MapPartitionsRDD[10] at textFile at :24
    手动转换
    scala> val rdd2 = rdd1.map(line => { val paras = line.split(", "); (paras(0), paras(1).toInt)})

rdd2: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[11] at map at :26

// 转换为 DataFrame 的时候手动指定每个数据字段名
scala> rdd2.toDF(“name”, “age”).show
±------±–+
| name|age|
±------±–+
|Michael| 29|
| Andy| 30|
| Justin| 19|
±------±–+
通过样例类反射转换(最常用)

  1. 创建样例类
    scala> case class People(name :String, age: Int)
    defined class People
  2. 使用样例把 RDD 转换成DataFrame
    scala> val rdd2 = rdd1.map(line => { val paras = line.split(", "); People(paras(0), paras(1).toInt) })

rdd2: org.apache.spark.rdd.RDD[People] = MapPartitionsRDD[6] at map at :28

scala> rdd2.toDF.show
±------±–+
| name|age|
±------±–+
|Michael| 29|
| Andy| 30|
| Justin| 19|
±------±–+
通过 API 的方式转换(了解)
package day05

import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}
import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}

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

    val spark: SparkSession = SparkSession.builder()
        .master("local[*]")
        .appName("Word Count")
        .getOrCreate()
    val sc: SparkContext = spark.sparkContext
    val rdd: RDD[(String, Int)] = sc.parallelize(Array(("lisi", 10), ("zs", 20), ("zhiling", 40)))
    // 映射出来一个 RDD[Row], 因为 DataFrame其实就是 DataSet[Row]
    val rowRdd: RDD[Row] = rdd.map(x => Row(x._1, x._2)) 

    // 创建 StructType 类型
    val types = StructType(Array(StructField("name", StringType), StructField("age", IntegerType)))
    val df: DataFrame = spark.createDataFrame(rowRdd, types)
    df.show

}

}
2. 从 DataFrame到RDD
直接调用DataFrame的rdd方法就完成了从转换.
scala> val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> val rdd = df.rdd
rdd: org.apache.spark.rdd.RDD[org.apache.spark.sql.Row] = MapPartitionsRDD[6] at rdd at :25

scala> rdd.collect
res0: Array[org.apache.spark.sql.Row] = Array([null,Michael], [30,Andy], [19,Justin])
说明:
• 得到的RDD中存储的数据类型是:Row.


10.3 使用 DataSet 进行编程
DataSet 和 RDD 类似, 但是DataSet没有使用 Java 序列化或者 Kryo序列化, 而是使用一种专门的编码器去序列化对象, 然后在网络上处理或者传输.
虽然编码器和标准序列化都负责将对象转换成字节,但编码器是动态生成的代码,使用的格式允许Spark执行许多操作,如过滤、排序和哈希,而无需将字节反序列化回对象。
DataSet是具有强类型的数据集合,需要提供对应的类型信息。


10.3.1 创建DataSet

  1. 使用样例类的序列得到DataSet
    scala> case class Person(name: String, age: Int)
    defined class Person
    // 为样例类创建一个编码器
    scala> val ds = Seq(Person(“lisi”, 20), Person(“zs”, 21)).toDS
    ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: int]
    scala> ds.show
    ±—±–+
    |name|age|
    ±—±–+
    |lisi| 20|
    | zs| 21|
    ±—±–+
  2. 使用基本类型的序列得到 DataSet
    // 基本类型的编码被自动创建. importing spark.implicits._
    scala> val ds = Seq(1,2,3,4,5,6).toDS
    ds: org.apache.spark.sql.Dataset[Int] = [value: int]
    scala> ds.show
    ±----+
    |value|
    ±----+
    | 1|
    | 2|
    | 3|
    | 4|
    | 5|
    | 6|
    ±----+
    说明:
    • 在实际使用的时候, 很少用到把序列转换成 DataSet, 更多的是通过RDD来得到DataSet

10.3.2 RDD 和 DataSet 的交互

  1. 从 RDD 到 DataSet
    使用反射来推断包含特定类型对象的RDD的 schema 。
    这种基于反射的方法可以生成更简洁的代码,并且当您在编写Spark应用程序时已经知道模式时,这种方法可以很好地工作。
    为 Spark SQL 设计的 Scala API 可以自动的把包含样例类的 RDD 转换成 DataSet.
    样例类定义了表结构: 样例类参数名通过反射被读到, 然后成为列名.
    样例类可以被嵌套, 也可以包含复杂类型: 像Seq或者Array.
    scala> val peopleRDD = sc.textFile(“examples/src/main/resources/people.txt”)
    peopleRDD: org.apache.spark.rdd.RDD[String] = examples/src/main/resources/people.txt MapPartitionsRDD[1] at textFile at :24

scala> case class Person(name: String, age: Long)
defined class Person

scala> peopleRDD.map(line => {val para = line.split(",");Person(para(0),para(1).trim.toInt)}).toDS
res0: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
2. 从 DataSet 到 RDD
调用rdd方法即可
scala> val ds = Seq(Person(“lisi”, 40), Person(“zs”, 20)).toDS
ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]

// 把 ds 转换成 rdd
scala> val rdd = ds.rdd
rdd: org.apache.spark.rdd.RDD[Person] = MapPartitionsRDD[8] at rdd at :27

scala> rdd.collect
res5: Array[Person] = Array(Person(lisi,40), Person(zs,20))


10.4 DataFrame 和 DataSet 之间的交互

  1. 从 DataFrame到DataSet

scala> val df = spark.read.json(“examples/src/main/resources/people.json”)
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> case class People(name: String, age: Long)
defined class People

// DataFrame 转换成 DataSet
scala> val ds = df.as[People]
ds: org.apache.spark.sql.Dataset[People] = [age: bigint, name: string]
2. 从 DataSet到DataFrame
scala> case class Person(name: String, age: Long)
defined class Person

scala> val ds = Seq(Person(“Andy”, 32)).toDS()
ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]

scala> val df = ds.toDF
df: org.apache.spark.sql.DataFrame = [name: string, age: bigint]

scala> df.show
±—±–+
|name|age|
±—±–+
|Andy| 32|
±—±–+


10.5 RDD, DataFrame和 DataSet 之间的关系
在 SparkSQL 中 Spark 为我们提供了两个新的抽象,分别是DataFrame和DataSet。他们和RDD有什么区别呢?首先从版本的产生上来看:
RDD (Spark1.0) —> Dataframe(Spark1.3) —> Dataset(Spark1.6)

如果同样的数据都给到这三个数据结构,他们分别计算之后,都会给出相同的结果。不同是的他们的执行效率和执行方式。
在后期的 Spark 版本中,DataSet会逐步取代RDD和DataFrame成为唯一的 API 接口。
10.5.1 三者的共性

  1. RDD、DataFrame、Dataset全都是 Spark 平台下的分布式弹性数据集,为处理超大型数据提供便利
  2. 三者都有惰性机制,在进行创建、转换,如map方法时,不会立即执行,只有在遇到Action如foreach , show时,三者才会开始遍历运算。
  3. 三者都会根据 Spark 的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出
  4. 三者都有partition的概念
  5. 三者有许多共同的函数,如map, filter,排序等
  6. 在对 DataFrame和Dataset进行操作许多操作都需要这个包进行支持 import spark.implicits._ (和以前的导完全不一样,要注意)
  7. DataFrame和Dataset均可使用模式匹配获取各个字段的值和类型
    10.5.2 三者的区别
    1.RDD
    • RDD一般和spark mlib同时使用
    • RDD不支持sparksql操作
  8. DataFrame
    • 与RDD和Dataset不同,DataFrame每一行的类型固定为Row,每一列的值没法直接访问,只有通过解析才能获取各个字段的值,
    • DataFrame与DataSet一般不与 spark mlib 同时使用
    • DataFrame与DataSet均支持 SparkSQL 的操作,比如select,groupby之类,还能注册临时表/视窗,进行 sql 语句操作
    • DataFrame与DataSet支持一些特别方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然(后面专门讲解)
  9. DataSet
    • Dataset和DataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同。 DataFrame其实就是DataSet的一个特例
    • DataFrame也可以叫Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的getAS方法或者共性中的第七条提到的模式匹配拿出特定字段。而Dataset中,每一行是什么类型是不一定的,在自定义了case class之后可以很自由的获得每一行的信息

    查看 ppt
    10.5.3 三者的互相转换

10.6 使用 IDEA 创建 SparkSQL 程序

步骤1: 添加 SparkSQL 依赖

org.apache.spark
spark-sql_2.11
2.1.1

步骤2: 具体代码

package day05

object DataFrameDemo {
def main(args: Array[String]): Unit = {
// 创建一个新的 SparkSession 对象
val spark: SparkSession = SparkSession.builder()
.master(“local[*]”)
.appName(“Word Count”)
.getOrCreate()
// 导入用到隐式转换. 如果想要使用: " a g e " 则 必 须 导 入 v a l d f = s p a r k . r e a d . j s o n ( " f i l e : / / " + C l a s s L o a d e r . g e t S y s t e m R e s o u r c e ( " u s e r . j s o n " ) . g e t P a t h ) / / 打 印 信 息 d f . s h o w / / 查 找 年 龄 大 于 19 岁 的 d f . f i l t e r ( "age" 则必须导入 val df = spark.read.json("file://" + ClassLoader.getSystemResource("user.json").getPath) // 打印信息 df.show // 查找年龄大于19岁的 df.filter( "age"valdf=spark.read.json("file://"+ClassLoader.getSystemResource("user.json").getPath)//df.show//19df.filter(“age” > 19).show

    // 创建临时表
    df.createTempView("user")
    spark.sql("select * from user where age > 19").show

    //关闭连接
    spark.stop()

}

}


10.7 自定义 SparkSQL 函数
在 Shell 窗口中可以通过spark.udf功能用户可以自定义函数。


10.7.1 自定义 UDF 函数
scala> val df = spark.read.json(“examples/src/main/resources/people.json”)
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> df.show
±—±------+
| age| name|
±—±------+
|null|Michael|
| 30| Andy|
| 19| Justin|
±—±------+
// 注册一个 udf 函数: toUpper是函数名, 第二个参数是函数的具体实现
scala> spark.udf.register(“toUpper”, (s: String) => s.toUpperCase)
res1: org.apache.spark.sql.expressions.UserDefinedFunction = UserDefinedFunction(,StringType,Some(List(StringType)))

scala> df.createOrReplaceTempView(“people”)

scala> spark.sql(“select toUpper(name), age from people”).show
±----------------±—+
|UDF:toUpper(name)| age|
±----------------±—+
| MICHAEL|null|
| ANDY| 30|
| JUSTIN| 19|
±----------------±—+


10.7.2 用户自定会聚合函数
强类型的Dataset和弱类型的DataFrame都提供了相关的聚合函数, 如 count(),countDistinct(),avg(),max(),min()。除此之外,用户可以设定自己的自定义聚合函数
重写方法

继承UserDefinedAggregateFunction
package day05

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types._

object UDFDemo1 {
def main(args: Array[String]): Unit = {
// 测试自定义的聚合函数
val spark: SparkSession = SparkSession
.builder()
.master(“local[*]”)
.appName(“UDFDemo1”)
.getOrCreate()
// 注册自定义函数
spark.udf.register(“myAvg”, new MyAvg)
val df = spark.read.json(“file://” + ClassLoader.getSystemResource(“user.json”).getPath)
df.createTempView(“user”)
spark.sql(“select myAvg(age) age_avg from user”).show

}

}

object MyAvg extends UserDefinedAggregateFunction {
/**
* 返回聚合函数输入参数的数据类型
*
* @return
*/
override def inputSchema: StructType = {
StructType(StructField(“inputColumn”, DoubleType) :: Nil)
}

/**
  * 聚合缓冲区中值的类型
  *
  * @return
  */
override def bufferSchema: StructType = {
    StructType(StructField("sum", DoubleType) :: StructField("count", LongType) :: Nil)
}

/**
  * 最终的返回值的类型
  *
  * @return
  */
override def dataType: DataType = DoubleType

/**
  * 确定性: 比如同样的输入是否返回同样的输出
  *
  * @return
  */
override def deterministic: Boolean = true

/**
  * 初始化
  *
  * @param buffer
  */
override def initialize(buffer: MutableAggregationBuffer): Unit = {
    // 存数据的总和
    buffer(0) = 0d
    // 储存数据的个数
    buffer(1) = 0L
}

/**
  * 相同 Executor间的合并
  *
  * @param buffer
  * @param input
  */
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
    if (!input.isNullAt(0)) {
        buffer(0) = buffer.getDouble(0) + input.getDouble(0)
        buffer(1) = buffer.getLong(1) + 1
    }
}


/**
  * 不同 Executor间的合并
  *
  * @param buffer1
  * @param buffer2
  */
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
    if (!buffer2.isNullAt(0)) {
        buffer1(0) = buffer1.getDouble(0) + buffer2.getDouble(0)
        buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
    }
}

/**
  * 计算最终的结果.  因为是聚合函数, 所以最后只有一行了
  *
  * @param buffer
  * @return
  */
override def evaluate(buffer: Row): Double = {
    println(buffer.getDouble(0), buffer.getLong(1))
    buffer.getDouble(0) / buffer.getLong(1)
}

}


第 11 章 SparkSQL 数据源
本章介绍SparkSQL 的各种数据源(Data Sources).
Spark SQL 的DataFrame接口支持操作多种数据源. 一个 DataFrame类型的对象可以像 RDD 那样操作(比如各种转换), 也可以用来创建临时表.
把DataFrame注册为一个临时表之后, 就可以在它的数据上面执行 SQL 查询.


11.1 通用加载和保存函数
默认数据源是parquet, 我们也可以通过使用:spark.sql.sources.default这个属性来设置默认的数据源.
val usersDF = spark.read.load(“examples/src/main/resources/users.parquet”)
usersDF.select(“name”, “favorite_color”).write.save(“namesAndFavColors.parquet”)
说明:
• spark.read.load 是加载数据的通用方法. ==默认是qarquet
• df.write.save 是保存数据的通用方法.

手动指定选项
也可以手动给数据源指定一些额外的选项. 数据源应该用全名称来指定, 但是对一些内置的数据源也可以使用短名称:json, parquet, jdbc, orc, libsvm, csv, text
al peopleDF = spark.read.format(“json”).load(“examples/src/main/resources/people.json”)
peopleDF.select(“name”, “age”).write.format(“parquet”).save(“namesAndAges.parquet”)

在文件上直接运行 SQL
我们前面都是使用read API 先把文件加载到 DataFrame, 然后再查询. 其实, 我们也可以直接在文件上进行查询
scala> spark.sql(“select * from json.examples/src/main/resources/people.json”)
说明:
• json表示文件的格式. 后面的文件具体路径需要用反引号括起来.
文件保存选项(SaveMode)
保存操作可以使用 SaveMode, 用来指明如何处理数据. 使用mode()方法来设置.
有一点很重要: 这些 SaveMode 都是没有加锁的, 也不是原子操作. 还有, 如果你执行的是 Overwrite 操作, 在写入新的数据之前会先删除旧的数据.
Scala/Java Any Language Meaning
SaveMode.ErrorIfExists(default) “error”(default) 如果文件已经存在则抛出异常
SaveMode.Append “append” 如果文件已经存在则追加
SaveMode.Overwrite “overwrite” 如果文件已经存在则覆盖
SaveMode.Ignore “ignore” 如果文件已经存在则忽略


11.2 加载 JSON 文件
Spark SQL 能够自动推测 JSON数据集的结构,并将它加载为一个Dataset[Row].
可以通过SparkSession.read.json()去加载一个JSON 文件。 也可以通过SparkSession.read.format(“json”).load()来加载.
注意: 这个JSON文件不是一个传统的JSON文件,每一行都得是一个完整的JSON串。
{“name”: “lisi”, “age” : 20, “friends”: [“lisi”, “zs”]}
{“name”: “zs”, “age” : 30, “friends”: [“lisi”, “zs”]}
{“name”: “wangwu”, “age” : 15, “friends”: [“lisi”, “zs”]}
package day05

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

object DataSourceDemo {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession
.builder()
.master(“local[*]”)
.appName(“Test”)
.getOrCreate()
import spark.implicits._

    val df: DataFrame = spark.read.json("target/classes/user.json")
    val ds: Dataset[User] = df.as[User]
    ds.foreach(user => println(user.friends(0)))

}

}
case class User(name:String, age: Long, friends: Array[String])


11.3 读取 Parquet 文件
Parquet 是一种流行的列式存储格式,可以高效地存储具有嵌套字段的记录。Parquet 格式经常在 Hadoop 生态圈中被使用,它也支持 Spark SQL 的全部数据类型。Spark SQL 提供了直接读取和存储 Parquet 格式文件的方法。
package day05

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

object DataSourceDemo {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession
.builder()
.master(“local[*]”)
.appName(“Test”)
.getOrCreate()
import spark.implicits._

    val jsonDF: DataFrame = spark.read.json("target/classes/user.json")
    jsonDF.write.mode(SaveMode.Overwrite).parquet("target/classes/user.parquet")

    val parDF: DataFrame = spark.read.parquet("target/classes/user.parquet")
    val userDS: Dataset[User] = parDF.as[User]
    userDS.map(user => {user.name = "zl"; user.friends(0) = "志玲";user}).show()
}

}
case class User(var name:String, age: Long, friends: Array[String])

注意:
Parquet格式的文件是 Spark 默认格式的数据源.所以, 当使用通用的方式时可以直接保存和读取.而不需要使用format
spark.sql.sources.default 这个配置可以修改默认数据源


11.3 JDBC ==》不了解
Spark SQL 也支持使用 JDBC 从其他的数据库中读取数据. JDBC 数据源比使用 JdbcRDD更爽一些. 这是因为返回的结果直接就是一个 DataFrame, DataFrame更加容易被处理或者与其他的数据源进行 join.
Spark SQL 可以通过 JDBC 从关系型数据库中读取数据的方式创建 DataFrame,通过对DataFrame一系列的计算后,还可以将数据再写回关系型数据库中。
注意: 如果想在spark-shell操作 jdbc, 需要把相关的 jdbc 驱动 copy 到 jars 目录下.
导入依赖: ==>导入依赖

mysql
mysql-connector-java
5.1.27

从 jdbc 读数据
可以使用通用的load方法, 也可以使用jdbc方法

  1. 使用通用的load方法加载

import org.apache.spark.sql.SparkSession

object JDBCDemo {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession
.builder()
.master(“local[*]”)
.appName(“Test”)
.getOrCreate()
import spark.implicits._

    val jdbcDF = spark.read
        .format("jdbc")
        .option("url", "jdbc:mysql://hadoop102:3306/rdd")
        .option("user", "root")
        .option("password", "aaa")
        .option("dbtable", "user")
        .load()
    jdbcDF.show
}

}
2. 使用 jdbc方法加载

package day05

import java.util.Properties

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

object JDBCDemo2 {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession
.builder()
.master(“local[*]”)
.appName(“Test”)
.getOrCreate()

    val props: Properties = new Properties()
    props.setProperty("user", "root")
    props.setProperty("password", "aaa")
    val df: DataFrame = spark.read.jdbc("jdbc:mysql://hadoop102:3306/rdd", "user", props)
    df.show
}

}
向 jdbc 写入数据
也分两种方法: 通用write.save和write.jdbc
package day05

import java.util.Properties

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

object JDBCDemo3 {

def main(args: Array[String]): Unit = {
    val spark: SparkSession = SparkSession
        .builder()
        .master("local[*]")
        .appName("Test")
        .getOrCreate()
    import spark.implicits._
    val rdd: RDD[User1] = spark.sparkContext.parallelize(Array(User1("lisi", 20), User1("zs", 30)))
    val ds: Dataset[User1] = rdd.toDS
    ds.write
        .format("jdbc")
        .option("url", "jdbc:mysql://hadoop102:3306/rdd")
        .option("user", "root")
        .option("password", "aaa")
        .option("dbtable", "user")
        .mode(SaveMode.Append)
        .save()
    val props: Properties = new Properties()
    props.setProperty("user", "root")
    props.setProperty("password", "aaa")
    ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://hadoop102:3306/rdd", "user", props)
}

}

case class User1(name: String, age: Long)


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值