Spark

Spark2.4.6

Spark介绍

什么是Spark?

Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎。 Spark 是加州大学伯克利分校的AMP实验室所开源的类 Hadoop MapReduce 的通用并行计算框架, Spark 拥有 Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job 中间输出结果可以缓存在内存中,从而不再需要读写 HDFS ,减少磁盘数据交互,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的算法。

Spark 是 Scala 编写,方便快速编程。

Spark特点

Spark_第1张图片

总体技术栈

Spark_第2张图片

Spark 提供了 Sparkcore RDD 、 Spark SQL(结构化数据) 、 Spark Streaming (流式计算框架)、 Spark MLlib(机器学习) 、 Spark GraphX(图计算引擎)等技术组件,可以一站式地完成大数据领域的离线批处理、交互式查询、流式计算、机器学习、图计算等常见的任务。这就是 spark 一站式开发的特点。

Spark与MR的区别

Spark_第3张图片

Hadoop 与MR的区别?

都是分布式计算框架Spark 计算中间结果基于内存缓存MapReduce 基于 HDFS 存储。也正因此,Spark 处理数据的能力一般是 MR 的三到五倍以上, Spark 中除了基于内存计算这一个计算快的原因,还有 DAG(DAGShecdule) 有向无环图来切分任务的执行先后顺序。

hadoop 和 spark 的相同点和不同点?
相同点:都属于计算框架,都可以进行并行计算,都是 MR 模型计算
不同点:

  1. MR 的一个任务为一个 job,一个 job 里分为 map task 阶段和 reduce task 阶段;
    spark 中的提交的一个任务成为一个 application,一个 application 中存在多个
    job,没出发一个 action 算子就会产生一个 job,一个 job 可以被分为多个 stage,一
    个 stage 存在多个 task 任务

  2. spark 使用 DAG 来组织逻辑,实现逻辑计划,DAG 由一系列 RDD 通过血缘关系组成,
    通过 job,stage 的划分,来实现计算机过程的优化。

  3. spark application 在启动前会进行粗粒度的资源申请,即将本次 application 所需的所
    有资源全部申请;MR 是 job 基于进程,每次执行一个 job 都会开启一个进程并申请
    资源,在迭代计算时,MR 进程的切换以及资源的回收会大大降低 MR 计算性能,而
    spark 没有这个问题

  4. spark 下的 Task 执行有优化:推测优化,数据本地化。这点 MR 也有但 spark 更先进

  5. spark 基于内存,迭代计算的中间结果不会落盘可以直接在内存中传递,只有 shuffle 时
    或者中间结果内存放不下才会落盘。而 MR 的中间结果一定会落盘。读写的消耗和序
    列化与反序列化在上百次的迭代计算就会很明显的体现出来

  6. spark 的持久化机制,可以把数据持久化内存或磁盘中,减少重复计算的次数,可以容
    错。MR 也有缓存,但目的是为了提高磁盘 IO 效率

  7. spark 提供了丰富的算子,其底层以及已经被优化的很好了,可以大大减少开发成本,不
    像 MR 需要自己写代码逻辑

Spark API

多编程语言支持:Scala,Java,Python,R,SQL。

spark运行模式

  • Local
    • 多用于本地测试,如在 eclipse , idea 中写程序测试等。
  • Standalone
    • Standalone 是 Spark 自带的一个资源调度框架,它支持完全分布式。
  • Yarn
    • Hadoop 生态圈里面的一个资源调度框架, Spark 也是可以基于 Yarn 来计算的。
  • Mesos
    • 资源调度框架

若要基于 Yarn 来进行资源调度,必须实现 AppalicationMaster 接口, Spark 实现了这个接口,所以可以基于 Yarn 来进行资源调度。

SparkCore

Partition

概念

  • Spark RDD 是一种分布式的数据集由于数据量很大,因此要它切分并存储在各个节点的分区当中
  • Spark中,RDD(Resilient Distributed Dataset)是其最基本的抽象数据集,其中每个RDD是由若干个Partition组成
  • RDD1包含了5个Partition,RDD2包含了3个Partition,这些Partition分布在4个节点中。
    Spark_第4张图片

分区方式

Spark_第5张图片

  • Spark包含两种数据分区方式:HashPartitioner(哈希分区)和RangePartitioner(范围分区)

  • 在Spark Shuffle阶段中,共分为Shuffle Write阶段和Shuffle Read阶段,其中在Shuffle Write阶段中,Shuffle Map Task对数据进行处理产生中间数据,然后再根据数据分区方式对中间数据进行分区。最终Shffle Read阶段中的Shuffle Read Task会拉取Shuffle Write阶段中产生的并已经分好区的中间数据。
    Spark_第6张图片

    • Hash分区
    • HashPartitioner采用哈希的方式对键值对数据进行分区。
    • 其数据分区规则为 partitionId = Key.hashCode % numPartitions
      • partitionId代表该Key对应的键值对数据应当分配到的Partition标识
      • Key.hashCode表示该Key的哈希值
      • numPartitions表示包含的Partition个数
  • RangePartitioner

    • 范围分区
    • Spark引入RangePartitioner的目的是为了解决HashPartitioner所带来的分区倾斜问题,也即
      分区中包含的数据量不均衡问题。
    • HashPartitioner采用哈希的方式将同一类型的Key分配到同一个Partition中,当某几种类型数据量较多时,就会造成若干Partition中包含的数据过大
    • 在Job执行过程中,一个Partition对应一个Task,此时就会使得某几个Task运行过慢。
    • RangePartitioner基于抽样的思想来对数据进行分区
      Spark_第7张图片

HDFS与Partition

  • hdfs中的block是分布式存储的最小单元,类似于盛放文件的盒子,一个文件可能要占多个盒子,但一个盒子里的内容只可能来自同一份文件。假设block设置为128M,你的文件是260M,那么这份文件占3个block(128+128+4)。这样的设计虽然会有一部分磁盘空间的浪费,但是整齐的
    block大小,便于快速找到、读取对应的内容。(p.s. 考虑到hdfs冗余设计,默认三份拷贝,实际上3*3=9个block的物理空间。)

  • spark中的partition 是弹性分布式数据集RDD的最小单元,RDD是由分布在各个节点上的partition组成的。partition 是指的spark在计算过程中,生成的数据在计算空间内最小单元,同一份数据(RDD)的partition 大小不一,数量不定,是根据application里的算子和最初读入的数据分块数量决定的

  • block位于存储空间、partition 位于计算空间,

    block的大小是固定的、partition 大小是不固定的,

    block是有冗余的、不会轻易丢失,partition(RDD)没有冗余设计、丢失之后重新计算得到

  • Spark从HDFS读入文件的分区数默认等于HDFS文件的块数(blocks),HDFS中的block是分布式存储的最小单元。如果我们上传一个30GB的非压缩的文件到HDFS,HDFS默认的块容量大小128MB,因此该文件在HDFS上会被分为235块(30GB/128MB);Spark读取SparkContext.textFile()读取该文
    件,默认分区数等于块数即235。

RDD

RDD(Resilient Distributed Dataset) 弹性分布式数据集。

Spark_第8张图片

RDD五大属性

  • A list of partitions
    • RDD 是由一系列的 partition 组成的。
  • A function for computing each split
    • 函数是作用在每一个 partition/split 上。
  • A list of dependencies on other RDDs
    • RDD 需要依赖其他RDD。
  • Optionally, a Partitioner for key-value RDDs
    • 分区器是作用在 (K,V) 格式的 RDD 上。
  • Optionally, a list of preferred locations to compute each split on
    • RDD默认会寻找最优的计算位置
      计算向数据靠拢
      尽可能少的进行数据拉取操作

RDD流程图

Spark_第9张图片

注意:

  • textFile 方法底层封装的是 MR 读取文件的方式,读取文件之前先进行 split 切片,默认 split大小是一个 block 大小。
  • RDD 实际上不存储数据,这里方便理解,暂时理解为存储数据。
  • 什么是 K,V格式的RDD ?
    • 如果 RDD 里面存储的数据都是二元组对象,那么这个 RDD 我们就叫做 K,V格式的RDD 。
  • 哪里体现 RDD 的弹性(容错)?
    • partition 数量,大小没有限制,体现了 RDD 的弹性。
    • RDD 之间依赖关系,可以基于上一个 RDD 重新计算出 RDD
  • 哪里体现 RDD 的分布式?
    • RDD 是由 Partition 组成, partition 是分布在不同节点上的。
    • RDD 提供计算最佳位置,体现了数据本地化。体现了大数据中“计算移动数据不移动”的理念。

Lineage血统

RDD 的最重要的特性之一就是血缘关系(Lineage ),它描述了一个 RDD 是如何从父 RDD 计算得来的。如果某个 RDD 丢失了,则可以根据血缘关系,从父 RDD 计算得来

系统架构

Spark_第10张图片

  • Master ( standalone 模式):资源管理的主节点(进程)。
  • Cluster Manager :在集群上获取资源的外部服务(例如: standalone ; yarn ; mesos )。
  • Worker ( standalone 模式):资源管理的从节点(进程)或者说是管理本机资源的进程
  • Application :基于 Spark 的用户程序,包含 driver 程序和运行在集群上的 executor 程序,即一个完整的 spark 应用 。
  • Dirver ( program ):用来连接工作进程( worker )的程序 。
  • Executor :是在一个 worker 进程所管理的节点上为某 Application 启动的一个个进程,这个进程负责运行任务,并且负责将数据存在内存或者磁盘上,每个应用之间都有各自独立的executors 。
  • Task :被发送到 executor 上的工作单元。
  • Job :包含很多任务( Task )的并行计算,和 action 算子对应
  • Stage :一个 job 会被拆分成很多组任务,每组任务被称为 Stage (就像 MapReduce 分为MapTask 和 ReduceTask 一样)。

算子(单文件)

Spark 记录了 RDD 之间的生成和依赖关系。但是只有当 F 进行行动操作时,Spark 才会根据 RDD的依赖关系生成 DAG,并从起点开始真正的计算。

Spark_第11张图片

转换算子

  • 概念:
    • Transformations 类算子叫做转换算子(本质就是函数), Transformations 算子是延迟执行,也叫懒加载执行
  • 常见Transformation 类算子
    • filter :过滤符合条件的记录数, true 保留, false 过滤掉。
    • map :将一个 RDD 中的每个数据项,通过 map 中的函数映射变为一个新的元素。**特点:**输入一条,输出一条数据。
    • flatMap :先 map 后 flat 。与 map 类似,每个输入项可以映射为0到多个输出项。
    • sample 随机抽样算子,根据传进去的小数按比例进行有放回或者无放回的抽样。
    • reduceByKey 将相同的 Key 根据相应的逻辑进行处理。
    • sortByKey / sortBy 作用在 K,V格式的RDD 上,对 key 进行升序或者降序排序。

