Spark学习

0 阅读53分钟

第一个spark程序

准备maven程序,jdk1.8,scala-2.13.12

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>groupId</groupId>
    <artifactId>scala-test</artifactId>
    <version>1.0.0</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_2.12</artifactId>
            <version>3.4.3</version>
        </dependency>
    </dependencies>
</project>

WordCountScala.scala

object WordCountScala {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf();
    conf.setAppName("scalaWordCount")
    // 单机本地运行
    conf.setMaster("local")
    val sc = new SparkContext(conf)
    // 单词统计
    val fileRDD = sc.textFile("data/testdata.txt", 16)
    // 下面的逻辑可以用一行实现
    fileRDD.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).foreach(println)

//    // hello java
//    val words = fileRDD.flatMap(x => x.split(" "))
//    // hello
//    // java
//    val pairWord = words.map(x => (x, 1))
//    // (hello,1)
//    // (java,1)
//    // (hello,1)
//    val res = pairWord.reduceByKey((x, y) => x + y)
//    res.foreach(println)
  }
}

WordCountJava.java

public class WordCountJava {
    public static void main(String[] args) {
        SparkConf conf = new SparkConf();
        conf.setAppName("javaWordCount");
        // 单机本地运行
        conf.setMaster("local");
        JavaSparkContext jsc = new JavaSparkContext(conf);
        JavaRDD<String> fileRDD = jsc.textFile("data/testdata.txt", 16);
        // 这里也可以用点连续写,代码量比scala多很多
        JavaRDD<String> words = fileRDD.flatMap((FlatMapFunction<String, String>) line -> Arrays.asList(line.split(" ")).iterator());
        JavaPairRDD<String, Integer> pairWord = words.mapToPair((PairFunction<String, String, Integer>) word -> new Tuple2<>(word, 1));
        JavaPairRDD<String, Integer> res = pairWord.reduceByKey((Function2<Integer, Integer, Integer>) Integer::sum);
        res.foreach((VoidFunction<Tuple2<String, Integer>>) value -> System.out.println(value._1 + "\t" + value._2));
    }
}

准备数据文件data/testdata.txt

hello java
hello scala
hello java scala
hello spark scala
hello flink scala

java代码和scala代码的运行结果如下

hello	5
spark	1
scala	4
flink	1
java	2

MR架构回顾

image.png

spark和MR都属于计算层,spark可以说是MR的升级

MR执行流程

image.png

client调用Yarn获取资源(CPU、内存),然后在相应节点上启动mapTask和reduceTask,mapTask直连HDFS上的block块读取数据并处理数据,mapTask将处理的结果写到本地;相应的reduceTask去mapTask拉取其输出,将其作为reduceTask的输入然后处理并将结果写入到HDFS

image.png

术语

  • application 应用程序,一个MR程序
  • job 作业,client提交的一个MR,称为一个作业
  • stage 阶段,有map阶段和reduce阶段
  • task 任务,每一个阶段中都有一系列并行的任务,map阶段有若干个mapTask,reduce阶段有若干个reduceTask

一个application对应一个job,一个job对应1-2个stage(可能没有reduce阶段),一个stage对应N个task

每个mapTask都有input,通过InputFormat接口定义输入格式化,默认实现是TextInputFormat;client获取splits、mapTask中的LineRecordReader会使用到InputFormat。mapTask通过InputFormat读取input,然后交给自定义的map()方法处理,一般会涉及过滤(filter)和映射(map、flatmap);从这可以看出MR有点不友好,需要人实现数据集迭代逻辑,而spark中已经将常见的数据集迭代逻辑封装好,只需调用即可;spark中将业务处理逻辑和数据集迭代逻辑分开,只需要关心业务逻辑即可。mapTask的输出output会输出到本地文件

每个reduceTask都有input,input是mapTask的输出,reduceTask会将mapTask的输出按照key排序,然后调用reduce()方法,更准确地说应该是reduceByKey,因为相同的key会调用一次reduce()方法。reduceTask的输出output会输出到hdfs

多个MR的job可以组成作业链,一个job执行完毕,会将其资源释放掉(JVM进程),然后再执行后面的job,当一个任务需要很多job组合完成时,可能会涉及到大量资源的申请和释放,因此MR偏向于冷启动。以“笔记本 听歌 上网 打游戏”为例,按照MR的执行模式是:开机-听歌-关机、开机-上网-关机、开机-打游戏-关机,会有这3个job依次执行,缺点很明显,每个job都是冷启动

MR虽然只涉及两个阶段,比较简单,但是当实现复杂任务时,job的组合以及资源的调度会比较复杂,每个job的冷启动也比较消耗资源,MR执行的中间结果都是写入磁盘的IO也较慢,这些问题都在spark中得到了解决

image.png

spark也有application、job、stage、task这些术语,一个application对应N个job,一个job对应N个stage,一个stage对应N个task。这样一个复杂的任务对应的多个job可以复用资源(除了第一个job,其它的job不需要冷启动)。spark执行的中间结果都是写入内存的,读写速度远大于磁盘

Spark Web UI

在“第一个spark程序”,单词数量统计的基础上,统计出现相同次数的单词的数量

object WordCountScala {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf();
    conf.setAppName("scalaWordCount")
    conf.setMaster("local")
    val sc = new SparkContext(conf)
    val fileRDD = sc.textFile("data/testdata.txt", 16)
    val words = fileRDD.flatMap(x => x.split(" "))
    val pairWord = words.map(x => (x, 1))
    val res = pairWord.reduceByKey((x, y) => x + y)
    // 统计:单词出现的次数-数量
    val fanzhuan = res.map(x => (x._2, 1))
    val resOver = fanzhuan.reduceByKey(_ + _)
    resOver.foreach(println)
    // res.foreach(println)
    // 让程序阻塞住,这样可以查看webui
    Thread.sleep(Long.MaxValue)
  }
}

http://127.0.0.1:4040/ 可以查看webui

image.png

点击job可以查看详情

image.png

这个job分成三个阶段(stage)

  • stage0: textFile读取文件(读取其中一个块),flatMap切割单词,map映射成(word,1),这些操作可以在同一个节点上执行,不需要出节点
  • stage1: reduceByKey拿相同的key做计算,涉及到从多个map(stage0中的)中拉取数据(shuffle),这必须是新的stage;接着执行map映射成(count,1),不需要出节点,是同一个stage
  • stage2: reduceByKey与上面一样,需要拉取数据(shuffle),必须是新的stage

从上可以看出stage的边界是是否需要shuffle拉取数据,一个stage,表示可以在一个节点上完成所有的计算,不需要跨节点,当需要跨节点时(shuffle拉取数据),必须开启一个新的stage

这里一个application,执行了一个job,这个job中执行了3个阶段,这就比MR好,MR中只能有2个阶段

res.foreach(println)放开,再次执行,这个application对应两个job

image.png

image.png

image.png

job0,3个阶段都执行了,job1,第一个阶段(stage3)跳过了,只执行了stage4。job是顺序执行的,前面job的执行结果,可以被后面的job复用。在application的一个生命周期内,RDD是可以复用的,相同顺序执行的RDD结果可以被复用(不需要重复执行),RDD是复用计算结果的最小单元,job执行时会给RDD一个编号,以此来判断RDD是否可复用。job不仅复用了资源,也复用了计算结果,相对于MR这是性能的巨大提升

代码中前面的转换操作(map、flatMap等)是不会执行job的,只有遇到行动操作(foreach等)才会执行job(调用sc.runJob)

RDD说明

image.png

  • SparkContext.textFile创建HadoopRDD,该RDD的compute方法创建了一个NextIterator迭代器,该迭代器通过RecordReader(inputFormat.getRecordReader)记录读取器依次读取文件中的每条记录
  • RDD.flatMap(_.split(" "))创建MapPartitionsRDD(父RDD调用的),该RDD,prev指向父RDD(HadoopRDD),compute方法会返回一个新的迭代器,该迭代器包装了父RDD的迭代器;会对父迭代器中的每个元素执行一个自定义操作,这里是_.split(" ")即对父迭代器中的每个元素使用空格分割成单词,然后将结果扁平化成新的迭代器返回
  • RDD.map((_, 1))创建MapPartitionsRDD(父RDD调用的),该RDD,prev指向父RDD(上一个MapPartitionsRDD),compute方法会返回一个新的迭代器,该迭代器包装了父RDD的迭代器;会对父迭代器中的每个元素执行一个自定义操作,这里是(_, 1)即将父迭代器中的每个元素处理成(key, 1)的形式,处理的结果会成为新的迭代器返回
  • RDD.reduceByKey(_ + _)创建ShuffledRDD(父RDD调用的,reduceByKey方法在RDD的隐式转换类型PairRDDFunctions中)。reduceByKey方法会调用combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner),相当于会自动执行MR中的combiner,该方法的作用是对相同的key做合并处理。方法中有3个函数,第一个函数是如何处理key,默认是key直接返回,将相同的key做合并处理,可以将key处理成元组或集合以包含更多的数据用于业务处理;第二个函数是map时如何处理value,这里是将所有value累加;第三个函数是combine时(溢写)如何处理value,这里是将所有value累加。该RDD,prev指向父RDD(上一个MapPartitionsRDD),ShuffledRDD的prev是@transient注解的,表示prev不会参与序列化,RDD会通过网络发送,ShuffledRDD需要拉取数据,prev对应的RDD不需要重复执行。ShuffledRDD重写了getDependencies方法,返回该RDD依赖的RDD列表(因为prev不会被序列化,可以通过这个方法重建)。ShuffledRDD的compute方法调用shuffleManager.getReader.read方法返回一个迭代器。ShuffleManager将shuffle读写整合起来,上一个RDD的迭代器会将结果通ShuffleManager.getWriter.write写入内存或文件,ShuffledRDD通过ShuffleManager.getReader.read读取上一个RDD的shuffle写,这样shuffle两端的迭代器就串联起来了
  • RDD.map(x => (x._2, 1))创建MapPartitionsRDD(父RDD调用的),与前面的RDD.map((_, 1))类似,将父迭代器中的每个元素(key,value)处理成(value, 1)的形式,处理的结果会成为新的迭代器返回
  • RDD.foreach(println)会调用SparkContext.runJob去执行job,这里会调用迭代器将job中的每个元素打印出来

RDD是一个数据集,里面有很多算子,每次调用算子都会将算子应用到数据集上并生成新的RDD。同一个阶段的RDD连起来,形成一个管道;不同阶段的RDD通过shuffle读写连接起来,形成一个流水线(是通过迭代器模式实现的)

image.png

  • RDD可以通过SparkContext创建出来,RDD后续操作都是算子
  • transformation转换算子生成新的RDD,例如,map,flatmap,filter等(不需要shuffle),reduceByKey,groupByKey等(需要shuffle)
  • action执行算子用于执行计算,只有调用执行算子才会执行真正的计算,例如,foreach,collect,saveasfile等
  • controller控制算子,可用于缓存前面的执行结果,例如,cache,checkpoint等

image.png

  • OneToOneDependency 一对一窄依赖关系,指的是分区一一对应,用于将一个RDD转换成另一个RDD
  • RangeDependency 多对一窄依赖关系(包括一对一),指的是分区多对一,用于将多个RDD合并成一个RDD。多对一指的是合并之前RDD的多个分区可以对应到合并之后RDD的一个分区,但是前面RDD的每个分区只能指向后面RDD的一个分区不能是多个,即前面RDD与后面RDD的分区是多对一的关系,后面RDD与前面RDD的分区是一对一的关系
  • ShuffleDependency 一对多宽依赖关系,指的是分区一对多,用于shuffle。前面RDD的一个分区会被打散到后面RDD的多个分区

需要shuffle的是宽依赖关系,不需要shuffle的是窄依赖关系。连在一起的窄依赖关系属于同一个stage,不同stage的分界线是宽依赖关系

