Spark面试整理
一、spark的优势:
1、每一个作业独立调度,可以把所有的作业做一个图进行调度,各个作业之间相互依赖,在调度过程中一起调度,速度快。
2、所有过程都基于内存,所以通常也将Spark称作是基于内存的迭代式运算框架。
3、spark提供了更丰富的算子,让操作更方便。
二、为什么Spark比Map Reduced运算速度快:Spark在计算模型和调度上比MR做了更多的优化,不需要过多地和磁盘交互。
1、Spark计算比MapReduce快的根本原因在于DAG计算模型。DAG相比Hadoop的MapReduce在大多数情况下可以减少shuffle次数。spark遇到宽依赖才会出现shuffer,通常每次MapReduce都会有一次shuffer;DAG 相当于改进版的 MapReduce,可以说是由多个 MapReduce 组成,当数据处理流程中存在多个map和多个Reduce操作混合执行时,MapReduce只能提交多个Job执行,而Spark可以只提交一次application即可完成。
2、MapReduce 每次shuffle 操作后,必须写到磁盘,然后每次计算都需要从磁盘上读书数据,磁盘上的I/O开销比较大。spark的Executor中有一个BlockManager存储模块,会将内存和磁盘共同作为存储设备,当需要多轮迭代计算时,可以将中间结果存储到这个存储模块里,下次需要时,就可以直接读该存储模块里的数据,而不需要读写到HDFS等文件系统里,因而有效减少了IO开销;还有一点就是spark的RDD数据结构,RDD在每次transformation后并不立即执行,而且action后才执行,有进一步减少了I/O操作。
3、MR它必须等map输出的所有数据都写入本地磁盘文件以后,才能启动reduce操作,因为mr要实现默认的根据Key的排序!所以要排序肯定得写完所有数据,才能排序,然后reduce来拉取。但是spark不需要,spark默认情况下,是不会对数据进行排序的。因此shufflemaptask每写入一点数据,resulttask就可以拉取一点数据,然后在本地执行我们定义的聚合函数和算子,进行计算.
4、利用多线程来执行具体的任务(Hadoop MapReduce采用的是进程模型),减少任务的启动和切换开销;
三、spark的RDD与DataFrame以及Dataset的区别:
1、基本数据结构RDD:是弹性分布式数据集。
(1)RDD特点
1)弹性:RDD的每个分区在spark节点上存储时默认是放在内存中的,若内存存储不下,则存储在磁盘中。
2)分布性:每个RDD中的数据可以处在不同的分区中,而分区可以处在不同的节点中.
3)容错性:当一个RDD出现故障时,可以根据RDD之间的依赖关系来重新计算出发生故障的RDD.
(2)RDD与DataFrame以及DataSet的区别
1)RDD
a、具有面向对象的风格,是一组表示数据的Java或Scala对象,编译时类型安全,方便处理非结构化数据。
b、处理结构化数据比较麻烦;默认采用的是java序列号方式,序列化性能开销大,而且数据存储在java堆内存中,导致gc比较频繁
2)DataFrame:
a、是一个按指定列组织的分布式数据集合。类似于表。处理结构化数据方便;可以将数据序列化为二进制格式,数据保存在堆外内存中,可以减少了gc次数。
b、不支持编译时类型安全,若结构未知,则不能操作数据。不具有面向对象风格。
3)DataSet
a、表示行(row)的JVM对象或行对象集合形式的数据,在编译时检查类型安全。方便处理结构化和非结构化数据。采用堆外内存存储,gc友好。
2、spark的算子
(1)transform算子:map转换算子,filter筛选算子,flatmap,groupByKey,reduceByKey,sortByKey,join,cogroup,combinerByKey。
(2)action算子:reduce,collect,count,take,aggregate,countByKey。
transformation是得到一个新的RDD,方式很多,比如从数据源生成一个新的RDD,从RDD生成一个新的RDD,action是得到一个值,或者一个结果(直接将RDDcache到内存中)所有的transformation都是采用的懒策略,就是如果只是将transformation提交是不会执行计算的,计算只有在action被提交的时候才被触发。
(3)map与mapPartitions的区别
1)map是对rdd中的每一个元素进行操作;mapPartitions则是对rdd中的每个分区的迭代器进行操作
2)假如是普通的map,若一个partition中有1万条数据。那么map中的方法要执行和计算1万次。若是MapPartitions,一个task仅仅会执行一次function,此function一次接收所有的partition数据,执行一次即可,性能比较高。SparkSql或DataFrame默认会对程序进行mapPartition的优化。
3)普通的map操作通常不会导致内存的OOM异常,因为可以将已经处理完的1千条数据从内存里面垃圾回收掉。 但是MapPartitions操作,对于大量数据来说,将一个partition的数据一次传入一个function以后,那么可能一下子内存不够,但是又没有办法去腾出内存空间来,可能就OOM,内存溢出。
(4)treeReduce与reduce的区别
1)treeReduce:是在reduce的时候,先在自己的本地节点分区进行本地聚合一下,然后在进行全局聚合,相当于预处理.
2)reduce:是在reduce的时候,没有本地聚合,直接返回给driver端。
(5)coalesce与repartition的区别
1)coalesce 与 repartition 都是对RDD进行重新划分,repartition只是coalesce接口中参数shuffle为true的实现。
2)若coalesce中shuffle为false时,传入的参数大于现有的分区数目,RDD的分区数不变,也就是说不经过shuffle,是无法将RDD的分区数变多的。
3)若存在过多的小任务的时候,可以通过coalesce方法,收缩合并分区,减少分区的个数,减小任务调度成本,尽量避免shuffle,这样会比repartition效率高。
(6)reduceByKey与groupByKey的区别:
pairRdd.reduceByKey(_+_).collect.foreach(println)等价于pairRdd.groupByKey().map(t => (t._1,t._2.sum)).collect.foreach(println)
reduceByKey的结果:(hello,2)(world,3) groupByKey的结果:(hello,(1,1))(world,(1,1,1))
使用reduceByKey()的时候,会对同一个Key所对应的value进行本地聚合,然后再传输到不同节点的节点。而使用groupByKey()的时候,并不进行本地的本地聚合,而是将全部数据传输到不同节点再进行合并,groupByKey()传输速度明显慢于reduceByKey()。虽然groupByKey().map(func)也能实现reduceByKey(func)功能,但是,优先使用reduceByKey(func).
(7)spark的cache和persist的区别:
1)计算流程DAG特别长,服务器需要将整个DAG计算完成得出结果,若计算流程中突然中间算出的数据丢失了,spark又会根据RDD的依赖关系重新计算,这样会浪费时间,为避免浪费时间可以将中间的计算结果通过cache或者persist放到内存或者磁盘中
2)cache最终调用了persist方法,默认的存储级别仅是存储内存中的;persist是最根本的底层函数,有多个存储级别,executor执行时,60%用来缓存RDD,40%用来存放数据.
三、spark的小知识点。
1、DAG叫做有向无环图
原始的RDD通过依赖关系形成了DAG,根据RDD之间依赖类型不同可以将DAG划分成不同的Stage(调度阶段)。对于窄依赖,partition的转换处理在一个Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。
2、spark如何从HDFS中读取数据(参数MR的分片)
Spark从HDFS读入文件的分区数默认等于HDFS文件的块数(blocks),HDFS中的block是分布式存储的最小单元。如果我们上传一个30GB的非压缩的文件到HDFS,HDFS默认的块容量大小128MB,因此该文件在HDFS上会被分为235块(30GB/128MB);Spark读取SparkContext.textFile()读取该文件,默认分区数等于块数即235。
(1)读取文件生成RDD时
1)从本地文件读取生成RDD:rdd的分区数 = max(本地file的分片数, sc.defaultMinPartitions)
2)从HDFS上读取文件生成RDD:rdd的分区数 = max(hdfs文件的block数目, sc.defaultMinPartitions)
(2)通过RDD生成时:
1)分区的默认个数等于spark.default.parallelism的指定值
2)根据父rdd的reduceTask数量
3、spark的checkpoint操作
checkpoint的意思就是建立检查点,类似于快照,若DAG计算流程特别长,则需要将整个DAG计算完成得出结果,但是如果中间计算出的数据出错,spark又会根据RDD的依赖关系重新计算,这样子很费性能;当然我们可以将中间的计算结果通过cache或者persist放到内存或者磁盘中,但是这样也不能保证数据完全不会丢失,存储的这个内存出问题了或者磁盘坏了,也会导致spark从头再根据RDD计算一遍,所以就有了checkpoint,其中checkpoint的作用就是将DAG中比较重要的中间数据做一个检查点将结果存储到一个高可用的地方(通常这个地方就是HDFS里面)
4、spark广播变量和累加器
(1)广播变量:广播变量只能在Driver定义,且在Exector端不可改变。当在Executor端用到了Driver变量而不使用广播变量,那么在每个Executor中有多少task就有多少Driver端变量副本。如果使用广播变量,则在每个Executor端中只有一份Driver端的变量副本,减少了executor端的备份,节省了executor的内存,同时减少了网络传输.
1、广播变量的创建:广播变量的创建发生在Driver端,当调用b=sc.broadcast(URI)来创建广播变量时,会把该变量的数据切分成多个数据块,保存到driver端的BlockManger中,使用的存储级别是:MEMORY_AND_DISK_SER。广播变量的值必须是本地的可序列化的值,不能是RDD。广播变量一旦创建就不应该再修改,这样可以保证所有的worker节点上的值是一致的。
2、广播变量的读取:b.value(),广播变量的读取也是懒加载的,此时广播变量的数据只在Driver端存在,只有在Executor端需要广播变量时才会去加载。加载后,首先从Executor本地的BlockManager中读取广播变量的数据,若存在就直接获取。只要有一个worker节点的Executor从Driver端获取到了广播变量的数据,则其他的Executor就不需要从Driver端获取了。
(2)累加器:Accumulator则可以让多个task共同操作一份变量,主要可以进行累加操作。Accumulator是存在于Driver端的,集群上运行的task进行Accumulator的累加,随后把值发到Driver端,在Driver端汇总。Accumulator只提供了累加的功能,但是却给我们提供了多个task对于同一个变量并行操作的功能,但是task只能对Accumulator进行累加操作,不能读取它的值,只有Driver端可以读取Accumulator的值。
注意:比较经典的应用场景是用来在Spark Streaming应用中记录某些事件的数量。
5、task之间的内存分配:
为了更好地使用使用内存,Executor 内运行的 Task 之间共享着 Execution 内存。
(1)Spark 内部维护了一个 HashMap 用于记录每个 Task 占用的内存。当 Task 需要在 Executor 中申请内存时,先判断 HashMap 里面是否维护着这个 Task 的内存使用情况,如果没有,则将 TaskId 为 key,内存使用量 value为0 加入到 HashMap 里面。
(2)之后为这个 Task 申请 numBytes 内存,如果 Executor 内存区域正好有大于 numBytes 的空闲内存,则在 HashMap 里面将当前 Task 使用的内存加上 numBytes,然后返回;如果当前 Executor 内存区域无法申请到每个 Task 最小可申请的内存,则当前 Task 被阻塞,直到有其他任务释放了足够的执行内存,该任务才可以被唤醒。
(3)每个 Task 可以使用 Execution 内存大小范围为 1/2N ~ 1/N,其中 N 为当前 Executor 内正在运行的 Task 个数。一个 Task 能够运行必须申请到最小内存为 (1/2N * Execution 内存);当 N = 1 的时候,Task 可以使用全部的 Execution 内存。比如如果 Execution 内存大小为 10GB,当前 Executor 内正在运行的 Task 个数为5,则该 Task 可以申请的内存范围为 10 / (2 * 5) ~ 10 / 5,也就是 1GB ~ 2GB的范围。
6、spark与MapReduce的shuffle的区别:
(1)相同点:都是将 mapper(Spark 里是 ShuffleMapTask)的输出进行 partition,不同的 partition 送到不同的 reducer(Spark里reducer 可能是下一个 stage 里的 ShuffleMapTask,也可能是 ResultTask)
(2)不同点:
1)MapReduce默认是排序的,spark默认不排序,除非使用sortByKey算子。
2)MapReduce可以划分成split,map()、spill、merge、shuffle、sort、reduce()等阶段,spark没有明显的阶段划分,只有不同的stage和算子操作。
3)MR落盘,Spark不落盘,spark可以解决mr落盘导致效率低下的问题。
四、spark的运行模式
1、基于yarn运行的基本流程
(1)首先通过spark-submit向yarn提交Application应用,ResouceManager选择一个NodeManager为Application启动ApplicationMaster。
(2)ApplicationMaster向ResouceManager注册和申请Container,ResouceManager收到ApplicationMaster的请求后,使用自己的资源调度算法为applicationMaster分配多个Container。
(3)ApplicationMaster在不同的Container中启动executor,executor启动之后会反向注册到ApplicationMaster;
(4)随后初始化Sparkcontext,Sparkcontext是用户通向spark集群的入口,在初始化sparkContext的同时,会初始化DAGScheduler、TaskScheduler对象。
(5)初始化后的sparkContext对RDD的所有操作形成一个DAG有向无循环图,每执行到action操作就会创建一个job到DAGScheduler中,而job又根据RDD的依赖关系划分成多个stage,每个stage根据最后一个RDD的分区数目来创建相应数量的task,这些task形成一个taskset。
(6)DAGScheduler将taskset送到taskscheduler中,然后taskscheduler对task进行序列化,封装到launchTask中,最后将launchTask发送到指定的executor中。
(7)executor接收到了TaskScheduler发送过来的launchTask 时,会对launchTask 进行反序列化,封装到一个TaskRunner 中,然后从executor线程池中获取一个线程来执行指定的任务.
(8)最终当所有的task任务完成之后,整个application执行完成,关闭sparkContext对象。
2、spark运行模式的类型
(1)本地模式:master和worker分别运行在一台机器的不同进程上,不会启动executor,由SparkSubmit进程生成指定数量的线程数来执行任务,启动多少个线程取决于local的参数:local/只启动一个线程,local[k]启动k个线程,local[*]启动跟CPU数目相同的线程。
(2)standalone模式:standalone模式既独立模式,自带完整服务,可单独部署到一个集群中,无需依赖其他任何资源管理系统,只支持FIFO调度器。在standalone模式中,没有AM和NM的概念,也没有RM的概念,用户节点直接与master打交道,由driver负责向master申请资源,并由driver进行资源的分配和调度等等。
(3)基于yarn模式:yarn-cluster和yarn-client模式,区别在于driver端启动在本地(client),还是在Yarn集群内部的AM中(cluster)
1)yarn-client:Driver是运行在本地客户端,它的AM只是作为一个Executor启动器。负责调度Application,会与yarn集群产生大量的网络传输。好处是,执行时可以在本地看到所有的log,便于调试。所以一般用于测试环境。
2)yarn-cluster:driver运行在NodeManager,每次运行都是随机分配到NM机器上去,不会产生大量的网络传输。缺点就是本地提交后看不到log,只能通过yarn application-logs application id命令来查看,比较麻烦。
五、spark的数据倾斜
1、数据倾斜的现象
(1)大部分的task执行的特别快,剩下的几个task执行的特别慢.
(2)运行一段时间后,其他task都已经执行完成,但是有的task可能会出现OOM异常。
2、数据倾斜的原因及其后果:
(1)根本原因是某个Key所对应的数据特别多,同一个key所对应的数据进入同一个reduce中,而其他的reduce中数据特别少。
(2)后果:某些任务执行特别慢,有的task可能会出现OOM异常,因为task的所分配的数据量太大,而且task每处理一条数据还要创建大量的对象,内存存储不下.
3、如何定位数据倾斜
就是看哪些地方用了会产生shuffle的算子,distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition。
4、解决数据倾斜的方法
(1)若是数据分区太少,导致部分分区中数据量相对较大,产生轻度的数据倾斜,此时增加分区数即可解决。
(2)某个Key特别多,增大分区也无效。
1)数据倾斜的类型。
a、map端的数据倾斜:map端的主要功能是从磁盘中将数据读入内存。在map端读数据时,由于读入数据的文件大小分布不均匀,因此会导致有些map读取和处理的数据特别多,而有些map处理的数据特别少,造成map端长尾。
1.上游表文件的大小特别不均匀,并且小文件特别多(读取的记录数少),导致当前表map端读取的数据分布不均匀,引起长尾。
解决方案:可以合并上游小文件,同时调节本节点的小文件的参数来进行优化。
2.Map端做聚合时,由于某些map读取文件的某个值特别多(某些文件记录数特别多)而引起长尾。
解决方案:来打乱数据分布,使数据尽可能分布均匀。
2)reduce端解决数据倾斜的方法:
a、聚合源数据:在数据的源头将数据聚合成一个key对应多个value值.这样在进行操作时就可能不会出现shuffle过程.
b、将导致数据倾斜的key提取出来,若是key对应的null或者无效数据,就将其删除,若是正常的数据,就将其单独处理,再与正常处理的数据进行union操作.
c、对key添加随机值,操作后去掉随机值,再操作一次。将原始的 key 转化为 key + 随机值(例如Random.nextInt),对数据进行操作后将 key + 随机值 转成 key.
六、Spark中的OOM问题:
1、map类型的算子执行中内存溢出如flatMap,mapPatitions
(1)原因:map端过程产生大量对象导致内存溢出:这种溢出的原因是在单个map中产生了大量的对象导致的针对这种问题。
(2)解决方案:
1)增加堆内内存。
2)在不增加内存的情况下,可以减少每个Task处理数据量,使每个Task产生大量的对象时,Executor的内存也能够装得下。具体做法可以在会产生大量对象的map操作之前调用repartition方法,分区成更小的块传入map。
2、shuffle后内存溢出如join,reduceByKey,repartition。
shuffle内存溢出的情况可以说都是shuffle后,单个文件过大导致的。在shuffle的使用,需要传入一个partitioner,大部分Spark中的shuffle操作,默认的partitioner都是HashPatitioner,默认值是父RDD中最大的分区数.这个参数spark.default.parallelism只对HashPartitioner有效.如果是别的partitioner导致的shuffle内存溢出就需要重写partitioner代码了.
3、driver内存溢出
(1)用户在Dirver端口生成大对象,比如创建了一个大的集合数据结构。解决方案:将大对象转换成Executor端加载,比如调用sc.textfile或者评估大对象占用的内存,增加dirver端的内存
(2)从Executor端收集数据(collect)回Dirver端,建议将driver端对collect回来的数据所作的操作,转换成executor端rdd操作。
七、spark的性能优化
1、参数优化
(1)计算资源的优化:调整--executor-memory和--executor-cores的大小;core表示executor同时计算的task数,memory表示执行的内存,这两个参数过大过小都不合适,内存调大会出现内存瓶颈,内存过小会出现作业失败;core太小导致并行计算度小,计算慢,太大会引起磁盘IO瓶颈。
(2)shuffle并行度优化:shuffleReadTask并行度增大,可以设置spark.sql.shuffle.partitions值来设置并行度。数据能分配到更多的分区,减少数据倾斜默认为200。
(3)设置spark.default.parallelism=600 每个stage的默认task数量。
(4)大小表join:对于两表join,若一张表是另外一张表的2个数量级倍数大,可以考虑将小表broadcast到每一个executor,来达到降低网络传输开销优化目标;进而完全规避掉shuffle类的操作。
2、代码优化:
(1)RDD的优化:避免重复创建RDD即避免创建多个从文件读取而成的RDD,尽量复用RDD,对于多次使用的RDD需要cache或者persist;
3、算子的优化:
(1)尽量避免使用shuffle算子
1)能避免则尽量避免使用reduceByKey,join,distinct,repartition等会进行shuffle的算子
2)Broadcast小数据在map端进行join,避免shuffle
(2)使用高性能算子
1)使用reduceByKey代替groupByKey(reduceByKey在map端聚合数据)
2)使用mappartitions代替map(减少函数重复调用的计算开销)
3)使用treeReduce代替reduce(treeReduce的计算会在executor中进行本地聚合)
4)使用foreachPartitions代替foreach(原理同mapPartitions)
5)使用filter之后使用coalesce操作(目的减少分区数,减少task启动开销)
6)使用Broadcast广播变量
Executor中有一个BlockManager存储模块,会将内存和磁盘共同作为存储设备,当需要多轮迭代计算时,可以将中间结果存储到这个存储模块里,下次需要时,就可以直接读该存储模块里的数据,而不需要读写到HDFS等文件系统里,因而有效减少了IO开销;或者在交互式查询场景下,预先将表缓存到该存储系统上,从而可以提高读写IO性能。
八、spark的内存管理机制:
作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,Spark 对 JVM 的堆内(On-heap)空间做了详细的分配,以充分利用内存。同时,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。
1、堆内内存:堆内内存的大小,由 Spark 应用程序启动时的 –executor-memory参数配置,分别是execution内存,storage内存,other内存。
(1)execution内存是执行内存,文档中说join,map,aggregate都在这部分内存中执行,shuffle的数据也会先缓存在这个内存中,满了再写入磁盘,能够减少磁盘IO。
(2)storage内存是存储broadcast,cache,persist数据的地方。
(3)other内存是程序执行时预留给自己的内存。
2、堆外内存:
Off-heap memory不在 JVM 内申请内存,而是调用 Java 的 unsafe 相关 API (类似于malloc()函数)直接向操作系统申请内存。堆外内存只区分 Execution 内存和 Storage 内存。
(1)优点与缺点:因为堆外内存不进过 JVM 内存管理,所以可以避免频繁的 GC,这种内存申请的缺点是必须自己编写内存申请和释放的逻辑。
(2)作用:为了进一步优化内存的使用以及提高Shuffle时排序的效率,存储经过序列化的二进制数据。
注意:无论堆内和堆外内存目前 Execution 内存和 Storage 内存可以互相共享的。也就是说,如果 Execution 内存不足,而 Storage 内存有空闲,那么 Execution 可以从 Storage 中申请空间;反之亦然.
九、spark如何分区:
分区是RDD内部并行计算的一个计算单元,RDD的数据集在逻辑上被划分为多个分片,每一个分片称为分区,分区的个数决定了并行计算的粒度,而每个分区的数值计算都是在一个任务中进行的,因此任务的个数,也是由RDD(准确来说是作业最后一个RDD)的分区数决定。spark默认分区方式是HashPartitioner.只有Key-Value类型的RDD才有分区的,非Key-Value类型的RDD分区的值是None,每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的。
1、HashPartitioner分区:
partition = key.hashCode () % numPartitions,如果余数小于0,则用余数+分区的个数,最后返回的值就是这个key所属的分区ID。
缺点:可能导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据
2、RangePartitioner分区(范围分区):
通过抽样确定各个Partition的Key范围。首先会对采样的key进行排序,然后计算每个Partition平均包含的Key权重,最后采用平均分配原则来确定各个Partition包含的Key范围。尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大;但是分区内的元素是不能保证顺序的。(计算每个Key所在Partition:当分区范围长度在128以内,使用顺序搜索来确定Key所在的Partition,否则使用二分查找算法来确定Key所在的Partition。)
3、CustomPartitioner自定义分区:
需要继承org.apache.spark.Partitioner类,sc.parallelize(List((1,'a'),(1,'aa'),(2,'b'),(2,'bb'),(3,'c')), 3).partitionBy(new CustomPartitioner(3))
十、sparkSQL
1、sparkSQL执行的流程
SQL语句首先通过Parser模块被解析为语法树,此棵树称为Unresolved Logical Plan;Unresolved Logical Plan通过Analyzer模块借助于Catalog中的表信息解析为Logical Plan;此时,Optimizer再通过各种基于规则的优化策略进行深入优化,得到Optimized Logical Plan;优化后的逻辑执行计划依然是逻辑的,并不能被Spark系统理解,此时需要将此逻辑执行计划转换为Physical Plan。
2、sparkSQL是如何读写hive表的
(1)写到hive表
1)方式一:是利用spark Rdd的API将数据写入hdfs形成hdfs文件,之后再将hdfs文件和hive表做加载映射。
2)方式二:利用sparkSQL将获取的数据Rdd转换成dataFrame,再将dataFrame写成缓存表,最后利用sparkSQL直接插入hive表中。而对于利用sparkSQL写hive表官方有两种常见的API,第一种是利用JavaBean做映射,第二种是利用StructType创建Schema做映射
3、RDDJoin中宽依赖与窄依赖的判断
如果Join之前被调用的RDD是宽依赖(存在shuffle), 而且两个join的RDD的分区数量一致,join结果的rdd分区数量也一样,这个时候join是窄依赖,除此之外的,rdd 的join是宽依赖
十一、SparkStreaming
1、基本概念
(1)处理方式:SparkStreaming实际上处理并不是像Flink一样来一条处理一条数据,而是对接的外部数据流之后,按照一定时间间隔切分,按批处理一个个切分后的文件,与Spark处理离线数据的逻辑是相同的。
(2)Dstream:SparkStreaming提供表示连续数据流和离散流的DStream,假如外部数据不断涌入,按照一分钟切片,每个一分钟内部的数据是连续的(连续数据流),而一分钟与一分钟的切片却是相互独立的(离散流)。Spark的RDD可以理解为空间维度,Dstream的RDD理解为在空间维度上又加了个时间维度。
1)Dstream特点
a、持久化:接收到的数据暂存。目的做容错的,当数据流出错,把数据从源头进行回溯,暂存的数据可以进行恢复。
b、离散化:按时间分片,形成处理单元。
c、分片处理:分批处理。
(3)SparkStreaming 窗口操作:
任何基于窗口操作需要指定两个参数:窗口总长度(window length):你想计算多长时间的数据,滑动时间间隔(slide interval):你每多长时间去更新一次
(3)SparkStreaming的两种处理方式
(1)receiver方式:将数据拉取到executor中做操作,若数据量大,内存存储不下,可以通过WAL,设置了本地存储,保证数据不丢失,然后使用Kafka高级API通过zk来维护偏移量,保证消费数据。receiver消费的数据偏移量是在zk获取的,此方式效率低,容易出现数据丢失。
1)receiver方式的容错性:在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制(Write Ahead Log,WAL)。该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。
2)Kafka中的topic的partition,与Spark中的RDD的partition是没有关系的。在1、KafkaUtils.createStream()中,提高partition的数量,只会增加Receiver方式中读取partition的线程的数量。不会增加Spark处理数据的并行度。 可以创建多个Kafka输入DStream,使用不同的consumer group和topic,来通过多个receiver并行接收数据。
(2)基于Direct方式:使用Kafka底层Api,其消费者直接连接kafka的分区上,因为createDirectStream创建的DirectKafkaInputDStream每个batch所对应的RDD的分区与kafka分区一一对应,但是需要自己维护偏移量,即用即取,不会给内存造成太大的压力,效率高。
1)优点:简化并行读取:如果要读取多个partition,不需要创建多个输入DStream然后对它们进行union操作。Spark会创建跟Kafka partition一样多的RDD partition,并且会并行从Kafka中读取数据。所以在Kafka partition和RDD partition之间,有一个一对一的映射关系。
2)高性能:如果要保证零数据丢失,在基于receiver的方式中,需要开启WAL机制。这种方式其实效率低下,因为数据实际上被复制了两份,Kafka自己本身就有高可靠的机制,会对数据复制一份,而这里又会复制一份到WAL中。而基于direct的方式,不依赖Receiver,不需要开启WAL机制,只要Kafka中作了数据的复制,那么就可以通过Kafka的副本进行恢复。
(3)receiver与和direct的比较:
1)基于receiver的方式,是使用Kafka的高阶API来在ZooKeeper中保存消费过的offset的。这是消费Kafka数据的传统方式。这种方式配合着WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为Spark和ZooKeeper之间可能是不同步的。
2)基于direct的方式,使用kafka的简单api,Spark Streaming自己就负责追踪消费的offset,并保存在checkpoint中。Spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。
3)Receiver方式是通过zookeeper来连接kafka队列,Direct方式是直接连接到kafka的节点上获取数据
(4)sparkStreaming与Kafka的应用
1)kafka到spark streaming如何保证数据完整性?
a、spark RDD内部机制可以保证数据at-least语义。
b、Receiver方式开启WAL(预写日志),将从kafka中接受到的数据写入到日志文件中,所有数据从失败中可恢复。
c、Direct方式 依靠checkpoint机制来保证。 保证数据不重复使用Exactly once语义。
2)kafka到spark streaming如何保证数据不重复消费?
a、幂等操作:重复执行不会产生问题,不需要做额外的工作即可保证数据不重复。
b、业务代码添加事务操作:针对每个partition的数据,产生一个uniqueId,若此partition的所有数据被完全消费,则成功,否则算失效,要回滚。下次重复执行这个uniqueId时,如果已经被执行成功,则skip掉。
(5)sparkStreaming出现数据堆积如何处理
1)spark.streaming.concurrentJobs=10:提高Job并发数,从源码中可以察觉到,这个参数其实是指定了一个线程池的核心线程数而已,没有指定时,默认为1。
2)spark.streaming.kafka.maxRatePerPartition=2000:设置每秒每个分区最大获取日志数,控制处理数据量,保证数据均匀处理。
3)spark.streaming.kafka.maxRetries=50:获取topic分区leaders及其最新offsets时,调大重试次数。