行动算子

  • 概念
    • Action 类算子叫做行动算子, Action 类算子是触发执行
    • 一个 application 应用程序中有几个 Action 类算子执行,就有几个 job 运行
  • 常见Action类算子
    • count :返回数据集中的元素数。会在结果计算完成后回收到 Driver 端。
    • take(n) :返回一个包含数据集前 n 个元素的集合。
    • first :效果等同于 take(1) ,返回数据集中的第一个元素。
    • foreach :循环遍历数据集中的每个元素,运行相应的逻辑。
    • collect :将计算结果回收到 Driver 端。

控制算子

  • 概念
    • 将 RDD 持久化,持久化的单位是 partition 。
    • 控制算子有三种, cache , persist , checkpoint 。 cache 和 persist 都是懒执行的。必须有一个 action 类算子触发执行。
    • checkpoint 算子不仅能将 RDD 持久化到磁盘,还能切断 RDD 之间的依赖关系

cache

  • 默认将 RDD 的数据持久化到内存中。 cache 是懒执行
  • cache() = persist() = persist(StorageLevel.Memory_Only)
  • rdd.cache().count() 返回的不是持久化的RDD,而是一个数值

persist

可以指定持久化的级别懒执行。最常用的是 MEMORY_ONLY 和 MEMORY_AND_DISK 。

checkpoint

  • checkpoint 将 RDD 持久化到磁盘,还可以切断 RDD 之间的依赖关系,也是懒执行

  • 执行原理:

    • 当 RDD 的 job 执行完毕后,会从 finalRDD 从后往前回溯。
    • 当回溯到某一个 RDD 调用了 checkpoint 方法,会对当前的 RDD 做一个标记。
    • Spark 框架会自动启动一个新的 job ,重新计算这个 RDD 的数据,将数据持久化到Checkpint目录中。
  • 使用 checkpoint 时常用优化手段:

  • 对 RDD 执行 checkpoint 之前,最好对这个 RDD 先执行 cache

  • 这样新启动的 job 只需要将内存中的数据拷贝到Checkpint目录中就可以,省去了重新计算这一步。

  • def main(args: Array[String]): Unit = {
    	val sparkConf = new
    SparkConf().setMaster("local").setAppName("SparkCheckPoint" +
    System.currentTimeMillis())
    	val sparkContext = new SparkContext(sparkConf)
    	sparkContext.setCheckpointDir("./checkpoint")
    	val lines: RDD[String] =
    sparkContext.textFile("src/main/resources/NASA_access_log_Aug95")
    	val words: RDD[String] = lines.flatMap(_.split(" "))
    	println("words" + words.getNumPartitions)
    	words.checkpoint
    	words.count
    	sparkContext.stop
    }
    

Spark集群搭建

standalone(Single)

  • 启动集群
    • [root@node01 ~]# cd /opt/yjx/spark-2.4.6/sbin/
    • [root@node01 sbin]# ./start-all.sh
  • 访问
    http://192.168.88.101:8080/
  • Tips :
    • 8080 是 Spark WEBUI 界面的端口, 7077 是 Spark 任务提交的端口。

standalone(HA)

高可用原理

  • Standalone集群只有一个Master,如果Master挂了就无法提交应用程序,需要给Master进行高可用配置

  • Master的高可用可以使用fileSystem(文件系统)和zookeeper(分布式协调服务)

    • fileSystem只有存储功能,可以存储Master的元数据信息,用fileSystem搭建的Master高可用,在Master失败时,需要我们手动启动另外的备用Master,这种方式不推荐使用。
    • zookeeper有选举和存储功能,可以存储Master的元数据信息,使用zookeeper搭建的Master高可用,当Master挂掉时,备用的Master会自动切换,推荐使用这种方式搭建Master的HA。
      Spark_第12张图片
  • 启动集群

    • Zookeeper :
      • 【123】zkServer.sh start
  • 主节点:

    • [root@node01 ~]# cd /opt/yjx/spark-2.4.6/sbin/
    • [root@node01 sbin]# ./start-all.sh
  • 备用节点

    • [root@node02 ~]# cd /opt/yjx/spark-2.4.6/sbin/
    • [root@node02 sbin]# ./start-master.sh
  • 注意点:

  • 主备切换过程中不能提交 Application 。

  • 主备切换过程中不影响已经在集群中运行的 Application 。因为 Spark 是粗粒度资源调度。

standalone(UI)

UI环境基于Single或者HA环境

  • 启动集群
    • 启动Hadoop集群
      • [root@node01 ~]# start-all.sh
      • [root@node01 ~]# hdfs dfs -mkdir -p /spark-logs
    • 启动日志服务
      • [root@node01 sbin]# ./start-history-server.sh
  • 查看服务
    • http://192.168.88.101:18080/

yarn模式

借助于Hadoop的Yarn进行集群的资源管理,启动集群前配置和Standalone相同

  • 启动集群
    • 启动Zookeeper:
      • 【123】zkServer.sh start
    • 启动Hadoop :
      • [root@node01 ~]# start-all.sh
    • 启动Spark:
      • [root@node01 ~]# cd /opt/yjx/spark-2.4.6/sbin/
      • [root@node01 sbin]# ./start-all.sh
  • 访问
    • spark: http://192.168.88.101:8080/
    • hdfs: http://192.168.88.101:9870/
    • yarn: http://192.168.88.101:8088

任务提交方式

Spark_第13张图片

Standalone-client

spark-submit --master spark://node01:7077 --deploy-mode client --class
org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/sparkexamples_2.12-2.4.6.jar 10

  • 执行流程:
    Spark_第14张图片

    • client 模式提交任务后,会在客户端启动 Driver 进程。
    • Driver 会向 Master 申请启动 Application 启动的资源。资源申请成功,
    • Driver 端将 task 分发到 worker 端执行,启动 executor 进程(任务的分发)。
    • Worker 端( exectuor 进程)将 task 执行结果返回到 Driver 端(任务结果的回收)。
  • 总结

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

Standalone-cluster

  • spark-submit --master spark://node01:7077 --deploy-mode cluster --class
    org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
    
  • 执行流程:

Spark_第15张图片

  • cluster 模式提交应用程序后,会向 Master 请求启动 Driver 。

  • Master 接受请求,随机在集群一台节点启动 Driver 进程。

  • Driver 启动后为当前的应用程序申请资源。

  • Driver 端发送 task 到 worker 节点上执行(任务的分发)。

  • worker 上的 executor 进程将执行情况和执行结果返回给 Driver 端(任务结果的回收)。

  • 总结

    • Standalone-cluster 提交方式,应用程序使用的所有 jar 包和文件,必须保证所有的worker 节点都要有,因为此种方式, spark 不会自动上传包。
      • 将所有的依赖包和文件打到同一个包中,然后放在 hdfs 上。
      • 将所有的依赖包和文件各放一份在 worker 节点上。

yarn-client

  • 提交命令

    • spark-submit --master yarn --class org.apache.spark.examples.SparkPi
      $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
      
      spark-submit --master yarn–client --class
      org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
      
      spark-submit --master yarn --deploy-mode client --class
      org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
      
  • 执行流程:

Spark_第16张图片

  • 客户端提交一个 Application ,并在客户端启动一个 Driver 进程。

  • 应用程序启动后会向 RS ( ResourceManager )(相当于 standalone 模式下的 master 进程)发送请求,启动 AM ( ApplicationMaster )。

  • RS 收到请求,随机选择一台 NM ( NodeManager )启动 AM 。这里的 NM 相当于 Standalone 中的 Worker 进程。

  • AM 启动后,会向 RS 请求一批 container 资源,用于启动 Executor 。

  • RS 会找到一批 NM (包含 container )返回给 AM ,用于启动 Executor 。

  • AM 会向 NM 发送命令启动 Executor 。

  • Executor 启动后,会反向注册给 Driver , Driver 发送 task 到 Executor ,并将执行情况和结果返回给 Driver 端。

  • 总结

    • Yarn-client 模式同样是适用于测试,因为 Driver 运行在本地, Driver 会与 yarn 集群中的 Executor 进行大量的通信
    • ApplicationMaster (executorLauncher)的在此模式中的作用:
      • 为当前的 Application 申请资源给 NodeManager 发送消息启动 Executor 。
      • 注意: ApplicationMaster 在此种模式下没有作业调度的功能

yarn-cluster

  • 提交命令

    • spark-submit --master yarn --deploy-mode cluster --class
      org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
      
      spark-submit --master yarn-cluster --class
      org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 1
      
  • 执行流程
    Spark_第17张图片

    • 客户机提交 Application 应用程序,发送请求到 RS ( ResourceManager ),请求启动AM ( ApplicationMaster )。
    • RS 收到请求后随机在一台 NM ( NodeManager )上启动 AM (相当于 Driver 端)。
    • AM 启动, AM 发送请求到 RS ,请求一批 container 用于启动 Excutor 。
    • RS 返回一批 NM 节点给 AM 。
    • AM 连接到 NM ,发送请求到 NM 启动 Excutor 。
    • Excutor 反向注册到 AM 所在的节点的 Driver 。 Driver 发送 task 到 Excutor 。
  • 总结

    • Yarn-Cluster 主要用于生产环境中,因为 Driver 运行在 Yarn 集群中某一台 nodeManager中,每次提交任务的 Driver 所在的机器都是不再是提交任务的客户端机器,而是多个 NM 节点中的一台,不会产生某一台机器网卡流量激增的现象,但同样也有**缺点**,任务提交后不能看到日志。只能通过 yarn 查看日志
    • ApplicationMaster 在此模式中的的作用:
      • 为当前的 Application 申请资源
      • 给 NodeManger 发送消息启动 Executor 。
      • 任务调度。

算子(多文件)

Spark_第18张图片

转换算子

  • 转换算组join
    • leftOuterJoin 左连接
    • rightOuterJoin 右链接
    • fullOuterJoin 全连接
    • 这些join都是作用在K,V格式的RDD上。根据key值进行连接, (K,V)join(K,W)返回(K,(V,W))
    • 注意: join后的分区数,取多的那个分区数
  • union
    • 合并两个数据集,但两个数据集类型要一致
    • 返回的是RDD分区数的总和
  • intersection
    • 取两个数集的交集
    • 新RDD是分区数量最多的
  • subtract
    • 取两个数据集的差集
    • 新RDD是前面那个分区数
  • mapPartitions
    • mapPartition与 map 类似,单位是每个 **partition **上的数据。
    • RDD分区数不会发生变化
  • distinct(map+reduceByKey+map)
    • 对 RDD 内数据去重。(会进行整个对象的匹配,完全相同才会去重)
    • RDD分区数不会发生变化
  • cogroup
    • 当调用类型 (K,V) 和 (K,W) 的数据上时,返回一个数据集 (K,(Iterable,Iterable)) 。
    • RDD分区最多的那个分区数