窄依赖(同一个stage)可以跑在一台主机上,这个并不是指数据不需要通过网络传输,窄依赖的不同分区可能需要通过IO全量拷贝过来(拷贝一整个分区),然后在一台主机上做计算,数据不需要经过shuffle处理(一个分区数据需要通过分区器分散到不同的主机)。宽依赖,就是需要shuffle,一个分区数据必须分散到不同的节点(本质是分散到不同的分区,经过分区器进行分区)做计算。窄依赖,每个分区数据可能需要通过IO全量拷贝到需要计算的节点(每个计算的节点需要的都是分区的全量数据,因此只是简单的IO拷贝,不是shuffle)。窄依赖就是前面一个分区的数据,后面处理时还是作为同一个分区处理,不需要打散;宽依赖就是前面一个分区的数据,后面处理时需要作为不同的分区处理,需要打散(shuffle);因此shuffle的本质是是否需要将同一个分区的数据打散到不同的分区处理,如果不需要打散,可以通过直接IO拷贝一整个分区的数据,否则需要打散那就是shuffle

如果数据,不需要区分每一条记录归属于那个分区。间接的,这样的数据不需要分区器partitioner,即不需要shuffle。因为shuffle的语义是洗牌,面向每一条记录计算他的分区号。如果有行为,不需要区分记录,本地IO拉取数据,那么这种直接IO一定比先partitioner计算,shuffle落文件,最后再IO拉取速度快

当两个RDD做关联查询,理论上是要走shuffle的,shuffle会涉及到所有数据集的移动,将相同的key拉到一起处理。如果有一个RDD数据集很小,这个时候可以优化成不走shuffle,大数据集的RDD直接全量拉取小数据集,然后做关联,这样大数据集可以不移动。这也是直接IO相对于shuffle的优势,可以对小数据集直接IO拉取,大数据集可以不移动,这样可以大量减少IO

Spark整体说明

image.png

spark资源层可以使用yarn,也可以使用standalone,还有mecos/k8s。使用yarn只需要实现自己的ApplicationMaster,MR、Spark、Flink等都有自己的ApplicationMaster。spark client 与ResourceManager通信,会创建一个ApplicationMaster,然后创建Driver,里面有SparkContext用于创建和执行RDD。Driver与ApplicationMaster通信,负责调度和资源申请,Driver可以跑在Client所在的jvm,也可以跑在NodeManager的jvm中(即集群中)。spark 模式,分为client和cluster,描述的就是Driver。Spark程序只有执行时,才能知道job,stage的数量。new SparkContext时,就抢占了资源Executor。stage会被序列化,然后发送给某个Executor执行,下一个stage也是同样的逻辑,只不过下一个stage需要到上一个stage中shuffle拉取数据。Spark产生的中间数据可以存储在内存、本地磁盘、hdfs中

API使用

基础

面向数据集操作

  • 带函数的非聚合: map,flatmap
  • 单元素: union,cartesion 没有函数计算
  • kv元素: cogroup,join 没有函数计算
  • 排序: sortBy,sortByKey
  • 聚合计算: reduceByKey,combineByKey 有函数

面向数据集的API,有基础API和复合API,复合API使用基础API组合而成。例如: map,flatmap,cogroup等是基础api,distinct是复合api(调用map,reduceByKey实现),join是复合api(调用cogroup实现)

object ApiTest {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local").setAppName("ApiTest")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    val dataRDD: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 4, 3, 2, 1))
    println("------------filter--------------")
    // filter过滤元素
    val filterRDD: RDD[Int] = dataRDD.filter(_ > 3)
    val res01: Array[Int] = filterRDD.collect()
    // 4, 5, 4
    res01.foreach(println)
    println("-------------去重---------------")
    // 先将数据转换成(key,1)的格式,再将相同key的值相加得到(key,count),最后取出key,这就相当于对key进行去重
    val res02 = dataRDD.map((_, 1)).reduceByKey(_ + _).map(_._1)
    // 4, 1, 3, 5, 2
    res02.foreach(println)
    // 可以直接使用distinct算子对元素进行去重
    // distinct源码中也是使用类似的逻辑,map(x => (x, null)).reduceByKey((x, _) => x, numPartitions).map(_._1)
    println("-----------distinct-------------")
    val res03 = dataRDD.distinct()
    // 4, 1, 3, 5, 2
    res03.foreach(println)

    val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5))
    val rdd2: RDD[Int] = sc.parallelize(List(3, 4, 5, 6, 7))

    // 并集,union只是简单地将两个RDD合并起来,不会涉及到数据分区分发(shuffle),这个可以在WebUI中`DAG Visualization`清晰地观察到
    // union只是将RDD逻辑合并起来,UnionRDD执行时,还是会将合并之前的RDD放到不同的task中执行,这个可以在WebUI中Tasks查看
    // UnionRDD的compute方法调用`parent[T](part.parentRddIndex).iterator(part.parentPartition, context)`,
    // 这个只会调用合并之前一个RDD的迭代器,前面的RDD会分别在不同的task中执行
    println("-----------union-------------")
    val unionRDD = rdd1.union(rdd2)
    // 1
    println(rdd1.partitions.length)
    // 1
    println(rdd2.partitions.length)
    // 2
    println(unionRDD.partitions.length)
    // 这里的map执行,实际上相当于rdd1和rdd2分别执行map
    val unionDoubleRDD = unionRDD.map(_ * 2)
    unionDoubleRDD.foreach(println)
    // cartesian笛卡尔积,将rdd1中的所有元素和rdd2中的所有元素关联,不会涉及到数据分区分发(shuffle),
    // 这个是数据的全量IO拷贝,相当于将rdd2中的所有元素都拷贝到rdd1的所有分区然后进行关联
    println("---------cartesian-----------")
    val cartesianRDD: RDD[(Int, Int)] = rdd1.cartesian(rdd2)
    cartesianRDD.foreach(println)
    // 差集,将rdd1中在rdd2中也存在的元素减去。原始的rdd1和rdd2中的元素已经被转换成(k,v)格式然后调用subtractByKey创建SubtractedRDD,
    // SubtractedRDD处理数据时会拉取相同分区的数据做处理。默认需要shuffle,将rdd1和rdd2中元素处理成(k,v)格式,然后拉取相同的分区数据做关联处理
    println("----------subtract------------")
    val subtractRDD: RDD[Int] = rdd1.subtract(rdd2)
    // 1, 2
    subtractRDD.foreach(println)
    // 交集,输出rdd1和rdd2中共同的元素。原始的rdd1和rdd2中的元素已经被转换成(k,v)格式然后调用cogroup创建CoGroupedRDD,
    // 接着调用CoGroupedRDD.mapValues返回MapPartitionsRDD,其迭代器返回`RDD[(K, (Iterable[V], Iterable[W]))]`
    // 类型(核心是ExternalAppendOnlyMap,比较复杂),将两个rdd相同的key关联了起来,然后将key对应的空迭代器过滤掉(交集要求两边都有),
    // 最后将keys返回,即为交集。需要shuffle,将rdd1和rdd2中元素处理成(k,v)格式,然后拉取相同的分区数据做关联处理
    println("---------intersection-----------")
    val intersectionRDD: RDD[Int] = rdd1.intersection(rdd2)
    // 3, 4, 5
    intersectionRDD.foreach(println)

    val kv1: RDD[(String, Int)] = sc.parallelize(List(
      ("zhangsan", 11),
      ("zhangsan", 12),
      ("lisi", 13),
      ("wangwu", 14)
    ))
    val kv2: RDD[(String, Int)] = sc.parallelize(List(
      ("zhangsan", 21),
      ("zhangsan", 22),
      ("lisi", 23),
      ("zhaoliu", 28)
    ))
    println("-----------cogroup-------------")
    // cogroup是比较重要且复杂的算子,很多算子都是基于这个算子实现的,cogroup用于将多个rdd的相同key对应的迭代器组合成元组返回
    // 内部是通过ExternalAppendOnlyMap实现这些迭代器的,比较复杂。需要shuffle,将所有rdd中元素处理成(k,v)格式,然后拉取相同的分区数据做关联处理
    val cogroupRDD: RDD[(String, (Iterable[Int], Iterable[Int]))] = kv1.cogroup(kv2)
    // 输出下面格式的数据
    // (zhangsan,(CompactBuffer(11, 12),CompactBuffer(21, 22)))
    // (wangwu,(CompactBuffer(14),CompactBuffer()))
    cogroupRDD.foreach(println)
    // join,leftOuterJoin,rightOuterJoin,fullOuterJoin与sql查询中的join,left join,right join,full join类似
    // spark中对各种join的处理方式都是类似的,先调用cogroup算子,将相同的key关联起来,然后对相同key对应的元素做处理
    // 然后根据以下的条件返回结果迭代器:join左右两边都有,leftOuterJoin以左边为主并关联右边,rightOuterJoin以右边为主并关联左边
    // fullOuterJoin包含左右两边(有关联的会关联起来)
    println("------------join--------------")
    val joinRDD: RDD[(String, (Int, Int))] = kv1.join(kv2)
    joinRDD.foreach(println)
    println("--------leftOuterJoin----------")
    val leftRDD: RDD[(String, (Int, Option[Int]))] = kv1.leftOuterJoin(kv2)
    leftRDD.foreach(println)
    println("--------rightOuterJoin----------")
    val rightRDD: RDD[(String, (Option[Int], Int))] = kv1.rightOuterJoin(kv2)
    rightRDD.foreach(println)
    println("--------fullOuterJoin----------")
    val fullRDD: RDD[(String, (Option[Int], Option[Int]))] = kv1.fullOuterJoin(kv2)
    fullRDD.foreach(println)

    Thread.sleep(Long.MaxValue)
  }
}

排序

准备数据data/pvuvdata

