Spark的一些重要的知识点
1.spark的shuffle有几种方式
shuffle过程中分为shuffle write和shuffle read,而且会在不同的stage中进行的
在进行一个key对应的values的聚合时, 首先,上一个stage的每个map task就必须保证将自己处理的当前分区中的数据相同key写入一个分区文件中,可能会多个不同的分区文件,接着下一个stage的reduce task就必须从上一个stage的所有task所在的节点上,将各个task写入的多个分区文件中找到属于自己的分区文件,然后将属于自己的分区数据拉取过来,这样就可以保证每个key对应的所有values都汇聚到一个节点上进行处理和聚合,
2.stage,task和job的区别与划分方式
Application
用户构建在 Spark 上的程序。由集群上的一个 driver 程序和多个 executor 组成。
Worker node
任何在集群中可以运行应用代码的节点。
Executor
一个为了在 worker 节点上的应用而启动的进程,它运行 task 并且将数据保持在内存中或者硬盘存储。每个应用有它自己的 Executor。
Job:一个由多个任务组成的并行计算,当需要执行一个rdd的action的时候,会生成一个job
Stage:每个Job被拆分成更小的被称作stage(阶段)的task(任务)组,stage彼此之间是相互依赖的,各个stage会按照执行顺序依次执行.
Task: 一个将要被发送到Executor中的工作单元,是stage的一个任务执行单元,一般来说,一个rdd有多少个partition,就会有多少个task,因为每一个task知识处理一个partition上的数据
比如:一个Mapreduce程序就是一个Job,而一个Job里面可以有一个或多个Task,
Task又可以区分为MapTask和ReduceTask.一个Application和一个SparkContext相关联,
每个Application中可以有一个或多个Job,可以并行或者串行运行Job。Spark中的一个Action可以触发一个Job的运行。
在Job里面又包含了多个Stage,Stage是以Shuffle进行划分的。在Stage中又包含了多个Task,多个Task构成了Task Set。
Mapreduce中的每个Task分别在自己的进程中运行,当该Task运行完的时候,该进程也就结束了。和Mapreduce不一样的是,Spark中多个Task可以运行在一个进程里面,而且这个进程的生命周期和Application一样,即使没有Job在运行。
Stage的划分过程
Spark Application只有遇到action操作的时候才会真正的提交任务并行计算,DAGScheduler 会根据各个RDD之间的依赖关系形成一个DAG,并根据ShuffleDependency 来进行stage的划分,stage包含多个tasks,个数由该stage的finalRDD决定,stage里面的task完全相同,DAGScheduler 完成stage的划分后基于每个Stage生成TaskSet,并提交给TaskScheduler,TaskScheduler负责具体的task的调度,在Worker节点上启动 task。
stage的划分过程:从最后一个RDD开始,调用递归,从后往前推,找该RDD和父RDD之间的依赖关系,如果是窄依赖,会继续找父RDD的父RDD,如果是宽依赖,就会从该RDD开始到前面所有的RDD划分为一个stage,递归的出口是直到找不到父RDD,最后把所有的RDD划分为一个stage
##3 什么是RDD
RDD(Resilient Distributed Dataset)叫做分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。
4 RDD的五大基本属性
1)A list of partitions 一组分片(Partition),即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。
2)A function for computing each split 一个计算每个分区的函数。Spark中RDD的计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。
3)A list of dependencies on other RDDs RDD之间的依赖关系。RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。
4)Optionally,a Partitioner for key-value RDDs 一个Partitioner,即RDD的分片函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量。
5) Optionally,a list of preferred locations to compute each split 一个列表,存储存取每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
5.flatMap和map的区别
map()是将函数用于RDD中的每个元素,将返回值构成新的RDD。
map的作用是对rdd之中的元素进行逐一进行函数操作映射为另外一个rdd。flatMap的操作是将函数应用于rdd之中的每一个元素,将返回的迭代器的所有内容构成新的rdd。通常用来切分单词。
map函数会对每一条输入进行指定的操作,然后为每一条输入返回一个对象;而flatMap函数则是两个操作的集合——正是“先映射后扁平化”
6.foreach和map的区别
map一个是Transformation
foreach是action
map()是将函数用于RDD中的每个元素,将返回值构成新的RDD。而foreach是我们的方法被传入了迭代器foreach(每个元素遍历执行一次函数)
8.spark任务的提交执行流程
极简版:
1,Driver端向Master发送任务信息
2,Master接收到任务信息后,把任务信息放到一个队列中
3,Master找到比较空闲的Worker,并通知Worker来拿取任务信息
4,Worker向Master拿取任务信息,同时启动Executor子进程
5,Executor启动后,开始向Driver端反向注册
6,Driver开始向向相应Executor发送任务(task)
7,Executor开始执行任务
如上图所示,Spark程序的运行过程与MR程序的运行过程很类似,具体如下(源码简介分析):
-
spark客户端提交应用程序时,启动Cient向YARN的ResourceManager提交应用程序,包括启动ApplicationMaster的命令、提交给ApplicationMaster的程序和需要在Executor中运行的程序等。
-
ResourceManager接收到请求后,在集群中选择一个NodeManager,为该应用程序分配第一个Container,并在这个Container中启动此应用的ApplicationMaster,ApplicationMaster负责SparkContext等的初始化。
-
ApplicationMaster向ResourceManager注册这样用户可以直接通过ResourceManage查看应用程序的运行状态,然后它将采用轮询的方式为各个任务申请资源,并监控它们的运行状态直到运行结束。
-
一旦ApplicationMaster申请到资源(也就是Container)后,便与对应的NodeManager通信,要求它在获得的Container中启动CoarseGrainedExecutorBackend(即Spark的粗粒度执行器后台程序),CoarseGrainedExecutorBackend启动后会向ApplicationMaster中的SparkContext注册并申请任务集。这一点和独立运行模式一样,只不过SparkContext在SparkApplication中初始化时,使用CoarseGrainedSchedulerBackend配合YarnClusterScheduler进行任务的调度,其中YarnClusterScheduler只是对TaskSchedulerImpl的一个简单包装,增加了对Executor的等待逻辑等。
-
剩余的事情就是等待Executor中的Task执行完成了。
源码级分析
Spark任务本质是对我们编写的RDD的依赖关系切分成一个个Stage,将Stage按照分区分批次的生成TaskSet发送到Executor进行任务的执行
Spark任务分两种:
1、shuffleMapTask:shuffle之后的task
2、resultTask:shuffle之前的task
Spark任务分配有两种方式:
1、尽量打散方式(系统默认)
2、尽量集中方式
首先把App打包上传到集群上,并开始分配资源及调用包中的主类
然后
- 在Driver端,调用SparkSubmit类,调用action内部执行submit–>拿到远程代理对象,创建一个线程执行RunMain–>通过反射获取应用程序的入口类找到main方法–>执行主类的main方法,这是提交,
- Driver端构建SparkConf和SparkContext对象,在SparkContext入口类做了三件事,创建了SparkEnv对象(创建了ActorSystem对象)、TaskScheduler(用来生成并发送task给Executor)、DAGScheduler(用来划分Stage)
- ClientActor将任务信息封装到ApplicationDescription对象里并且提交给Master
- Master收到ClientActor提交的任务信息后,把任务信息存到内存中,然后又将任务信息放到队列中(waitingApps)
- 当开始执行这个任务信息时,调用scheduler方法,进行资源调度。
- 将调度好的资源封装到LaunchExecutor并发送给对应的Worker
- Worker接收到Master发送过来的调度信息(LaunchExecutor)后,将信息封装成一个ExecutorRunner对象
- 封装成ExecutorRunner后,调用ExecutorRunner的Start方法,开始启动CoarseGrainedExecutorBackend对象(启动Executor)
- Executor启动后向DriverActor进行反向注册
- 与DriverActor注册成功后,创建一个线程池(ThreadPool),用来执行任务
- 当所有Executor注册完成后,意味着作业环境准备好了,Driver端会结束与SparkContext对象的初始化
- 当Driver初始化完成后(创建了一个sc示例),会持续执行我们自己提交的App的代码,当触发了Action的RDD算子时,就触发了一个job,这时会调用DAGScheduler对象进行Stage划分
- DAGScheduler开始进行Stage划分
- 将划分好的Stage按照分区生成一个一个的task,并且封装到TaskSet对象,然后TaskSet提交到TaskScheduler
- TaskScheduler接收到提交过来的TaskSet,拿到一个序列化器对TaskSet序列化,将序列化好的TaskSet封装到LaunchExecutor并提交到DriverActor
- DriverActor把LaunchExecutor发送到Executor上
- Executor接收到DriverActor发送过来的任务(LaunchExecutor),会将其封装成TaskRunner,然后从线程池中获取线程来执行TaskRunner
- TaskRunner拿到反序列化器,反序列化TaskSet,然后执行App代码,也就是对RDD分区上执行的算子和自定义函数
##9.spark调优的几种方法
####spark.shuffle.file.buffer 默认值是32k
map side task的内存buffer大小,写数据到磁盘文件之前,会先保存在缓冲中,如果内存充足,可以适当加大,从而减少map side磁盘IO次数,提升性能
####spark.reducer.maxSizeInFlight 默认值是48m
reduce task的buffer缓冲,代表了每个reduce task每次能够拉取的map side数据最大大小,如果内存充足,可以考虑加大,从而减少网络传输次数,提升性能
####spark.shuffle.blockTransferService 默认值 netty
shuffle过程中,传输数据的方式,两种选项,netty或nio,spark 1.2开始,默认就是netty,比较简单而且性能较高,spark 1.5开始nio就是过期的了,而且spark 1.6中会去除掉
####spark.shuffle.compress 默认值是 true
是否对map side输出的文件进行压缩,默认是启用压缩的,压缩器是由spark.io.compression.codec属性指定的,默认是snappy压缩器,该压缩器强调的是压缩速度,而不是压缩率
####spark.shuffle.consolidateFiles 默认值是 false
默认为false,如果设置为true,那么就会合并map side输出文件,对于reduce task数量特别的情况下,可以极大减少磁盘IO开销,提升性能
spark.shuffle.io.maxRetries 默认值是 3
网络传输数据过程中,如果出现了网络IO异常,重试拉取数据的次数,默认是3次,对于耗时的shuffle操作,建议加大次数,以避免full gc或者网络不通常导致的数据拉取失败,进而导致task lost,增加shuffle操作的稳定性
spark.shuffle.io.retryWait 默认值是 5 s
每次重试拉取数据的等待间隔,默认是5s,建议加大时长,理由同上,保证shuffle操作的稳定性
spark.shuffle.io.numConnectionsPerPeer 默认值是1
机器之间的可以重用的网络连接,主要用于在大型集群中减小网络连接的建立开销,如果一个集群的机器并不多,可以考虑增加这个值
spark.shuffle.io.preferDirectBufs 默认值是true
启用堆外内存,可以避免shuffle过程的频繁gc,如果堆外内存非常紧张,则可以考虑关闭这个选项
spark.shuffle.manager 默认值是 sort
ShuffleManager,Spark 1.5以后,有三种可选的,hash、sort和tungsten-sort,sort-based ShuffleManager会更高效实用内存,并且避免产生大量的map side磁盘文件,从Spark 1.2开始就是默认的选项,tungsten-sort与sort类似,但是内存性能更高
spark.shuffle.memoryFraction 默认值是 0.2
如果spark.shuffle.spill属性为true,那么该选项生效,代表了executor内存中,用于进行shuffle reduce side聚合的内存比例,默认是20%,如果内存充足,建议调高这个比例,给reduce聚合更多内存,避免内存不足频繁读写磁盘
####spark.shuffle.service.enabled 默认值是 false
启用外部shuffle服务,这个服务会安全地保存shuffle过程中,executor写的磁盘文件,因此executor即使挂掉也不要紧,必须配合spark.dynamicAllocation.enabled属性设置为true,才能生效,而且外部shuffle服务必须进行安装和启动,才能启用这个属性
####spark.shuffle.service.port 默认值是 7337
外部shuffle服务的端口号,具体解释同上
spark.shuffle.sort.bypassMergeThreshold 默认值是200
对于sort-based ShuffleManager,如果没有进行map side聚合,而且reduce task数量少于这个值,那么就不会进行排序,如果你使用sort ShuffleManager,而且不需要排序,那么可以考虑将这个值加大,直到比你指定的所有task数量都打,以避免进行额外的sort,从而提升性能
spark.shuffle.spill 默认值是 true
当reduce side的聚合内存使用量超过了spark.shuffle.memoryFraction指定的比例时,就进行磁盘的溢写操作
spark.shuffle.spill.compress 默认值是true
同上,进行磁盘溢写时,是否进行文件压缩,使用spark.io.compression.codec属性指定的压缩器,默认是snappy,速度优先