窄依赖与宽依赖

RDD 之间有一系列的依赖关系,依赖关系又分为窄依赖宽依赖

Spark_第19张图片

窄依赖

父 RDD 和子 RDD 的 partition 之间的关系是一对一的。或者父 RDD 和子 RDD 的 partition 关系是多对一的。不会有 shuffle 的产生。 目的地是唯一的

宽依赖

父 RDD 与子 RDD 的 partition 之间的关系是一对多。会有 shuffle 的产生目的地有多个

宽窄依赖图理解

Spark_第20张图片

Stage

如果父RDD与子RDD不需要进行Shuffle(窄依赖)我们可以将他们连接到一起。减少数据的传输

pipeline

因为将窄依赖的RDD连接到一起,当前RDD链和其他RDD链数据是不相关的,子RDD链不必等父RDD全部执行完毕后才开始执行,只需要等当前链的上—个task计算出结果当前Task就可以执行

Stage

  • Spark 任务会根据 RDD 之间的依赖关系,形成一个 DAG 有向无环图, DAG 会提交给DAG Scheduler , DAGScheduler 会把 DAG 划分成相互依赖的多个 stage ,划分 stage 的依据就是 RDD 之间的宽窄依赖。遇到宽依赖就划分 stage ,每个 stage 包含一个或多个 task 任务。然后将这些 task 以 taskSet 的形式提交给 TaskScheduler 运行。
  • stage 是由一组并行的 task 组成

Stage的切分规则

  • 切割规则:从后往前,遇到宽依赖就进行切分stage
    • 1.从后向前推理,遇到宽依赖就断开,遇到窄依赖就把当前的RDD加入到Stage中
    • 2.每个Stage里面的Task的数量是由该Stage中最后一个RDD的Partition数量决定的;
    • 3.最后一个Stage里面的任务的类型是ResultTask,前面所有其他Stage里面的任务类型都是ShuffleMapTask;
    • 4.代表当前Stage的算子一定是该Stage的最后一个计算步骤;
  • 总结:由于spark中stage的划分是根据shuffle来划分的,而宽依赖必然有shuffle过程,因此可以说spark是根据宽窄依赖来划分stage的

stage的计算模式

Spark_第21张图片

  • pipeline 管道计算模式, pipeline 只是一种计算思想、模式。
  • 在spark中pipeline是一个partition对应一个partition,所以在stage内部只有窄依赖
  • 数据一直在管道里面什么时候数据会落地?
    • 对 RDD 进行持久化( cache , persist )。
    • shuffle write 的时候。
    • 如何改变 RDD 的分区数?
      • reduceByKey(XXX,3)
      • GroupByKey(4)
      • sc.textFile(path,numpartition)
  • 使用算子时传递 分区num参数 就是分区 partition 的数量

SparkShuffle

什么是shuffle?

​ 有些运算需要将各节点上的同一类数据汇集到某一节点进行计算,把这些分布在不同节点的数据按照一定的规则汇集到一起的过程称为 Shuffle

SparkShuffle概念

  • reduceByKey会将上一个RDD中的每一个key对应的所有value聚合成一个value,然后生成一个新的RDD,元素类型是对的形式,这样每一个key对应一个聚合起来的value。

  • 问题:

    • 聚合之前,每一个key对应的value不一定都是在一个partition中,也不太可能在同一个节点上,因为RDD是分布式的弹性的数据集,RDD的partition极有可能分布在各个节点上。
  • 如何聚合?

Spark_第22张图片

  • Shuffle Write:上一个stage的每个map task就必须保证将自己处理的当前分区,数据相同的key写入一个分区文件中,可能会写入多个不同的分区文件中。

  • Shuffle Read:reduce task就会从上一个stage的所有task所在的机器上寻找属于自己的那些分区文件,这样就可以保证每一个key所对应的value都会汇聚到同一个节点上去处理和聚合。

  • Spark中有两种Shuffle类型,HashShuffle和SortShuffle

HashShuffle

Spark_第23张图片

普通机制

Spark_第24张图片

  • 执行流程:
    • 每一个map task将不同结果写到不同的buffer中,每个buffer的大小为32K。buffer起到数据缓存的作用
    • 每个buffer文件最后对应一个磁盘小文件
    • reduce task来拉取对应的磁盘小文件。
  • 总结:
    • map task的计算结果会根据分区器(默认是hashPartitioner)来决定写入到哪一个磁盘小文
      件中去。ReduceTask会去Map端拉取相应的磁盘小文件
    • 产生的磁盘小文件的个数:
      • M(map task的个数)*R(reduce task的个数)
  • 产生的磁盘小文件过多,会导致以下问题:
    • 在Shuffle Write过程中会产生很多写磁盘小文件的对象。
    • 在Shuffle Read过程中会产生很多读取磁盘小文件的对象。
    • 在JVM堆内存中对象过多会造成频繁的gc(垃圾回收机制),gc还无法解决运行所需要的内存的话,就会OOM。
    • 在数据传输过程中会有频繁的网络通信,频繁的网络通信出现通信故障的可能性大大增加,一
      旦网络通信出现了故障会导致shuffle file cannot find 由于这个错误导致的task失败,TaskScheduler不负责重试,由DAGScheduler负责重试Stage

合并机制

Spark_第25张图片

  • 执行流程:
    • 合并机制就是复用buffer,开启合并机制的配置是spark.shuffle.consolidateFiles。该参数默认值为false,将其设置为true即可开启优化机制。
    • 在shuffle write过程中,task就不是为下游stage的每个task创建一个磁盘文件了。此时会出现shuffleFileGroup的概念,每个shuffleFileGroup会对应一批磁盘文件,磁盘文件的数量与下游stage的task数量是相同的。一个Executor上有多少个CPU core,就可以并行执行多少个task。而第一批并行执行的每个task都会创建一个shuffleFileGroup,并将数据写入对应的磁盘文件内。
    • 假设第一个stage有50个task,第二个stage有100个task,总共还是有10个Executor,每个Executor执行5个task。那么原本使用未经优化的HashShuffleManager时,每个Executor会产生500个磁盘文件,所有Executor会产生5000个磁盘文件的。但是此时经过优化之后,每个Executor创建的磁盘文件的数量的计算公式为:CPU core的数量 * 下一个stage的task数量。也就是说,每个Executor此时只会创建100个磁盘文件,所有Executor只会创建1000个磁盘文件。
  • 总结
    • 产生磁盘小文件的个数: C(core的个数)*R(reduce的个数)

SortShuffle

Spark_第26张图片

普通机制

Spark_第27张图片

    • map task 的计算结果会写入到一个内存数据结构里面,内存数据结构默认是 5M
    • 在 shuffle 的时候会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的
      数据超过 5M 时,比如现在内存结构中的数据为 5.01M ,那么他会申请 5.01*2-5=5.02M 内存
      给内存数据结构。
    • 如果申请成功不会进行溢写,如果申请不成功,这时候会发生溢写磁盘。
    • 在溢写之前内存结构中的数据会进行排序分区
      然后开始溢写磁盘,写磁盘是以 batch 的形式去写(批量),一个 batch 是1万条数据
    • map task 执行完成后,会将这些 磁盘小文件 合并成一个大的磁盘文件同时生成一个 索引文
      件 。
    • reduce task 去 map 端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取对应的
      数据。
  • 总结
    • 产生磁盘小文件的个数: 2*M(map task的个数)

bypass机制

  • bypass 运行机制的触发条件如下:
    • shuffle reduce task 的数量小于 spark.shuffle.sort.bypassMergeThreshold 的参数值。这个值默认是 200 。
    • 不需要进行 map 端的预聚合,比如 groupBykey , join 。
    • 产生的磁盘小文件为: 2*M(map task的个数)

shuffle文件寻址

  • MapOutPutTracker:MapOutPutTracker 是 Spark 里面的一个模块,主从架构,用来管理磁盘小文件的地址。
    • MapOutPutTrackerMaster 是主,存在于 Driver 中;
    • MapOutPutTrackerWorker 是从,存在于 Executor 中;
  • BlockManager:块管理者,也是一个 spark 中的一个模块,主从架构。
    • BlockManagerMaster 是主,存在于 Driver 中。用于在集群中传递广播变量或缓存数据或删除数据的时候通知其他的跟随节点来进行相应的操作。说白了就是指挥。
    • BlockManagerWorker是从,存在于 Executor 中。会与BlockManagerMaster节点进行通信。
  • 无论在 Driver 端的 BlockManager 还是在 Excutor 端的BlockManager 都含有四个对象:
    • DiskStore:负责磁盘的管理。
    • MemoryStore:负责内存的管理。
    • ConnectionManager负责连接其他BlockManagerWorker。
    • BlockTransferService:负责数据的传输。

Spark_第28张图片

  • Shuffle 文件寻址流程:
    1. 当 map task 执行完成后,会将 task 的执行情况和磁盘小文件的地址封装到 MapStatus 对象中,通过 MapOutputTrackerWorker 对象向 Driver 中的 MapOutputTrackerMaster 汇报。
    2. 在所有的 map task 执行完毕后, Driver 中就掌握了所有的磁盘小文件的地址。
    3. 在 reduce task 执行之前,会通过 Excutor 中 MapOutPutTrackerWorker 向 Driver 端的
      MapOutputTrackerMaster 获取磁盘小文件的地址。
    4. 获取到磁盘小文件的地址后,会通过 BlockManager 中的 ConnectionManager 连接数据所在节点
      上的 ConnectionManager ,然后通过 BlockTransferService 进行数据的传输。
    5. BlockTransferService 默认启动 5 个 task 去节点拉取数据。默认情况下, 5 个 task 拉取数据
      量不能超过 48M 。

Spark资源调度和任务调度

Spark_第29张图片

