Spark学习笔记2
本次学习还是为了实现之前搁置了很久的项目:网站日志流量分析系统,之前使用Docker搭建了基础环境:使用Docker搭建Spark集群(用于实现网站流量实时分析模块),这次再补补Spark的理论基础,再编写Scala代码实现网站流量实时分析
1、Spark架构
①Driver Program:用户编写的驱动程序,每个Driver中都会有SparkContext对象,sc是和spark集群交互的入口对象
②SparkContext对象:每一个Driver Program里都有一个SparkContext对象。
1)负责当前Driver的任务调度、分配以及任务的失败恢复
2)向Master申请资源用于启动任务
③cluster manager 集群管理器(Master)
Spark集群的资源管理者,专门用于管理和分配Spark集群的资源(CPU、内存等)
④Worker Node
Worker节点,集群上的计算节点,对应一台物理机器
⑤Worker进程
它对应Worker进程,用于和Master进程交互,向Master注册和回报自身节点的使用情况并管理和启动Executor进程
⑥Executor
JVM进程,用于启动和运行Task任务,并将计算结果回传到Driver中
⑦Task
在执行器上的最小单元。比如RDD Transformation操作时对RDD每个分区的计算都会对应一个Task。
注:
①Master====>ResourceManager(类比)
②SparkContext=====>ApplicationMaster
Spark将资源管理和任务管理分开,这样设计可以让资源管理被替换(YARN或Mesos),任务管理器不可替换一直都是SparkContext
2、Spark底层工作细节
3、Spark调度模块
1、Driver程序的sc负责和Executor交互,完成任务的分配和调度。
在底层,任务调度模块主要包含2大部分:
1)DAGScheduler
2)TaskScheDuler
它们负责将用户提交的计算任务按照DAG划分为不同的阶段并且将不同阶段的计算任务提交到集群进行最终的计算。
RDD Objects可以理解为用户实际代码中创建RDD,这些代码逻辑上组成一个DAG
DAGScheduler主要负责分析依赖关系,然后将DAG划分为不同的Stage(阶段),其中每个Stage由可以并发执行的一组Task构成,这些Task的逻辑完全相同,只是作用于不同的数据。
在DAGScheduler将这组Task划分完成后,会将这组Task提交到TaskScheduler。TaskScheduler通过Cluster Manager 申请计算资源,比如在集群中的某个Worker Node启动专属的Executor,并分配CPU、内存等资源。接下来就是在Executor中运行Task任务,如果缓存中没有计算结果,那么就需要开始计算,同时计算结果会回传到Driver或保存在本地。
根据以上描述可以表示如下图:
2、Scheduler的实现概述
①org.apache.spark.scheduler.DAGScheduler:将一个DAG划分为一个一个的Stage阶段(每一个Stage是一组Task集合),然后将Task集合交给TaskScheduler模块
②org.apache.spark.scheduler.TaskScheduler:为创建它的SparkContext调度任务,即从DAGScheduler接受不同的Stage的任务。向Cluster Manager申请资源,然后Cluster Manager收到请求资源后,在Worker Node节点为其启动进程
③org.apache.spark.scheduler.SchedulerBackend:是一个trait,作用是分配当前可用的资源,具体指:向当前等待分配资源的Task分配计算资源(Executor),并且在分配的Executor上启动Task,完成计算的调度过程。
④AKKA:是一个网络通信框架,类似于Netty,此框架在Spark在1.8之后全部替换为Netty
3、任务调度流程图
4、Spark-Shuffle
1、Shuffle定义
Shuffle中文意思就是洗牌,之所以需要Shuffle,还是因为具有某种共同特征的一类数据需要最终汇聚到一个计算节点上进行计算。
这些数据重新打乱,然后汇聚到不同节点的过程就是Shuffle。实际上,Shuffle的过程可能会非常复杂:
①数据量大,比如单位为TB甚至PB的数据分散到几百、几千甚至数万台机器
②为了将这个数据汇聚到正确的节点,需要将这些数据放入正确的Partition,可能会发生多次溢写。
③为了节省带宽,这个数据可能需要压缩(如何在压缩率和压缩解压时间中做好一个选择?)
④数据需要通过网络传输,因此数据的序列化和反序列化也变得相对复杂
一般来说,每个Task处理的数据完全可以载入内存(如果不能,可以减少每个partition的大小),因此Task的计算可以做到在内存中计算。但是对于Shuffle来说,如果不持久化这个中间结果,一旦数据丢失,就需要重新计算依赖的全部RDD,因此有必要持久化这个中间结果,所以这就是为什么Shuffle过程会产生文件的原因。如果Shuffle不落地,①可能造成内存溢出②当某分区丢失时,会重新计算所有父分区数据
2、Shuffle 管理器
目前Spark有2种Shuffle Manager:①Hash Based Shuffle Write ②Sort Based Shuffle Manager
2.1Hash Based Shuffle Manager
上图演示的是Spark的Hash Base Shuffle管理器,可以类比于Hadoop的MapReduce的Shuffle,有一处改进:没有排序过程,所以处理速度更快。但是这样的弊端:Shuffle的临时文件数量=MapTask数量*ReduceTask数,所以当上下游Task数量增多时,文件会变得特别多,影响性能
①当文件数过多,同时打开这些文件需要耗费很多内存
②当文件数过多,而且都是一些小文件,即带来大量的磁盘的随机I/O,特别慢
每个Shuffle Map Task根据key的哈希值,计算出每个key需要写入的partition,然后将数据单独写入一个文件,这个partition实际上就对应了一个Shuffle的Map Task或Reduce Task,因此,下游的Task在计算时会通过网络(如果该Task与上游的Shuffle Map Task运行在同一个节点上,那么此时就是一个本地的磁盘读写) 读取这个文件并进行计算。
2.2Sort Based Shuffle Manager
基于Hash Based Shuffle Manager的缺点(临时文件过多),引出了新一代的Sort Based Shuffle管理器,Shuffle临时文件数=MapTask数量,极大地减少了临时文件数,目前Spark的底层默认使用的就是Sort Based Shuffle管理器(Sort指的是对分区的编号排序,分区内的数据不排序)
每个Shuffle Map Task不会为每个Reducer生成一个单独的文件;相反,它会将所有的结果写到一个文件里,同事会生成一个Index文件。Reducer可以通过这个Index文件取得它需要处理的数据。避免产生大量文件的直接收益就是节省了内存和磁盘顺序写I/O带来的低延时。节省内存的使用可以减少GC的风险和频率,而减少文件的数量可以避免同时写多个文件给系统带来的压力。
Shuffle Map Task会按照key相对应的Partition ID进行Sort,其中属于同一个Partition的key不会排序
3、Shuffle相关参数配置
①spark.shuffle.manager
spark.shuffle.manager=hash
spark.shuffle.manager=sort(默认)
应用场景:当产生的临时文件数量不是很多时,hash Shuffle Manager的性能可能会Sort Shuffle要好;对于不需要进行排序且Shuffle产生的文件数量不是特别多时,Hash Based Shuffle可能是更好的选择,因为Sort Based Shuffle会按照Reducer的Partition进行排序
②spark.shuffle.spill
这个参数的默认值是true,用于指定Shuffle过程中如果内存中的数据超过阈值(spark.shuffle.memoryFraction)时是否需要将部分数据写入外部存储。如果设置为false,那么整个过程就会使用内存,会有内存溢出的风险,因此只有确定内存足够使用时,才可以将选项设置为false。
③spark.shuffle.memoryFraction
在启用spark.shuffle.spill的情况下,spark.shuffle.memoryFraction决定了当Shuffle过程中使用的内存到总内存的比例的时候开始spill,在spark1.2.0中,这个值是0.2
此参数可以适当调整,可以控制在0.4~0.6
通过此参数可以设置Shuffle过程中占用内存的大小,可以适当调大此值,可以减少磁盘I/O次数
④spark.shuffle.blockTransferService
Shuffle过程中数据通信框架,在spark1.2之后默认值为netty,之前都是nio。它主要用于各个Executor之间传输Shuffle数据
⑤spark.shuffle.consolidateFiles
默认值为false(即默认不开启)开启之后,对于同一个核上MapTask共用一个临时文件,所以开启后,临时文件数=Core数*ResultTask数
注:此机制不稳定,不建议在生产环境下使用。
⑥spark.shuffle.compress和spark.shuffle.spill.compress
这两个参数默认都为true,都是用来设置Shuffle过程中是否对Shuffle数据进行压缩。其中,前者是针对最终写入本地文件系统的输出文件;后者针对在处理过程需要写入到外部存储的中间数据(即针对最终的Shuffle输出文件)
⑦spark.reducer.maxMblnFlight
这个参数用于限制一个ResultTask向其他Executor请求Shuffle数据时所占的最大内存数,默认为64MB。
5、Spark调优
1、更好的序列化实现
Spark用到序列化的地方:
①Shuffle时需要将对象写入到外部的临时文件
②每个Partition中的数据要发送到worker上,Spark先把RDD包装成Task对象,将Task通过网络发给worker
③RDD如果支持内存+硬盘,只要往硬盘中写数据也会涉及序列化
Spark默认采用的是Java的序列化,但是java序列化的性能相对较低,另外它序列化完二进制的内容长度也比较大,造成网络传输时间比较长。业界有更好的实现如kryo,比Java的序列化快10倍以上。
3种使用方法如下:
①修改spark-defaults.conf配置文件
spark.serializer org.apache.spark.serializer.KryoSerializer #用空格隔开
②启动spark-shell或spark-submit时配置
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer
③代码中设置
val conf = new SparkConf() conf.set(“spark.serializer”,“org.apache.spark.serializer.KryoSerializer”)
注:3种方式实现效果相同,优先级越来越高
2、配置多临时文件目录
spark.local.dir参数。当Shuffle、归并排序(sort、merge)时都会产生临时文件。这些临时文件都在这个指定的目录下。那这个文件夹有很多临时文件,如果都发生读写操作,有的线程在读这个文件,有的线程在往这个文件里写,磁盘I/O性能就非常低。此时可以创建多个文件夹,每个文件夹对应一个真实的硬盘。假如原来是3个程序同时读写一个硬盘,效率肯定低,现在让三个程序分别读取这个3个磁盘,这样冲突减少,效率就提高了。
配置方式:spark.local.dir=/home/tmp,/home/tmp2 #中间逗号分隔
3、启用推测执行机制
spark.speculation参数,设置为true
开启后,spark会检测执行比较慢的Task,并复制这个Task在其他节点运行,最后哪个节点先运行完,就用其结果,然后将慢Task杀死。
4、collect速度慢
collect只适合在测试时,因为把结果都手机到Driver服务器上,数据要跨网络传输,同事要求Driver服务器内存大,所以收集过程慢。解决办法就是直接输出到分布式文件系统中。
5、RDD-MapPartitions代替map
map方法对RDD的每一条记录逐一操作。mapPartitions是对RDD里的每个分区操作
rdd.map{x => conn=getDBConn.conn;write(x.toString);conn close;},这样频繁的链接、断开数据库,效率差。
rdd.mapPartitions{(record:=>conn.getDBConn.conn;for(item <-recorders;write(item.toString);conn.close;))},这样就一次的链接,一次的断开,中间的批量操作,效率提升。
Spark优化总结:
6、CheckPoint机制
7、Spark共享变量
Spark程序的大部分操作都是RDD操作,通过传入函数给RDD操作函数来计算。这些函数在不同的操作节点上并发执行,但每个内部的变量有不同的作用域,不能相互访问,所以有时会不太方便。Spark提供了两类共享变量供编程使用:广播变量和计数器
(1)广播变量
这是一个只读对象,在所有节点上缓存一份,创建方法:SparkContext.broadcast(),例:
scala>val broadcastVar=sc.broadcast(Array(1,2,3)) broadcastVar:org.apache.spark.broadcast[Array[Int]]=Broadcast(0) scala>broadcastVar.value res0:Array[Int]=Array(1,2,3) #广播变量是只读的,所以创建之后再更新它的值是无意义的,一般用val修饰符来修饰广播变量
(2)计数器
计数器只能增加,是共享变量,用于计数或求和
计数器变量的创建方法是SparkContext.accumulator(v,name),其中v是初始值,name是名称
scala>val accum = sc.accumulator(0,"my accumulator") accum:org.apache.spark.Accumulator[Int]=0 scala>sc.paralize(Array(1,2,3,4)).foreach(x=>accum+=x) scala>accum.value res1:Int=10
8、数据倾斜问题
在利用MapReduce做Join操作时,经常会出现数据倾斜问题,产生的主要原因来自于业务,比如Mapper输出的是热销商品的pid,这样会造成某个join操作收到的数据特别多。
如何解决数据倾斜问题,不同的框架有不同的处理方案:
①如果是MR框架的话,则可以使用DistributedCache(Hadoop内置的分布式缓存机制),DistributedCache是一个提供给Map/Reduce框架的工具,用来缓存指定的文件,当我们使用了这个机制后,MR框架底层会将指定的文件拷贝至slave节点上的缓存中。使用DistributedCache机制,尤其是在做join操作,可以大大提高作业的运行效率,并且可以避免产生数据倾斜。
实现思路:将join操作中的小表进行缓存,这样每个Map Task在执行时,都是可以在Map Task所在节点的缓冲区拿到小表数据,从而在Map阶段就可以完成join操作。这样一来,就不需要引入Reducer组件,也就不会产生数据倾斜问题。图解如下,代码实现:Map Side Join代码实现:https://github.com/Simple-Coder/map-side-join
②Spark框架的话,也可以利用上述思路,基于广播变量来解决数据倾斜,当然还有其他解决方案,画图说明如下:
③GroupBy引起的数据倾斜,例如wordcount单词计数中,解决数据倾斜的思路:在分组key后边拼随机数字,先达到随机分区的效果,然后再将随机数字去掉,统计最终的结果。这样的处理方式可以降低数据倾斜的影响,图解说明如下:
sc.textFile(path,2) .flatMap{_.split( )} .map{(_+|+random,1)} .reduceByKey{_+_} .map{(_.split(|)[0],1)} .reduceByKey{_+_}
WordCount的数据倾斜本质上是由Group By操作引起的倾斜,解决方案:在分组key后面拼上随机数字,先到随机分区的效果,再将随机数字去掉,统计最终的结果。这种处理方式可以降低数据倾斜 的影响。
9、Spark SQL
Spark为结构化数据处理引入了一个称为SparkSQL的编程模块。它提供了一个称为DataFrame(数据框)的编程抽象,DF的底层依然是RDD,并且可以充当分布式SQL查询引擎。
1)引入了新的RDD类型--SchemaRDD,可以像传统数据库定义表一样来定义SchemaRDD
2)在应用程序中可以混合使用不同来源的数据,如可以将来自HQL的数据和来自SQL的数据进行Join操作
3)内嵌了查询优化框架,在把SQL解析成逻辑执行计划之后,最后变成RDD的运算
1、SparkSQL性能提升如此大的原因?
(1)内存列存储
①海量数据查询时,不存在冗余问题,如果是基于行存储,查询时会产生冗余列,消除冗余列一般在内存中进行的,或者基于行存储的查询,实现物化索引(建立B-Tree,B+Tree),但是物化索引也是需要耗费CPU的。
②基于列存储,每一列的数据类型都是同质的。好处:可以避免数据在内存中类型的频繁转换;可以采用更高效的压缩算法,比如增量压缩算法,二进制压缩算法。例如:男 女 男 女 0101
SparkSQL的表数据在内存中存储不是采用原生态的JVM对象存储方式,而是采用内存列存储,如下图所示:
总结:
具体笔记:SparkSQL个人记录
10、SparkStreaming
SparkStreaming是一种构建在Spark上的实时计算框架,它扩展了Spark处理大规模流式数据的能力,以吞吐量高和容错能力著称。
此模块用于实现Spark对于数据的实时处理,即随着数据流的实时到达进行计算。所以对于计算框架要求较高,需要在秒级甚至毫米级,所以之前的MapReduce计算框架并不适合做实时处理,因为它的延迟在分钟级别。目前做实时处理的计算框架:
①Storm
②SparkStreaming
③Flink
SparkStreaming会将源数据指定的批大小离散化成一批一批的数据来进行处理,每一批数据封装成DStream,即SparkStreaming处理的就是一个一个的DStream,而DStream底层就是RDD。
具体笔记:SparkStreaming个人记录