43.169.217.152	河北	2018-11-12	1542011088714	3292380437528494072	www.dangdang.com	Login
43.169.217.152	河北	2018-11-12	1542011088714	3292380437528494072	www.mi.com	View
43.169.217.152	河北	2018-11-12	1542011088714	3292380437528494072	www.suning.com	Click
42.134.182.213	山东	2018-11-12	1542011088714	3445974150374613566	www.jd.com	Buy
42.62.88.214	新疆	2018-11-12	1542011088714	734986595720971991	www.baidu.com	Click
42.62.88.214	新疆	2018-11-12	1542011088714	734986595720971991	www.gome.com.cn	Comment
42.62.88.214	新疆	2018-11-12	1542011088714	734986595720971991	www.mi.com	View
42.62.88.214	新疆	2018-11-12	1542011088714	734986595720971991	www.suning.com	Comment
42.62.88.214	新疆	2018-11-12	1542011088714	734986595720971991	www.baidu.com	Click
199.111.148.214	重庆	2018-11-12	1542011088714	6755235587059844279	www.suning.com	Regist
199.111.148.214	重庆	2018-11-12	1542011088714	6755235587059844279	www.dangdang.com	Login
199.111.148.214	重庆	2018-11-12	1542011088714	6755235587059844279	www.taobao.com	Comment
38.56.129.131	江苏	2018-11-12	1542011088714	8702416521942550873	www.taobao.com	Comment
38.56.129.131	江苏	2018-11-12	1542011088714	8702416521942550873	www.baidu.com	Regist
38.56.129.131	江苏	2018-11-12	1542011088715	8702416521942550873	www.gome.com.cn	Comment
119.84.168.158	贵州	2018-11-12	1542011088715	5857482780010208273	www.dangdang.com	Click
119.84.168.158	贵州	2018-11-12	1542011088715	5857482780010208273	www.dangdang.com	Buy
119.84.168.158	贵州	2018-11-12	1542011088715	5857482780010208273	www.suning.com	Regist
119.84.168.158	贵州	2018-11-12	1542011088715	5857482780010208273	www.taobao.com	Click
119.84.168.158	贵州	2018-11-12	1542011088715	5857482780010208273	www.suning.com	Login
45.23.35.53	山西	2018-11-12	1542011088715	1301348123877460710	www.jd.com	Regist
45.23.35.53	山西	2018-11-12	1542011088715	1301348123877460710	www.baidu.com	Buy
45.23.35.53	山西	2018-11-12	1542011088715	1301348123877460710	www.suning.com	Login
61.110.25.65	河北	2018-11-12	1542011088715	1508694707109465431	www.suning.com	View
93.25.99.182	香港	2018-11-12	1542011088715	3150433864389465552	www.jd.com	Click
210.194.31.33	吉林	2018-11-12	1542011088715	5637538432970238335	www.dangdang.com	Comment
210.194.31.33	吉林	2018-11-12	1542011088715	5637538432970238335	www.mi.com	View
210.194.31.33	吉林	2018-11-12	1542011088715	5637538432970238335	www.gome.com.cn	Buy
173.42.20.105	山东	2018-11-12	1542011088715	4079146439210804560	www.taobao.com	Click
163.220.52.175	西藏	2018-11-12	1542011088715	6896510364090486931	www.taobao.com	Comment
163.220.52.175	西藏	2018-11-12	1542011088715	6896510364090486931	www.dangdang.com	Click
163.220.52.175	西藏	2018-11-12	1542011088715	6896510364090486931	www.gome.com.cn	Buy
163.220.52.175	西藏	2018-11-12	1542011088715	6896510364090486931	www.mi.com	Click
163.220.52.175	西藏	2018-11-12	1542011088715	6896510364090486931	www.gome.com.cn	Buy
141.31.4.186	贵州	2018-11-12	1542011088716	5268949894385639228	www.dangdang.com	Buy
141.31.4.186	贵州	2018-11-12	1542011088716	5268949894385639228	www.dangdang.com	View
141.31.4.186	贵州	2018-11-12	1542011088716	5268949894385639228	www.baidu.com	Login
121.1.22.187	湖南	2018-11-12	1542011088716	3476508798435319353	www.suning.com	Login
167.121.63.79	广西	2018-11-12	1542011088716	7910646026572205914	www.mi.com	Login
167.121.63.79	广西	2018-11-12	1542011088716	7910646026572205914	www.suning.com	Click
167.121.63.79	广西	2018-11-12	1542011088716	7910646026572205914	www.suning.com	Login
120.75.146.181	北京	2018-11-12	1542011088716	4933452878523270316	www.jd.com	Login
120.75.146.181	北京	2018-11-12	1542011088716	4933452878523270316	www.gome.com.cn	Regist
120.75.146.181	北京	2018-11-12	1542011088716	4933452878523270316	www.gome.com.cn	Comment
219.170.221.181	四川	2018-11-12	1542011088716	2633888254925382570	www.dangdang.com	Comment
219.170.221.181	四川	2018-11-12	1542011088716	2633888254925382570	www.mi.com	View
219.170.221.181	四川	2018-11-12	1542011088716	2633888254925382570	www.mi.com	Click
200.152.116.157	天津	2018-11-12	1542011088716	5517041872894122647	www.taobao.com	View
200.152.116.157	天津	2018-11-12	1542011088716	5517041872894122647	www.suning.com	Click
200.152.116.157	天津	2018-11-12	1542011088716	5517041872894122647	www.dangdang.com	Buy
163.0.9.29	海南	2018-11-12	1542011088716	532408424460444672	www.gome.com.cn	Buy
163.0.9.29	海南	2018-11-12	1542011088716	532408424460444672	www.suning.com	Login
163.0.9.29	海南	2018-11-12	1542011088717	532408424460444672	www.baidu.com	View
92.69.72.209	吉林	2018-11-12	1542011088717	4659564693178957605	www.taobao.com	Click
135.71.189.40	四川	2018-11-12	1542011088717	7611839728353795784	www.mi.com	Login
135.71.189.40	四川	2018-11-12	1542011088717	7611839728353795784	www.gome.com.cn	Buy
135.71.189.40	四川	2018-11-12	1542011088717	7611839728353795784	www.jd.com	View
222.67.60.134	河北	2018-11-12	1542011088717	8453354782733786967	www.gome.com.cn	Click
56.59.131.147	河南	2018-11-12	1542011088717	1376065164260188803	www.dangdang.com	Click
56.59.131.147	河南	2018-11-12	1542011088717	1376065164260188803	www.taobao.com	Click
56.59.131.147	河南	2018-11-12	1542011088717	1376065164260188803	www.gome.com.cn	View
56.59.131.147	河南	2018-11-12	1542011088717	1376065164260188803	www.baidu.com	Comment
56.59.131.147	河南	2018-11-12	1542011088717	1376065164260188803	www.mi.com	Regist
14.163.142.177	福建	2018-11-12	1542011088717	6157734588800785954	www.gome.com.cn	Login
14.163.142.177	福建	2018-11-12	1542011088717	6157734588800785954	www.jd.com	View
14.163.142.177	福建	2018-11-12	1542011088717	6157734588800785954	www.jd.com	Buy
160.31.142.138	青海	2018-11-12	1542011088717	7288565426254633650	www.jd.com	Comment
142.35.167.197	山东	2018-11-12	1542011088717	2559179413765866490	www.gome.com.cn	Click
180.119.159.210	新疆	2018-11-12	1542011088718	2021297540185889848	www.taobao.com	View
180.119.159.210	新疆	2018-11-12	1542011088718	2021297540185889848	www.mi.com	Comment
180.119.159.210	新疆	2018-11-12	1542011088718	2021297540185889848	www.baidu.com	Regist
220.128.9.91	广东	2018-11-12	1542011088718	1046293197132671059	www.jd.com	View
220.128.9.91	广东	2018-11-12	1542011088718	1046293197132671059	www.suning.com	Click
220.128.9.91	广东	2018-11-12	1542011088718	1046293197132671059	www.jd.com	Click
174.157.1.146	山东	2018-11-12	1542011088718	2756308592022842621	www.dangdang.com	Login
174.157.1.146	山东	2018-11-12	1542011088718	2756308592022842621	www.mi.com	Comment
174.157.1.146	山东	2018-11-12	1542011088718	2756308592022842621	www.dangdang.com	Buy
174.157.1.146	山东	2018-11-12	1542011088718	2756308592022842621	www.gome.com.cn	Buy
174.157.1.146	山东	2018-11-12	1542011088718	2756308592022842621	www.dangdang.com	Click
object SortTest {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local").setAppName("SortTest")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    // PV,UV, 原始数据格式如下
    // 43.169.217.152	河北	2018-11-12	1542011088714	3292380437528494072	www.dangdang.com	Login
    // 需求:根据数据计算各网站的PV,UV,同时,只显示top5
    // 解题:要按PV值,或者UV值排序,取前5名
    val fileRDD: RDD[String] = sc.textFile("data/pvuvdata", 8)
    println("----------PV:-----------")
    val res1: RDD[(String, Int)] =
      fileRDD.map(line => (line.split("\t")(5), 1)) // 将一行数据切割取第6个元素url,组合成(url, 1)的格式
        .reduceByKey(_ + _) // 对元组(k,v)相同k的v执行相加,即统计同一个url的数量
        .map(_.swap) // 将(k,v)反转
        .sortByKey(ascending = false) // 按照k倒序排序,上面执行了反转,所以这里是按照url的数量进行倒序排序
        .map(_.swap) // 再次反转,结果变成(url,count)格式
    // sortByKey会产生一个job,这个job会先对数据进行抽样,将数据划分成不同的区间,然后在每个区间对数据进行排序,结果就是全排序的
    // sortByKey需要通过shuffle进行排序
    val pv: Array[(String, Int)] = res1.take(5) // 取前5条数据
    pv.foreach(println)
    println("----------UV:-----------")
    // uv需要对同一个用户访问同一个网站进行去重,这里使用ip代表用户
    val keys: RDD[(String, String)] = fileRDD.map(line => {
      val arr = line.split("\t")
      (arr(5), arr(0))
    }) // 将一行数据切割取第6个元素url和第1个元素ip,组合成(url,ip)的格式,代表一个用户的一次访问
    val res2: RDD[(String, Int)] = keys.distinct() // 对(url,ip)去重,这样一个用户的多次访问被规整成一次
      .map(k => (k._1, 1)) // 将(url,ip)转换成(url,1),下面的操作与上面统计pv相同
      .reduceByKey(_ + _) // 对元组(k,v)相同k的v执行相加,即统计同一个url的数量
      .sortBy(_._2, ascending = false) // 对元组中的数据按照v倒序排序(不需要经过反转,可以直接对v进行排序)
    // sortBy内部会将待排序的数据转换成key,然后调用sortByKey,最后再转回来
    val uv: Array[(String, Int)] = res2.take(5) // 取前5条数据
    uv.foreach(println)

    Thread.sleep(Long.MaxValue)
  }
}

聚合

object AggTest {

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local").setAppName("AggTest")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    val data: RDD[(String, Int)] = sc.parallelize(List(
      ("zhangsan", 234),
      ("zhangsan", 5667),
      ("zhangsan", 343),
      ("lisi", 212),
      ("lisi", 44),
      ("lisi", 33),
      ("wangwu", 535),
      ("wangwu", 22)
    ))
    println("----------groupByKey------------")
    // 按照key分组,groupByKey最终会调用combineByKeyWithClassTag实现分组
    val group: RDD[(String, Iterable[Int])] = data.groupByKey()
    group.foreach(println)
    // 行列转换
    println("------------行列转换--------------")
    // 将(zhangsan,(234, 5667, 343))转换成(zhangsan,234)这样的格式,然后扁平化处理,这样就可以实现行转列
    val res01: RDD[(String, Int)] = group.flatMap(e => e._2.map(v => (e._1, v)))
    res01.foreach(println)
    println("---------flatMapValues-----------")
    // 对values这个整体做扁平化处理,与上面的res01结果一样,内部实现也与上面的类似(flatMap和map组合实现的)
    group.flatMapValues(e => e).foreach(println)
    // 对values这个整体做处理
    println("-----------mapValues-------------")
    group.mapValues(e => e.toList.sorted).foreach(println)
    // 对values这个整体做扁平化处理,这里将values排序并取前2个
    println("---------flatMapValues-----------")
    group.flatMapValues(e => e.toList.sorted.take(2)).foreach(println)
    // 求sum,count,min,max,avg
    val sum: RDD[(String, Int)] = data.reduceByKey(_ + _)
    val count: RDD[(String, Int)] = data.mapValues(_ => 1).reduceByKey(_ + _)
    val min: RDD[(String, Int)] = data.reduceByKey((k1, k2) => if (k1 > k2) k2 else k1)
    val max: RDD[(String, Int)] = data.reduceByKey((k1, k2) => if (k1 > k2) k1 else k2)
    // 这里求平均值,拉取两次数据(求次数、求总和),可以优化
    val avg: RDD[(String, Int)] = sum.join(count).mapValues(e => e._1 / e._2)
    println("-------------sum-----------------")
    sum.foreach(println)
    println("-------------count----------------")
    count.foreach(println)
    println("-------------min-----------------")
    min.foreach(println)
    println("-------------max-----------------")
    max.foreach(println)
    println("-------------avg-----------------")
    avg.foreach(println)
    // 一次求出sum,count,avg
    println("---------sum,count,avg-----------")
    // combineByKey内部调用combineByKeyWithClassTag
    // 将value转换成(value, 1),这样可以对每个(value, 1)相加,最终得到(sum, count),这样就可以一次把sum,count,avg求出来
    data.combineByKey(
      // 将value转换成元组(value, 1)
      (value: Int) => (value, 1),
      // 如果有第二条记录,第二条以及以后他们的value如何合并到元组中
      (oldValue: (Int, Int), newValue: Int) => (oldValue._1 + newValue, oldValue._2 + 1),
      // 合并溢写结果的函数,如何合并两个元组
      (v1: (Int, Int), v2: (Int, Int)) => (v1._1 + v2._1, v1._2 + v2._2)
    ).foreach(e => println(s"${e._1} sum=${e._2._1} count=${e._2._2} avg=${e._2._1 / e._2._2}"))
    // 一次求出sum,count,min,max,avg
    println("-----sum,count,min,max,avg--------")
    data.combineByKey(
      // 将value转换成元组(sum, count, min, max)
      (value: Int) => (value, 1, value, value),
      (oldValue: (Int, Int, Int, Int), newValue: Int) => (oldValue._1 + newValue, oldValue._2 + 1,
        if (oldValue._3 > newValue) newValue else oldValue._3,
        if (oldValue._4 < newValue) newValue else oldValue._4),
      (v1: (Int, Int, Int, Int), v2: (Int, Int, Int, Int)) => (v1._1 + v2._1, v1._2 + v2._2,
        if (v1._3 > v2._3) v2._3 else v1._3,
        if (v1._4 < v2._4) v2._4 else v1._4)
    ).foreach(e => println(s"${e._1} sum=${e._2._1} count=${e._2._2} avg=${e._2._1 / e._2._2} min=${e._2._3} max=${e._2._4}"))
    Thread.sleep(Long.MaxValue)
  }
}