调度流程

  • 启动集群后, Worker 节点会向 Master 节点汇报资源情况, Master 掌握了集群资源情况。
  • 当 Spark 提交一个 Application 后,根据 RDD 之间的依赖关系将 Application 形成一个 DAG 有向无环图
  • 任务提交后, Spark 会在 Driver 端创建两个对象: DAGScheduler 和 TaskScheduler ,DAGScheduler 是任务调度的高层调度器,是一个对象。
  • DAGScheduler 的主要作用就是将 DAG 根据 RDD 之间的宽窄依赖关系划分为一个个的 Stage ,然后将这些 Stage 以 TaskSet 的形式提交给 TaskScheduler ( TaskScheduler 是任务调度的低层调度器,这里 TaskSet 其实就是一个集合,里面封装的就是一个个的 task 任务,也就是 stage 中的并行的 task 任务)。
  • TaskSchedule 会遍历 TaskSet 集合,拿到每个 task 后会将 task 发送到 Executor 中去执行(其实就是发送到 Executor 中的线程池 ThreadPool 去执行)。
  • task 在 Executor 线程池中的运行情况会向 TaskScheduler 反馈,当 task 执行失败时,则由TaskScheduler 负责重试,将 task 重新发送给 Executor 去执行,默认重试3次。如果重试3次依然失败,那么这个 task 所在的 stage 就失败了。
  • stage 失败了则由 DAGScheduler 来负责重试,重新发送 TaskSet 到 TaskScheduler , Stage 默认重试4次。如果重试4次以后依然失败,那么这个 job 就失败了。 job 失败了, Application 就失败了。
  • TaskScheduler 不仅能重试失败的 task ,还会重试 straggling (落后,缓慢) task ( 也就是执行速度比其他task慢太多的task )。如果有运行缓慢的 task 那么 TaskScheduler 会启动一个新的task 来与这个运行缓慢的 task 执行相同的处理逻辑。两个 task 哪个先执行完,就以哪个 task的执行结果为准。这就是 Spark 的推测执行机制。在 Spark 中推测执行默认是关闭的。推测执行可以通过 spark.speculation 属性来配置。
    • 注意:
      • 对于 ETL 类型要入数据库的业务要关闭推测执行机制,这样就不会有重复的数据入库
      • 如果遇到数据倾斜的情况,开启推测执行则有可能导致一直会有 task 重新启动处理相同的逻辑,任务可能一直处于处理不完的状态

粗细粒度资源申请

粗粒度资源申请(Spark)

在 Application 执行之前,将所有的资源申请完毕,当资源申请成功后,才会进行任务的调度,当所有的 task 执行完成后,才会释放这部分资源。

优点:
	在 Application 执行之前,所有的资源都申请完毕,每一个 task 直接使用资源就可以了,不需要 task 在执行前自己去申请资源, task 启动就快了, task 执行快了, stage 执行就快了,job 就快了, application 执行就快了。
	
缺点:
	直到最后一个 task 执行完成才会释放资源,集群的资源无法充分利用。

细粒度资源申请(MR)

Application 执行之前不需要先去申请资源,而是直接执行,让 job 中的每一个 task 在执行前自己去申请资源, task 执行完成就释放资源。

优点:集群的资源可以充分利用。
缺点: task 自己去申请资源, task 启动变慢, Application 的运行就响应的变慢了。

算子(分区)

转换算子

  • mapPartitionsWithIndex
    • 类似于 mapPartitions ,除此之外还会携带分区的索引值。
  • repartition
    • 增加或减少分区。此算子会产生 shuffle 。
  • coalesce
    • coalesce 常用来减少分区,算子中第二个参数是减少分区的过程中是否产生 shuffle 。
    • true 为产生 shuffle , false 不产生 shuffle 。默认是 false 。
    • 如果 coalesce 设置的分区数比原来的 RDD 的分区数还多的话,第二个参数设置为 false 不会起作用(转换之后分区数大于之前),如果设置成 true ,效果和 repartition 一样。
  • groupByKey
    • 作用在 K,V 格式的 RDD 上。根据 Key 进行分组。作用在 (K,V) ,返回 (K,Iterable) 。
  • zip
    • 将两个 RDD 中的元素( KV格式/非KV格式 )变成一个 KV 格式的 RDD ,两个 RDD 的个数必须相同
  • zipWithIndex
    • 该函数将 RDD 中的元素和这个元素在 RDD 中的索引号(从0开始)组合成 (K,V) 对。

行动算子

  • countByKey
    • 作用到 K,V 格式的 RDD 上,根据 Key 计数相同 Key 的数据集元素。
  • countByValue
    • 根据数据集每个元素相同的内容来计数。返回相同内容的元素对应的条数。
  • reduce
    • 根据聚合逻辑聚合数据集中的每个元素。

广播变量与累加器

广播变量

Spark_第30张图片

  • 如果使用了广播变量技术,则 Driver 端将共享数据只会发送到每个 Executor 一份。Executor 中的所有 Task 都复用这个对象。
  • 如果不用广播变量技术,则 Driver 端默认会将共享数据分发到每个 Task 中,造成网络分发压力大。甚至导致你在进行RDD持久化到内存时,因内存不足而被迫存到磁盘,增加了磁盘IO,严重降低性能。
  • 注意事项:
    • 广播变量只能在 Driver 端定义,不能在 Executor 端定义。
    • 在 Driver 端可以修改广播变量的值,在 Executor 端无法修改广播变量的值

累加器

Spark_第31张图片

  • 主要用于多个节点对一个变量进行共享性的操作。Accumulator 只提供了累加的功能,即确提供了多个 task 对一个变量并行操作的功能
  • 累加器在 Driver 端定义赋初始值,累加器只能在 Driver 端读取,在 Excutor 端更新

Spark SQL

简介

Shark

SharkSparkSQL的前身,SparkSQL产生的根本原因是其完全脱离了Hive的限制。

  • SparkSQL支持查询原生的RDDRDDSpark核心概念,是Spark能够高效的处理大数据的各种场景的基础
  • 能够在scala/java中写SQL语句。支持简单的SQL语法检查,能够在SQL中写Hive语句访问Hive数据,并将结果取回作为RDD使用。

Spark on Hive和Hive on Spark

Spark_第32张图片

  • Spark on Hive :Hive只作为储存角色,Spark负责sql解析优化,执行。
  • Hive on Spark :Hive即作为存储又负责sql的解析优化,Spark负责执行。

Dataset与DataFrame

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

DataFrame

Spark_第33张图片

  • 特征:
    • DataFrame是一种以RDD为基础的分布式数据集,提供了详细的结构信息。(即DataFrame所表示的二维表数据集的每一列都带有名称)
    • DataFrame可以从很多数据源构建对象 ,如已经存在的RDD、结构化文件、外部数据库、Hive表。
    • RDD可以把它的内部元素看成是一个java对象,DataFrame内部是一个个Row对象,它表示一行一行的数据
  • 区别:
    • 左侧的RDD[Person]虽然以Person为类型参数,但Spark框架本身不了解Person类的内部结构。
    • 右侧的DataFrame却提供了详细的结构信息,DataFrame多了数据的结构信息,即schema。
  • 优点:
    • 提升执行效率
    • 减少数据读取
    • 执行优化

DataSet

DataSet是分布式的数据集合,DataSet提供了强类型支持,在RDD的每行数据加了类型约束

Dataset的底层封装的是RDD,当RDD的泛型是Row类型的时候,我们也可以称它为DataFrame

DSL数据操作

action

  • show以表格的形式在输出中展示 jdbcDF 中的数据,类似于 select * from spark_sql_test 的功能。
    • show 只显示前20条记录。
    • show(numRows: Int) 显示 numRows 条
    • show(truncate: Boolean) 是否最多只显示20个字符,默认为 true 。
  • collect 方法会将 jdbcDF 中的所有数据都获取到,并返回一个 Array 对象。
  • collectAsList:获取所有数据到List
  • describe(cols: String*):获取指定字段的统计信息
  • first, head, take, takeAsList:获取若干行记
    • (1) first 获取第一行记录
    • (2) head 获取第一行记录, head(n: Int) 获取前n行记录
    • (3) take(n: Int) 获取前n行数据
    • (4) takeAsList(n: Int) 获取前n行数据,并以 List 的形式展现

查询

  • where(conditionExpr: String) :SQL语言中where关键字后的条件
    • 可以用 andor 。得到DataFrame类型的返回结果
  • filter :根据字段进行筛选
    • 得到DataFrame类型的返回结果。和 where 使用条件相同
  • select :获取指定字段值
    • 根据传入的 String 类型字段名,获取指定字段的值,以DataFrame类型返回
  • selectExpr :可以对指定字段进行特殊处理
    • 可以直接对指定字段调用UDF函数,或者指定别名等。传入 String 类型参数,得到DataFrame对象。
  • col :获取指定字段
    • 只能获取一个字段,返回对象为Column类型。
  • apply :获取指定字段
    • 只能获取一个字段,返回对象为Column类型
  • drop :去除指定字段,保留其他字段
    • 返回一个新的DataFrame对象,其中不包含去除的字段,一次只能去除一个字段。

Limit

limit 方法获取指定DataFrame的前n行记录,得到一个新的DataFrame对象。

排序

  • orderBysort按指定字段排序,默认为升序
    • 按指定字段排序。加个 - 表示降序排序。 sort 和 orderBy 使用方法相同
    • jdbcDF.orderBy(- jdbcDF(“c4”)).show(false)
    • jdbcDF.orderBy(jdbcDF(“c4”).desc).show(false)
  • sortWithinPartitions
    • 和上面的 sort 方法功能类似,区别在于 sortWithinPartitions 方法返回的是按Partition排好序的DataFrame对象。

组函数

  • groupBy :根据字段进行 group by 操作

    • groupBy 方法有两种调用方式,可以传入 String 类型的字段名,也可传入 Column 类型的对象。
  • cuberollup :group by的扩展

    • 功能类似于 SQL 中的 group by cube/rollup
  • GroupedData对象

    • 该方法得到的是 GroupedData 类型对象,在 GroupedData 的API中提供了 group by 之后的操作

    max(colNames: String)方法,获取分组中指定字段或者所有的数字类型字段的最大值,只能作用于数字型字段

    min(colNames: String)方法,获取分组中指定字段或者所有的数字类型字段的最小值,只能作用于数字型字段

    mean(colNames: String)方法,获取分组中指定字段或者所有的数字类型字段的平均值,只能作用于数字型字段

    sum(colNames: String)方法,获取分组中指定字段或者所有的数字类型字段的和值,只能作用于数字型字段

    count()方法,获取分组中的元素个数

去重

  • distinct :回一个不包含重复记录的DataFrame
    • 返回当前DataFrame中不重复的Row记录。该方法和接下来的 dropDuplicates() 方法不传入指定字段时的结果相同。
  • dropDuplicates :根据指定字段去重
    • 根据指定字段去重。类似于 select distinct a, b 操作

