1.编写WordCount(读取一个本地文件),并打包到集群运行,说明需要添加的主要参数。
代码
val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName)
val sc = new SparkContext(conf)
val resRDD: RDD[(String, Int)] = sc.textFile(args(0)).flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
resRDD.saveAsTextFile(args(1))
sc.stop()
运行jar命令:
bin/spark-submit \
--master yarn \
--deploy-mode cluster \
--executor-cores 2 \
--class com.aura.spark.day06.Spark09_Interview1 \
/home/hadoop/jar/WordCount.jar \
/word_in \
/word_out
命令解析
- master:指定程序运行模式。
- deploy-mode:指定Driver运行在集群还是本地。
- executor-cores:每个Executor的核数。
- class:Spark程序主类全限定名。
2.RDD的五个特性
- 一组分区;
- 每个分区数据的计算规则;
- 和其他RDD之间的血缘依赖关系;
- 对于KV类型存储的数据,有一个可选的分区器。
- 存储切片优先位置列表。
3.如何创建一个RDD,有几种方式,举例说明
四种。
- makeRDD()或者parallelize()方法创建RDD;
- textFile()读取文件创建RDD;
- transformation转换算子通过其他RDD生成新的RDD;
- 通过new方式创建RDD,比如new JdbcRDD()。
4.创建一个RDD,使其一个分区的数据转变为一个String。例如(Array(“a”,“b”,“c”,“d”),2)=>(“ab”,“cd”)
val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName).setMaster("local[*]")
val sc = new SparkContext(conf)
val rdd: RDD[String] = sc.makeRDD(Array("a", "b", "c", "d"), 2)
// 将同一分区数据转为数组
val glomRDD: RDD[Array[String]] = rdd.glom()
glomRDD.foreach(println)
val resRDD: RDD[String] = glomRDD.map(
arr => {
arr.mkString("")
}
)
resRDD.foreach(println)
sc.stop()
5.map与mapPartitions的区别
- map是对RDD中的元素按照指定规则进行一一映射。
- mapPartitons是以分区为单位对RDD中的元素按照指定规则进行映射。
6.coalesce与repartition两个算子的作用以及区别与联系
作用
重新分区。
联系
repartition底层调用的就是coalesce,但是shuffle参数设置的是true,表示执行shuffle的重新分区。
区别
coalesce默认shuffle参数为false,表示不执行shuffle重新分区,一般用于减少分区。repartition一般用于增加分区。
7.使用zip算子时需要注意的是什么(即哪些情况不能使用)
- 在scala集合中,zip函数即使两个集合元素数量不相同,也可以进行zip操作,但是多余的元素不会被返回。
- 但是在Spark RDD算子中,zip算子只能操作两个分区数和元素数量都一致的RDD。
8.reduceByKey跟groupByKey之间的区别
- reduceByKey:按照key进行聚合,在shuffle之前,会对分区内数据进行预聚合操作,返回的结果是k-v类型的RDD(RDD[K,V])。
- groupByKey:按照key进行分组,直接进行shuffle。返回的结果是k-迭代器类型的RDD(RDD[K,Iterable[V]])。
- 在不影响业务逻辑的情况下,优先使用reduceByKey。求和操作不影响业务逻辑,求平均值的操作影响业务逻辑。
9.reduceByKey跟aggregateByKey、foldByKey之间的区别与联系
联系
- 都是按照key做聚合操作,都会在shuffle之前对数据做预聚合操作。
区别
- reduceByKey:没有初始值,分区内和分区间的计算规则一致。
- aggregateByKey:有初始值,初始值参与分区内和分区间的计算,分区内和分区间规则可以不一致。
- foldByKey:有初始值,初始值参与分区内的计算,分区内和分区间计算规则相同。
10.combineByKey的参数作用,说明其参数调用时机
- 参数1:可以对数据的结构进行转换。
- 参数2:分区内的计算规则。
- 参数3:分区间的计算规则。
11.使用RDD实现Join的多种方式
- rdd1.join(rdd2):将相同的key对应的value关联到一起。如果key只是某个RDD存在,那么不返回。
- rdd1.leftOuterJoin(rdd2):返回rdd1中的全部key-value和关联后的key-value。
- rdd1.rightOuterJoin(rdd2):返回rdd2中的全部key-value和关联后的key-value。
- rdd1.cogroup(rdd2):每个rdd中先关联自己的key形成集合,然后再合并。
12.aggregateByKey与aggregate之间的区别与联系
联系
- 两者都是对分区内和分区间的元素做聚合操作,而且都有初始值。
区别
- aggregateByKey是转换算子,是对k-v类型的RDD进行操作,初始值参与分区内和分区间的计算,初始值会和RDD中每一个元素进行迭代并运算。
- aggregate是行动算子,初始值参与分区内和分区间的计算,分区内计算时,初始值和RDD每个分区中的一个元素按照指定规则运算,分区间计算时,初始值只会参与一次运算。
13.创建一个RDD,自定义一种分区规则并实现?spark中是否可以按照Value分区
自定义分区器
object Spark12_Interview13 {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName).setMaster("local[*]")
val sc = new SparkContext(conf)
val rdd: RDD[(String, String)] = sc.makeRDD(List(("13698624174", "河北"), ("13766887551", "广东"),
("13876543211", "上海"), ("17677885551", "河南")), 2)
// 使用自定义分区器重新分区
val resRDD: RDD[(String, String)] = rdd.partitionBy(new MyPartitoner(4))
resRDD.mapPartitionsWithIndex{
case (index, datas) => {
println(index + "--->" + datas.mkString(","))
datas
}
}.collect()
sc.stop()
}
}
// 自定义分区器
class MyPartitoner(partitons: Int) extends Partitioner {
// 分区数量
override def numPartitions: Int = partitons
// 自定义分区逻辑
override def getPartition(key: Any): Int = {
val strKey: String = key.asInstanceOf[String]
// 返回分区号
if (strKey.startsWith("136")) 0
else if (strKey.startsWith("137")) 1
else if (strKey.startsWith("138")) 2
else 3
}
}
按照Value分区
- RDD通过map映射,把value和key的位置调换。
14.读取文件,实现WordCount功能。(使用不同的算子实现,至少3种方式)
方式一
val rdd: RDD[String] = sc.textFile("D:\\develop\\workspace\\bigdata2021\\spark2021\\input\\word.txt")
// 方式一
val wordCountRDD: RDD[(String, Int)] = rdd.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
方式二
val wordCountRDD: RDD[(String, Int)] = rdd.flatMap(_.split(" ")).map((_, 1)).groupByKey().map {
case (key, datas) => {
(key, datas.size)
}
}
方式三
val wordCountRDD: RDD[(String, Int)] = rdd.flatMap(_.split(" ")).groupBy(word => word).map {
case (key, datas) => {
(key, datas.size)
}
}
方式四
val wordCountRDD: RDD[(String, Int)] = rdd.flatMap(_.split(" ")).map((_, 1)).aggregateByKey(0)(_+_,_+_)
方式五
val wordCountRDD: RDD[(String, Int)] = rdd.flatMap(_.split(" ")).map((_, 1)).foldByKey(0)(_+_)
方式六
val wordCountRDD: RDD[(String, Int)] = rdd.flatMap(_.split(" ")).map((_, 1)).combineByKey(
// 转换数据结构
elem => elem,
// 分区内计算规则
(a: Int, b: Int) => {
a + b
},
// 分区间计算规则
(a: Int, b: Int) => {
a + b
}
)
15.说说你对RDD血缘关系的理解
- RDD通过转换算子生成一系列的RDD,Spark会记录每个RDD之间的依赖关系,此关系称为血缘,可以通过toDebugString算子查看RDD之间的血缘关系。
- Spark会根据RDD之间的血缘关系形成DAG有向无环图,根据此有向无环图,Spark可以高效的处理容错和数据的恢复工作。
16.Spark是如何进行任务切分的,请说明其中涉及到的相关概念
- 应用:一个spark程序就是一个应用,一个应用可以有多个job。
- job作业:触发一次action算子就是一次job,一个job可以有多个stage。
- stage阶段:宽依赖切分不同的stage,stage的数量=宽依赖数量+1。一个stage可以有多个task。
- task任务:每个stage的最后一个RDD的分区数量=task的数量。
17.RDD的cache和checkPoint的区别和联系
联系
- cache和checkPoint都是对RDD中的数据做缓存,后面计算逻辑相同的RDD,可以直接从缓存中取数据,而不用重新进行计算。
- 只有触发action算子时,才会真正的缓存。
区别
- cache不会切断RDD的血缘关系,缓存默认存储在内存中,可以设置存储在本地磁盘上,但是随着程序运行结束,cache缓存的数据都会丢失。
- checkpoint检查点会切断RDD的血缘关系,可以把数据存储在HDFS等高可用、可靠性高的存储系统中。
- 为了确保数据的准确性,checkpoint检查点在第一次使用时,会根据RDD的血缘关系,从头到尾执行一遍。
- 一般checkpoin和cache搭配来使用。
18.Spark读取HDFS文件默认的切片机制
-
创建RDD时,会将文件路径和最小分区数minPartitions传递到HadoopRDD中。
-
调用FileInputFormat中的getSplits方法计算切片信息。
-
首先计算目标切片大小goalSize:目标切片大小 = 文件大小 / 最小分区数 。
-
再计算切片最小值minSize:max(FileInputFormat.SPLIT_MINSIZE,minSplitSize),最小值为1。
-
接着计算splitSize = Math.max(minSize, Math.min(goalSize, blockSize)),所以SplitSize一般为goalSize和blockSize两者的最小值。
-
如果剩余待处理文件大小 / splitSize > 1.1,那么就切一片。
19.说说你对广播变量的理解
- 广播变量:分布式共享只读变量。
- Spark程序在运行时,通过算子传递函数,函数中可能需要访问Driver端定义的变量,spark会为Executor端的每个task都发送一份这个变量的副本,如果变量比较大,那么就会消耗过多的Executor端的内存。
- 如果我们把变量定义为广播变量,那么Spark只会发送一个广播变量的副本到Executor端,供所有的task访问。
20.自定义一个累加器,实现计数功能
计算以H开头的单词数量
object Spark14_Interview20 {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName).setMaster("local[*]")
val sc = new SparkContext(conf)
val rdd: RDD[String] = sc.makeRDD(List("Hello", "HaHa", "spark", "scala", "java", "Hive", "Hi"), 2)
// 声明累加器
val acc = new MyAccumulator20
// 注册
sc.register(acc, "myacc")
// 使用
rdd.foreach(datas => {
acc.add(datas)
})
// 打印累加器中的值
println(acc.value)
sc.stop()
}
}
// 自定义累加器 泛型为输入类型和输出类型
class MyAccumulator20 extends AccumulatorV2[String, Int] {
var num = 0
// 判断是否为初始值
override def isZero: Boolean = {
num == 0
}
// 复制累加器
override def copy(): AccumulatorV2[String, Int] = {
val MyAcc = new MyAccumulator20
MyAcc.num = this.num
MyAcc
}
// 重置累加器
override def reset(): Unit = {
num = 0
}
// 添加元素
override def add(v: String): Unit = {
if (v.startsWith("H")) {
num += 1
}
}
// 合并累加器
override def merge(other: AccumulatorV2[String, Int]): Unit = {
num = this.num + other.value
}
// 取值
override def value: Int = num
}