分区

object PartitionsTest {

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local").setAppName("PartitionsTest")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    // 元素1-10,2个分区
    val data: RDD[Int] = sc.parallelize(1 to 10, 2)
    // 模拟外关联sql查询
    // 这里相当于每处理一个元素都创建并关闭一次数据库连接,性能很差
    val res01: RDD[String] = data.map(value => {
      println("------conn--mysql----")
      println(s"-----select $value-----")
      println("-----close--mysql------")
      value + "selected"
    })
    res01.foreach(println)
    // 优化成每个分区创建并关闭一次数据库连接
    println("-------------------优化1--------------------")
    // pIndex是分区索引,pIterator是分区对应的迭代器
    val res02: RDD[String] = data.mapPartitionsWithIndex((pIndex: Int, pIterator: Iterator[Int]) => {
      // 致命的!根据之前源码发现spark就是一个pipeline,迭代器嵌套的模式,数据不会在内存积压
      // 这里数据会在内存积压,当一个分区数据很多时一定会发生OOM
      val listBuffer = new ListBuffer[String]
      println(s"--$pIndex----conn--mysql----")
      while (pIterator.hasNext) {
        val value: Int = pIterator.next()
        println(s"---$pIndex--select $value-----")
        listBuffer += value + "selected"
      }
      println(s"---$pIndex---close--mysql")
      listBuffer.iterator
    })
    res02.foreach(println)
    // 继续优化不在内存积压数据
    println("-------------------优化2--------------------")
    val res03: RDD[String] = data.mapPartitionsWithIndex((pIndex: Int, pIterator: Iterator[Int]) => {
      new Iterator[String] {
        println(s"--$pIndex----conn--mysql----")

        override def hasNext: Boolean = if (pIterator.hasNext) true else {
          println(s"---$pIndex---close--mysql")
          false
        }

        override def next(): String = {
          val value = pIterator.next()
          println(s"---$pIndex--select $value-----")
          value + "selected"
        }
      }
    })
    res03.foreach(println)

    Thread.sleep(Long.MaxValue)
  }
}

高级

object GaojiTest {

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local").setAppName("PartitionsTest")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    val data: RDD[Int] = sc.parallelize(1 to 10, 5)
    println("----------------------")
    // 第一个参数表示是否重复抽样,第二个参数不重复抽样时表示抽样的比例,重复抽样时表示每个元素被选择的平均次数(可以大于1),第三个参数表示随机抽样时的种子
    // 在其它条件一样的情况下,种子相同多次抽样的结果也相同,种子默认是一个随机值表示每次抽样的结果都不同
    data.sample(true, 0.1, 222).foreach(println)
    println("----------------------")
    data.sample(true, 0.1, 222).foreach(println)
    println("----------------------")
    data.sample(false, 0.1).foreach(println)
    // 下面调整分区数量
    println("----------修改分区数量------------")
    println(s"data:${data.getNumPartitions}")
    // 将原始数据value转换成(value, 分区索引)的格式
    val data1: RDD[(Int, Int)] = data.mapPartitionsWithIndex((pIndex, pIterator) => pIterator.map(e => (pIndex, e)))
    // 增大分区数量
    println("----repartition增大分区数量------")
    val data2 = data1.repartition(6)
    println(s"data2:${data2.getNumPartitions}")
    // 清晰地打印出原始的分区和现在的分区,可以看到分区被打乱了,执行了shuffle
    data2.mapPartitionsWithIndex((pIndex, pIterator) => pIterator.map(e => (pIndex, e))).foreach(println)
    // 减少分区数量
    println("----repartition减少分区数量------")
    val data3 = data1.repartition(4)
    println(s"data3:${data3.getNumPartitions}")
    // 清晰地打印出原始的分区和现在的分区,可以看到分区被打乱了,执行了shuffle
    data3.mapPartitionsWithIndex((pIndex, pIterator) => pIterator.map(e => (pIndex, e))).foreach(println)
    // RDD.repartition方法会调用`coalesce(numPartitions, shuffle = true)`,这样不管增大分区还是减小分区都会执行shuffle
    // 可以直接调用coalesce减少分区而不执行shuffle(会直接IO全量拷贝分区)

    // 不执行shuffle是不可以增大分区的,增大分区一定会存在将某个分区的数据拆分到多个分区中,这就是shuffle的语义
    // 调用RDD.coalesce不执行shuffle增大分区是没有效果的,分区数量还是和原来的一样
    println("---coalesce不shuffle增大分区数量---")
    val data4 = data1.coalesce(6, shuffle = false)
    println(s"data4:${data4.getNumPartitions}")
    data4.mapPartitionsWithIndex((pIndex, pIterator) => pIterator.map(e => (pIndex, e))).foreach(println)

    println("---coalesce不shuffle减少分区数量---")
    val data5 = data1.coalesce(4, shuffle = false)
    println(s"data5:${data5.getNumPartitions}")
    // 可以看到原始分区是全量落到新的分区中,分区全量拷贝没有执行shuffle,可以在WebUI中`DAG Visualization`清晰地观察到
    data5.mapPartitionsWithIndex((pIndex, pIterator) => pIterator.map(e => (pIndex, e))).foreach(println)

    // 从上面可以看出,调用repartition重新分区,数据有一个散列的过程,因为需要把一个分区的数据映射到其它不同的多个分区,因此会产生shuffle
    // 调用底层算子coalesce,可以不产生shuffle。分区数减少,可以不产生shuffle;但是如果分区数增大,需要将一个分区数据散落到多个分区,一定会产生shuffle,此时调用coalesce参数shuffle=false是不起作用的
    // 数据移动有IO直接移动,也有shuffle移动。直接IO只需要拿到前面IO对应的迭代器即可,不需要做其它处理
    // 大多数情况下直接IO拷贝不产生shuffle性能会更好,但是当数据不均衡时,或者想增大执行的并行度时,shuffle也是必须的,会使执行效率更高(可以通过shuffle使数据重新均衡,可以通过shuffle增大并行度)
    Thread.sleep(Long.MaxValue)
  }

}

TopN

准备数据data/tqdata

2019-6-1	39
2019-5-21	33
2019-6-1	38
2019-6-2	31
2019-6-3	33
2019-6-4	35
2019-6-4	40
2018-3-11	18
2018-4-23	22
1970-8-23	23
1970-8-8	32
object TopNTest {

  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local").setAppName("TopNTest")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    // 统计每个月温度最高的2天
    val file: RDD[String] = sc.textFile("data/tqdata")
    // 2019-6-1	39
    // 将数据转换成 (年,月,日,温度)
    val data: RDD[(Int, Int, Int, Int)] = file.map(line => line.split("\t")).map(arr => {
      val info: Array[String] = arr(0).split("-")
      // (year,month,day,wd)
      (info(0).toInt, info(1).toInt, info(2).toInt, arr(1).toInt)
    })
    // 第一版,如果一个键的值太多groupByKey可能会导致OOM,且自己的算子实现了函数:去重、排序
    println("-------------------第一版---------------------")
    // 将数据转换成((年,月), (日,温度))然后按照(年,月)分组
    val group1: RDD[((Int, Int), Iterable[(Int, Int)])] =
      data.map(t4 => ((t4._1, t4._2), (t4._3, t4._4))).groupByKey()
    // 已经按月分组,下面取出每月中温度最高的2天
    val res1: RDD[((Int, Int), List[(Int, Int)])] = group1.mapValues(arr => {
      // 使用map保存某天的最高温度(会对某天的温度进行去重取最大值),arr本身可能会导致OOM,这里使用的map最多保存31天的数据不会导致OOM
      val map = new scala.collection.mutable.HashMap[Int, Int]()
      arr.foreach(x => if (map.getOrElse(x._1, Int.MinValue) < x._2) map.put(x._1, x._2))
      // map中保存的数据是(天,温度),转换成list然后按照温度倒序,最后取前2个
      map.toList.sorted((x: (Int, Int), y: (Int, Int)) => y._2.compareTo(x._2)).take(2)
    })
    res1.foreach(println)
    // 第二版,使用groupByKey容易OOM  取巧:通过RDD.reduceByKey取max间接达到去重,让自己的算子变的简单点。但是如果reduceByKey没有大幅减少数据,则后面的groupByKey仍然容易OOM
    // 第一种做法可能会产生内存溢出,因此应该先做去重,这样会多一次shuffle,但是不会产生内存溢出,因此产生shuffle不一定是坏事,需要具体问题具体分析
    println("-------------------第二版---------------------")
    // 将数据转换成((年,月,日), 温度),然后按照(年,月,日)执行reduce(逻辑是取最大温度),这样就对每天的数据就进行了去重,每天只会保留一条数据(温度最高的),经过这一次处理数据量就大大减少了
    val reduced2: RDD[((Int, Int, Int), Int)] =
      data.map(t4 => ((t4._1, t4._2, t4._3), t4._4)).reduceByKey((x, y) => if (y > x) y else x)
    // 接下来与第一版的处理方式一样,这里数据已经大大减少groupByKey是安全的
    // 将数据转换成((年,月), (日,温度))然后按照(年,月)分组
    val group2: RDD[((Int, Int), Iterable[(Int, Int)])] =
    reduced2.map(t2 => ((t2._1._1, t2._1._2), (t2._1._3, t2._2))).groupByKey()
    // 注意:执行shuffle时,尽量不要破坏多级shuffle的key的子集关系,即后面执行shuffle时的key是前面执行shuffle时的key的子集。
    // 这样就保证了后面的shuffle是前面shuffle的子集,这样shuffle就不是全分区shuffle,而是单分区shuffle,效率比全分区shuffle高很多。
    // 即后面的shuffle只需要从前面的某一个分区拉数据数据,而不需要在所有分区拉取数据,这样可以大幅提升IO的效率,因为后面shuffle需要IO的数据都在一起。
    // 这里reduceByKey是按照(年,月,日)shuffle的,groupByKey是按照(年,月)shuffle的,没有破坏多级shuffle的key的子集关系