聚合

  • 聚合操作调用的是 agg 方法,该方法有多种调用方式。一般与 groupBy 方法配合使用。

  • 以下示例其中最简单直观的一种用法,对 id 字段求最大值,对 c4 字段求和。

    jdbcDF.agg("id" -> "max", "c4" -> "sum")
    

Union

  • unionAll 方法:对两个DataFrame进行组合 ,类似于 SQL 中的 UNION ALL 操作。

Join

  • 笛卡尔积
    • joinDF1.join(joinDF2)
  • using 一个字段形式
    • 下面这种join类似于 a join b using column1 的形式,需要两个DataFrame中有相同的一个列名,
    • joinDF1.join(joinDF2, “id”)
  • using 多个字段形式
    • 上面这种 using 一个字段的情况外,还可以 using 多个字段

save

  • save可以将data数据保存到指定的区域

    dataFrame.write.format("json").mode(SaveMode.Overwrite).save()
    
    

SparkSQL的数据源

  • SparkSQL 的数据源可以是 JSON 类型的字符串, JDBC , Parquet , Hive , HDFS 等。

SparkSQL底层架构

首先拿到sql解析一批未被解决的逻辑计划,再经过分析得到分析后的逻辑计划,再经过一批优化规则转换成一批最佳优化的逻辑计划,再经过SparkPlanner的策略转化成一批物理计划,随后经过消费模型转换成一个个的Spark任务执行。

谓词下推

Predicate Pushdown简称谓词下推,简而言之,就是在不影响结果的情况下,尽量将过滤条件提
前执行。谓词下推后,过滤条件在map端执行,减少了map端的输出,降低了数据在集群上传输的
量,节约了集群的资源,也提升了任务的性能。

Spark_第34张图片

创建Dataset

parquet文件

  • 将 Dataset 存储成 parquet 文件。保存成 parquet 文件的方式有两种。
df.write().mode(SaveMode.Overwrite)format("parquet").save("src/main/data/parquet");
df.write().mode(SaveMode.Overwrite).parquet("src/main/data/parquet");

SaveMode指定文件保存时的模式。

Overwrite:覆盖

Append:追加

ErrorIfExists:如果存在就报错

Ignore:如果存在就忽略

  • 开始读取数据
object HelloSourceParquet {
	def main(args: Array[String]): Unit = {
		//创建SQL环境
		val sparkSession =
SparkSession.builder().master("local").appName("HelloSourceParquet").get OrCreate()
		import sparkSession.implicits._
		var dataSet: Dataset[_] =sparkSession.read.format("parquet").load("src/main/data/parquet")
		//打印数据
		dataSet.show()
	}
}

JDBC

  • pom.xml

<dependency>
		<groupId>mysqlgroupId>
		<artifactId>mysql-connector-javaartifactId>
		<version>5.1.32version>
dependency>

  • 数据的读写
//创建SQL环境
    val sparkSession = SparkSession.builder().master("local").appName("Hello02DataFrameAvg").getOrCreate()
    //读取数据库的参数
    val map: mutable.Map[String, String] = new mutable.HashMap[String, String]()
    map.put("url", "jdbc:mysql://192.168.88.101:3306/scott")
    map.put("driver", "com.mysql.jdbc.Driver")
    map.put("user", "root")
    map.put("password", "123456")
    map.put("dbtable", "emp")
    //读取JDBC数据
    val dataFrame: DataFrame = sparkSession.read.format("jdbc").options(map).load()

    //打印数据
    dataFrame.show()

    //    val dataFrame1 = sparkSession.sql("select empno,ename from emp where sal > 3000")
    //    dataFrame1.show()

    println(dataFrame.schema)

    val properties = new Properties()
    properties.setProperty("driver", "com.mysql.jdbc.Driver")
    properties.setProperty("user", "root")
    properties.setProperty("password", "123456")
    dataFrame.write.mode(SaveMode.Overwrite).jdbc("jdbc:mysql://192.168.88.101:3306/scott", "empcopy", properties)
  }

Spark On Hive

UDF

UDF:one to one,进来一个出去一个,row mapping。是row级别操作,如:upper、substr函数

UDAF

  • UDAF:many to one,进来多个出去一个,row mapping。是row级别操作,如sum/min。
  • 实现UDAF函数如果要自定义类要实现UserDefinedAggregateFunction类实现其中的方法。

UDTF

  • UDTF:one to mang,进来一个出去多行。如lateral view 与 explode,T:table-generating
  • 通过实现抽象类org.apache.hadoop.hive.ql.udf.generic.GenericUDTF来自定义UDTF算子

开窗函数

  • row_number() 开窗函数是按照某个字段分组,然后取另一字段的前几个的值,相当于分组取topN。

  • --开窗函数格式:
    row_number() over (partitin by XXX order by XXX)
    
    

Spark Streaming

Spark Streaming简介

  • SparkStreaming是流式处理框架,是Spark API(RDD)的扩展,支持可扩展、高吞吐量、容错的准实时数据流处理
  • 实时数据的来源可以是:Kafka, Flume, Twitter, ZeroMQ或者TCP sockets,在接受数据同时可以使用高级功能的复杂算子来处理流数据
  • 最终处理后的数据可以存放在文件系统,数据库等,方便实时展现。

SparkStreaming与Storm的区别

Spark_第35张图片

  • Storm是纯实时的流式处理框架,SparkStreaming是准实时的处理框架(微批处理)。因为微批处理,SparkStreaming的吞吐量比Storm要高。
  • Storm 的事务机制要比SparkStreaming的要完善。
  • Storm支持动态资源调度。(spark1.2开始和之后也支持)
  • SparkStreaming擅长复杂的业务处理,Storm不擅长复杂的业务处理,擅长简单的汇总型计算。

Spark Streaming流式计算

流式计算过程

  • 计算流程
    Spark_第36张图片

Spark_第37张图片

  • Spark Streaming是将流式计算分解成一系列短小的批处理作业。
  • 批处理引擎是Spark Core,也就是把Spark Streaming的输入数据按照batch size(如1秒)分成一段一段的数据(Discretized Stream),
  • 每一段数据都转换成Spark中的RDD(Resilient Distributed Dataset)
  • 然后将Spark Streaming中对DStream的Transformation操作变为针对Spark中对RDD的
    Transformation操作
  • 将RDD经过操作变成中间结果保存在内存中。整个流式计算根据业务的需求可以对中间的结
    果进行叠加或者存储到外部设备
  • Spark Streaming在内部的处理机制:
    • 接收实时流的数据,并根据一定的时间间隔拆分成一批批的数据,然后通过Spark Engine处
      理这些批数据,最终得到处理后的一批批结果数据。
    • 对应的批数据,在Spark内核对应一个RDD实例,因此,对应流数据的DStream可以看成是一
      组RDDs,即RDD的一个序列。
    • 通俗点理解的话,在流数据分成一批一批后,通过一个先进先出的队列,然后 Spark Engine
      从该队列中依次取出一个个批数据,把批数据封装成一个RDD,然后进行处理,这是一个典
      型的生产者消费者模型,对应的就有生产者消费者模型的问题,即如何协调生产速率和消费速率
  • receiver task
    • Spark_第38张图片

    • Spark_第39张图片

    • 工作原理:

      • receiver task是 7*24 小时一直在执行,一直接收数据,将一段时间内接收来的数据保存到batch中。假设batchInterval 为 5s,那么会将接收来的数据每隔 5 秒封装到一个 batch 中,batch 没有分布式计算特性,这一个batch的数据又被封装到一个RDD中最终封装到一个DStream中,然后sparkStreaming回启动一个job去计算。
      • 例如:假设batchInterval为5秒,每隔5秒通过SparkStreamin将得到一个DStream,在第6秒的时候计算这5秒的数据,假设执行任务的时间是3秒,那么第6~9秒一边在接收数据,一边在计算任务,9 ~10秒只是在接收数据。然后在第11秒的时候重复上面的操作。
    • 如果job执行的时间大于batchInterval会有什么样的问题?

      • 如果接受过来的数据设置的级别是仅内存,接收来的数据会越堆积越多,最后可能会导致OOM(如果设置StorageLevel包含disk, 则内存存放不下的数据会溢写至disk, 加大延迟 )。

流式计算的特性

  • 容错性
    • 底层RDD之间存在依赖关系,DStream直接也有依赖关系,RDD具有容错性,那么DStream也具有容错性
    • 每一个RDD都是一个不可变的分布式可重算的数据集,其记录着确定性的操作继承关系(lineage)
    • 只要输入数据是可容错的,那么任意一个RDD的分区(Partition)出错或不可用,都是可以利用原始输入数据通过转换操作而重新算出的。
  • 实时性
    • Spark Streaming将流式计算分解成多个Spark Job,对于每一段数据的处理都会经过Spark
      DAG图分解以及Spark的任务集的调度过程。
    • 对于目前版本的Spark Streaming而言,其最小的Batch Size的选取在0.5~2秒钟之间(Storm
      目前最小的延迟是100ms左右)
    • 所以Spark Streaming能够满足除对实时性要求非常高(如高频实时交易)之外的所有流式准
      实时计算场景。
  • 扩展性与吞吐量
    • Spark目前在EC2上已能够线性扩展到100个节点(每个节点4Core),可以以数秒的延迟处理
      6GB/s的数据量(60M records/s),其吞吐量也比流行的Storm高2~5倍

编程模型DStream

Spark_第40张图片

  • Discretized Stream是Spark Streaming的基础抽象,代表持续性的数据流经过各种Spark算子操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据
  • Dstream与RDD
    • DStream由—组时间序列上连续的RDD来表示。每个RDD都包含了自己特定时间间隔内的数据流;DStream和RDD是包含的关系,DStream是对 RDD的增强,但是行为表现和RDD是基本上差不多的。
  • 生命周期
    • 在InputDStream会将接受到的数据转化成RDD,比如DirectKafkaInputStream 产生的就是KafkaRDD
    • 接着通过MappedDStream等进行数据转换,这个时候是直接调用RDD对应的map方法进行转换的
    • 在进行输出类操作时,才暴露出RDD,可以让用户执行相应的存储,其他计算等操作。

SparkStreaming代码实现

代码实现

  • pom.xml

    • 
      <dependency>
      	<groupId>org.apache.sparkgroupId>
      	<artifactId>spark-streaming_2.12artifactId>
      	<version>2.4.6version>
      dependency>
      
      
      <dependency>
      	<groupId>org.apache.sparkgroupId>
      	<artifactId>spark-streaming-kafka-0-10_2.12artifactId>
      	<version>2.4.6version>
      dependency>
      
      
  • 启动socket server 服务器(指定9999端口号):

    • 【Linux】nc –lk 9999
    • 【Window】nc –lp 9999

