Sparkcore学习(二)
RDD创建
根据本地创建
- makeRDD: 底层就是使用的parallelize
- parallelize
读取文件创建
- 根据读取文件创建RDD
- spark读取文件的方式:
- 如果集群配置文件中有配置
- HADOOP_CONF_DIR配置,此时默认读取是HDFS文件 【公司一般有配置HADOOP_CONF_DIR】
- 读取HDFS文件
- sc.textFile("/../...") [默认情况]
- sc.textFile("hdfs:///../...")
- sc.textFile("hdfs://namenode ip:端口/../..")
- 读取本地文件: sc.textFile("file:///../..")
- 读取HDFS文件
- HADOOP_CONF_DIR配置,此时默认读取是HDFS文件 【公司一般有配置HADOOP_CONF_DIR】
- 如果集群配置文件中没有配置HADOOP_CONF_DIR配置,此时默认读取是本地文件
- 读取HDFS文件
- sc.textFile("hdfs://namenode ip:端口/../..")
- 读取本地文件:
- sc.textFile("file:///../..")
- sc.textFile("/../...") [默认情况]
- 读取HDFS文件
- 如果集群配置文件中有配置
- spark读取文件的方式:
其他rdd衍生
通过其他RDD(计算逻辑)推导出的
拓展
- -–master yarn 状态下读取本地文件时报错,时因为任务是集群上运行的,可能在某台机器上可以读取到文件,而有些机器没法督导文件,因此报错。
RDD分区数
通过集合创建的RDD分区数
- parallelize方法numSlices参数有设置, 分区数 = numSlices的值
- parallelize方法numSlices参数没有设置, 分区数 = defaultParallelism
- defaultParallelism的值分为两种情况:
- 有配置spark.default.parallelism参数,分区数 = spark.default.parallelism参数值
- 没有配置spark.default.parallelism参数,
- master=local, 分区数 = 1
- master=local[N], 分区数 = N
- maser=local[*] ,分区数 = cpu个数
- master=spark://.../.. ,分区数 = math.max( 本次任务的所有executor cpu总个数 , 2 )
- defaultParallelism的值分为两种情况:
根据文件创建的RDD的分区数,
分区数大于等于MinPartitions
- minPartitions默认 = math.min(defaultParallelism, 2)
- 分区数最终是多少个由文件的切片决定。
通过其他RDD衍生的RDD
根据其他rdd衍生出新RDD的分区数 = 依赖的第一个rdd的分区数,即和父RDD一致
算子
- spark rdd算子分为两大类:
- 转换算子[Transformation]: 不会触发任务的计算,只是封装数据的计算过程,转换算子的结果类型是RDD
- 行动算子[Action]: 会触发任务的计算,行动算子的结果不是RDD类型
Transformation转换算子
value类型
map* * * *
- 语法:map(func: RDD元素类型=> B ): 一对一映射
- map里面的方法是针对RDD每个元素操作,返回一个结果,rdd有多少元素,函数就调用多少次
- map生成的新的RDD中元素个数 = 原RDD元素个数
- map的应用场景: 对数据进行值/类型转换,即数据类型转换/值转换[一对一]
- spark map算子是针对每个分区并行计算
- 总的来讲和Scala中类似
mapPartitions* * * *
- 语法:mapPartitions(func: Iterator[RDD元素类型]=>Iterator[B]): 一对一映射[ 一个分区经过mapPartitions里面的函数计算之后返回一个新的分区 ]
- mapPartitions里面的函数是针对RDD每个分区操作,rdd有多少分区,函数就调用多少次
- mapPartitions生成新的RDD,新RDD的分区的元素是由函数返回的,因此新RDD元素个数不一定等于原RDD元素个数
- mapPartitions的应用场景: 一般用于从外部查询数据[mysql],减少资源链接创建与销毁的次数
map与mapPartitions的区别:
- 函数的参数针对的对象不一样
- map里面的函数是针对RDD每个元素操作,元素有多少个,函数就调用多少次
- mapPartitions里面的函数是针对RDD每个分区操作,分区有多少个,函数就调用多少次
- 函数返回值不一样
- map里面的函数是针对原RDD每个元素操作,操作完成之后返回一个新的元素
- mapPartitions里面的函数是针对RDD每个分区所有数据的的迭代器操作,操作完成之后返回一个新的迭代器作为新RDD一个分区的所有数据
- 内存回收的时机不一样
- map里面的函数是针对原RDD每个元素操作,元素操作完成之后就可以进行垃圾回收
- mapPartitions里面的函数是针对RDD每个分区所有数据的的迭代器操作,此时单个元素操作完之后不能立即回收,必须等到该迭代器里面的元素全部操作完成之后才能回收,所以如果RDD分区数据特别多,可能出现内存溢出,出现内存溢出可以用map代替。
mapPartitionsWithIndex* * * * * * * *
- 语法 :mapPartitionsWithIndex( func: ( Int, Iterator[RDD元素类型] ) => Iterator[B] )
- mapPartitionsWithIndex里面的函数第一个参数是分区号,第二个参数对应该分区数据的迭代器
- mapPartitionsWithIndex与mapPartitions的区别:
- mapPartitions的函数只有一个参数,参数是就是分区数据的迭代器
- mapPartitionsWithIndex的函数相比mapPartitions里面的函数多了一个分区号
flatmap* * * *
- 语法:flatMap(func: RDD元素类型=>集合 ) = map + flatten
- flatMap里面的函数是针对RDD每个元素操作,元素有多少个,函数就调用多少次
- flatMap的应用场景: 一对多[一个元素返回一个或者多个结果]
glom
- glom: 将每个分区所有数据用数组封装起来
- glom生成的新的RDD的元素类型是Array[原RDD元素类型]
- glom生成的新的RDD的元素个数 = 原RDD分区数
groupby* * * *
- groupBy(func: RDD元素类型=>K): 按照指定字段分组
- groupBy里面的函数是针对RDD每个元素操作
- groupBy后续是按照函数的返回值进行分组
- groupBy生成的新的RDD中的元素类型是KV键值对
- K: 就是函数的返回值
- V:是K在原RDD中对应的所有元素的集合
- groupBy会产生shuffle操作
- spark shuffle阶段: 写入缓冲区[分组 [排序] ] -> [combiner] ->溢写 -> 合并小文件 -> 分区拉取数据 -> 归并[排序]
- 复习拓展:
- MR的执行过程: 数据->InputFormat->map->写入环形缓冲区[80% 分组排序] -> [combiner] ->溢写 -> 合并小文件 -> reduce拉取数据 -> 归并排序 -> reduce -> outputformat -> 磁盘
- MR shuffle阶段: 写入环形缓冲区[80% 分组排序] -> [combiner] ->溢写 -> 合并小文件 -> reduce拉取数据 -> 归并排序
filter* * * *
- 语法filter(func: RDD元素类型=>Boolean ): 按照指定条件过滤
- filter中的函数是针对RDD每个元素操作,元素有多少个,函数就调用多少次
- filter保留的是函数返回值为true的数据
sample* * * * * * * *
- sample(withReplacement,fraction[,seed]): 采样
- withReplacement: 同一个元素是否可以被多次采样 <工作中一般设置为false>
- fraction
- withReplacement=false, fraction代表每个元素被采样的概率[0,1] <工作中一般设置为0.1-0.2左右>
- withReplacement=true, fraction代表每个元素期望被采样的次数[>0]
- 应用场景:sample在工作中一般只用于数据倾斜场景。 通过采样的数据映射整个数据集的情况
出现数据倾斜之后,一般先通过sample采样数据,再统计采样数据中每个key的个数从而得知哪个key出现了数据倾斜(后续采样之后可以通过countByKey这个action算子统计采样结果集中每个key出现了多少次,从而判断哪个key出现了数据倾斜)
distinct* * * *
- distinct会产生shuffle操作
- 数据分布在不同块上,需要进行shuffle过程
- 需要将相同数据聚到一个分区中才能进行去重,不然会导致结果不准确
coalesce* * * *
- coalesce: 合并分区
- coalesce: 默认只能合并分区,此时不会产生shuffle操作
- 如果想要增大分区数,此时需要设置coalesce的shuffle参数=true,此时会使用shuffle重分区
- 如果后续想要减少分区数,此时推荐使用coalesce,可以避免shuffle操作。
repartition* * * *
- repartition: 重分区
- repartition默认既可以增大分区也可以减少分区,都会产生shuffle,底层就是使用的coalesce(分区数,shuffle=true).
- 如果想要增大分区,推荐使用repartition,使用简单
repartition与coalesce的区别:
- coalesce默认只能合并分区,此时不会产生shuffle操作,如果想要增大分区数,此时需要设置coalesce的shuffle参数=true,此时会使用shuffle重分区 <如果想要减少分区推荐使用coalesce,一般是搭配filter使用,可以避免shuffle>
- repartition默认既可以增大分区也可以减少分区,但是都会产生shuffle <如果想要增大分区推荐使用repartition,因为更简单>
t
sortBy* * * *
- 语法sortBy(func: RDD元素类型=> K ): 按照指定字段排序
- sortBy里面的函数是针对RDD单个元素操作
- sortBy是根据函数的返回值进行排序
- sortBy默认是升序,如果想要降序将ascding参数设置为false既可以
- sortBy会产生shuffle操作,是全局排序
pipe
- pipe: 调用脚本
- pipe是每个分区调用一次脚本
- pipe后续会将一个分区所有数据传递给脚本作为参数
- pipe会生成新的RDD,新RDD的元素是在脚本中通过echo返回的
双value类型交互
intersection交集
-
函数签名:def intersection(other: RDD[T]): RDD[T]
//使用 val rdd1 = sc.parallelize(List(1,2,3,4,5)) val rdd2 = sc.parallelize(List(4,5,6,7,8)) val rdd3 = rdd1.intersection(rdd2)
-
会产生shuffle过程,且产生两次
union并集
-
函数签名:def union(other: RDD[T]): RDD[T]
//使用 val rdd1 = sc.parallelize(List(1,2,3,4,5)) val rdd2 = sc.parallelize(List(4,5,6,7,8)) val rdd3 = rdd1.union(rdd2)
-
不会产生shuffle,合并后的分区数等于两个父RDD分区数之和
subtract差集
-
函数签名:def subtract(other: RDD[T]): RDD[T]
//使用 val rdd1 = sc.parallelize(List(1,2,3,4,5)) val rdd2 = sc.parallelize(List(4,5,6,7,8)) val rdd3 = rdd1.subtract(rdd2)
-
会产生shuffle过程,且产生两次
zip拉链
-
函数签名:def zip[U: ClassTag](other:
RDD[U]): RDD[(T, U)] -
两个RDD要想拉链必须元素个数与分区数都相同
val rdd = sc.parallelize(List("lisi","wangwu","zhoaliu"),2) val rdd2 = sc.parallelize(List(20,30,40),2) val rdd3 = rdd.zip(rdd2)
Key-Value类型
partitionBy()按照K重新分区
- 函数签名:def partitionBy(partitioner: Partitioner): RDD[(K, V)]
- partitionBy: 根据指定的分区器重分区
- 将RDD[K,V]中的K按照指定Partitioner重新进行分区;
- 如果原有的RDD和新的RDD是一致的话就不进行分区,否则会产生Shuffle过程。
自定义分区
- 要实现自定义分区器,需要继承org.apache.spark.Partitioner抽象类,并实现下面三个方法。
- numPartitions: Int:返回创建出来的分区数。
- getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)。
- equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样Spark才可以判断两个RDD的分区方式是否相同
groupByKey()按照K重新分组
- 函数签名:def groupByKey(): RDD[(K,
Iterable[V])] - groupByKey: 根据key分组
- groupBykey生成的新RDD的元素是[K,V]键值对
- K: 是分组的K
- V: 是K在原RDD中对应的所有的value值的集合
- groupBykey生成的新RDD的元素是[K,V]键值对
val rdd = sc.parallelize(List( ("aa",10) ,("cc",20),("aa",100),("dd",40),("cc",300) ))
val rdd2 = rdd.groupByKey()
//结果
List((aa,CompactBuffer(10, 100)), (dd,CompactBuffer(40)), (cc,CompactBuffer(20, 300)))
reduceByKey()按照K聚合V* * * *
- 语法:reduceByKey(func: (value值类型, value值类型)=>value值类型 ): 按照key分组之后,对每个组所有的value值聚合
- reduceByKey在combiner阶段针对每个组所有的value值第一次聚合的时候,函数的第一个参数的初始值 = 第一个value值
- reduceByKey的combiner阶段与reducer阶段的聚合逻辑是完全一样,逻辑就是传入的func函数
- groupByKey与reduceByKey的区别:
- groupByKey: 只是单纯分组, shuffle过程中没有预聚合功能, shuffle效率比较低
- reduceByKey: 分组+聚合, shuffle过程中有预聚合功能, shuffle效率更高
- 工作中尽量使用高性能的shuffle算子
- groupByKey与reduceByKey的区别:
combineByKey()转换结构后分区内和分区间操作
- 函数签名: def combineByKey[C] (createCombiner: V => C, mergeValue: (C, V) => C,mergeCombiners: (C, C) => C): RDD[(K, C)]
- combineByKey(createCombiner: RDDvalue值类型 => C,mergeValue: (C, RDDvalue值类型) => C,mergeCombiners: (C, C) => C)
- createCombiner: 在combiner阶段对每个组第一个value值进行转换
- mergeValue: combiner聚合逻辑
- mergeCombiners: reducer聚合逻辑
foldByKey()分区内
- foldByKey(默认值)(func: (value值类型,value值类型)=>value值类型) : 按照key分组,对每个组所有value值聚合
- foldByKey在combiner阶段针对每个组第一次计算的时候, 函数第一个参数的初始值 = 默认值
aggregateByKey()分区间相同的
- aggregateByKey(默认值)(seqOp,combOp)
- seqOp: combiner聚合函数
- combiner阶段针对每个组第一个计算的时候, seqOp函数的第一个参数的初始值 = 默认值
- combOp: reducer聚合函数
- seqOp: combiner聚合函数
combineByKey()转换结构后分区内和分区间操作
- 函数签名:def combineByKey[C] (createCombiner: V => C,mergeValue: (C, V) => C,mergeCombiners: (C, C) => C): RDD[(K, C)]
reduceByKey、foldByKey、aggregateByKey、combineByKey的区别
- reduceBykey:
combineByKeyWithClassTag[V] ((v: V) => v, func, func, partitioner) 第一个初始值不变
- combineByKey:
combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners)(null) 分区内和分区间规则一致 - foldByKey:
combineByKeyWithClassTag[V] ((v: V) => cleanedFunc(默认值, v),cleanedFunc, cleanedFunc, partitioner) 第一个初始值和分区内处理规则一致 - aggregateBykey:
combineByKeyWithClassTag[U] ((v: V) => cleanedSeqOp(默认值, v),cleanedSeqOp, combOp, partitioner) 把第一个值变成特定的结构
- reduceByKey、foldByKey、combineByKey、aggregateByKey区别:
- reduceByKey: 在combiner阶段对每个组所有value值第一次计算的时候,函数的第一个参数的初始值 = 该组第一个value值, combiner与reducer计算逻辑完全一样
- foldByKey: 在combiner阶段对每个组所有value值第一次计算的时候,函数的第一个参数的初始值 = 默认值, combiner与reducer计算逻辑完全一样
- combinerByKey: 在combiner阶段对每个组所有value值第一次计算的时候,函数的第一个参数的初始值 = 第一个函数的转换结果
- aggregateByKey: 在combiner阶段对每个组所有value值第一次计算的时候,函数的第一个参数的初始值 = 默认值
sortByKey()按照K进行排序
-
函数签名:
def sortByKey(ascending: Boolean = true, numPartitions: Int =elf.partitions.length) : RDD[(K, V)]// 默认,升序
-
sortByKey: 根据key排序
-
在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
-
可以通过sortBy完成sortByKey操作
mapValues()只对V进行操作
- map(func: 元素=>新元素)
- mapValues(func: 元素的value值=>元素的新value值): 一对一的映射[将原来的value值做转换,返回新的vlaue值]
- mapValues里面的函数是针对元素的value值操作, 元素有多少个,函数就调用多少次
Join连接
- join: 两个rdd只有key相同的才能连接,join生成的RDD的元素类型是 (key,(左rdd value值,右rdd value值)) 【类似sql inner join】
- leftOuterJoin: 两个rdd key相同的能连接+ 左rdd不能连接的数据,join生成的RDD的元素类型是 (key,(左rdd value值,Option(右rdd value值)))
- rightOuterJoin: 两个rdd key相同的能连接+ 右rdd不能连接的数据,join生成的RDD的元素类型是 (key,(Option(左rdd value值),右rdd value值))
- fullOuterJoin: 两个rdd key相同的能连接+ 右rdd不能连接的数据+左rdd不能连接的数据,join生成的RDD的元素类型是 (key,(Option(左rdd value值),Option(右rdd value值)))
cogroup()类似全连接
- 类似 先对两个rdd实现groupByKey ,然后针对两个rdd 的分组结果执行全外连接
Action行动算子
reduce()聚合* * * * * *
- 函数签名:def reduce(f: (T, T) => T) : T
- reduce是对rdd所有元素聚合
- reduce是先对每个分区所有数据聚合,将每个分区的聚合结果发给Driver,由Driver统一汇总
collect()以数组的形式返回数据集
- 函数签名:def collect(): Array[T]
- 将rdd每个分区的数据收集到Driver端口
- 特别提醒:所有的数据都会被拉取到Driver端,慎用
- 如果rdd分区数据量比较大,而Driver内存默认只有1G,所以可能出现内存溢出
- 在工作中一般都需要调整Driver的内存大小,一般调整为5-10G.
- 在spark-submit的是可以通过--driver-memory调整Driver内存大小
count()返回RDD中元素个数
- 函数签名:def count(): Long
- 返回RDD中元素的个数
first()返回RDD中的第一个元素
- 函数签名:def first(): T
- 返回RDD中的第一个元素
take()返回由RDD前n个元素组成的数组
- def take(num: Int): Array[T]
- 返回一个由RDD的前n个元素组成的数组
takeOrdered()返回该RDD排序后前n个元素组成的数组
- 函数签名:def takeOrdered(num:Int) (implicit ord: Ordering[T]): Array[T]
- 返回该RDD排序后的前n个元素组成的数组
- 猜测会产生shuffle过程
fold()
- 函数签名:def fold(zeroValue: T) (op: [T , T]): T
- fold(默认值)(func: (RDD元素类型,RDD元素类型)=> RDD元素类型): 对RDD所有元素聚合
- fold首先会对每个分区所有元素中进行聚合,然后将每个分区的聚合结果发给Driver进行汇总
- fold在每个分区中第一次计算的时候,函数第一个参数的初始值 = 默认值
- fold在Driver第一次计算的时候,函数第一个参数的初始值 = 默认值
- fold首先会对每个分区所有元素中进行聚合,然后将每个分区的聚合结果发给Driver进行汇总
aggregate()
- 函数签名:def aggregate(U: ClassTag) (zeroValue: U) (seqOp: (U , T): U,combOp:(U : U)=>U):U
- aggregate(默认值)(seqOp: (默认值类型,RDD元素类型)=> 默认值类型 , comop: ( 默认值类型,默认值类型 )=>默认值类型 ) : 对RDD所有元素聚合
- seqOp: 是在每个分区中对所有元素聚合逻辑
- comop: 在driver中对所有分区的聚合结果的汇总逻辑
fold与aggregate的区别:
- fold的分区内聚合逻辑与driver汇总逻辑一样
- aggregate的分区内聚合逻辑与driver汇总逻辑可以不一样
countByKey()统计每种key的个数* * * * * *
- 函数签名:def countByKey(): Map[K, Long]
- countByKey一般结合sample一起使用
- 后续如果出现了数据倾斜,一般先用sample采样数据,然后用countByKey统计采样结果,从而得知哪个key出现了数据倾斜
save相关算子
saveAsTextFile(path)保存成Text文件
- 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本
saveAsSequenceFile(path)保存成Sequencefile文件
- 将数据集中的元素以Hadoop Sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。
- 注意:只有kv类型RDD有该操作,单值的没有
saveAsObjectFile(path) 序列化成对象保存到文件
- 用于将RDD中的元素序列化成对象,存储到文件中。
foreach(f)遍历RDD中每一个元素
- 函数签名:def foreach(f:T=>Unit): Unit
- 遍历RDD中每一个元素
- foreach(func: RDD元素类型=>Unit): Unit
- foreach里面的函数是针对单个元素操作,元素有多少个,函数就调用多少次,没有返回值
- foreach是并行执行
- foreach与map的区别:
- map是转换算子,会生成一个新的RDD
- foreach是行动算子,不会生成新的RDD,会触发任务计算,得到具体的结果
foreachPartition()* * * * * *
- foreachPartition(func: Iterator[RDD元素类型]=>Unit):Unit
- foreachPartition里面的函数是针对每个分区操作,函数的参数是每个分区所有数据的迭代器,rdd有多少分区,函数就执行多少次。
- foreachPartition一般用于将数据保存到mysql,hbase,redis等存储介质中,可以减少资源链接的创建与销毁的次数。
序列化
闭包
- 函数体中使用了外部变量的函数称之为闭包.
spark序列化原因:
- spark算子里面的函数是在executor中的task执行的,spark算子外面的代码是在driver执行的,如果spark算子里面的函数体中使用了dirver定义的变量
- 此时spark会将该driver变量序列化之后传给task使用,所以就必须要求该driver变量类型必须能够序列化才行。
spark里面的有两种序列化: java序列化[默认使用],kryo序列化
- java序列化: 在序列化对象的时候会将对象的全类名、属性、属性类型、继承信息等全部都会序列化进去
- kryo序列化: 只会序列化类名、属性名、属性值、属性类型信息[序列化性能比java序列化高10倍左右]
- 在实际工作中一般选择使用kryo序列化
如何配置spark使用kryo序列化:
- 在sparkconf中配置spark默认使用的序列化器: new SparkConf().set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
- 注册哪些类使用kryo序列化【可选】: new SparkConf().registerKryoClasses(Array(classOf[待序列化的类的类名])
- 使用registerKryoClasses与不使用egisterKryoClasses的区别:
- 使用registerKryoClasses,此时再序列化类的时候不会将类的全类名序列化进去
- 没有使用registerKryoClasses,此时在序列化类的时候会将类的全类名序列化进去
- 使用registerKryoClasses与不使用egisterKryoClasses的区别:
RDD依赖关系
查看血缘关系
- 一个job中多个rdd之间的关系
- rdd的血统可以通过toDebugString查看
查看依赖关系
要想理解RDDS是如何工作的,最重要的就是理解Transformations。
RDD之间的关系可以从两个维度来理解:一个是RDD是从哪些RDD转换而来,也就是 RDD的parent RDD(s)是什么; 另一个就是RDD依赖于parent RDD(s)的哪些Partition(s),这种关系就是RDD之间的依赖。
窄依赖
- 窄依赖表示每一个父RDD的Partition最多被子RDD的一个Partition使用,最简单的理解就是是否有shuffle过程,没有就是窄依赖。
宽依赖
- 宽依赖表示同一个父RDD的Partition被多个子RDD的Partition依赖,会引起Shuffle
- 具有宽依赖的transformations包括:sort、reduceByKey、groupByKey、join和调用rePartition函数的任何操作。
- 宽依赖对Spark去评估一个transformations有更加重要的影响,比如对性能的影响。
Stage任务划分
- stage切分: 根据最后一个rdd的依赖关系向前找父RDD,然后根据父RDD的依赖关系继续向前,依次查找,一直找到第一个RDD位置。在中间查询的过程中如果父子依赖关系是宽依赖则切分stage.
- stage的执行: 一个job中的stage是从前往后执行,先执行第一个stage,在执行后面的stage
- Application: 应用, 一个sparkcontext称之为一个应用。
- job: 任务, 一般一个action算子产生一个Job [first与take除外,这两个可能产生多个Job]
- stage: 阶段, job中stage的个数 = shuffle个数+1
- task: 子任务, 一个stage中task的个数 = stage中最后一个rdd的分区数
- job: 任务, 一般一个action算子产生一个Job [first与take除外,这两个可能产生多个Job]
- 一个job中多个stage的执行是串行
- 一个stage中多个task的执行是并行
- RDD任务切分中间分为:Application、Job、Stage和Task
- Application:初始化一个SparkContext即生成一个Application;
- Job:一个Action算子就会生成一个Job;
- Stage:Stage等于宽依赖的个数加1;
- Task:一个Stage阶段中,最后一个RDD的分区个数就是Task的个数。
- 注意:Application->Job->Stage->Task每一层都是1对n的关系。
RDD持久化
RDD Cache缓存
- RDD的持久化过程:RDD通过Cache或者Persist方法将前面的计算结果缓存,默认情况下会把数据以序列化的形式缓存在JVM的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
- RDD持久化原因: RDD中不存储数据,如果一个rdd在多个job中重复使用,此时默认会重复执行该RDD之前的步骤,如果能将rdd的数据保存下来,后续job就不用重复执行该rdd之前的步骤了。
- 应用场景:
- 一个rdd在多个job中重复使用
- job中依赖链条太长,可以避免计算出错导致重新计算
如何持久化
- 缓存: 将RDD的数据保存到分区所在机器的内存/磁盘中 <常用>
- 使用方式:
- rdd.cache/ rdd.persist(参数…..)
- cache与persist的区别:
- cache是数据只能保存到内存中
- persist可以指定将数据保存到内存/磁盘中
- 常用的存储级别(persist的参数):
- MEMORY_ONLY[数据只保存到内存中]: 一般用于小数据量场景
- MEMORY_AND_DISK[数据一部分保存到内存中,一部分保存到磁盘中]: 一般用于大数据量场景
- 使用方式:
- checkpoint: 将RDD的数据保存到HDFS中
- 原因: 缓存是将数据保存到本机的内存/磁盘中,如果服务器宕机,数据丢失,spark会重写启动一个task,然后根据rdd依赖关系重新计算得到数据, 影响效率。
- 如何使用: rdd.checkpoint
- 每个job执行完成之后,spark会查看当前job中是否有rdd需要checkpoint,如果有则将该rdd之前的数据处理步骤再次执行得到数据之后保存到指定checkpoint路径。所以checkpoint会触发一次job操作。
- 所以如果想要避免checkpoint之前的处理重复执行可以将checkpoint操作与cache结合使用: rdd.cache ; rdd.checkpoint()
缓存与checkpoint的区别:
- 数据保存位置不一样:
- 缓存是将RDD的数据保存到分区所在机器的内存/本地磁盘中
- checkpoint是将RDD的数据保存到HDFS中
- 依赖关系是否保留不一样
- 缓存是将RDD的数据保存到分区所在机器的内存/本地磁盘中,如果服务器宕机,数据丢失之后需要根据依赖关系重新计算得到数据,所以RDD的依赖关系必须保留
- checkpoint是将RDD的数据保存到HDFS中,数据不会丢失,所以RDD的依赖关系会删除
键值对RDD数据分区
- 分区器: 用于shuffle阶段
- spark自带的分区器:
- HashPartitioner:
- 分区规则: key.hashCode % 分区数 < 0 ? key.hashCode % 分区数 + 分区数 : key.hashCode % 分区数
- RangePartitioner:
- 分区规则: 会采用采样的方式从数据集中抽取 分区数-1 个key,
- 通过这几个采样的key确定每个分区的边界,
- 后续将key与分区边界对比如果在边界范围内则放入对应分区中。
- HashPartitioner:
- 注意:
- 只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD分区的值是None
- 每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的。
转换算子和行动算子中带 * 号的相对重要