    // 对两个(Int, Int)排序的隐式转换,名称无所谓
    implicit val myCustomOrdering = new Ordering[(Int, Int)] {
      override def compare(x: (Int, Int), y: (Int, Int)) = y._2.compareTo(x._2)
    }
    // 下面将一个月中的数据(日,温度),按照温度排序并取前2个,上面已经有排序的隐式转换,sorted方法可以不用传Ordering了
    val res2: RDD[((Int, Int), List[(Int, Int)])] = group2.mapValues(arr => arr.toList.sorted.take(2))
    res2.foreach(println)
    // 第二版中需要先调用reduceByKey再调用groupByKey才能实现,需要执行2次shuffle;第三版调用一次combineByKey实现通用版的topN,只需要执行1次shuffle
    // 分布式是并行的,离线批量计算有个特征就是后续步骤(stage)依赖前一步骤(stage),如果前一步骤(stage)能够加上正确的combineByKey尽量压缩(减少)数据,
    // 使后一步骤处理的数据尽量少,这样后一步骤可以将数据全部放到内存中处理,这是调优的关键
    println("-------------------第三版---------------------")
    // 将新数据合并到数组中,原数组是有序的,返回的数组也是有序的,按照温度倒序,前面已经指定了myCustomOrdering隐式转换用于排序
    // 合并的规则是:有空位置直接占据空位置,同一天的数据取温度最大的,否则找到比新元素温度小的元素然后插入(这里的逻辑是基于数组是有序的)
    def combineArr(oldValue: Array[(Int, Int)], newValue: (Int, Int)): Array[(Int, Int)] = {
      var flag = true
      var i = 0
      // 新的温度大于数组中温度时数组的下标,数组是有序的只需求一次
      var j = -1
      while (i < oldValue.length && flag) {
        val day = oldValue(i)._1
        if (day == Int.MinValue) { // 表示该位置还没有被占用
          oldValue(i) = newValue
          flag = false
          // 修改了数组需要排序,不使用oldValue.sorted进行排序,它不是在原数据集上排序的会产生新的对象,combineByKey中第二函数(这里)会频繁地被调用,这样可以减少大量的GC
          scala.util.Sorting.quickSort(oldValue)
        } else if (day == newValue._1) { // 找到了同一天的数据
          if (oldValue(i)._2 < newValue._2) { // 有更大温度
            oldValue(i) = newValue
            // 修改了数组需要排序
            scala.util.Sorting.quickSort(oldValue)
          }
          flag = false
        } else {
          // 到这里说明没有空位置,且没有同一天的数据,则查询出新数据的温度大于数组中最大温度的下标
          // 数组是有序的只需求一次
          if (newValue._2 > oldValue(i)._2 && j == -1) {
            j = i
          }
        }
        i += 1
      }
      if (flag && j != -1) {
        // 到这里说明,新数据需要插入到数组中替换老的数据
        // 将数组中最后一个数据抛弃,从j位置开始数据往后移动,新数据放到j位置
        i = oldValue.length - 1
        while (i > j) {
          oldValue(i) = oldValue(i - 1)
          i -= 1
        }
        oldValue(j) = newValue
      }
      oldValue
    }

    // n表示top几
    val n: Int = 3
    // 将数据转换成((年,月), (日,温度)),调用一次combineByKey取topN
    val data3: RDD[((Int, Int), Array[(Int, Int)])] = data.map(t4 => ((t4._1, t4._2), (t4._3, t4._4))).combineByKey(
      // 第一条记录怎么放
      (value: (Int, Int)) => {
        // 使用Array,在求topN时,N可以方便地变化
        val arr = new Array[(Int, Int)](n)
        arr(0) = value
        for (i <- 1 until n) {
          arr(i) = (Int.MinValue, Int.MinValue)
        }
        arr
      },
      // 第二条记录及后续的怎么放
      combineArr,
      (v1: Array[(Int, Int)], v2: Array[(Int, Int)]) => {
        // 将v1和v2合并:同一天取温度最大的,最终只取温度最大的topN,n等于v1数组的长度
        // 遍历v2调用combineArr将v2中的元素合并到v1即可
        var res = v1
        for (elem <- v2) {
          res = combineArr(res, elem)
        }
        res
      }
    )
    // 将数组中空的数据过滤掉(上面处理时引入的Int.MinValue),并转换成列表(数组不可打印列表可以)
    val res3: RDD[((Int, Int), List[(Int, Int)])] = data3.map(x => (x._1, x._2.filter(_._1 != Int.MinValue).toList))
    res3.foreach(println)

    Thread.sleep(Long.MaxValue)
  }
}

其它

object OtherTest {

  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local").setAppName("OtherTest")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    // 统计单词数量,并将最终结果乘10
    val data: RDD[String] = sc.parallelize(List(
      "hello world",
      "hello spark",
      "hello world",
      "hello hadoop",
      "hello world",
      "hello flink",
      "hello world"
    ))
    val baseRes = data.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
    // 这里的groupByKey只是为了演示,没有实际意义
    // map方法创建MapPartitionsRDD,preservesPartitioning为false不保留分区器,后面调用groupByKey会使用新的分区器,这样会执行shuffle
    val res01 = baseRes.map(e => (e._1, e._2 * 10)).groupByKey()
    // mapValues方法创建MapPartitionsRDD,preservesPartitioning为true保留分区器,后面调用groupByKey会使用已有的分区器,这样不会执行shuffle
    // flatMapValues也是类似的,因此如果只是针对value的操作,应该使用mapValues和flatMapValues,这样可以保留分区器,进而减少shuffle
    val res02 = baseRes.mapValues(_ * 10).groupByKey()
    res01.foreach(println)
    println("---------------------------")
    res02.foreach(println)
    Thread.sleep(Int.MaxValue)
  }

}

image.png

集群模式概述

组件

Spark应用在集群上以独立的进程集合的形式运行,由主程序(称为驱动器程序)中的SparkContext对象协调

具体来说,为了在集群上运行,SparkContext可以连接到多种类型的集群管理器(Spark自己的独立集群管理器、Mesos、YARN或Kubernetes),这些管理器会在应用之间分配资源。一旦连接成功,Spark就会获取集群节点上的执行器进程(executor),这些进程是运行计算和为应用存储数据的进程。接下来,它会把应用代码(由传给SparkContext的JAR或Python文件定义)发送给执行器进程。最后,SparkContext将任务发送到执行器进程运行

image.png

关于这个架构,有几点需要注意

  1. 每个应用程序都有自己的执行器进程,这些进程在整个应用程序期间保持运行,并在多个线程中运行任务。这样做的好处是可以在调度端(每个驱动程序调度自己的任务)和执行端(来自不同应用程序的任务在不同的jvm中运行)将应用程序彼此隔离。然而,这也意味着如果不将数据写入外部存储系统,就不能在不同的Spark应用程序(SparkContext实例)之间共享数据
  2. Spark对底层集群管理器是不可知的。只要它可以获取执行进程,并且这些进程之间相互通信,即使在支持其他应用程序(例如Mesos/YARN/Kubernetes)的集群管理器上运行它也相对容易
  3. 驱动程序必须在其整个生命周期中侦听并接受来自其执行程序的传入连接(例如,参见网络配置部分中的spark.driver.port)。因此,驱动程序必须可以从工作节点进行网络寻址
  4. 因为驱动程序在集群上调度任务,所以它应该在工作节点附近运行,最好是在同一个局域网上。如果希望远程向集群发送请求,最好打开一个RPC请求到驱动程序,并让它从附近提交操作,而不是在远离工作节点的地方运行驱动程序

集群管理器类型

系统目前支持以下几种集群管理器

  • Standalone 一个简单的集群管理器,它包含在Spark中,可以很容易地设置集群
  • Apache Mesos 一个通用的集群管理器,也可以运行Hadoop MapReduce和服务应用程序(弃用)
  • Hadoop YARN Hadoop3中的资源管理器
  • Kubernetes 一个用于自动化部署、扩展和管理容器化应用程序的开源系统

提交应用程序

可以使用spark-submit脚本向任何类型的集群提交应用程序。应用程序提交指南描述了如何执行此操作

监控

每个驱动程序都有一个web UI,通常在端口4040上,显示有关运行任务、执行器和存储使用情况的信息。只需在web浏览器中访问http://<driver-node>:4040即可访问此UI。监控指南还描述了其他监控选项

作业调度

Spark 允许用户在不同层级控制资源分配:一方面是在应用程序之间进行控制(集群管理器层面),另一方面是在单个应用程序内部进行控制(如果同一 SparkContext 上正在进行多个计算任务)。作业调度概述提供了更详细的说明

术语表

以下表格总结了用于指代集群概念的术语

术语说明
Application基于 Spark 构建的用户程序。包含一个驱动程序和集群上的执行器
Application jar包含用户 Spark 应用程序的 JAR 文件。在某些情况下,用户可能希望创建一个“全量 jar”,其中包含他们的应用程序及其依赖项。然而,用户的 JAR 文件不应包含 Hadoop 或 Spark 的库,这些库将在运行时添加
Driver program运行应用程序的 main() 函数并创建 SparkContext 的进程
Cluster manager集群上用于获取资源的外部服务(例如 standalone manager, Mesos, YARN, Kubernetes)
Deploy mode区分驱动程序进程运行的位置。在"cluster"模式下,框架在集群内部启动驱动程序。在"client"模式下,提交者在集群外部启动驱动程序。
Worker node集群中任何能够运行应用程序代码的节点
Executor在工作节点上为应用程序启动的进程,它运行任务并在它们之间保留数据于内存或磁盘存储中。每个应用程序都有自己的执行器
Task将被发送到一个执行器上的工作单元
Job作为对 Spark 操作(如save, collect)响应而生成的包含多个任务的并行计算;您会在驱动程序的日志中看到这个术语
Stage每个作业被划分为更小的任务集,这些任务集称为阶段,它们相互依赖(类似于MapReduce中的map和reduce阶段);您会在驱动程序的日志中看到这个术语的使用

集群部署

hadoop使用3.3.6,spark使用3.4.3

准备192.168.91.61-192.168.91.644个节点,内存2G,CPU2C,硬盘20GB,主机名称node01-node04,集群划分如下

节点NNDNZKZKFCJNRMNM
node01
node02
node03
node04

Hadoop HA部署

准备

可以在一台节点准备,然后克隆虚拟机

# 所有节点 root用户

# hosts 配置
cat >> /etc/hosts << "EOF"
192.168.91.61  node01
192.168.91.62  node02
192.168.91.63  node03
192.168.91.64  node04
EOF

# 关闭防火墙
systemctl stop firewalld && systemctl disable firewalld

# 关闭selinux
sed -ri 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
setenforce 0

# 时间同步配置,最小化安装系统需要安装ntpdate软件
yum -y install ntpdate
echo "0 */1 * * * ntpdate time1.aliyun.com" >> /var/spool/cron/root
systemctl enable ntpdate && systemctl start ntpdate

# 在HDFS集群搭建完成后,在Namenode HA切换进行故障转移时采用SSH方式进行,底层会使用到fuster包
yum -y install psmisc

# 创建bigdata用户
useradd bigdata
passwd bigdata

# 创建大数据组件安装目录和数据目录
mkdir /opt/bigdata
chown -R bigdata:bigdata /opt/bigdata
mkdir /var/bigdata
chown -R bigdata:bigdata /var/bigdata

# 切换到bigdata用户
su - bigdata
cd /opt/bigdata

# JDK安装
wget https://builds.openlogic.com/downloadJDK/openlogic-openjdk/8u392-b08/openlogic-openjdk-8u392-b08-linux-x64.tar.gz
tar xf openlogic-openjdk-8u392-b08-linux-x64.tar.gz
mv openlogic-openjdk-8u392-b08-linux-x64 ./jdk8
rm -f openlogic-openjdk-8u392-b08-linux-x64.tar.gz
echo 'export JAVA_HOME=/opt/bigdata/jdk8' >> ~/.bash_profile
echo 'export PATH=$PATH:${JAVA_HOME}/bin' >> ~/.bash_profile
source ~/.bash_profile

zookeeper集群搭建

# node02、node03、node04 bigdata用户
cd /opt/bigdata

wget https://archive.apache.org/dist/zookeeper/zookeeper-3.6.4/apache-zookeeper-3.6.4-bin.tar.gz
tar xf apache-zookeeper-3.6.4-bin.tar.gz
mv apache-zookeeper-3.6.4-bin ./apache-zookeeper-3.6.4
rm -f apache-zookeeper-3.6.4-bin.tar.gz
echo 'export ZOOKEEPER_HOME=/opt/bigdata/apache-zookeeper-3.6.4' >> ~/.bash_profile
echo 'export PATH=$PATH:${ZOOKEEPER_HOME}/bin' >> ~/.bash_profile
source ~/.bash_profile
# 创建配置文件
cat > $ZOOKEEPER_HOME/conf/zoo.cfg << EOF
# 发送心跳的间隔时间,单位:毫秒
tickTime=2000
initLimit=10
syncLimit=5
# ZooKeeper保存数据的目录
dataDir=/var/bigdata/zookeeper/data
# 日志目录
dataLogDir=/var/bigdata/zookeeper/log
clientPort=2181
# 各个节点配置
server.1=node02:2881:3881
server.2=node03:2881:3881
# observer(表示对应节点不参与投票)
server.3=node04:2881:3881
EOF