DStream转换操作

-Spark_第41张图片

  • transform(func)操作
    • 该transform操作(转换操作)连同其其类似的 transformWith操作允许DStream 上应用任
      意RDD-to-RDD函数。它可以被应用于未在 DStream API 中暴露任何的RDD操作。例如,在
      每批次的数据流与另一数据集的连接功能不直接暴露在DStream API 中,但可以轻松地使用
      transform操作来做到这一点,这使得DStream的功能非常强大。例如,你可以通过连接预先
      计算的垃圾邮件信息的输入数据流(可能也有Spark生成的),然后基于此做实时数据清理的筛选,如下面官方提供的伪代码所示。事实上,也可以在transform方法中使用机器学习和图形计算的算法
  • updateStateByKey操作
    • 该 updateStateByKey 操作可以让你保持任意状态,同时不断有新的信息进行更新。要使用
      此功能,必须进行两个步骤 :
    • 定义状态 - 状态可以是任意的数据类型。
    • 定义状态更新函数 - 用一个函数指定如何使用先前的状态和从输入流中获取的新值 更新状态
    • 使用到updateStateByKey要开启checkpoint机制和功能

DStream窗口操作

Spark_第42张图片

  • 概念
    • 在Spark Streaming中,数据处理是按批进行的,而数据采集是逐条进行的,因此在Spark
      Streaming中会先设置好批处理间隔(batch duration),当超过批处理间隔的时候就会把采
      集到的数据汇总起来成为一批数据交给系统去处理。
    • 对于窗口操作而言,在其窗口内部会有N个批处理数据,批处理数据的大小由窗口间隔
      (window duration)决定,而窗口间隔指的就是窗口的持续时间,在窗口操作中,只有窗口
      的长度满足了才会触发批数据的处理。除了窗口的长度,窗口操作还有另一个重要的参数就是滑动间隔(slide duration),它指的是经过多长时间窗口滑动一次形成新的窗口,滑动窗口
      默认情况下和批次间隔的相同,而窗口间隔一般设置的要比它们两个大。在这里必须注意的一点是滑动间隔和窗口间隔的大小一定得设置为批处理间隔的整数倍。
  • 释义
    • 批处理间隔是1个时间单位,窗口间隔是3个时间单位,滑动间隔是2个时间单位。
    • 窗口长度和滑动间隔必须是batchInterval的整数倍。如果不是整数倍会检测报错。
      • window(windowLength, slideInterval)
      • – 窗口总长度(window length):你想计算多长时间的数据
      • – 滑动时间间隔(slide interval):你每多长时间去更新一次

DStream输出操作

-Spark_第43张图片

SparkStreaming检查点

介绍

  • 流应用程序必须保证7*24全天候运行,因此必须能够适应与程序逻辑无关的故障【例如:系统故
    障、JVM崩溃等】。为了实现这一点,SparkStreaming需要将足够的信息保存到容错存储系统中,以便它可以从故障中恢复。
  • 检查点有两种类型。
    • 元数据检查点
      • 将定义流式计算的信息保存到容错存储系统【如HDFS等】。这用于从运行流应用程序所在的节点故障中恢复。
    • 元数据包括:
      • 1.配置 :用于创建流应用程序的配置。
      • 2.DStream操作:定义流应用程序的DStream操作集。
      • 3.不完整的批次:在任务队列中而尚未完成的批次。
    • 数据检查点
      • 将生成的RDD保存到可靠的存储系统。在一些跨多个批次组合数据的有状态转换中,这
        是必须的。在这种转换中,生成的RDD依赖于先前批次的RDD,这导致依赖关系链的长
        度随着时间而增加。
      • 为了避免恢复时间的这种无限增加【与依赖链成正比】,有状态变换的中间RDD周期性
        地检查以存储到可靠的存储系统中,以切断依赖链。

需要检查点的情况

  • 有状态转换的使用
    • 如果在应用程序中使用了updateStateByKey或reduceByKeyAndWindow,则必须提供检查点以缓存之前批次的中间结果
  • 从运行应用程序的节点故障中恢复,元数据检查点用于使用进度信息进行恢复。

配置检查点

  • 可以通过在容错,可靠的文件系统中设置目录来启用检查点,检查点信息将保存到该文件系统中。
  • 使用:streamingContext.checkpoint(checkpointDirectory)来设置的。
    • 这将允许使用上述状态转换。此外,如果要使应用程序从节点故障中恢复,则应重写流应用程
      序以使其具有以下行为

SparkStreaming数据源

基础数据源

  • Spark Streaming提供了streamingContext.socketTextStream()方法,可以通过 TCP 套接字连接,从文本数据中创建了一个 DStream。
  • Spark Streaming提供了streamingContext.fileStream(dataDirectory)方法可以从任何文件系统(如:HDFS、S3、NFS 等)的文件中读取数据

高级数据源

  • Twitter Spark Streaming的TwitterUtils工具类使用Twitter4j,Twitter4J 库支持通过任何方法提
    供身份验证信息,你可以得到公众的流,或得到基于关键词过滤流。
  • Flume Spark Streaming可以从Flume中接受数据。
  • Kafka Spark Streaming可以从Kafka中接受数据。
  • Kinesis Spark Streaming可以从Kinesis中接受数据。

Kafka接受数据方式

  • Kafka与Spark Streaming集成时有两种方法:旧的基于receiver的方法,新的基于direct stream的方法。

Spark_第44张图片

offset操作

  • SparkStreaming Kafka 维护offset 官网有三种实现方式
    • Checkpoints
    • Kafka itself
    • Your own data store
  • Spark_第45张图片

数据的反压机制

数据流入的速度远高于数据处理的速度,对流处理系统构成巨大的负载压力,如果不能正确处理,可能导致集群资源耗尽最终集群崩溃,因此有效的反压机制(backpressure)对保障流处理系统的稳定至关重要。

storm反压

  • 旧版本
    • 开启了acker机制的storm程序,可以通过设置conf.setMaxSpoutPending参数来实现反压效
      果,如果下游组件(bolt)处理速度跟不上导致spout发送的tuple没有及时确认的数量超过了参数
      设定的值,spout会停止发送数据
    • 但是conf.setMaxSpoutPending参数的设置很难达到最好的反压效果
      • 设小了会导致吞吐上不去
      • 设大了会导致worker OOM;有震荡,数据流会处于一个颠簸状态,效果不如逐级反压;
      • 另外对于关闭acker机制的程序无效;
  • 新版本
    • storm自动反压机制(Automatic Back Pressure)通过监控bolt中的接收队列的情况当超过高水位值时专门的线程会将反压信息写到 Zookeeper ,Zookeeper上的watch会通知该拓扑的所有Worker都进入反压状态,最后Spout降低tuple发送的速度

spark反压

  • 旧版本
    • Spark Streaming程序中当计算过程中出现batch processing time > batch interval的情况时,(其中batch processing time为实际计算一个批次花费时间,batch interval为Streaming应用设置的批处理间隔)
    • 意味着处理数据的速度小于接收数据的速度,如果这种情况持续过长的时间,会造成数据在内存中堆积,导致Receiver所在Executor内存溢出等问题(如果设置StorageLevel包含disk, 则内存存放不下的数据会溢写至disk, 加大延迟)
    • 可以通过设置参数spark.streaming.receiver.maxRate限制Receiver的数据接收速率,此举虽然可以通过限制接收速率,来适配当前的处理能力,防止内存溢出,但也会引入其它问题。
    • 比如:producer数据生产高于maxRate,当前集群处理能力也高于maxRate,这就会造成资
      源利用率下降等问题。
  • 新版本
    • Spark_第46张图片

    • Spark Streaming 从v1.5开始引入反压机制(back-pressure),通过动态控制数据接收速率来适配集群数据处理能力

  • spark streaming的数据源方式有两种:
    • 若是基于Receiver的数据源,可以通过设置spark.streaming.receiver.maxRate来控制最
      大输入速率;
    • 若是基于Direct的数据源(如Kafka Direct Stream),则可以通过设置
      spark.streaming.kafka.maxRatePerPartition来控制最大输入速率。
    • 若设置spark.streaming.backpressure.enabled为true,Spark Streaming会自动根据处理能力来调整输入速率,从而在流量高峰时仍能保证最大的吞吐和性能。
  • 反压原理
    • 在原架构的基础上加上一个新的组件RateController,这个组件负责监听“OnBatchCompleted”事件,然后从中抽取processingDelay 及schedulingDelay信息. Estimator依据这些信息估算出最大处理速度(rate),最后由基于Receiver的Input Stream将rate通过ReceiverTracker与ReceiverSupervisorImpl转发给BlockGenerator(继承自RateLimiter).
    • Spark Streaming的反压机制中,有以下几个重要的组件:
      • RateController

        • 速率控制器
        • RateController继承自接口StreamingListener,并实现了onBatchCompleted方法。每一个Batch处理完成后都会调用此方法
      • RateEstimator

        • RateEstimator是**速率估算器,主要用来估算最大处理速率**
      • RateLimiter

        • RateLimiter是一个抽象类,它并不是Spark本身实现的,而是借助了第三方Google的GuavaRateLimiter来产生的
        • 它实质上是一个**限流器**,也可以叫做令牌,如果Executor中task每秒计算的速度大于该值则阻塞,如果小于该值则通过
        • 将流数据加入缓存中进行计算。这种机制也可以叫做令牌桶机制
      • Spark_第47张图片

      • 令牌桶机制: 大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。当进行某操作时需要令牌时会从令牌桶中取出相应的令牌数,如果获取到则继续操作,否则阻塞。用完之后不用放回。

  • 注意:
    • 反压机制真正起作用时需要至少处理一个批:
      • 由于反压机制需要根据当前批的速率,预估新批的速率,所以反压机制真正起作用前,
        应至少保证处理一个批。
    • 如何保证反压机制真正起作用前应用不会崩溃:
      • 要保证反压机制真正起作用前应用不会崩溃,需要控制每个批次最大摄入速率。
        若为Direct Stream,如Kafka Direct Stream,则可以通过
        spark.streaming.kafka.maxRatePerPartition参数来控制。

SparkStreaming事务

  • Exactly-once 语义是实时计算的难点之一。要做到每一条记录只会被处理一次,即使服务器或网络发生故障时也能保证没有遗漏,这不仅需要实时计算框架本身的支持,还对上游的消息系统、下游的数据存储有所要求。
  • 一个Spark Streaming程序由三步组成:输入、处理逻辑、输出。要达到exactly once的理想状态,需要三步协同进行,而不是只与处理逻辑有关。

