运行速度快:
Spark拥有DAG执行引擎,支持在内存中对数据进行迭代计算。官方提供的数据表明,如果数据由磁盘读取,速度是Hadoop MapReduce的10倍以上,如果数据从内存中读取,速度可以高达100多倍。
适用场景广泛:
大数据分析统计,实时数据处理,图计算及机器学习
易用性:
编写简单,支持80种以上的高级算子,支持多种语言,数据源丰富,可部署在多种集群中
容错性高:
Spark引进了弹性分布式数据集RDD (Resilient Distributed Dataset) 的抽象,它是分布在一组节点中的只读对象集合,这些集合是弹性的,如果数据集一部分丢失,则可以根据“血统”(即充许基于数据衍生过程)对它们进行重建。另外在RDD计算时可以通过CheckPoint来实现容错,而CheckPoint有两种方式:CheckPoint Data,和Logging The Updates,用户可以控制采用哪种方式来实现容错。
目前大数据处理场景有以下几个类型:
本地模式(方便本地调试):
Spark 程序运行于本地,通过local[]指定线程的数量,本地模式分为三类:
(1)local:只启动一个 executor
(2)local[k]:启动 k 个 executor
(3)local[*]:启动跟 cpu 数量相同的 executor
StandAlone 模式:
分布式部署集群,自带完整的服务,资源管理和任务监控是 Spark 自己监控,也是其他模式的基础
Spark on yarn 模式:
分布式部署集群,资源和任务监控交给 yarn 管理,Spark 客户端直接连接 Yarn,不需要额外构建 Spark 集群。有 yarn-client 和 yarn-cluster 两种模式,主要区别在于:Driver 程序的运行节点。
(1)cluster 适合生产,Driver 运行在集群子节点,具有容错功能
(2)client 适合调试,Driver 运行于客户端
Spark的架构
采用了分布式计算中的Master-Slave模型,Master是对应集群中的含有Master进程的节点,Slave是集群中含有Worker进程的节点。Master作为整个集群的控制器,负责整个集群的正常运行;Worker相当于是计算节点,接收主节点命令与进行状态汇报;Executor负责任务的执行;Client作为用户的客户端负责提交应用,Driver负责控制一个应用的执行,组成图如下:
Spark集群部署后,需要在主节点和从节点分别启动Master进程和Worker进程,对整个集群进行控制。在一个Spark应用的执行过程中,Driver和Worker是两个重要角色。Driver程序是应用逻辑执行的起点,负责作业的调度,即Task任务的分发,而多个Worker用来管理计算节点和创建Executor并行处理任务。在执行阶段,Driver会将Task和Task所依赖的file和jar序列化后传递给对应的Worker机器,同时Executor对相应数据分区的任务进行处理。
Spark的架构中的基本组件:
概念:
RDD(Resilient Distributed Dataset),弹性分布式数据集,是 Spark 中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合
RDD 五大特性:
宽依赖:
指子 RDD 的分区依赖于RDD 的所有分区,这是因为 shuffle 类操作
窄依赖:
指父 RDD 的每一个分区最多被一个子 RDD 的分区所用,表现为一个父 RDD 的分区对应一个子 RDD 分区,和多个父 RDD 对应一个子 RDD 分区,map/filter 和 union 属于第一类,对输入进行协同划分(co-partitioned)的 join 属于第二类
driver端的内存溢出
可以增大driver的内存参数:spark.driver.memory (default 1g)这个参数用来设置Driver的内存。在Spark程序中,SparkContext,DAGScheduler都是运行在Driver端的。对应rdd的Stage切分也是在Driver端运行,如果用户自己写的程序有过多的步骤,切分出过多的Stage,这部分信息消耗的是Driver的内存,这个时候就需要调大Driver的内存。
map过程产生大量对象导致内存溢出
这种溢出的原因是在单个 map 中产生了大量的对象导致的,例如:rdd.map(x=>for(i <- 1 to 10000) yield i.toString),这个操作在 rdd 中,每个对象都产生了10000个对象,这肯定很容易产生内存溢出的问题。针对这种问题,在不增加内存的情况下,可以通过减少每个 Task 的大小,以便达到每个 Task 即使产生大量的对象Executor的内存也能够装得下。具体做法可以在会产生大量对象的map操作之前调用 repartition 方法,分区成更小的块传入 map。例如:rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)。面对这种问题注意,不能使用rdd.coalesce方法,这个方法只能减少分区,不能增加分区,不会有 shuffle 的过程。
数据不平衡导致内存溢出
数据不平衡除了有可能导致内存溢出外,也有可能导致性能的问题,解决方法和上面说的类似,就是调用 repartition 重新分区。
shuffle后内存溢出
shuffle 内存溢出的情况可以说都是 shuffle 后,单个文件过大导致的。在 Spark 中,join,reduceByKey 这一类型的过程,都会有 shuffle 的过程,在 shuffle 的使用,需要传入一个partitioner,大部分 Spark 中的 shuffle 操作,默认的 partitioner 都是 HashPatitioner,默认值是父RDD 中最大的分区数,这个参数通过 spark.default.parallelism 控制(在 spark-sql 中用spark.sql.shuffle.partitions) , spark.default.parallelism 参数只对 HashPartitioner 有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle 的并发量了。如果是别的 partitioner 导致的 shuffle 内存溢出,就需要从 partitioner 的代码增加 partitions 的数量。
standalone 模式下资源分配不均匀导致内存溢出
在 standalone 的模式下如果配置了 --total-executor-cores 和 --executor-memory 这两个参数,但是没有配置 --executor-cores 这个参数的话,就有可能导致,每个 Executor 的 memory 是一样的,但是 cores 的数量不同,那么在 cores 数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。这种情况的解决方法就是同时配置 --executor-cores 或者spark.executor.cores 参数,确保 Executor 资源分配均匀。
使用 rdd.persist(StorageLevel.MEMORY_AND_DISK_SER) 代替 rdd.cache()
rdd.cache()和rdd.persist(Storage.MEMORY_ONLY) 是等价的,在内存不足的时候rdd.cache() 的数据会丢失,再次使用的时候会重算,而rdd.persist(StorageLevel.MEMORY_AND_DISK_SER) 在内存不足的时候会存储在磁盘,避免重算,只是消耗点 IO 时间。
groupByKey:
对每个 key 对应的多个 value 进行操作,但只能汇总成一个 sequence,本身不能自定义函数,只能额外通过map/mapValues来实现
reduceByKey:
用于对每个 key 对应的多个 value 进行 merge(合并)操作,最重要的是现在分区内进行 merge 操作,并且 merge操作可以自定义
combineKey:
reduceByKey 底层使用的就是 combineKey
总结:优先使用 reduceByKey,因为 reduceByKey 会在 shuffle 之前对数据合并,大大减少了 IO 消耗
相同点:
都是用于遍历集合对象,并对每一项执行指定的方法
不同点:
区别:
RDD 中每个分区数据量不大的情形:
RDD 中的每个分区数据量超大的情形,比如一个 Partition 有 100万条数据:
相同:
foreach 和 foreachPartition 都属于 action 算子
不同点:
区别:
repartition 的底层调用了 coalesce,并且默认发生 shuffle,如果分区数由多变少,建议使用 coalesce,不走 shuffle
针对键值类型的 RDD 可以使用 partitionBy,将相同的key分配到一个分区内,后面再执行类型相同 key 聚合操作的时候就可以避免发生 shuffle
效率比较:
如果后面有对 可以 进行聚合的操作,建议使用partitionBy,可以避免 shuffle,效率会更高(重点在避免 shuffle)
使用场景:
一般就是在数据初始加载,或者进行 filter 之后数据分配不均匀,需要对其进行重分区操作
什么是血统
利用内存加快数据加载,在其它的In-Memory类数据库或Cache类系统中也有实现。Spark的主要区别在于它采用血统(Lineage)来时实现分布式运算环境下的数据容错性(节点失效、数据丢失)问题。RDD Lineage被称为RDD运算图或RDD依赖关系图,是RDD所有父RDD的图。它是在RDD上执行transformations函数并创建逻辑执行计划(logical execution plan)的结果,是RDD的逻辑执行计划。相比其它系统的细颗粒度的内存数据更新级别的备份或者LOG机制,RDD的Lineage记录的是粗颗粒度的特定数据转换(Transformation)操作(filter, map, join etc.)行为。当这个RDD的部分分区数据丢失时,它可以通过Lineage找到丢失的父RDD的分区进行局部计算来恢复丢失的数据,这样可以节省资源提高运行效率。这种粗颗粒的数据模型,限制了Spark的运用场合,但同时相比细颗粒度的数据模型,也带来了性能的提升。
依赖的类型
依赖关系决定Lineage的复杂程度,同时也是的RDD具有了容错性。因为当某一个分区里的数据丢失了,Spark程序会根据依赖关系进行局部计算来恢复丢失的数据。依赖的关系主要分为2种,分别是 宽依赖(Wide Dependencies)和窄依赖(Narrow Dependencies)。
容错原理
在Spark的容错机制中,当一个节点宕机了,进行容错恢复时,对于窄依赖来讲,进行重计算时只要把丢失的父RDD分区重算即可,不依赖于其他节点。而对于Shuffle Dependency来说,进行重计算时需要父RDD的分区都存在,这样计算量就太大了比较耗费性能。
同时在RDD计算,也通过checkpoint进行容错,做checkpoint有两种方式,一个是checkpoint data,一个是logging the updates。用户可以控制采用哪种方式来实现容错,默认是logging the updates方式,通过记录跟踪所有生成RDD的转换(transformations)也就是记录每个RDD的lineage(血统)来重新计算生成丢失的分区数据。但是,在使用checkpoint算子来做检查点,不仅需要考虑Lineage长度,还也要考虑Lineage的复杂度(是否有宽依赖),对于Shuffle Dependency加Checkpoint是一个值得提倡的做法。
cache() 和 persist()
针对一个 RDD 反复执行多个操作的场景,可以进行 cache() 或 persist() 进行持久化,在该 RDD 第一次被计算出来时,就会直接缓存在每个节点中。而且 Spark 的持久化机制还是自动容错的,如果持久化的 RDD 的任何时候 partition 丢失了,那么 Spark 会自动通过其源 RDD,使用 transformation 操作重新计算该 partition
cache() 和 persist() 区别在于:cache() 是 persist() 的一种简化方式,cache() 底层调用的就是 persist() 的无参版本,同时就是调用 persist(MEMORY_ONLY),将数据持久化到内存中,如果需要从内存中去除缓存,那么可以使用 unpersist() 方法
checkpoint
持久化/缓存可以把数据放在内存中,虽然是快速的,但是也是最不可靠的;也可以把数据放在磁盘上,也不是完全可靠的!例如磁盘会损坏等
Checkpoint 的产生就是为了更加可靠的数据持久化,在 Checkpoint 的时候一般把数据放在在 HDFS 上,这就天然的借助了 HDFS 天生的高容错、高可靠来实现数据最大程度上的安全,实现了 RDD 的容错和高可用
DAG 是什么
DAG(Directed Acyclic Graph 有向无环图)指的是数据转换执行的过程,有方向,无闭环(其实就是 RDD 执行的流程);
原始的 RDD 通过一系列的转换操作就形成了 DAG 有向无环图,任务执行时,可以按照 DAG 的描述,执行真正的计算(数据被操作的一个过程)。
DAG 的边界
开始:通过 SparkContext 创建的 RDD;
结束:触发 Action,一旦触发 Action 就形成了一个完整的 DAG。
原因:为了并行计算
一个复杂的业务逻辑如果有 shuffle,那么就意味着前面阶段产生结果后,才能执行下一个阶段,即下一个阶段的计算要依赖上一个阶段的数据。那么我们按照 shuffle 进行划分(也就是按照宽依赖就行划分),就可以将一个 DAG 划分成多个 Stage/阶段,在同一个 Stage 中,会有多个算子操作,可以形成一个 pipeline 流水线,流水线内的多个平行的分区可以并行执行。
如何划分 Stage
Spark 会根据 shuffle/宽依赖使用回溯算法来对 DAG 进行 Stage 划分,从后往前,遇到宽依赖就断开,遇到窄依赖就把当前的 RDD 加入到当前的 stage/阶段中
在默认情况下,当 Spark 在集群的多个不同节点的多个任务上并行运行一个函数时,它会把函数中涉及到的每个变量,在每个任务上都生成一个副本。但是,有时候需要在多个任务之间共享变量,或者在任务(Task)和任务控制节点(Driver Program)之间共享变量。
为了满足这种需求,Spark 提供了两种类型的变量:
(1)累加器 accumulators:累加器支持在所有不同节点之间进行累加计算(比如计数或者求和)。
(2)广播变量 broadcast variables:广播变量用来把变量在所有节点的内存之间进行共享,在每个机器上缓存一个只读的变量,而不是为机器上的每个任务都生成一个副本。
累加器:val xx: Accumulator[Int] = sc.accumulator(0)
通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。这时使用累加器就可以实现我们想要的效果:
import org.apache.spark.rdd.RDD
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
object AccumulatorTest {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
//使用scala集合完成累加
var counter1: Int = 0;
var data = Seq(1,2,3)
data.foreach(x => counter1 += x )
println(counter1)//6
println("+++++++++++++++++++++++++")
//使用RDD进行累加
var counter2: Int = 0;
val dataRDD: RDD[Int] = sc.parallelize(data) //分布式集合的[1,2,3]
dataRDD.foreach(x => counter2 += x)
println(counter2)//0
//注意:上面的RDD操作运行结果是0
//因为foreach中的函数是传递给Worker中的Executor执行,用到了counter2变量
//而counter2变量在Driver端定义的,在传递给Executor的时候,各个Executor都有了一份counter2
//最后各个Executor将各自个x加到自己的counter2上面了,和Driver端的counter2没有关系
//那这个问题得解决啊!不能因为使用了Spark连累加都做不了了啊!
//如果解决?---使用累加器
val counter3: Accumulator[Int] = sc.accumulator(0)
dataRDD.foreach(x => counter3 += x)
println(counter3)//6
}
}
广播变量:sc.broadcast()
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object BroadcastVariablesTest {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
//不使用广播变量
val kvFruit: RDD[(Int, String)] = sc.parallelize(List((1,"apple"),(2,"orange"),(3,"banana"),(4,"grape")))
val fruitMap: collection.Map[Int, String] =kvFruit.collectAsMap
//scala.collection.Map[Int,String] = Map(2 -> orange, 4 -> grape, 1 -> apple, 3 -> banana)
val fruitIds: RDD[Int] = sc.parallelize(List(2,4,1,3))
//根据水果编号取水果名称
val fruitNames: RDD[String] = fruitIds.map(x=>fruitMap(x))
fruitNames.foreach(println)
//注意:以上代码看似一点问题没有,但是考虑到数据量如果较大,且Task数较多,
//那么会导致,被各个Task共用到的fruitMap会被多次传输
//应该要减少fruitMap的传输,一台机器上一个,被该台机器中的Task共用即可
//如何做到?---使用广播变量
//注意:广播变量的值不能被修改,如需修改可以将数据存到外部数据源,如MySQL、Redis
println("=====================")
val BroadcastFruitMap: Broadcast[collection.Map[Int, String]] = sc.broadcast(fruitMap)
val fruitNames2: RDD[String] = fruitIds.map(x=>BroadcastFruitMap.value(x))
fruitNames2.foreach(println)
}
}
具体可参考先前文章《大数据—— Spark 优化》
Spark2.1.1 版本之后只有 SortShuffle,之前版本还有 HashShuffle
(1)聚合源数据
在生成源数据时按照 key 进行分组聚合,就不需要在 Spark 中使用 reduceByKey 和 groupByKey
(2)过滤导致倾斜的 key
如果业务允许或与客户沟通同意后,可以把这些导致倾斜的 key进行过滤
(3)提高 shuffle 操作 reduce 的并行度
增加 task 的并行度,如果运行时间仍然很长,则需要考虑其他方案
(4)双重聚合
将 key 通过增加随机数前缀的方式进行打散,这样重复多的 key 会被分入多个组,然后进行局部聚合(第一次聚合),接着移除 前缀,然后进行全局聚合
(5)将 reduce join 转换成 map join(小表 和 大表 join 时出现数据倾斜,优先考虑广播)
如果两个 RDD 进行 join,其中有一个 RDD 比较小的话,可以通过将小的 RDD broadcast 广播出去,这样每个节点的 blockManager 中都有一份,这样就不存在发生 shuffle,也就不会有数据倾斜的问题了
(6)sample 抽样分解聚合
将导致数据倾斜的 key 可以单拉出来,然后用一个 RDD 进行打乱 join
(7)使用随机数和扩容表进行 join
通过 flatMap 进行扩容,然后将随机数打入进去,再进行 join,这样的话就不能根本的解决数据倾斜,但可以有效的缓解数据倾斜的问题,也能提高性能
默认分区数取 CPU 核数和 2 的最小值
《大数据——Java 知识点整理》
《大数据——MySQL 知识点整理》
《大数据—— Hadoop 知识点整理》
《大数据—— Hive 知识点整理》
《大数据—— HBase 知识点整理》
《大数据—— Scala 知识点整理》
《大数据——Flink 知识点整理》