# 为每个zk节点创建数据目录,并在该目录创建一个文件myid,在myid中写下当前zk的编号
# 所有zk节点
mkdir -p /var/bigdata/zookeeper/data
# node02
echo 1 > /var/bigdata/zookeeper/data/myid
# node03
echo 2 > /var/bigdata/zookeeper/data/myid
# node04
echo 3 > /var/bigdata/zookeeper/data/myid

# 在所有zk节点分别启动ZooKeeper,zkServer.sh start|stop|status
# 启动zk,这里暂不启动,后面启动hadoop时再启动
zkServer.sh start
# 停止zk
zkServer.sh stop
# 查看zk状态
zkServer.sh status

hadoop集群安装

# node01 bigdata用户

# 免密配置
ssh-keygen -t rsa -P '' -f  ~/.ssh/id_rsa
# 启动start-dfs.sh脚本的节点需要将公钥分发给所有节点
ssh-copy-id node01
ssh-copy-id node02
ssh-copy-id node03
ssh-copy-id node04
# 在HA模式下,每一个NN身边会启动ZKFC,ZKFC会用免密的方式控制自己和其他NN节点的NN状态
# 配置NN节点之间的互相免密,node01到node02免密已经配置过了,这里配置node02到node01免密
ssh node02
ssh-keygen -t rsa -P '' -f  ~/.ssh/id_rsa
ssh-copy-id node01
ssh-copy-id node02
exit

cd /opt/bigdata

# hadoop部署
wget https://dlcdn.apache.org/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz
tar xf hadoop-3.3.6.tar.gz
rm -f hadoop-3.3.6.tar.gz
chown -R bigdata:bigdata hadoop-3.3.6
echo 'export HADOOP_HOME=/opt/bigdata/hadoop-3.3.6' >> ~/.bash_profile
echo 'export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin' >> ~/.bash_profile
source ~/.bash_profile

# hadoop配置

# hadoop-env.sh脚本中JAVA_HOME配置
sed -i 's%# export JAVA_HOME=%export JAVA_HOME=/opt/bigdata/jdk8%' $HADOOP_HOME/etc/hadoop/hadoop-env.sh
# datanode节点配置
cat > $HADOOP_HOME/etc/hadoop/workers << "EOF"
node02
node03
node04
EOF
# start-dfs.sh和stop-dfs.sh脚本中用户配置
cat > user_tmp.txt << EOF
HDFS_DATANODE_USER=bigdata
HDFS_DATANODE_SECURE_USER=hdfs
HDFS_NAMENODE_USER=bigdata
HDFS_JOURNALNODE_USER=bigdata
HDFS_ZKFC_USER=bigdata
EOF
sed -i '17r user_tmp.txt' $HADOOP_HOME/sbin/start-dfs.sh
sed -i '17r user_tmp.txt' $HADOOP_HOME/sbin/stop-dfs.sh
rm -f user_tmp.txt

# core-site.xml和hdfs-site.xml配置文件如下所示

$HADOOP_HOME/etc/hadoop/core-site.xml

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
  <property>
	<name>fs.defaultFS</name>
	<value>hdfs://mycluster</value>   
  </property>
  <property>
	<name>hadoop.tmp.dir</name>
	<value>/var/bigdata/hadoop/ha</value>
  </property>
  <!-- 指定每个zookeeper服务器的位置和客户端端口号 -->
  <property>
	 <name>ha.zookeeper.quorum</name>
	 <value>node02:2181,node03:2181,node04:2181</value>
   </property>
  <!-- 建立连接的重试次数,ha模式下启动hdfs时,namenode需要与JournalNode建立连接,因为是同时启动的,JournalNode还没有准备好因此需要重试。这个配置默认是10,在虚拟机中的测试环境,节点性能较差,需要将这个配置改大,否则namenode可能起不来,需要单独手动启动 -->
  <property>
	<name>ipc.client.connect.max.retries</name>
	<value>100</value>   
  </property>
</configuration>

$HADOOP_HOME/etc/hadoop/hdfs-site.xml

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
  <!-- 指定副本的数量 -->
  <property>
    <name>dfs.replication</name>
    <value>3</value>
  </property>
  <!-- 以下是一对多,逻辑到物理节点的映射 -->
  <!-- 解析参数dfs.nameservices值hdfs://mycluster的地址 -->
  <property>
    <name>dfs.nameservices</name>
    <value>mycluster</value>
  </property>
  <!-- 测试环境中可以通过这个配置,关闭权限检查,方便操作 -->
  <!--
  <property>
    <name>dfs.permissions.enabled</name>
    <value>false</value>
  </property>
  -->
  <!-- mycluster由以下两个namenode支撑 -->
  <property>
    <name>dfs.ha.namenodes.mycluster</name>
    <value>nn1,nn2</value>
  </property>
  <!-- 指定nn1地址和端口号 -->
  <property>
    <name>dfs.namenode.rpc-address.mycluster.nn1</name>
    <value>node01:8020</value>
  </property>
  <!-- 指定nn2地址和端口号 -->
  <property>
    <name>dfs.namenode.rpc-address.mycluster.nn2</name>
    <value>node02:8020</value>
  </property>
  <!-- 下面两个http端口的配置,配置的是默认值,可以不用配置 -->
  <property>
    <name>dfs.namenode.http-address.mycluster.nn1</name>
    <value>node01:9870</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.mycluster.nn2</name>
    <value>node02:9870</value>
  </property>
  <!-- 指定客户端查找active的namenode的策略:会给所有namenode发请求,以决定哪个是active的 -->
  <property>
    <name>dfs.client.failover.proxy.provider.mycluster</name>
    <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
  </property>
  <!-- 指定三台journal node服务器的地址,以及数据存储的目录 -->
  <!-- JournalNode可以在不同hdfs集群之间共享,不同的hdfs集群这里配置不同的存储目录即可 -->
  <property>
    <name>dfs.namenode.shared.edits.dir</name>
    <value>qjournal://node01:8485;node02:8485;node03:8485/mycluster</value>
  </property>
  <!-- journal服务数据存储目录 -->
  <property>
    <name>dfs.journalnode.edits.dir</name>
    <value>/var/bigdata/hadoop/journal/node/local/data</value>
  </property>
  <!-- 当active nn出现故障时,免密ssh到对应的服务器,将namenode进程kill掉  -->
  <property>
    <name>dfs.ha.fencing.methods</name>
    <value>sshfence</value>
  </property>
  <!-- ssh免密公钥路径 -->
  <property>
    <name>dfs.ha.fencing.ssh.private-key-files</name>
    <value>/bigdata/.ssh/id_rsa</value>
  </property>
  <!-- 启动NN故障自动切换,即启动zkfc -->
  <property>
    <name>dfs.ha.automatic-failover.enabled</name>
    <value>true</value>
  </property>
</configuration>
# node01 bigdata用户
cd /opt/bigdata
tar -zcvf hadoop-3.3.6-config.tar.gz hadoop-3.3.6/
for i in 2 3 4; do scp hadoop-3.3.6-config.tar.gz node0$i:`pwd`; done
# node02-node04 节点 bigdata用户
cd /opt/bigdata
tar xf hadoop-3.3.6-config.tar.gz
rm -f hadoop-3.3.6-config.tar.gz
echo 'export HADOOP_HOME=/opt/bigdata/hadoop-3.3.6' >> ~/.bash_profile
echo 'export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin' >> ~/.bash_profile
source ~/.bash_profile

启动HA 的HDFS

# 以下都是在bigdata用户

# 1.启动zookeeper集群
# node02、node03、node04
$ZOOKEEPER_HOME/bin/zkServer.sh start
# 2.格式化zookeeper
# node01
hdfs zkfc -formatZK
# 3.启动Journalnode,格式化namenode时需要先启动Journalnode
# node01、node02、node03
hdfs --daemon start journalnode
# 4.选择一个Namenode格式化,格式化Namenode,会连接journalnode创建相应目录(因此journalnode需要先启动,可以通过journalnode的日志和数据目录观察到)
# node01
hdfs namenode -format
# 5.启动这个格式化的namenode
# 在node01上启动NameNode,便于后期同步Namenode
hdfs --daemon start namenode
# 6.同步元数据
# 在node02上同步NameNode元数据
hdfs namenode -bootstrapStandby
# 7.启动HDFS集群,只有第一次启动hdfs集群时需要先执行上面的步骤,后面启动hdfs集群时只需要执行start-dfs.sh即可,该脚本会同时启动NN,ZKFC、JN、DN,但是zk还是得先启动
# node01
$HADOOP_HOME/sbin/start-dfs.sh

# node02,查看zk的锁
zkCli.sh
# 可以看到node01上的NN是active状态的
get /hadoop-ha/mycluster/ActiveStandbyElectorLock

        myclusternn1node01
# 推出zkCli
quit

Spark Standalone 部署

# node01 bigdata用户
cd /opt/bigdata
wget https://dlcdn.apache.org/spark/spark-3.4.3/spark-3.4.3-bin-hadoop3.tgz
tar xf spark-3.4.3-bin-hadoop3.tgz
rm -f spark-3.4.3-bin-hadoop3.tgz
echo 'export SPARK_HOME=/opt/bigdata/spark-3.4.3-bin-hadoop3' >> ~/.bash_profile
echo 'export PATH=$PATH:$SPARK_HOME/bin' >> ~/.bash_profile
source ~/.bash_profile

# worker节点配置
cat > $SPARK_HOME/conf/workers << EOF
node02
node03
node04
EOF

# 环境配置
cp $SPARK_HOME/conf/spark-env.sh.template $SPARK_HOME/conf/spark-env.sh
# 下面分别配置:hadoop配置所在目录、master host(每台改成自己的主机名)、master port、webui port(不要用8080与zk冲突)、cpu核心数、内存
# 通过sed,在指定行插入配置
sed -i "25a HADOOP_CONF_DIR=/opt/bigdata/hadoop-3.3.6/etc/hadoop" $SPARK_HOME/conf/spark-env.sh
# 通过sed,查找并追加配置
sed -i \
-e '/SPARK_MASTER_HOST/a export SPARK_MASTER_HOST=node01' \
-e '/SPARK_MASTER_PORT/a export SPARK_MASTER_PORT=7077' \
-e '/SPARK_MASTER_WEBUI_PORT/a export SPARK_MASTER_WEBUI_PORT=8090' \
-e '/SPARK_WORKER_CORES/a export SPARK_WORKER_CORES=4' \
-e '/SPARK_WORKER_MEMORY/a export SPARK_WORKER_MEMORY=2g' \
$SPARK_HOME/conf/spark-env.sh

# 配置JAVA_HOME
echo 'export JAVA_HOME=/opt/bigdata/jdk8' >> $SPARK_HOME/sbin/spark-config.sh

# 将配置好的spark发送到其它节点
tar -zcvf spark-3.4.3-bin-hadoop3-config.tar.gz spark-3.4.3-bin-hadoop3/
for i in 2 3 4; do scp spark-3.4.3-bin-hadoop3-config.tar.gz node0$i:`pwd`; done
# node02-node04 节点 bigdata用户

cd /opt/bigdata
tar xf spark-3.4.3-bin-hadoop3-config.tar.gz
rm -f spark-3.4.3-bin-hadoop3-config.tar.gz
echo 'export SPARK_HOME=/opt/bigdata/spark-3.4.3-bin-hadoop3' >> ~/.bash_profile
echo 'export PATH=$PATH:$SPARK_HOME/bin' >> ~/.bash_profile
source ~/.bash_profile
# node01 bigdata用户

# 启动spark
$SPARK_HOME/sbin/start-all.sh

部署好了可以通过webui查看

image.png

准备启停脚本

/opt/bigdata/my-start-all.sh