Spark_第48张图片

逻辑处理

  • Spark容错分为:Driver级别的容错和Executor级别的容错。
    • 在Driver级别的容错具体为DAG生成的模板,即DStreamGraph,RecevierTracker中存储的
      元数据信息和JobScheduler中存储的Job进行的进度情况等信息,只要通过checkpoint就可以
      了,每个Job生成之前进行checkpoint,在Job生成之后再进行checkpoint,如果出错的话就
      从checkpoint中恢复。
    • 在Executor级别的容错具体为接收数据的安全性和任务执行的安全性。在接收数据安全性方
      面,一种方式是Spark Streaming接收到数据默认为MEMORY_AND_DISK_2的方式,在两台
      机器的内存中,如果一台机器上的Executor挂了,立即切换到另一台机器上的Executor,这
      种方式一般情况下非常可靠且没有切换时间。另外一种方式是WAL(Write Ahead Log),在
      数据到来时先通过WAL机制将数据进行日志记录,如果有问题则从日志记录中恢复,然后再
      把数据存到Executor中,再进行其他副本的复制。WAL这种方式对性能有影响,在生产环境
      中不常用,一般使用Kafka存储,Spark Streaming接收到数据丢失时可以从Kafka中回放。在任务执行的安全性方面,靠RDD的容错。
  • Spark Streaming的容错机制是基于RDD的容错机制。
    1. checkpoint
    2. 基于血统(lineage)的高度容错机制
    3. 出错了之后会从出错的位置从新计算,而不会导致重复计算

Spark 优化

资源调优

在部署spark集群中指定资源分配的默认参数

在spark安装包的conf下spark-env.sh文件

SPARK_WORKER_CORES
SPARK_WORKER_MEMORY
SPARK_WORKER_INSTANCES	#每台机器启动worker数

在提交Application的时候给当前的Application分配更多的资源

提交命令选项:(在提交Application的时候使用选项)

--executor-cores
--executor-memory
--total-executor-cores

配置信息:(Application的代码中设置或在Spark-default.conf中设置)

spark.executor.cores
spark.executor.memory
spark.max.cores

动态分配资源

spark.shuffle.service.enabled true //启用External shuffle Service服务
spark.shuffle.service.port 7337 //Shuffle Service服务端口,必须和yarn-site中的一致
spark.dynamicAllocation.enabled true //开启动态资源分配
spark.dynamicAllocation.minExecutors 1 //每个Application最小分配的executor数
spark.dynamicAllocation.maxExecutors 30 //每个Application最大并发分配的executor数
spark.dynamicAllocation.schedulerBacklogTimeout 1s
spark.dynamicAllocation.sustainedSchedulerBacklogTimeout 5s

并行度调优

并行度的合理调整,可以降低资源浪费提高spark任务的运行效率。

task的数量应该设置为sparkCPU cores的2-3倍。

  1. 如果读取的数据在HDFS中,降低block大小,相当于提高了RDD中partition个数sc.textFile(xx,numPartitions)

  2. sc.parallelize(xxx, numPartitions)

  3. sc.makeRDD(xxx, numPartitions)

  4. sc.parallelizePairs(xxx, numPartitions)

  5. repartitions/coalesce

  6. redecByKey/groupByKey/join —(xxx, numPartitions)

  7. spark.default.parallelism 500

  8. spark.sql.shuffle.partitions—200

    1. spark.default.parallelism并行度调节对sparksql无效(即这个并行度只要是没有使用sparkSql的stage中都会生效)一般我们的操作就是使用repartition去增大sparksql查询出的rdd的分区数。
  9. 自定义分区器

  10. 如果读取数据是在SparkStreaming中

    1. Receiver: spark.streaming.blockInterval—200ms定义的是max

    2. Direct:读取的topic的分区数

代码调优

避免创建重复的RDD

val rdd1 = sc.textFile(path1)
val rdd2 = sc.textFile(path1) 

这就是创建了重复的RDD

有什么问题? 对于执行性能来说没有问题,但是呢,代码乱。

对多次使用的RDD进行持久化

//如何选择一种最合适的持久化策略?

默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。

如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。

如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。

通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。

持久化算子

cache:
	MEMORY_ONLY
persist:
	MEMORY_ONLY
	MEMORY_ONLY_SER
	MEMORY_AND_DISK_SER

​ 一般不要选择带有_2的持久化级别。

checkpoint:

① 如果一个RDD的计算时间比较长或者计算起来比较复杂,一般将这个RDD的计算结果保存到HDFS上,这样数据会更加安全。

② 如果一个RDD的依赖关系非常长,也会使用checkpoint,会切断依赖关系,提高容错的效率。

尽量让RDD中只操作有效数据

尽量避免使用shuffle类的算子

使用广播变量来模拟使用join,使用情况:一个RDD比较大,一个RDD比较小(条件)。

join算子=广播变量+filter、广播变量+map、广播变量+flatMap

使用map-side预聚合的shuffle操作

即尽量使用有combiner的shuffle类算子。

combiner概念:

​ 在map端,每一个map task计算完毕后进行的局部聚合。

combiner好处:

  1. 降低shuffle write写磁盘的数据量。

  2. 降低shuffle read拉取数据量的大小。

  3. 降低reduce端聚合的次数。

有combiner的shuffle类算子:

  1. reduceByKey:这个算子在map端是有combiner的,在一些场景中可以使用reduceByKey代替groupByKey。

  2. aggregateByKey

  3. combinerByKey

尽量使用高性能的算子

使用reduceByKey替代groupByKey

使用mapPartition替代map

使用foreachPartition替代foreach

filter后使用coalesce减少分区数

使用repartitionAndSortWithinPartitions替代repartition与sort类操作

使用repartition和coalesce算子操作分区。

使用广播变量

开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能,函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC,都会极大地影响性能。如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。

广播大变量发送方式:Executor一开始并没有广播变量,而是task运行需要用到广播变量,会找executor的blockManager要,bloackManager找Driver里面的blockManagerMaster要。

使用广播变量可以大大降低集群中变量的副本数。不使用广播变量,变量的副本数和task数一致。使用广播变量变量的副本和Executor数一致。

使用Kryo优化序列化性能

