一、RDD概述
RDD (Resilient Distributed Dataset):弹性分布式数据集,是Spark中最基本的数据抽象
1.1 RDD的属性
一组分区(partition),即数据集的基本组成单位;
一个计算每个分区的函数;
RDD之间的依赖关系;
一个Partitioner,即RDD的分片函数;
一个列表,存储存取每个Partition的优先位置(preferred location)
1.2 RDD的特点
- 分区
RDD和MapReduce都要支持分区是因为它们处理的是非常大的数据集 - 只读
由一个RDD转换到另一个RDD,可以通过算子实现
RDD的操作算子包括两类:
- transformations:转换算子,将RDD进行转换,构建RDD的血缘关系
- actions:动作算子(立即执行,返回都是结果),用来触发RDD的计算,得到RDD的相关计算结果/将RDD保存到文件系统中
- 依赖
RDD通过操作算子进行转换,所以之间存在依赖
RDD依赖包括两种:
- 窄依赖:RDDs之间分区是一一对应的
- 宽依赖:下游RDD的每个分区与上游RDD(父RDD)的每个分区都有关(多对一的关系)
- 缓存
cache/persist
内存中缓存,内部的优化机制;当RDD重复被使用了,不需要再重新计算,直接从内存中获取使用,加速后期重用 - 容错
spark的容错有两种:
- Lineage:血缘关系,根据血缘关系重新计算,进行容错
- CheckPoint:设置检查点,一般都是文件系统,磁盘IO
二、RDD编程
2.1 编程模型
在Spark中,RDD被表示为对象,通过对象上的方法调用来对RDD进行转换。
经过一系列的transformations定义RDD之后,就可以调用actions触发RDD的计算,action可以是向应用程序返回结果(count,collect等),或是向存储系统保存数据。
在Spark中,只有遇到action,才会执行RDD的计算(即延迟计算)。
RDD的并行度?
一个RDD可以有多个分片,一个分片对应一个task,分片的个数决定并行度;并行度并不是越高越好,还要考虑资源的情况。
2.2 RDD的创建
- 通过本地集合创建RDD
两种函数:parallelize()和makeRDD() - 通过外部数据创建RDD
textfile(" "):传入的是读取路径;hdfs://… 、file://…
/…/…:这种方式分为在集群中执行和在本地执行;如果在集群中,读的是HDFS,本地读的是文件系统
假如传入的path是hdfs://…,分区是由HDFS中文件的block决定的 - 通过其他RDD衍生新的RDD
通过算子操作,RDD是不可变的
2.3 RDD的转化
RDD整体上分为value类型和key-value类型
2.3.1 Value类型
算子类型 | 作用介绍 |
map | 返回一个新的RDD,该RDD由每一个输入元素经过函数转换后组成 |
mapPartitions | 类似于map,但独立地在RDD地每一个分片上运行,因此在类型上为T的RDD上运行时,函数类型必须是Iteraror[T] => Iterator[U] |
mapPatitionsWithIndex | 类似于mapPartitions,但函数带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,函数类型必须是(Int, Iterator[T]) => Iterator[U] |
flatMap | 类似于map,但是每一个输入元素可以被映射为0个过多个输出元素,所以函数应该返回一个序列,而不是单一元素 |
glom | 将每一个分区形成一个数组,形成新的RDD类型是RDD[Array[T]] |
groupBy | 分组,按照传入函数的返回值进行分组,将相同的key对应的值放入一个迭代器 |
filter | 过滤,返回一个新的RDD,该RDD由经过函数计算后返回值为true的输入元素组成 |
sample | 以指定的随机种子随机抽样出数量为fraction的数据 |
distinct([numTasks]) | 对原RDD进行去重后返回一个新的RDD,默认情况下,只有8个并行任务来操作,但是可以传入一个可选的numTasks参数改变它 |
coalesce(numTasks) | 缩减分区数,用于大数据集过滤后,提高小数据集的执行效率 |
repatition(numPartitions) | 根据分区数,重新通过网络随机洗牌所有数据 |
sortBy(func, [ascending], [numTasks]) | 使用func先对数据进行处理,按照处理后的数据比较结果排序,默认正序 |
pipe(command, [envVars]) | 管道,针对每个分区,都执行一个shell脚本,返回输出的RDD;注意:脚本需要放在Worker节点可以访问到的位置 |
- map()和mapPartitions()的区别?
map():每次处理一条数据
mapPartitions():每次处理一个分区的数据,这个分区的数据处理完后,原RDD分区的数据才能释放,可能导致OOM(内存溢出)
当内存空间较大的时候建议使用mapPartitions(),以提高处理效率 - coalesce和repartition的区别?
coalesce:重新分区,可以选择是否进行shuffle过程,由参数shuffle: Boolean = false/true决定
reparation:实际上是调用coalesce,默认是进行shuffle的
2.3.2 双Value类型交互
算子类型 | 作用介绍 |
union(otherDataSet) | 并集;对原RDD和参数RDD求并集后返回一个新的RDD |
subtract(otherDataSet) | 差集;计算差的一种函数,去除两个RDD中相同的元素,不同的RDD将保留下来 |
intersection(otherDataSet) | 交集;对原RDD和参数RDD求交集后返回一个新的RDD |
cartesian(otherDataSet) | 笛卡尔积(尽量避免使用) |
zip(otherDataSet) | 将两个RDD组合成key/value形成的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常 |
2.3.3 Key-Value类型
算子类型 | 作用介绍 |
partitionBy | 对pairRDD进行分区操作,如果原有的partionRDD和现有的partionRDD是一致的话就不进行分区,否则会生成shuffleRDD,即会产生shuffle过程 |
groupByKey | groupByKey也是对每个key进行操作,但只生成一个sequence |
reduceByKey(func, [numTasks]) | 在一个(k, v)的RDD上调用,返回一个(k, v)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,reduce任务的个数可以通过第二个可选参数来设置 |
aggregateByKey | 在kv对的RDD中,按key将value进行分组合并,合并时,将每个value和初始值作为seq函数的参数,进行计算,返回的结果作为一个新的kv对,然后再将结果按照key进行合并,最后将每个分组的value传递给combine函数进行计算(先将前两个value进行计算,将返回结果和下一个value传给combine函数,以此类推),将key与计算结果作为一个新的kv对输出 |
foldByKey | aggregateByKey的简化操作,seqop和combop相同 |
combineByKey[c] | 对相同k,把v合并成一个集合 |
sortByKey([ascending], [numTasks]) | 在一个(k, v)的RDD上调用,k必须实现Ordered接口,返回一个按照key进行排序的(k, v)的RDD |
mapValues | 针对于(k, v)形式的参数类型只对v进行操作 |
join(otherDataset, [numTasks]) | 在类型为(k, v)和(k, w)的RDD上调用,返回一个相同的key对应的所有元素对在一起的(k, (v, w))的RDD |
cogroup(otherDataset, [numTasks]) | 在类型为(k, v)和(k, w)的RDD上调用,返回一个(k, (Iterable< v>, Iterable< w>))类型的RDD |
- reduceByKey和groupByKey的区别?
reduceByKey:按照key进行聚合,在shuffle之前由combine(预聚合)操作,返回结果是RDD[k, v]
groupByKey:按照key进行分组,直接进行shuffle
reduceByKey相比于GroupByKey更建议使用,但需要注意是否会影响业务逻辑 - 使用什么方法可以代替join?
广播变量 + map + filter
2.4 Action
算子类型 | 作用介绍 |
reduce | 通过函数聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据 |
collect | 在驱动程序中,以数组的形式返回数据集的所有元素 |
count | 返回RDD中元素的个数 |
first | 返回RDD中的第一个元素 |
take(n) | 返回一个由RDD的前n个元素组成的数组 |
takeOrdered(n) | 返回该RDD排序后的前n个元素组成的数组 |
aggregate | 将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作;这个函数最终返回的类型不需要和RDD中元素类型一致 |
fold(num) | 折叠操作,aggregate的简化操作,seqop和combop一样 |
countByKey | 针对(k, v)类型的RDD,返回一个(k, Int)的map,表示每一个key对应的元素个数 |
foreach | 在数据集的每一个元素上,运行函数进行更新 |
saveAsTextFile(path) | 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本 |
saveAsSequenceFile(path) | 将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统 |
saveAsObjectFile(path) | 用于将RDD中的元素序列化成对象,存储到文件中 |
宽依赖算子:
所有的ByKey算子;repartition,coalesce算子;部分join算子
2.5 RDD的依赖关系
2.5.1 Lineage
RDD只支持粗粒度转换,即在大量记录上执行的单个操作;将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区;RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。
Lineage:血缘关系,根据血缘关系从新计算,进行容错。
注意:RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)
2.5.2 DAG(Directed Acyclic Graph) 有向无环图
原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage;
对于窄依赖,partition的转换处理在Stage中完成计算;
对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算;
因此宽依赖是划分Stage的依据。
2.5.3 任务划分
RDD 任务切分中间分为:Application、Job、Stage和Task
- Application:初始化一个SparkContext即生成一个Application
- Job:一个Action算子就会生成一个Job
- Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage
DAG如何划分Stage?
会把DAG划分为不同的阶段,划分依据就是看算子有没有shuffle,从最后的一个RDD开始往前面倒推,如果上一个RDD变成这个RDD没有发生shuffle,上一个RDD和这个RDD在一个stage里面,如果上一个RDD变成这个RDD发生了shuffle,那么上一个RDD就在一个新的stage里面,这个stage就结束了;然后新的stage里面,上一个RDD就是这个stage最后一个RDD,然后继续前面的操作,往前面追溯,直到把整个DAG全部追溯完,这个DAG就被划分为了多个stage - Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task
spark程序执行的最小单位,spark集群的worker里面运行的是一个个的task
Task分为shuffleMapTask和resultTask
注意:Application -> Job -> Stage -> Task每一层都是1对n的关系
2.6 RDD的缓存
RDD通过persist方法或cache方法可以将前面的计算结果缓存,默认情况下persist()会把数据以序列化的形式缓存在JVM的堆空间中
但是并不是这两个方法被调用时立即缓存,而是触发后面的action 时,该RDD将会被缓存在计算节点的内存中,并供后面重用
存储级别StorageLevel
末尾加“_2”表示把持久化的数据存为两份
级别 | 使用的空间 | CPU时间 | 是否在内存中 | 是否在磁盘上 |
NONE | / | / | 否 | 否 |
DISK_ONLY | 低 | 高 | 否 | 是 |
MEMORY_ONLY | 高 | 低 | 是 | 否 |
MEMORY_ONLY_SER | 低 | 高 | 是 | 否 |
MEMORY_AND_DISK | 高 | 中等 | 部分 | 部分 |
MEMORY_AND_DISK_SER | 低 | 高 | 部分 | 部分 |
OFF_HEAP | / | / | 是 | 是 |
spark持久化的选择?
如果数据在内存中放不下,则在内存中存放序列化数据,最后选择磁盘
2.7 RDD CheckPoint
设置检查点(本质是通过将RDD写入DISK做检查点)是为了通过Lineage做容错的辅助,Lineage过长会造成容错成本高,所以在中间阶段做检查点容错,这样会减少开销。
在CheckPoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除;对RDD进行CheckPoint操作并不会马上被执行,必须执行Action操作才能触发。
三、键值对RDD数据分区器
Spark目前支持Hash分区和Range分区,用户也可以自定义分区。
Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce的个数。
注意:
- 只有Key-Value类型的RDD才有分区器的,非Key-Value类型的RDD分区器的值是None
- 每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的
Rdd partition的个数由什么来决定的?
- 默认的,两个
- 指定的,numPartitions
- 从hdfs读取数据,由块的个数决定
- 从kafka读取数据,由topic的partition个数决定
3.1 获取RDD分区
获取分区:.partitioner
重新分区:HashPartitioner()
3.2 Hash分区
HashPartitioner分区的原理:对于给定的key,计算其hashCode,并除以分区的个数取余,如果余数小于0,则用余数 + 分区的个数(否则加0),最后返回的值就是这个key所属的分区ID
弊端:可能导致每个分区数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据
3.3 Ranger分区
将一定范围的数据映射到某一个分区内
RangePartitioner作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的
3.4 自定义分区
要实现自定义的分区器,需要继承org.apache.spark.Partitioner类,并实现下面三个方法
- numPartitions: Int:返回创建出来的分区数
- getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)
- equals():Java判断相等性的标准方法
四、数据读取与保存
Spark的数据读取,SparkCore连接MySQL及HBase的数据读取
五、RDD编程进阶
5.1 累加器
累加器用来对信息进行聚合,通常在向Spark传递函数时,如果想实现所有分片处理时更新共享变量的功能,那么累加器可以实现想要的效果
调用SparkContext.accumulator(initialValue)方法
声明累加器:val accu = new LongAccumulator
注:工作节点上的任务不能访问累加器的值;从任务的角度来看,累加器是一个只写变量
对于要在行动操作中使用的累加器,Spark只会把每个任务对各累加器的修改应用一次
因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,必须把它放在foreach()这样的行动操作中
转化操作中累加器可能会发生不止一次更新
累加器的特点
- 累加器在全局唯一的,只增不减,记录全局集群的唯一状态
- 在exe中修改它,在driver读取
- executor级别共享的,广播变量是task级别的共享
- 两个application不可以共享累加器,但是同一个app不同的job可以共享
5.2 自定义累加器
自定义累加器(含代码)
5.3 广播变量(调优策略)
广播变量用来高效分发较大的对象
向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用
在多个并行操作中使用同一个变量,但是Spark会为每个任务分别发送
将变量(num)广播出去:val bc: Broadcast[Int] = sc.broadcast(num)
- 在算子中使用广播变量代替直接引用集合,只会复制executor一样的数量
- 在使用广播之前,赋值map了task数量份
- 在使用广播以后,赋值次数和executor数量一致
六、spark的提交流程
- 打包程序 xxx.jar,上传到某个节点上;
- 执行一个SparkSubmit,在SparkSubmit里会写各种配置信息,包括–master、需要的cpu、内存等;
- 以Client为例,会在提交的节点上启动一个Driver(就是Application)进程;
- 创建SparkContext对象,会在内部创建DAGScheduler和TaskScheduler;
- 在Driver代码中,如果遇到了Action算子,就会创建一个Job;
在Spark中,有多少个Action,就会产生多少个Job - DAGScheduler会接收Job,会为这个job生成DAG;
- 把DAG划分为Stage;
- 把Stage里面的Task切分出来,生成TaskSet;
- 接收TaskSet后,调度Task;
- TaskScheduler里会有一个后台程序,去专门连接Master,向Master注册(就是告诉Master是什么程序,需要什么资源(cpu、内存));
- Master接收到Dirver端的注册;
结合需要的资源和本身空闲的资源,利用资源调度算法来决定在哪些Worker上运行这个Application算法,有两种策略:
- 尽量打散:尽量让需要的资源平均的在不同的机器上启动
- 尽量集中:尽量在某一台或某几台机器上启动
- Master通知Worker去启动Executor;
- Worker启动Executor(需要运行的cpu和内存);
- Executor反向注册Driver里面的TaskScheduler;
- TaskScheduler接收Executor的反向注册Task的分配算法,把TaskSet里面的Task分配给executor;
接收到的是一个序列化的文件,先反序列化拷贝等,生成Task,Task里面由RDD的执行算子,一些方法需要的常量;
Executor接收到很多Task,每接收到一个Task都会从线程池里面获取一个线程,用TaskRunner来执行Task
Task分为两种:ShuffleMapTask和ResultTask,最后的一个Stage对应的Task是ResultTask,之前所有的Stage对应的Task都是ShuffleMapTask;如果Spark程序执行的是ShuffleMapTask,那么程序在执行完这个Stage之后,还需要继续执行下一个Stage - Spark程序就是Stage被切分为很多Task,封装到TaskSet里面,提交给Executor执行,一个Stage一个Stage的执行,每个Task对应一个RDD的partition,这个Task执行的就是所写的算子操作,最后直到最后一个Stage执行完毕。
七、扩展
7.1 spark的优化?
- 避免创建重复的RDD
- 尽可能使用同一个RDD
- 对多次使用的RDD进行持久化
- 尽量避免使用shuffle类算子
- 使用map-side预聚合的shuffle操作
- 使用高性能的算子
- 广播大变量
- 使用kryo优化序列化性能
- 优化数据结构
对象、字符串、集合都比较占用内存
字符串代替对象,数组代替集合,使用原始类型(比如Int、Long)代替字符串 - 资源调优
- 数据倾斜调优
map filter
7.2 excutor内存的分配?
内存会被分为几个部分:
- 第一块是让task执行自己编写的代码时使用,默认是占Executor总内存的20%;
- 第二块是让task通过shuffle过程拉取了上一个stage的task的输出后,进行聚合等操作时使用,默认也是占Executor总内存的20%;
spark.shuffle.memoryFraction
,用来调节executor中,进行数据shuffle所占用的内存大小默认是0.2 - 第三块是让RDD持久化时使用,默认占Executor总内存的60%。
spark.storage.memoryFraction
,用来调节executor中,进行数据持久化所占用的内存大小,默认是0.6
7.3 节点和task 执行的关系?
- 每个节点可以起一个或多个Executor
- 每个Executor由若干core组成,每个Executor的每个core一次只能执行一个Task
- 每个Task执行的结果就是生成了目标RDD的一个partiton
注意:
这里的core是虚拟的core而不是机器的物理CPU核,可以理解为就是Executor的一个工作线程
而Task被执行的并发度 = Executor数目 * 每个Executor核数
7.4 spark的序列化?
- Java序列化
在默认情况下,Spark采用Java的ObjectOutputStream序列化一个对象,该方式适用于所有实现了java.io.Serializable的类。
通过继承 java.io.Externalizable,能进一步控制序列化的性能。
Java序列化非常灵活,但是速度较慢,在某些情况下序列化的结果也比较大。 - Kryo序列化
Spark也能使用Kryo(版本2)序列化对象。
Kryo不但速度极快,而且产生的结果更为紧凑(通常能提高10倍)。
Kryo的缺点是不支持所有类型,为了更好的性能,需要提前注册程序中所使用的类(class)。 - 序列化的作用
将对象或者其他数据结构转换成二进制流,便于传输,后续再使用反序列化将其还原;因为二进制流是最便于网络传输的数据格式。
序列化可以减少数据的体积,减少存储空间,高效存储和传输数据;不好的是使用的时候要反序列化,非常消耗CPU。
7.5 Spark中数据倾斜引发原因?
- key本身分布不均衡
- 计算方式有误
- 过多的数据在一个task里面
- shuffle并行度不够
7.6 持久化和容错的应用场景?
- RDD数据持久化的应用场景
某个RDD数据被多次使用,即重复RDD
某个RDD数据来之不易(经过复杂的处理得到的RDD),使用超过1次 - RDD数据通常选择的持久化策略
MEMORY_ONLY_2
MEMORY_AND_DISK_SER_2 - 容错的应用场景(为什么需要容错)
节点挂点,数据丢失的时候需要容错机制,恢复分区,找回丢失的数据
7.7 hadoop和spark的shuffle过程?
hadoop:map端保存分片数据,通过网络收集到reduce端。
spark:spark的shuffle是在DAGSchedular划分Stage的时候产生的,TaskSchedule要分发Stage到各个worker的executor。
减少shuffle可以提高性能。
Spark Shuffle的优化