#!/bin/bash
for zknode in node02 node03 node04
do
    ssh $zknode "source ~/.bash_profile;zkServer.sh start"
done

sleep 1

$HADOOP_HOME/sbin/start-dfs.sh
sleep 1

$SPARK_HOME/sbin/start-all.sh
sleep 1

echo "=====node01 jps====="
jps

for other_node in node02 node03 node04
do
   echo "=====$other_node jps====="
   ssh $other_node "source ~/.bash_profile;jps"
done

/opt/bigdata/my-stop-all.sh

#!/bin/bash

$SPARK_HOME/sbin/stop-all.sh
sleep 1

$HADOOP_HOME/sbin/stop-dfs.sh
sleep 1

for zknode in node02 node03 node04
do
    ssh $zknode "source ~/.bash_profile;zkServer.sh stop"
done

echo "=====node01 jps====="
jps

for other_node in node02 node03 node04
do
   echo "=====$other_node jps====="
   ssh $other_node "source ~/.bash_profile;jps"
done
# node01 bigdata用户

# 给脚本授予执行权限
chmod +x /opt/bigdata/start-all.sh /opt/bigdata/stop-all.sh

# 停止
/opt/bigdata/stop-all.sh
# 启动
/opt/bigdata/start-all.sh

cd ~
# 准备数据
cat > data.txt << EOF
hello java
hello scala
hello java scala
hello spark scala
hello flink scala
EOF
# 上传到hdfs
hdfs dfs -mkdir /test
hdfs dfs -put data.txt /test/

# 使用spark-shell连接服务端
spark-shell --master spark://node01:7077
...
Spark context Web UI available at http://node01:4040
Spark context available as 'sc' (master = spark://node01:7077, app id = app-20240619171328-0000)
...

# 下面在spark-shell中操作
# 这里的路径是hdfs中的路径,hdfs://mycluster/test/data.txt 前缀可以省略
# 需要先调用collect将数据收集在客户端才可以打印
sc.textFile("/test/data.txt").flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).collect().foreach(println)
(scala,4)
(flink,1)
(hello,5)
(java,2)
(spark,1)

# 退出
:quit

Master HA

# node01 bigdata用户

cd $SPARK_HOME/conf
cp spark-defaults.conf.template spark-defaults.conf
cat >> spark-defaults.conf << EOF
spark.deploy.recoveryMode        ZOOKEEPER
spark.deploy.zookeeper.url       node02:2181,node03:2181,node04:2181
spark.deploy.zookeeper.dir       /myspark
EOF
# 分发配置文件到其它节点
for i in 2 3 4; do scp spark-defaults.conf node0$i:`pwd`;done
# 修改node02节点也是master,SPARK_MASTER_HOST每台master改成自己的主机名
ssh node02 "sed -i 's/SPARK_MASTER_HOST=node01/SPARK_MASTER_HOST=node02/' $SPARK_HOME/conf/spark-env.sh"

# 重启spark
$SPARK_HOME/sbin/stop-all.sh && $SPARK_HOME/sbin/start-all.sh

# 在node02启动master
ssh node02 "$SPARK_HOME/sbin/start-master.sh"

# 停止node01的master,通过webui http://node01:8090/ 和 http://node02:8090/ 可以观察到node02 master 变成ALIVE状态了(需要等1分钟左右)。查看zk中的数据`get /myspark/master_status`依然是192.168.91.61,不知道为什么???
$SPARK_HOME/sbin/stop-master.sh

# 再次启动node01的master,变成STANDBY状态
$SPARK_HOME/sbin/start-master.sh

# 使用spark-shell连接服务端,在node02上执行`$SPARK_HOME/sbin/stop-master.sh`停止master
spark-shell --master spark://node01:7077,node02:7077
...
# 可以观察到报错,再次按回车键
24/06/19 20:30:35 WARN StandaloneAppClient$ClientEndpoint: Connection to node02:7077 failed; waiting for master to reconnect...

# spark-shell中操作
# 成功执行,故障自动转移了
sc.textFile("/test/data.txt").flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).collect().foreach(println)

# 退出
:quit

history server

每个SparkContext启动一个Web UI,默认在端口4040上,显示有关应用程序的有用信息。默认情况下,此信息仅在应用程序期间可用。要在事后查看web UI,请在启动应用程序之前将spark.eventLog.enabled设置为true。这将Spark配置为记录将UI中显示的信息编码到持久化存储的Spark事件

application将事件日志记录到hdfs中,history server服务从hdfs中读取日志记录然后通过Web UI展示

# node01 bigdata用户

cd $SPARK_HOME/conf
# 第一个配置:启动记录事件日志,第二个配置:事件日志保存的路径,第三个配置:history服务读取日志记录的路径
cat >> spark-defaults.conf << EOF
spark.eventLog.enabled        true
spark.eventLog.dir            hdfs://mycluster/spark_log
spark.history.fs.logDirectory hdfs://mycluster/spark_log
EOF

# 分发配置文件到其它节点
for i in 2 3 4; do scp spark-defaults.conf node0$i:`pwd`;done

# 先在hdfs中创建日志目录
hdfs dfs -mkdir /spark_log

# node04
# 启动,不需要重启spark
$SPARK_HOME/sbin/start-history-server.sh

# node01
# 执行application,计算层会将自己的计算日志存入hdfs
spark-shell --master spark://node01:7077,node02:7077
sc.textFile("/test/data.txt").flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).collect().foreach(println)
:quit

http://node04:18080/可以通过这个访问history,即使SparkContext对应的Web服务关闭了,application执行的日志也可以在这里查到

image.png

提交程序 spark-submit

./bin/spark-submit \
  --class <main-class> \
  --master <master-url> \
  --deploy-mode <deploy-mode> \
  --conf <key>=<value> \
  ... # other options
  <application-jar> \
  [application-arguments]