在Spark中,主要有三个地方涉及到了序列化

  1. 在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输。

  2. 将自定义的类型作为RDD的泛型类型时(比如JavaRDD,SXT是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。

  3. 使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。

Kryo序列化器介绍:

Spark支持使用Kryo序列化机制。Kryo序列化机制,比默认的Java序列化机制,速度要快,序列化后的数据要更小,大概是Java序列化机制的1/10。所以Kryo序列化优化以后,可以让网络传输的数据变少;在集群中耗费的内存资源大大减少

对于这三种出现序列化的地方,我们都可以通过使用Kryo序列化类库,来优化序列化和反序列化的性能。Spark默认使用的是Java的序列化机制,也就是ObjectOutputStream/ObjectInputStream API来进行序列化和反序列化。但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。

Spark中使用Kryo:

Sparkconf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer").registerKryoClasses(new Class[]{SpeedSortKey.class})
使用注册器
public class KryoTest implements KryoRegistrator{
	@Override
	public void registerClasses(Kryo kryo) {
		kryo.register(需要注册的类.class);
	}
}

优化数据结构

java中有三种类型比较消耗内存:

  1. 对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。

  2. 字符串,每个字符串内部都有一个字符数组以及长度等额外信息。

  3. 集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。

因此Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。

使用高性能的库fastutil

fasteutil介绍:

fastutil是扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的类库,提供了特殊类型的map、set、list和queue;fastutil能够提供更小的内存占用,更快的存取速度;我们使用fastutil提供的集合类,来替代自己平时使用的JDK的原生的Map、List、Set,好处在于,fastutil集合类,可以减小内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素的值的时候,提供更快的存取速度。fastutil的每一种集合类型,都实现了对应的Java中的标准接口(比如fastutil的map,实现了Java的Map接口),因此可以直接放入已有系统的任何代码中。

fastutil最新版本要求Java 7以及以上版本。

使用:

见RandomExtractCars.java类

数据本地化

数据本地化的级别:

PROCESS_LOCAL

task要计算的数据在本进程(Executor)的内存中。

Spark_第49张图片

NODE_LOCAL

① task所计算的数据在本节点所在的磁盘上。

② task所计算的数据在本节点其他Executor进程的内存中。

Spark_第50张图片

NO_PREF

task所计算的数据在关系型数据库中,如mysql。

Spark_第51张图片

RACK_LOCAL

task所计算的数据在同机架的不同节点的磁盘或者Executor进程的内存中

Spark_第52张图片

ANY

跨机架。

Spark数据本地化调优:

Spark_第53张图片

Spark中任务调度时,TaskScheduler在分发之前需要依据数据的位置来分发,最好将task分发到数据所在的节点上,如果TaskScheduler分发的task在默认3s依然无法执行的话,TaskScheduler会重新发送这个task到相同的Executor中去执行,会重试5次,如果依然无法执行,那么TaskScheduler会降低一级数据本地化的级别再次发送task。

如上图中,会先尝试1,PROCESS_LOCAL数据本地化级别,如果重试5次每次等待3s,会默认这个Executor计算资源满了,那么会降低一级数据本地化级别到2,NODE_LOCAL,如果还是重试5次每次等待3s还是失败,那么还是会降低一级数据本地化级别到3,RACK_LOCAL。这样数据就会有网络传输,降低了执行效率。

如何提高数据本地化的级别?

可以增加每次发送task的等待时间(默认都是3s),将3s倍数调大, 结合WEBUI来调节:

​ • spark.locality.wait

​ • spark.locality.wait.process

​ • spark.locality.wait.node

​ • spark.locality.wait.rack

注意:等待时间不能调大很大,调整数据本地化的级别不要本末倒置,虽然每一个task的本地化级别是最高了,但整个Application的执行时间反而加长。

如何查看数据本地化的级别?

通过日志或者WEBUI

内存调优

总结堆内存不足造成的影响:

频繁的minor gc

老年代中大量的短声明周期的对象会导致full gc

gc 多了就会影响Spark的性能和运行的速度

Spark JVM调优主要是降低gc时间,可以修改Executor内存的比例参数。

RDD缓存、task定义运行的算子函数,可能会创建很多对象,这样会占用大量的堆内存。堆内存满了之后会频繁的GC,如果GC还不能够满足内存的需要的话就会报OOM。比如一个task在运行的时候会创建N个对象,这些对象首先要放入到JVM年轻代中。比如在存数据的时候我们使用了foreach来将数据写入到内存,每条数据都会封装到一个对象中存入数据库中,那么有多少条数据就会在JVM中创建多少个对象。

Spark中如何内存调优?

Spark Executor堆内存中存放(以静态内存管理为例):RDD的缓存数据和广播变量(spark.storage.memoryFraction 0.6),shuffle聚合内存(spark.shuffle.memoryFraction 0.2),task的运行(0.2)那么如何调优呢?

  1. 提高Executor总体内存的大小

  2. 降低储存内存比例或者降低聚合内存比例

如何查看gc?

Spark WEBUI中job->stage->task

调节Executor的堆外内存

Spark底层shuffle的传输方式是使用netty传输,netty在进行网络传输的过程会申请堆外内存(netty是零拷贝),所以使用了堆外内存。默认情况下,这个堆外内存上限默认是每一个executor的内存大小的10%;真正处理大数据的时候,这里都会出现问题,导致spark作业反复崩溃,无法运行;此时就会去调节这个参数,到至少1G(1024M),甚至说2G、4G。

Spark_第54张图片

executor在进行shuffle write,优先从自己本地关联的mapOutPutTrackerWorker中获取某份数据如果本地mapOutPutTrackerWorker没有的话,那么会通过TransferService,去远程连接其他节点上executor的block manager去获取,如果本地block manager没有的话,那么会通过TransferService,去远程连接其他节点上executor的block manager去获取,尝试建立远程的网络连接,并且去拉取数据。频繁创建对象让JVM堆内存满溢,进行垃圾回收。正好碰到那个exeuctor的JVM在垃圾回收。处于垃圾回过程中,所有的工作线程全部停止;相当于只要一旦进行垃圾回收,spark / executor停止工作,无法提供响应,spark默认的网络连接的超时时长是60s;如果卡住60s都无法建立连接的话,那么这个task就失败了。task失败了就会出现shuffle file cannot find的错误。

那么如何调节等待的时长呢?

在./spark-submit提交任务的脚本里面添加:

--conf spark.core.connection.ack.wait.timeout=300

Executor由于内存不足或者堆外内存不足了,挂掉了,对应的Executor上面的block manager也挂掉了,找不到对应的shuffle map output文件,Reducer端不能够拉取数据。我们可以调节堆外内存的大小,如何调节?

在./spark-submit提交任务的脚本里面添加

yarn下:

--conf  spark.yarn.executor.memoryOverhead=2048 #单位M

standalone下:

--conf spark.executor.memoryOverhead=2048	#单位M

Spark Shuffle调优

  1. buffer大小——32KB
  2. shuffle read拉取数据量的大小——48M
  3. shuffle聚合内存的比例——20%
  4. 拉取数据重试次数——5次
  5. 重试间隔时间60s
  6. Spark Shuffle的种类
  7. SortShuffle bypass机制 200次调大

解决数据倾斜

使用Hive ETL预处理数据

方案适用场景:

如果导致数据倾斜的是Hive表。如果该Hive表中的数据本身很不均匀(比如某个key对应了100万数据,其他key才对应了10条数据),而且业务场景需要频繁使用Spark对Hive表执行某个分析操作,那么比较适合使用这种技术方案。

方案实现思路:

此时可以评估一下,是否可以通过Hive来进行数据预处理(即通过Hive ETL预先对数据按照key进行聚合,或者是预先和其他表进行join),然后在Spark作业中针对的数据源就不是原来的Hive表了,而是预处理后的Hive表。此时由于数据已经预先进行过聚合或join操作了,那么在Spark作业中也就不需要使用原先的shuffle类算子执行这类操作了。

方案实现原理:

这种方案从根源上解决了数据倾斜,因为彻底避免了在Spark中执行shuffle类算子,那么肯定就不会有数据倾斜的问题了。但是这里也要提醒一下大家,**这种方式属于治标不治本。**因为毕竟数据本身就存在分布不均匀的问题,所以Hive ETL中进行group by或者join等shuffle操作时,还是会出现数据倾斜,导致Hive ETL的速度很慢。我们只是把数据倾斜的发生提前到了Hive ETL中,避免Spark程序发生数据倾斜而已。

过滤少数导致倾斜的key

方案适用场景:

如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。

方案实现思路:

如果我们判断那少数几个数据量特别多的key,对作业的执行和计算结果不是特别重要的话,那么干脆就直接过滤掉那少数几个key。比如,在Spark SQL中可以使用where子句过滤掉这些key或者在Spark Core中对RDD执行filter算子过滤掉这些key。如果需要每次作业执行时,动态判定哪些key的数据量最多然后再进行过滤,那么可以使用sample算子对RDD进行采样,然后计算出每个key的数量,取数据量最多的key过滤掉即可

方案实现原理:

将导致数据倾斜的key给过滤掉之后,这些key就不会参与计算了,自然不可能产生数据倾斜。

提高shuffle操作的并行度

方案实现思路:

在对RDD执行shuffle算子时,给shuffle算子传入一个参数,比如reduceByKey(1000),该参数就设置了这个shuffle算子执行时shuffle read task的数量。对于Spark SQL中的shuffle类语句,比如group by、join等,需要设置一个参数,即spark.sql.shuffle.partitions,该参数代表了shuffle read task的并行度,该值默认是200,对于很多场景来说都有点过小。

方案实现原理:

增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据。举例来说,如果原本有5个不同的key,每个key对应10条数据,这5个key都是分配给一个task的,那么这个task就要处理50条数据。而增加了shuffle read task以后,每个task就分配到一个key,即每个task就处理10条数据,那么自然每个task的执行时间都会变短了。

缺点:不是万能的,没有从根本上改变数据的格式所以数据倾斜问题还是可能存在

例如极端情况,有一个key对应数据就是100w,别的key对应1w,你再怎么增加task,相同的可以还是会分配到一个task中去计算

不能根本解决问题,可以缓解一波。

双重聚合

方案适用场景:

对RDD执行reduceByKey、groupByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时,比较适用这种方案。

方案实现思路:

这个方案的核心实现思路就是进行两阶段聚合第一次是局部聚合,先给每个key都打上一个随机数,Spark 优化 比如10以内的随机数,此时原先一样的key就变成不一样的了,比如(hello, 1) (hello, 1) (hello, 1) (hello, 1),就会变成(1_hello, 1) (1_hello, 1) (2_hello, 1) (2_hello, 1)。接着对打上随机数后的数据,执行reduceByKey等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1_hello, 2) (2_hello, 2)。然后将各个key的前缀给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。

方案实现原理:

将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果

Spark_第55张图片

如果一个RDD中有一个key导致数据倾斜,同时还有其他的key,那么一般先对数据集进行抽样,然后找出倾斜的key,再使用filter对原始的RDD进行分离为两个RDD,一个是由倾斜的key组成的RDD1,一个是由其他的key组成的RDD2,那么对于RDD1可以使用加随机前缀进行多分区多task计算,对于另一个RDD2正常聚合计算,最后将结果再合并起来

只适用于聚合类的shuffle操作,join这类不太适合。

将reduce join转为map join

BroadCast+filter(或者map)

方案适用场景:

在对RDD使用join类操作,或者是在Spark SQL中使用join语句时,而且join操作中的一个RDD或表的数据量比较小(比如几百M或者一两G),比较适用此方案。

方案实现思路:

不使用join算子进行连接操作,而使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuffle类的操作,彻底避免数据倾斜的发生和出现。将较小RDD中的数据直接通过collect算子拉取到Driver端的内存中来,然后对其创建一个Broadcast变量;接着对另外一个RDD执行map类算子,在算子函数内,从Broadcast变量中获取较小RDD的全量数据,与当前RDD的每一条数据按照连接key进行比对,如果连接key相同的话,那么就将两个RDD的数据用你需要的方式连接起来。

方案实现原理:

普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉取到一个shuffle read task中再进行join,此时就是reduce join。但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不会发生shuffle操作,也就不会发生数据倾斜。

采样倾斜key并分拆join操作

方案适用场景:

两个RDD/Hive表进行join的时候,如果数据量都比较大,无法采用“解决方案五”,那么此时可以看一下两个RDD/Hive表中的key分布情况。如果出现数据倾斜,是因为其中某一个RDD/Hive表中的少数几个key的数据量过大,而另一个RDD/Hive表中的所有key都分布比较均匀,那么采用这个解决方案是比较合适的。

方案实现思路:

对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下每个key的数量,计算出来数据量最大的是哪几个key。然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD。接着将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD。再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打散成n份,分散到多个task中去进行join了。而另外两个普通的RDD就照常join即可。最后将两次join的结果使用union算子合并起来即可,就是最终的join结果 。

Spark_第56张图片

使用随机前缀和扩容RDD进行join

方案适用场景:

如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能使用最后一种方案来解决问题了。

方案实现思路:

该方案的实现思路基本和“解决方案六”类似,首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。然后将该RDD的每条数据都打上一个n以内的随机前缀。同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀。最后将两个处理后的RDD进行join即可。

Spark_第57张图片

rdd中数据倾斜,sample抽样取出样本(key),统计key对应的数量,以及最大的数据量的key。

把这些key从rdd中抽取出去,形成单独的rdd,给key打上随机数作为前缀,另一部分没有抽出去的数据在形成另外一个rdd。

另一个rdd(join),也将上次抽取出来的key过滤出一个单独的rdd,但是需要做扩容(数据量的膨胀)。在这些数据上追加上随机数,和之前的rdd加随机数的规则一致,没有抽离的数据也还是形成一个新的rdd。

两两join,有随机数的记得去掉前缀,然后最终结果union得出。

Spark故障解决(troubleshooting)

shuffle file cannot find:磁盘小文件找不到。

connection timeout ----shuffle file cannot find

提高建立连接的超时时间,或者降低gc,降低gc了那么spark不能堆外提供服务的时间就少了,那么超时的可能就会降低。

fetch data fail ---- shuffle file cannot find

提高拉取数据的重试次数以及间隔时间。

OOM/executor lost ---- shuffle file cannot find

提高堆外内存大小,提高堆内内存大小。

reduce OOM

BlockManager拉取的数据量大,reduce task处理的数据量小

解决方法:

  1. 降低每次拉取的数据量

  2. 提高shuffle聚合的内存比例

  3. 提高Executor的内存比例

序列化问题

Null值问题

val rdd = rdd.map{x=>{
	x+”~”; -1
}}
filter(-1).coalesce
rdd.foreach{x=>{
	System.out.println(x.getName())
}}

你可能感兴趣的:(hadoop,spark)