选项说明
--class应用程序的入口点(例如:org.apache.spark.examples.SparkPi)
--master集群的主URL(例如:spark://node01:7077)
--deploy-mode是在工作节点(cluster)上部署驱动程序,还是在本地作为外部客户端(client)部署驱动程序(默认:client)
--confkey=value格式的任意Spark配置属性。对于包含空格的值,用引号括起来"key=value"。多个配置应该作为单独的参数传递(例如--conf <key>=<value> --conf <key2>=<value2>
application-jar包含应用程序和所有依赖项的打包jar的路径。URL必须在集群中全局可见,例如,hdfs://路径或所有节点上存在的file://路径
--driver-memory驱动内存(例如1000M, 2G)(默认:1024M)
--executor-memory每个执行程序的内存(例如1000M, 2G)(默认:1G)
--driver-cores驱动程序使用的核数,仅在集群模式下使用(默认:1)
--total-executor-cores所有执行器的总核数,仅在standalone 和 Mesos 中使用
--executor-cores每个执行器使用的核数。(YARN和K8S模式下默认是1,在standalone模式下是工作节点上的所有可用核心)
--num-executors要启动的执行器数量(默认值:2)。 如果启用了动态分配,初始执行器数量将至少为 NUM(该选项的配置)。只用于YARN 和 Kubernetes

driver和executor会在jvm中存储中间计算过程的数据的元数据,其使用的最大内存可以通过--driver-memory和--executor-memory参数调整,当小文件非常多时,元数据也会比较多,可以根据需要调整这两个参数

会通过--total-executor-cores和--executor-cores两个值计算executor的数量,总核心数不能超过--total-executor-cores,至少启动一个executor,如果剩余的核心数小于executor-cores,则不会再启动executor

client模式

# node01 bigdata用户
# 执行求π的程序,参数表示分区的数量。默认客户端模式,可以在客户端看到完整的日志
$SPARK_HOME/bin/spark-submit \
--master spark://node01:7077,node02:7077 \
--class org.apache.spark.examples.SparkPi \
$SPARK_HOME/examples/jars/spark-examples_2.12-3.4.3.jar \
10
...
Pi is roughly 3.1445751445751444
...

cluster模式

# node01 bigdata用户
# 集群模式,大部分日志不在客户端显示
$SPARK_HOME/bin/spark-submit \
--master spark://node01:7077,node02:7077 \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
$SPARK_HOME/examples/jars/spark-examples_2.12-3.4.3.jar \
10

应用日志在master ui和history ui中都可以查看,driver日志只能在master ui中查看,master重启之后在其ui中将看不到任何日志

image.png

image.png

image.png

调整核心数

# node01 bigdata用户

$SPARK_HOME/bin/spark-submit \
--master spark://node01:7077,node02:7077 \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
--total-executor-cores 6 \
$SPARK_HOME/examples/jars/spark-examples_2.12-3.4.3.jar \
1000


$SPARK_HOME/bin/spark-submit \
--master spark://node01:7077,node02:7077 \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
--total-executor-cores 6 \
--executor-cores 4 \
$SPARK_HOME/examples/jars/spark-examples_2.12-3.4.3.jar \
1000

image.png

--total-executor-cores=6,--executor-cores没有指定,则启动3个executor每个占用2个core

image.png

--total-executor-cores=6, --executor-cores=4,启动1个executor占用4个core,还剩2个core,不足以启动一个executor,则不再启动

image.png

Spark On Yarn 部署

停止集群

# bigdata用户

# node01
/opt/bigdata/my-stop-all.sh

# node02
$SPARK_HOME/sbin/stop-master.sh

# node04
$SPARK_HOME/sbin/stop-history-server.sh

YARN RM-HA搭建

在node01节点上修改配置,然后发送到其它节点,注意切换到bigdata用户

$HADOOP_HOME/etc/hadoop/mapred-site.xml,MR可以与Spark共存,这里也配置MR

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
  <!-- 让MapReduce任务运行时使用Yarn资源调度框架进行调度 -->
  <property>
    <name>mapreduce.framework.name</name>
    <value>yarn</value>
  </property>
  <!-- 起MapReduce任务时需要下面的配置 -->
  <property>
    <name>yarn.app.mapreduce.am.env</name>
    <value>HADOOP_MAPRED_HOME=${HADOOP_HOME}</value>
  </property>
  <property>
    <name>mapreduce.map.env</name>
    <value>HADOOP_MAPRED_HOME=${HADOOP_HOME}</value>
  </property>
  <property>
    <name>mapreduce.reduce.env</name>
    <value>HADOOP_MAPRED_HOME=${HADOOP_HOME}</value>
  </property>
  <!-- 配置MR的history服务 -->
  <property>
    <name>mapreduce.jobhistory.address</name>
    <value>node03:10020</value>
  </property>
  <property>
    <name>mapreduce.jobhistory.webapp.address</name>
    <value>node03:50060</value>
  </property>
  <property>
    <name>mapreduce.jobhistory.intermediate-done-dir</name>
    <value>/work/mr_history_tmp</value>
  </property>
  <property>
    <name>mapreduce.jobhistory.done-dir</name>
    <value>/work/mr-history_done</value>
  </property>    
</configuration>

$HADOOP_HOME/etc/hadoop/yarn-site.xml

<?xml version="1.0"?>
<configuration>
  <!-- 让yarn的容器支持mapreduce的shuffle,开启shuffle服务 -->
  <property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
  </property>
  <!-- 启用resourcemanager的HA -->
  <property>
    <name>yarn.resourcemanager.ha.enabled</name>
    <value>true</value>
  </property>
  <!-- 指定zookeeper集群的各个节点地址和端口号 -->
  <property>
    <name>hadoop.zk.address</name>
    <value>node02:2181,node03:2181,node04:2181</value>
  </property>
  <!-- 标识集群,以确保RM不会接管另一个集群的活动,与core-site.xml中保持一致 -->
  <!-- 这个会反应在zk中,可以用于在zk中隔离不同的yarn集群 -->
  <property>
    <name>yarn.resourcemanager.cluster-id</name>
    <value>mycluster</value>
  </property>
  <!-- RM HA的两个resourcemanager的名字 -->
  <property>
    <name>yarn.resourcemanager.ha.rm-ids</name>
    <value>rm1,rm2</value>
  </property>
  <!-- 指定rm1的reourcemanager进程所在的主机名称 -->
  <property>
    <name>yarn.resourcemanager.hostname.rm1</name>
    <value>node03</value>
  </property>
  <!-- RM HTTP访问地址,不配置的话在shell中提交MR任务时会报错"org.apache.hadoop.mapreduce.v2.app.client.MRClientService: Webapps failed to start. Ignoring for now:java.lang.NullPointerException" -->
  <!-- 默认为 ${yarn.resourcemanager.hostname}:8088 -->
  <property>
    <name>yarn.resourcemanager.webapp.address.rm1</name>
    <value>node03:8088</value>
  </property>
  <!-- 指定rm2的reourcemanager进程所在的主机名称 -->
  <property>
    <name>yarn.resourcemanager.hostname.rm2</name>
    <value>node04</value>
  </property>
  <property>
    <name>yarn.resourcemanager.webapp.address.rm2</name>
    <value>node04:8088</value>
  </property>
  <!-- 关闭虚拟内存检查,生产环境不能关闭,测试环境资源不够应该关闭,否则后面对yarn的操作可能会报内存不足 -->
  <property>
    <name>yarn.nodemanager.vmem-check-enabled</name>  
    <value>false</value>  
  </property>
  <!-- ResourceManager重启恢复 -->
  <!-- 启用RM启动后恢复状态。如果为true,则必须指定yarn.resourcemanager.store.class。默认为false -->    
  <property>
    <name>yarn.resourcemanager.recovery.enabled</name>
    <value>true</value>
  </property>
  <!-- 指定RM将自己的状态存储在哪里,下面是默认值,存储在文件系统(本地或hdfs) -->  
  <property>
    <name>yarn.resourcemanager.store.class</name>
    <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.FileSystemRMStateStore</value>
  </property>
  <!-- URI指向存储RM状态的文件系统路径,上面配置了RM状态存储在文件系统,这个必须配置,可以配置本地或hdfs -->
  <property>
    <name>yarn.resourcemanager.fs.state-store.uri</name>
    <value>hdfs://mycluster/rmstore</value>
  </property>
  <!-- 配置NodeManager重启自动恢复 -->  
  <!-- 启用节点管理器启动后恢复 -->
  <property>
    <name>yarn.nodemanager.recovery.enabled</name>
    <value>true</value>
  </property>
  <!-- 当恢复功能启用时,节点管理器将用于存储状态的本地文件系统目录。 -->
  <property>
    <name>yarn.nodemanager.recovery.dir</name>
    <value>/var/bigdata/hadoop/yarn-nm-recovery</value>
  </property>
  <!-- NM的RPC地址,默认为${yarn.nodemanager.hostname}:0,即随机使用临时端口。一定要指定为一个固定端口,否则NM重启之后会更换端口,就无法恢复Container的状态了 -->
  <property>
    <name>yarn.nodemanager.address</name>
    <value>${yarn.nodemanager.hostname}:8041</value>
  </property>
  <!-- 可以分配给容器的物理内存大小,以MB为单位 -->
  <property>
    <name>yarn.nodemanager.resource.memory-mb</name>
    <value>2048</value>
  </property>
  <!-- 可以分配给容器的vcore数量 -->  
  <property>
    <name>yarn.nodemanager.resource.cpu-vcores</name>
    <value>4</value>
  </property>
</configuration>
# node01 bigdata用户

# 在start-yarn.sh和stop-yarn.sh脚本中加入两行配置,指定操作Yarn的用户
cat > user_tmp.txt << EOF
YARN_RESOURCEMANAGER_USER=bigdata
YARN_NODEMANAGER_USER=bigdata
EOF

sed -i '17r user_tmp.txt' $HADOOP_HOME/sbin/start-yarn.sh
sed -i '17r user_tmp.txt' $HADOOP_HOME/sbin/stop-yarn.sh
rm -f user_tmp.txt

# NM节点配置,修改这个配置文件 $HADOOP_HOME/etc/hadoop/workers,与DN节点配置公用一个配置文件,即NM与DN一一对应且在同一个节点,上面启动HDFS时已经配置过了(如果只启动yarn这里不要忘记配置)

# yarn配置文件和启停脚本发送到所有其它节点
for i in 2 3 4; do scp $HADOOP_HOME/etc/hadoop/mapred-site.xml node0$i:$HADOOP_HOME/etc/hadoop/; scp $HADOOP_HOME/etc/hadoop/yarn-site.xml node0$i:$HADOOP_HOME/etc/hadoop/; scp $HADOOP_HOME/sbin/*-yarn.sh node02:$HADOOP_HOME/sbin/;done

修改spark配置文件

spark on yarn :不需要 master,worker的配置

# node01 bigdata用户
cd $SPARK_HOME/conf

cp spark-env.sh.template spark-env.sh
# 只需要配制hadoop路径
sed -i "25a HADOOP_CONF_DIR=/opt/bigdata/hadoop-3.3.6/etc/hadoop" spark-env.sh

cp spark-defaults.conf.template spark-defaults.conf
# 只需要配制history服务
cat >> spark-defaults.conf << EOF
spark.eventLog.enabled        true
spark.eventLog.dir            hdfs://mycluster/spark_log
spark.history.fs.logDirectory hdfs://mycluster/spark_log
EOF

# 删除workers
rm -f workers

# 分发文件
for i in 2 3 4; do scp spark-env.sh spark-defaults.conf node0$i:`pwd`; ssh node0$i "rm -f `pwd`/workers"; done

修改启停脚本

/opt/bigdata/my-start-all.sh

#!/bin/bash
for zknode in node02 node03 node04
do
    ssh $zknode "source ~/.bash_profile;zkServer.sh start"
done

sleep 1

$HADOOP_HOME/sbin/start-all.sh
sleep 1

echo "=====node01 jps====="
jps

for other_node in node02 node03 node04
do
   echo "=====$other_node jps====="
   ssh $other_node "source ~/.bash_profile;jps"
done

/opt/bigdata/my-stop-all.sh

#!/bin/bash

$HADOOP_HOME/sbin/stop-all.sh
sleep 1

for zknode in node02 node03 node04
do
    ssh $zknode "source ~/.bash_profile;zkServer.sh stop"
done

echo "=====node01 jps====="
jps

for other_node in node02 node03 node04
do
   echo "=====$other_node jps====="
   ssh $other_node "source ~/.bash_profile;jps"
done
# node01 bigdata用户

cd ~

# 启动hadoop和yarn集群
/opt/bigdata/my-start-all.sh

# 测试MR
rm -f data.txt
hdfs dfs -mkdir -p /data/test/input
for i in `seq 100000`;do echo "hello hadoop $i" >> data.txt;done
hdfs dfs -put data.txt /data/test/input
cd $HADOOP_HOME/share/hadoop/mapreduce

hadoop jar hadoop-mapreduce-examples-3.3.6.jar wordcount /data/test/input /data/test/output

可以在yarn页面中看到History按钮,需要启动history服务才可以查看

image.png

# node03 bigdata用户

# 查看historyserver在hdfs中的数据目录,已经有数据了
hdfs dfs -ls -R /work
drwxrwx---   - bigdata supergroup          0 2024-06-24 16:59 /work/mr-history_done
drwxrwxrwt   - bigdata supergroup          0 2024-06-24 16:57 /work/mr_history_tmp
drwxrwx---   - bigdata supergroup          0 2024-06-24 16:57 /work/mr_history_tmp/bigdata
-rwxrwx---   3 bigdata supergroup      22894 2024-06-24 16:57 /work/mr_history_tmp/bigdata/job_1719219350251_0001-1719219424082-bigdata-word+count-1719219439511-1-1-SUCCEEDED-default-1719219428618.jhist
-rwxrwx---   3 bigdata supergroup        442 2024-06-24 16:57 /work/mr_history_tmp/bigdata/job_1719219350251_0001.summary
-rwxrwx---   3 bigdata supergroup     276035 2024-06-24 16:57 /work/mr_history_tmp/bigdata/job_1719219350251_0001_conf.xml

# 启动mr history服务
mapred --daemon start historyserver

在Yarn页面中点击History按钮,跳转到history

image.png

Spark操作

上面已经启动了yarn,不需要启动Spark的master和worker,只需要提交spark作业即可

# node01 bigdata用户

spark-shell --master yarn
...
24/06/25 11:35:51 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
24/06/25 11:35:53 WARN Client: Neither spark.yarn.jars nor spark.yarn.archive is set, falling back to uploading libraries under SPARK_HOME.
Spark context Web UI available at http://node01:4040
Spark context available as 'sc' (master = yarn, app id = application_1719286363901_0002).
Spark session available as 'spark'.
...

sc.textFile("/test/data.txt").flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).collect().foreach(println)
(scala,4)
(flink,1)
(hello,5)
(java,2)
(spark,1)

# 在退出spark-shell之前,使用jps查看各个节点启动的jvm,可以看到多了YarnCoarseGrainedExecutorBackend和ExecutorLauncher,分别对应于standalone模式下的worker中的executor和ApplicationMaster
:quit


# 在程序执行完毕之前,使用jps查看各个节点启动的jvm,可以看到多了YarnCoarseGrainedExecutorBackend和ApplicationMaster
$SPARK_HOME/bin/spark-submit \
--master yarn \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
$SPARK_HOME/examples/jars/spark-examples_2.12-3.4.3.jar \
10000

# node04
# 启动spark history,spark-shell退出,spark-submit执行完毕,执行job日志只能在history中查看
$SPARK_HOME/sbin/start-history-server.sh

# 在spark-shell退出之前,可以查看任务在hdfs中的元数据,这个包括依赖的所有jar包
hdfs dfs -ls -R /user/bigdata/.sparkStaging
drwx------   - bigdata supergroup          0 2024-06-25 13:45 /user/bigdata/.sparkStaging/application_1719293940066_0002
-rw-r--r--   3 bigdata supergroup     277006 2024-06-25 13:45 /user/bigdata/.sparkStaging/application_1719293940066_0002/__spark_conf__.zip
-rw-r--r--   3 bigdata supergroup  332485137 2024-06-25 13:45 /user/bigdata/.sparkStaging/application_1719293940066_0002/__spark_libs__7543354283231040335.zip
# node01 bigdata用户

# 启动spark job时,例如spark-shell启动时,会报spark.yarn.jars或spark.yarn.archive没有设置的警告,这里会上传$SPARK_HOME/jars中的jar包,每次启动都会执行这一步,启动会比较慢,且任务在hdfs中的元数据较大(包括依赖的jar包)

# 提前将依赖的jar包上传到hdfs中,然后在配置文件中配置路径

# 上传jar包到hdfs
hdfs dfs -mkdir -p /work/spark_lib/
cd $SPARK_HOME/jars
zip -q -r spark_jars_3.4.3.zip *
hdfs dfs -put spark_jars_3.4.3.zip /work/spark_lib/

# 修改配置
echo 'spark.yarn.archive            hdfs://mycluster/work/spark_lib/spark_jars_3.4.3.zip' >> $SPARK_HOME/conf/spark-defaults.conf

# 分发到其它节点
for i in 2 3 4; do scp $SPARK_HOME/conf/spark-defaults.conf node0$i:$SPARK_HOME/conf/; done

# 消除这个警告后,进入spark-shell时依然较慢
spark-shell --master yarn
...
24/06/25 14:07:08 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Spark context Web UI available at http://node04:4040
Spark context available as 'sc' (master = yarn, app id = application_1719295377340_0005).
Spark session available as 'spark'.
...

:quit

# 在spark-shell退出之前,可以查看任务在hdfs中的元数据,不再包括依赖的jar包,元数据大大减少了
hdfs dfs -ls -R /user/bigdata/.sparkStaging
drwx------   - bigdata supergroup          0 2024-06-25 13:57 /user/bigdata/.sparkStaging/application_1719293940066_0003
-rw-r--r--   3 bigdata supergroup     277018 2024-06-25 13:57 /user/bigdata/.sparkStaging/application_1719293940066_0003/__spark_conf__.zip