spark编程基础(算子详解)

一 spark简介

Spark是一种快速、通用、可扩展的大数据分析引擎,2009年诞生于加州大学伯克利分校AMPLab,2010年开源,2013年6月成为Apache孵化项目,2014年2月成为Apache的顶级项目,2014年5月发布spark1.0,2016年7月发布spark2.0,2020年6月18日发布spark3.0.0

1.spark特点

  • Ease of Use:简洁易用
    • Spark支持 Java、Scala、Python和R等编程语言编写应用程序,大大降低了使用者的门槛。自带了80多个高等级操作算子,并且允许在Scala,Python,R 的使用命令进行交互式运行,可以非常方便的在Spark Shell中地编写spark程序。
  • Generality:通用、全栈式数据处理
    • Spark提供了统一的大数据处理解决方案,非常具有吸引力,毕竟任何公司都想用统一的平台去处理遇到的问题,减少开发和维护的人力成本和部署平台的物力成本。 同时Spark还支持SQL,大大降低了大数据开发者的使用门槛,同时提供了SparkStream和Structed Streaming可以处理实时流数据;MLlib机器学习库,提供机器学习相关的统计、分类、回归等领域的多种算法实现。其高度封装的API 接口大大降低了用户的学习成本;Spark GraghX提供分布式图计算处理能力;PySpark支持Python编写Spark程序;SparkR支持R语言编写Spark程序。
  • Runs Everywhere:可以运行在各种资源调度框架和读写多种数据源
    • Spark支持的多种部署方案:Standalone是Spark自带的资源调度模式;Spark可以运行在Hadoop的YARN上面;Spark 可以运行在Mesos上(Mesos是一个类似于YARN的资源调度框架);Spark还可以Kubernetes实现容器化的资源调度
  • 丰富的数据源支持。Spark除了可以访问操作系统自身的本地文件系统和HDFS之外,还可以访问 Cassandra、HBase、Hive、Alluxio(Tachyon)以及任何 Hadoop兼容的数据源。这极大地方便了已经 的大数据系统进行顺利迁移到Spark。

2.Spark与MapReduce的对比

MapReduce和Spark的本质区别

  • MR只能做离线计算,如果实现复杂计算逻辑,一个MR搞不定,就需要将多个MR按照先后顺序连成一串,一个MR计算完成后会将计算结果写入到HDFS中,下一个MR将上一个MR的输出作为输入,这样就要频繁读写HDFS,网络IO和磁盘IO会成为性能瓶颈。从而导致效率低下。
  • 既可以做离线计算,有可以做实时计算,提供了抽象的数据集(RDD、Dataset、DataFrame、DStream)有高度封装的API,算子丰富,并且使用了更先进的DAG有向无环图调度思想,可以对执行计划优化后在执行,并且可以数据可以cache到内存中进行复用。
  • 注意:MR和Spark在Shuffle时数据都落本地磁盘

3.spark架构体系

  • 三种模式

    • standalone client模式
    • standalone cluster模式
    • Spark On YARN cluster模式
  • Spark执行流程简介

    • Job:RDD每一个行动操作都会生成一个或者多个调度阶段 调度阶段(Stage):每个Job都会根据依赖关系,以Shuffle过程作为划分,分为Shuffle Map Stage和Result Stage。每个Stage对应一个TaskSet,一个Task中包含多Task,TaskSet的数量与该阶段最后一个RDD的分区数相同。

    • Task:分发到Executor上的工作任务,是Spark的最小执行单元

    • DAGScheduler:DAGScheduler是将DAG根据宽依赖将切分Stage,负责划分调度阶段并Stage转成TaskSet提交给TaskScheduler

    • TaskScheduler:TaskScheduler是将Task调度到Worker下的Exexcutor进程,然后丢入到Executor的线程池的中进行执行

      image-20201221172758045
  • Spark中重要角色

    • Master :是一个Java进程,接收Worker的注册信息和心跳、移除异常超时的Worker、接收客户端提交的任务、负责资源调度、命令Worker启动Executor。
    • Worker :是一个Java进程,负责管理当前节点的资源管理,向Master注册并定期发送心跳,负责启动Executor、并监控Executor的状态。
    • SparkSubmit :是一个Java进程,负责向Master提交任务。
    • Driver :是很多类的统称,可以认为SparkContext就是Driver,client模式Driver运行在SparkSubmit进程中,cluster模式单独运行在一个进程中,负责将用户编写的代码转成Tasks,然后调度到Executor中执行,并监控Task的状态和执行进度。
    • Executor :是一个Java进程,负责执行Driver端生成的Task,将Task放入线程中运行。
  • Spark和Yarn角色对比(Spark StandAlone的Client模式对比YARN)

    • Master => ResourceManager
    • Worker => NodeManager
    • Executor => YarnChild
    • SparkSubmit(Driver) => ApplicationMaster

二 集群搭建及启动

1.下载与安装

  1. 下载spark,通过国内的镜像网站进行下载(如清华镜像站等)

  2. 传输到集群中的一台并解压

  3. 修改conf下的spark-env.sh文件

    export JAVA_HOME=/opt/apps/jdk1.8.0_141
    export SPARK_MASTER_HOST=linux01

  4. 修改conf下的slaves,加入worker的设备

2.spark-shell启动

/opt/apps/spark-3.0.1-bin-hadoop3.2/bin/spark-shell --master spark://linux01:7077 --executor-memory 2g --total-executor-cores 4

  • 设置的内存是每个worker占用的内存大小
  • 设置的核心数是整个集群的全部核心数量

三 spark编程入门

1. scala入门程序

使用Scala编写Spark的WorkCount

  1. 创建一个maven项目

  2. 导入依赖等信息

    <?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>cn._51doit</groupId>
        <artifactId>spark-in-action</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <!-- 定义了一些常量 -->
        <properties>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
            <scala.version>2.12.12</scala.version>
            <spark.version>3.0.1</spark.version>
            <encoding>UTF-8</encoding>
        </properties>
    
        <dependencies>
            <!-- 导入scala的依赖 -->
            <dependency>
                <groupId>org.scala-lang</groupId>
                <artifactId>scala-library</artifactId>
                <version>${scala.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.apache.spark</groupId>
                <artifactId>spark-core_2.12</artifactId>
                <version>${spark.version}</version>
            </dependency>
    
        </dependencies>
    
        <build>
            <pluginManagement>
                <plugins>
                    <!-- 编译scala的插件 -->
                    <plugin>
                        <groupId>net.alchim31.maven</groupId>
                        <artifactId>scala-maven-plugin</artifactId>
                        <version>3.2.2</version>
                    </plugin>
                    <!-- 编译java的插件 -->
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <version>3.5.1</version>
                    </plugin>
                </plugins>
            </pluginManagement>
            <plugins>
                <plugin>
                    <groupId>net.alchim31.maven</groupId>
                    <artifactId>scala-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>scala-compile-first</id>
                            <phase>process-resources</phase>
                            <goals>
                                <goal>add-source</goal>
                                <goal>compile</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>scala-test-compile</id>
                            <phase>process-test-resources</phase>
                            <goals>
                                <goal>testCompile</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
    
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <executions>
                        <execution>
                            <phase>compile</phase>
                            <goals>
                                <goal>compile</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
    
                <!-- 打jar插件 -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-shade-plugin</artifactId>
                    <version>2.4.3</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>shade</goal>
                            </goals>
                            <configuration>
                                <filters>
                                    <filter>
                                        <artifact>*:*</artifact>
                                        <excludes>
                                            <exclude>META-INF/*.SF</exclude>
                                            <exclude>META-INF/*.DSA</exclude>
                                            <exclude>META-INF/*.RSA</exclude>
                                        </excludes>
                                    </filter>
                                </filters>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    
    
    
    
    
    </project>
    
  3. 编写main方法

    package cn.doit.day01.demo01
    
    import org.apache.spark.rdd.RDD
    import org.apache.spark.{SparkConf, SparkContext}
    
    object WordCount {
      def main(args: Array[String]): Unit = {
    
        //创建SparkContext,只有使用SparkContext才可以向集群申请资源,才可以创建RDD
        val conf = new SparkConf().setAppName("WordCount")
    
        val sc = new SparkContext(conf)
    
        //1 创建RDD:指定[以后]从HDFS中读取数据
        val lines: RDD[String] = sc.textFile(args(0))
    
        //2 对数据进行切分压平
        val words = lines.flatMap(_.split(" "))
    
        //3 将单词和一组合
        val wordAndOne = words.map((_, 1))
    
        //4 单词聚合,先局部聚合,再全局聚合,使用reduceByKey
        val wordAndNums = wordAndOne.reduceByKey(_ + _)
    
        //5 输出结果并保存
        wordAndNums.saveAsTextFile(args(1))
      }
    }
    
  4. 打包

    可以使用provided 指定依赖是否打入包中

    <dependency>
        <groupId>org.scala-lang</groupId>
        <artifactId>scala-library</artifactId>
        <version>${scala.version}</version>
        <scope>provided</scope>
    </dependency>
    

    使用 指定main方法

    <!-- 指定maven(main)方法 -->
    <transformers>
    	<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
    		<mainClass>cn.doit.day01.demo01.WordCount</mainClass>
    	</transformer>
    </transformers>
    
  5. 将jar包导入集群中执行

    • 如果不指定main方法,需要指定class,指定类名

      –class cn.doit.day01.demo01.WordCount

    ./bin/spark-submit --master spark://linux01:7077 【--class cn.doit.day01.demo01.WordCount】 /root/spark_learn.jar hdfs://linux01:8020/anli/wordCount/input hdfs://linux01:8020/anli/wordCount/output02
    

2.使用java编写

java编写,并在本地运行

代码如下:

public class WordCount {
    public static void main(String[] args) {

        //创建连接,设置进程名()
        SparkConf conf = new SparkConf().setAppName("JavaWordCount");

        //如果在本地运行,设置Master所调用的线程资源数,一般使用local[*],调用全部资源(不能设置为1)
        conf.setMaster("local[*]");

        //javaSparkContext是对SparkContext的包装类
        JavaSparkContext context = new JavaSparkContext(conf);

        //设置文件输入路径
        JavaRDD<String> rdd = context.textFile(args[0]);

        //切分压平
        JavaRDD<String> words = rdd.flatMap(new FlatMapFunction<String, String>() {
            @Override
            public Iterator<String> call(String s) throws Exception {
                return Arrays.stream(s.split(" ")).iterator();
            }
        });

        //单词和一结合
        JavaPairRDD<String, Integer> wordAndOne = words.mapToPair(new PairFunction<String, String, Integer>() {
            @Override
            public Tuple2<String, Integer> call(String s) throws Exception {
                return Tuple2.apply(s, 1);
            }
        });

        //聚合
        JavaPairRDD<String, Integer> wordAndNums = wordAndOne.reduceByKey(new Function2<Integer, Integer, Integer>() {
            @Override
            public Integer call(Integer integer, Integer integer2) throws Exception {
                return integer+integer2;
            }
        });

        //交换顺序
        JavaPairRDD<Integer, String> numsAndWord = wordAndNums.mapToPair(new PairFunction<Tuple2<String, Integer>, Integer, String>() {
            @Override
            public Tuple2<Integer, String> call(Tuple2<String, Integer> stringIntegerTuple2) throws Exception {
                return Tuple2.apply(stringIntegerTuple2._2, stringIntegerTuple2._1);
            }
        });

        //排序
        JavaPairRDD<Integer, String> sortedNumsAndWord = numsAndWord.sortByKey(false);

        //再交换顺序
        JavaPairRDD<String, Integer> sortedWordAndNums = sortedNumsAndWord.mapToPair(new PairFunction<Tuple2<Integer, String>, String, Integer>() {
            @Override
            public Tuple2<String, Integer> call(Tuple2<Integer, String> integerStringTuple2) throws Exception {
                return Tuple2.apply(integerStringTuple2._2, integerStringTuple2._1);
            }
        });

        //保存
        sortedWordAndNums.saveAsTextFile(args[1]);

        //关闭资源
        context.stop();
    }
}

3.使用Lambda表达式

尝试在本地运行,读写hadoop上的文件

在IDEA设置(传参数):

  • Configurations — Program arguments:

    hdfs://linux01:8020/anli/wordCount/input hdfs://linux01:8020/anli/wordCount/output03

public class LambdaWordCount {
    public static void main(String[] args) {
        
        //在idea上执行,hadoop上的文件,并输出到hadoop中
        //因为windows的权限不够,需要修改调用者名称
        System.setProperty("HADOOP_USER_NAME","root");
        //结果:权限不足,hadoop权限调整为777,可以运行;或者使用相同方式在scala代码上,可以运行

        SparkConf conf = new SparkConf().setAppName("LambdaWordCount").setMaster("local[*]");

        JavaSparkContext context = new JavaSparkContext(conf);

        JavaRDD<String> lines = context.textFile(args[0]);

        JavaRDD<String> words = lines.flatMap(e -> Arrays.stream(e.split(" ")).iterator());

        JavaPairRDD<String, Integer> wordAndNum = words.mapToPair(e -> Tuple2.apply(e, 1));

        JavaPairRDD<String, Integer> wordAndNums = wordAndNum.reduceByKey(Integer::sum);

        JavaPairRDD<Integer, String> numsAndWord = wordAndNums.mapToPair(e -> Tuple2.apply(e._2, e._1));

        JavaPairRDD<Integer, String> sortedNumsAndWord = numsAndWord.sortByKey(false);

        JavaPairRDD<String, Integer> sortedWordAndNums = sortedNumsAndWord.mapToPair(e -> Tuple2.apply(e._2, e._1));

        sortedWordAndNums.saveAsTextFile(args[1]);

        context.stop();
    }
}

四 RDD详解

RDD:Resilient Distributed Dataset—有弹性的分布式的数据集合;里面没有真正的数据,是一个抽象的、不可变得、被分区的集合,集合内的元素可以被并行的操作(多个Task)。

五大特点:

  • 有多个分区,分区数量决定任务并行数
  • 函数作用在分区的迭代器中,决定了计算逻辑
  • 众RDD之间存在依赖关系,可以根据依赖关系恢复失败的任务和划分stage
  • 如果要发生shuffle,要使用分区器,默认使用HashPartitioner
  • 最优位置,即将Executor调度到数据所在的节点上,要求Worker和DataNode部署在同一节点或OnYarn,通过访问Namenode获取数据块位置信息

深层理解RDD:

  • RDD是一个抽象的数据集,保存的是元数据信息,和对应的运算逻辑
  • 即RDD记录了:数据从哪里来、所属Task的分区、对数据作何处理
  • 一个Task是由多个RDD相连组成的,而多个RDD之间是由各自的迭代器连接,形成迭代器链,将RDD的对应Task分区连接起来的
  • RDD是由,多个各自分区的迭代器(迭代器中记录了数据来源),及其Task的Function(运算逻辑:compute方法),组成的一个大集合

1.Spark中的基础概念

  • Application

    • 一个Application只能有一个Executor
    • 一个Application可以 一次提交多个Job
  • DAG

    • 有向无环图
    • 是对多个RDD转换过程和依赖关系的描述
    • 触发Action就会形成一个完整的DAG,一个DAG就是一个Job
  • Job

    • Driver向Executor提交的作业
    • 一个完整的DAG会产生一个Job,不Action就不会有Job
    • 一个Job中有一个到多个Stage,一个Stage对应多个TaskSet
  • Stage

    • 执行任务的阶段
    • 一个stage对应一个TaskSet
    • 一个stage中有一到多个Task,Task的数量取决于Stage中最后一个RDD分区的数量
    • Stage分为两种:
      • ShuffleMapStage:会进行shuffleWrite,为下一个stage做准备,里面的Task叫ShuffleMapTask
      • ResultStage:数据源不确定,但是一定会Action会输出结果,里面对应的Task叫ResultTask
    • stage结束的标志就是shuffle或者result,由于shuffle会写磁盘,在同一个Application中,如果执行重复的stage,会直接在shuffle到磁盘的数据上直接拉取,不再重新执行重复的stage
  • TaskSet

    • 保存相同处理逻辑的、不同数据目标的多个Task
  • Task

    • 是spark中最小的任务执行单元

    • Task有属性(从哪里读数据)、有方法(执行逻辑)

    • 是一个类的实例,但是Task的执行逻辑是根据代码动态生成的

      org.apache.spark.scheduler中的一个抽象类

    • 有两个继承类:

      • ShuffleMapTask
      • ResultTask
    • Task的数量决定了并行度,Task如果过多也不会阻塞,线程会在多个task中切换并行

  • dependency:

    • 依赖关系,指的是父RDD和子RDD之间的依赖关系,分为宽依赖和窄依赖
    • 窄依赖:是指父RDD的每个分区只被子RDD的一个分区所使用,子RDD一般对应父RDD的一个或者多个分区。(与数据规模无关)不会产生shuffle。
    • 宽依赖:指父RDD的多个分区可能被子RDD的一个分区所使用,子RDD分区通常对应所有的父RDD分区(与数据规模有关),会产生shuffle
  • pipeline

    • 迭代器链
1.1 shuffle
  • shuffle,中文字义为洗牌
  • 每个分区的数据按照一定的规则,分区写到本地磁盘中;
  • 下一个stage的各个分区的Task,到上个stage的磁盘中,拉取属于自己分区的数据,这个过程,为shuffle
  • 注意事项:
    • shuffle过程中,一定是一个分区的数据,有分散向多个分区的趋势
    • 也就是说,如果在已有数据中,刚好一个分区的数据全部到另一个分区中,不一定没有shuffle;有分区规则,但是数据最终都到了一个分区中(下游Stage只有一个分区),也不算shuffle
1.2 补充知识点
  • classOf[T] 方法

    • 获取类型T的Class对象,返回在运行过程中的类型。
  • 创建连接

    • 在创建对象或连接时,不要在Driver中创建,可能会导致Task无法序列化
    • 关闭资源时,可以判断迭代器没有下一条数据时,进行关闭
  • 分区器类型相同,分区数量相同,不再shuffle,也就没有新的stage

  • Partitioner分区器

    • 一个父类,需要重写两个方法,有三个实现类
  • 查看Executor的打印台

    • 在work目录—job文件夹的stdout中

2.分区

  • RDD分区方式
    • 直接设置最小分区数,通过调用parallelize方法(仅限scala数组使用)
    • 通过设置textFile方法中的minpartitions参数,调节分区数
  • spark读取hdfs文件时,分区数算法详述
    1. 在获取RDD时,sc调用textFile等方法,输入path,并可选输入minPartitions
    2. minPartitions会进行判断,取输入值和2之间的最小值
    3. textFile类:调用hadoopFile,HadoopFile会返回一个KV类型的RDD(指针位置和行内容),textFile使用map方法抽取value,返回一个String类型的RDD
    4. 在hadoopFile中,调入了一个类:TextInputFormat,其父类FileInputFormat中,包含了RDD的分区方法:getSplits
    5. 遍历所有文件,获取总的文件长度totalSize,再用总长度除numSplits(前面的minPartitions,默认为2)(numSplits == 0 ? 1 : numSplits),获取目标大小goalSize
    6. 遍历所有文件,首先调用computeSplitSize方法,取goalSize和128M的最小值,返回为splitSize
    7. 进入for循环,判断文件的实际长度是否大于splitSize的1.1倍,如果大于,调用makeSplit进行切分,切掉的大小为splitSize
    8. 而后该文件剩余部分继续循环,直到小于splitSize的1.1倍为止

3.Transformation算子

算子是对RDD进行操作

对RDD中的每个分区的迭代器进行函数处理

Transformation的算子都是Lazy的,不会触发Action

3.1 不产生shuffle

不会读取数据

3.1.1 原理分析
map方法详解
  • map方法传入自定义的函数返回一个RDD
  • 在该方法中调用了一个构造方法,创建了一个RDD对象:MapPartitionsRDD
  • MapPartitionsRDD继承RDD,利用多态,构造方法可以返回一个RDD对象
  • 函数有三个参数(TaskContext,Index,Iterater),对Iterater进行map操作
  • 在构造方法中,传入了调用map方法的RDD,和一个函数:(_, _, iter) => iter.map(cleanF)
  • 函数中的cleanF是经过处理后的传入的function,iter是一个读取上一个RDD信息的迭代器
  • 可以发现,在构造方法的函数中,对迭代器读取的信息,直接调用了map方法进行了处理,并返回了一个新的迭代器,与其他方法组合,成为一个RDD(MapPartitionsRDD)
3.1.2 map类算子
  • map(函数)
    • 做映射,对RDD内的数据一一进行处理
  • mapValues(函数)
    • 是对RDD中对偶元组的value进行处理,不用管key,将value应用的传入的函数处理后在跟key放到对偶元组中
  • mapPartitions(函数)
    • 对Partition进行map操作
    • 适用于关联外部的维度数据,例如查询数据库、外部的接口,套实现创建一下连接对象,一个分区的多条数据可以复用事先创建的对象
  • mapPartitionsWithIndex(函数)
    • 对Partition进行map操作,与此同时可以将分区编号获取到
    • 输入的是迭代器,返回的数据也是迭代器
    • 函数输入值:(index,iter)
    • 底层调用了mapPartitionsRDD,函数有三个参数(TaskContext,Index,Iterater)用到了分区编号
  • flatMap(函数)
    • 相当于先map在flatten,rdd中没有flatten方法
    • 返回值必须是一个或多个数组或集合
  • flatMapValues
    • 当RDD时KV类型,先执行mapValues,执行的结果应该是数组、集合或者迭代器
    • 然后将集合炸开取每个元素,与key结合成,多个新的元组进行返回
3.1.3 过滤型算子
  • filter(函数)
    • 过滤,也是一条一条的过滤,返回true的就保留
    • 底层new的是MapPartitionsRDD,对Iterater进行filter,传入你指定的的过滤函数
  • keys
    • Rdd中的数据类型为对偶元组,可以获取对偶元组中的第一个
    • 用到了隐式转换,将RDD[(K, V)]类型,即RDD中的数据类型为对偶元组,进行包装之后转成了PairRDDFunction
  • values
    • 同上,返回第二个
3.1.4 其他算子
  • union(++)
    • 只能相同类型RDD进行union
    • union不会去重,没有shuffle,其实就是将原来的两个RDD包装了一层
    • 会把所有的元素汇总为新的RDD
    • 相当于把多个RDD的多个分区sum到一个新的RDD中
3.2 产生shuffle

在分布式计算中,将数据按照一定的计算逻辑(分区器)

3.2.1 原理分析
reduceByKey方法详解
  • 调用了combineByKeyWithClassTag方法,传入4个参数:一个reduceByKey的默认函数,二和三都是传入的自定义函数,四是一个分区器

  • 在方法中,调用的构造函数是:ShuffledRDD(绝大部分带shuffle的Transformation都是该RDD)

  • 构造函数传入调用reduceByKey的RDD,和一个分区器,该构造方法构造出来的RDD只会执行一个shuffle,不会执行shuffle前后的聚合等方法

  • 需要通过set:Serializer(序列化方法)、Aggregator(传入上面那3个函数)、MapSideCombine(shuffle前是否聚合)等信息对ShuffledRDD进行完善

  • 可以自行创建ShuffledRDD,来替代reduceByKey方法:

    //手动编写reduceByKey方法
    
    //函数1:聚合时,首个元素传入时对value的处理方法
    val f1 = (e:Int) => e
    //函数2:聚合value的方法
    val f2 = (e1:Int ,e2: Int) => e1+ e2
    //在reduceByKey中,shuffle前后的聚合方法是一样的
    val f3 = f2
    
    //三个泛型:Key类型,原Value类型,聚合后的Value类型
    //分区器创建一个新的HashPartitioner,分区数量设置为源RDD的分区数量
    val shuffledRDD: ShuffledRDD[String, Int, Int] = new ShuffledRDD[String, Int, Int](wordAndOne, new HashPartitioner(wordAndOne.partitions.length))
    
    //创建Aggregator,传入函数
    var agg = new Aggregator[String, Int, Int](f1,f2,f3)
    
    shuffledRDD.setMapSideCombine(true)
    shuffledRDD.setAggregator(agg)
    
cogroup方法详解
  • 协分组、联合分组,是对多个RDD进行分组
  • RDD必须是KeyValue类型的,并且两个RDD的key类型一样才可以进行cogroup
  • 生成的RDD中,数据格式为最外层是KV类型的元组,value是由两个迭代器组成的元组构成的
  • cogroup是多个算子的实际执行方法,如join、leftOuterJoin、RightOuterJoin等
  • 其中,创建的RDD是一个独特的RDD:CoGroupedRDD
3.2.2 聚合型算子
  • groupBy(分组原则)

  • groupByKey

    • 底层调用的是combineByKeyWithClassTag,在该方法中new的ShuffleRDD,mapSideCombine = true
  • reudceByKey(函数)

  • 可以先局部聚合,在全局聚合,底层调用的是combineByKeyWithClassTag,并在方法中中new的ShuffleRDD, ,mapSideCombine = true

  • foldByKey(初始值)(函数)

    • 可以指定初始值的reduceByKey
  • combinByKey(函数1,函数2,函数3)

    • 将reduceByKey分解,可以单独设置:初始化函数、聚合前的value合并函数、聚合的合并函数
    • 底层调用的是combineByKeyWithClassTag, 并在方法中中new的ShuffleRDD
    • mapSideCombine(shuffle前是否聚合)参数默认为true
  • aggregateByKey(初始值)(函数1,函数2)

    • 与reduceByKey相同,更为灵活
    • 可以将shuffle前后的聚合方法分别编写
    • 局部聚合应用初始值,全局聚合不用初始值
  • distinct

    • 去重
    • 先调用map将数据变成K,V类型,Value是null,然后调用reduceByKey,先在每个分区进行局部去重,然后在全局去重
3.2.3 分区型算子
  • partitionBy(分区器)
    • 重新分区
    • 只分区,不聚合、不分组
    • 与new shuffledRDD什么也不设置是一样的
  • repartition(分区数)
    • 重新分区(分区方法还是HashPartitioner)
    • 使用场景:提高并行度、更改分区器
  • coalesce(分区数,是否shuffle)
    • 也是重新分区,可以设置是否shuffle
    • 如果不shuffle,分区数只能减少,不能增加(因为如果分区增加,一定要一个分区的数据发送到多个分区,必须要shuffle)
    • 先将数据调用mapPartitionerWithIndexInternal,根据分区的编号和分区的数量生成随机数最为key,然后在new shuffledRDD进行重新分区
    • repartition底层调用的就是coalesce,shuffle为true
3.2.4 联合型算子
  • join(RDD)

    • 类似于SQL中的inner

    • 底层调用的也是cogroup,每个RDD对应的迭代器都不为空就join上了

    • 然后调用flatMapValues,使用for循环和yield实现类似java的双层for循环,将返回的结果在压平

    • 使用cogroup实现join

      val rdd2: RDD[(String, (Iterable[Int], Iterable[Int]))] = rdd11.cogroup(rdd12)
      val out = rdd2.flatMapValues(e => for (x <- e._1; y <- e._2) yield (x, y))
      
  • leftOuterJoin

    • 左外连接
    • 手写实现leftOuterJoin(rightOuterJoin同理)
    val rdd2: RDD[(String, (Iterable[Int], Iterable[Int]))] = rdd11.cogroup(rdd12)
    val out = rdd2.flatMapValues(pair=>
        if (pair._2.isEmpty) pair._1.map((_,None))
        else for ( x<- pair._1 ; y <- pair._2 ) yield (x, Some(y)))
        //else pair._1.flatMap(e=> pair._2.map(y=> (e,Some(y)))))
    
  • fullOuterJoin

    • 空值连接(左外连接和右外连接的结合,没有数据填null)
    • 手写实现
    val rdd2: RDD[(String, (Iterable[Int], Iterable[Int]))] = rdd11.cogroup(rdd12)
    val out = rdd2.flatMapValues {
        case (Seq(), iter) => iter.map(e => (None, Some(e)))
        case (iter, Seq()) => iter.map(e => (Some(e), None))
        case (it1, it2) => it1.flatMap(x => it2.map(y => (Some(x), Some(y))))
    }
    
  • intersection 交集--------------------------------------------------------------------------------------------

    • 底层调用的cogroup
    • 使用map将两个RDD的数据本身当成key,null当成value,组成KV类型的RDD
    • 然后调用cogroup,再进行过滤,过滤的条数是,两个迭代器都不为空迭代器(类似join的选择结果,把join的结果的value丢掉),然后调用keys取出key
  • subtract(RDD)

    • 求差集,rdd1-rdd2
    • 调用了SubtractedRDD
      • 底层中,将第一个RDD的数据放到一个Map集合中,作为Key,出现的次数(会创建一个ArrayBuffer数组,出现一次,添加一个null,null的个数就是出现的此时)作为Value;
      • 再遍历第二个集合的数据,对其中的元素遍历,将所有的元素在Map集合中进行remove(这样不论value中的数组有多少个null),都会直接移除
      • 剩下的数据使用flatMapValue和keys,恢复成原始的数据格式,就求出了差集
3.2.5 排序型算子
  • sortBy(排序关键字,是否正序)

    • 先分桶排序,再全局聚合
    • 会进行采样(sample),collect结果,构建分区规则,划分桶的范围,将数据shuffle到不同范围的桶中
    • 对各个桶内进行排序,按照桶的分区号(序号)
  • repartitionAndSortWithinPartitions(分区器)

    • 重新分区且排序
    • 可以delimited排序器(Ordering)来自定义排序规则

4.action算子

所有的action算子都会调用:sc.runJob

调用sc.runJob方法,根据最后一个RDD从后往前推,触发Action就会生成DAG,切分Stage,生成TaskSet

4.1 收集型算子
  • collect
    • 收集数据到一个数组中
    • 将每个分区的数据通过网络收集到Driver端,然后方法到Driver端的内存中,然后的数据按照分区的先后存放在数组中,如果Driver端的内存不足,只会收集部分数据
  • saveAsTextFile
    • 保存为Text类型文件
    • 该方法内部会调用mapPartitions,将数据转成NullWirtable和Text类型,然后使用TextOutputFormat格式写入到HDFS中
4.2 遍历型算子
  • foreach
    • 遍历每一条数据
    • 底层调用的是迭代器的next(),用完就没了,返回Unit
  • foreachPartition
    • 与foreach相似,处理分区
    • 如果要将数据写入到数据库中,一个分区一个连接,效率更高
  • foreachPartitionAsync
    • 与foreachPartition相似,异步进行
4.3 聚合型算子
  • count
    • 先在每个分区内进行计数,使用的是迭代器遍历一条数量加1
    • 然后将每个分区计数的结果返回到Driver端进行全局的聚合
  • sum
    • 底层调用了fold(0)(函数)
    • 先局部求和,再全局求和
  • fold(初始值)(函数)
    • 带初始值执行函数
  • reduce(函数)
    • 不带初始值的聚合
    • 将所有数据聚合再一起,局部聚合和全局聚合都是这一个函数
  • aggregate(初始值)(局部聚合函数,全局聚合函数)
    • 更为灵活地聚合,类似transformation中的aggregateByKey,区别在于返回值可以为任意类型而不是RDD;初始值在局部聚合和全局聚合都会应用
    • 各个分区进行聚合时, 是没有顺序的,谁快谁来
4.4 取值型算子
  • take(数量)
    • 取n条数据,可能触发多次Action
    • 底层使用while循环,使用hasnext和next
    • 先从0号分区取数据,如果不够就到下一个分区接着取,直到取完全部分区
  • first
    • 返回第一个元素,调用的是take(1)
    • 不同之处:take返回数组,first返回元素
  • min/max
    • 最大值,最小值
    • 在每个分区内,使用冒泡的方式求出最大(最小)值,再将结果汇总到Driver端进行比较
4.5 排序型算子
  • top(n)
    • 取前n个,返回数组
    • 在每个分区内使用有界优先队列对N个数据进行排序,移除不符合的数据
    • 再在Driver端使用有界优先队列进行全局排序
    • 有界优先队列:
      • 有界优先队列类似一个TreeSet,但是不去重;
      • 如果取n个数据,则会创建一个长度为n的有界优先队列
      • 利用红黑树的高效排序,每存储一个数据,集合内的数据达到n+1条时,对集合内最小(最大)的数据进行remove
      • 使集合内始终保留最大(最小)的n条数据,这样就可以求出每个分区的前n条
      • 两个长度为n的队列结合(++=),同样会保留n条数据
    • 只在Driver端生成进行Map的操作,不需要shuffle
  • takeOrdered(获取数量)(排序规则)
    • 与top相反,获取最小的n条数据(top底层调用的就是takeOrdered)
    • 可以自定义排序规则

五 高级功能

spark提供了一些在实用场景下,为提高运行效率,添加的一些功能

1.cache/persist

将数据缓存到Executor的内存中,当RDD调用时,可以直接在内存中调用并且复用缓存的数据,相应速度快

  • 适用场景

    • 当需要多次调用action时,可以使用
  • 注意事项

    • 数据较大时占用内存,应先将数据初步处理后再cache
    • cache大量文件时,如果内存不足,会自行判断存储一部分文件(文件不会拆分)
    • cache(persist)不是算子,不会返回新的RDD
  • 使用优化

    • 常规设置cache序列化方法为java对象进行序列化,数据冗余度较高,占用空间大;
    • 使用spark提供的专用序列化方式:
    //设置序列化方式为kryo
    sc.getConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
    //使用persist进行缓存设置,以便选择存储位置
    //cache底层调用了persist
    rdd.persist(StorageLevel.MEMORY_ONLY_SER)
    
    //删除缓存数据(是否同步)
    rdd.unpersist(true)
    
    • 通过SrotageLevel可以选择多种存储方式,磁盘储存、内存储存、备份等
  • 使用建议

    • 生产环境中,建议使用kryo的序列化方式
    • 使用MEMORY_AND_DISK_BOTH_SER--------------------------

2.checkpoint

对于一些经过复杂计算得到的,重要的中间结果数据,为了避免数据丢失,可以将中间结果保存到hdfs中,以后在Application中可以反复使用(适合复杂计算)

  • 使用checkpoint

    • 在调用rdd的checkpoint前,一定要指定保存的目录:

      sc.setCheckPointDir

    • 在数据action之前,进行调用

      rdd.checkpoint

  • 执行机制

    • 执行过程中,调用checkpoint后的第一次触发action时,才会做checkpoint
    • 由于checkpoint会将数据写到hdfs中,所以会触发一次job
    • 如果RDD做了checkpoint,这个RDD以前的依赖关系就不再使用了
    • 如果同时设置了cache和checkpoint,RDD的迭代器在拉取数据时,会进行判断,按照cache、checkpoint、父迭代器的优先顺序进行选择
  • 注意事项

    • checkpoint不是Transformation,只是标记
    • 如果设置了checkpoint后,hdfs中的数据丢失了,再次调用RDD时,会报错
  • 使用建议

    • 由于使用checkpoint会触发至少两个action,建议在checkpoint前,对RDD进行cache,可以避免数据重复计算

3.广播变量

广播变量通常是为了实现mapSideJoin,减少网络传输,可以将小的数据广播到属于该application的Executor中,然后通过Driver广播变量返回的引用,获取实现广播到Executor的数据

  • 注意事项

    • 要广播的数据不宜太大,要根据服务器的内存和分配的executor-memory决定
    • 广播变量是readOnly的,一旦广播出去,只读不能改
    • 但是在Driver端可以调用unpersist将广播变量释放,如果以后在Executor中用到该变量,需要重新广播
  • 使用方法

    //将Driver中的数据广播到Executor中,这个是个阻塞方法
    val broadcastRef = sc.broadcast(数据集合)
    //会返回一个广播变量的引用
    //RDD可以通过该引用获取到已经广播到Executor中的数据
    broadcastRef.value
    
  • 使用演示:

    • 使用广播变量进行ip地址查询
    package cn.doit.day06
    
    import cn.doit.day06.IpTest.{binary2Decimal, ip2City}
    import org.apache.spark.rdd.RDD
    import utils.SparkUtils
    
    object IpTestEasy {
      def main(args: Array[String]): Unit = {
        //创建连接
        val sc = SparkUtils.createContext()
    
        //读取ip地址的匹配规则数据
        val ipRulerLines = sc.textFile(args(0))
    
        val idAndAddr: Array[(Long, Long, String, String)] = ipRulerLines.map(e => {
          val arr1: Array[String] = e.split("\\|")
          val startIp = arr1(2).toLong
          val endIp = arr1(3).toLong
          val province = arr1(6)
          val city = arr1(7)
          (startIp, endIp, province, city)
        }).collect()
    
        //使用broadcast进行广播
        val ip2AddrRuler = sc.broadcast(idAndAddr)
    
        val lines = sc.textFile(args(1))
        //一会用 mappartitions尝试,一个分区读取一次广播的变量是否会加快速度
        val addrAndOne = lines.map(e => {
          //获取广播变量
          val ipRulerArr: Array[(Long, Long, String, String)] = ip2AddrRuler.value
          //获取二进制地址
          val binaryIp: String = e.split("\\|")(1)
    
          //将二进制地址转为十进制IP地址
          val decimalIp = binary2Decimal(binaryIp)
          //使用二分法查找IP地址对应的实际地址
          val addr = ip2City(ipRulerArr, decimalIp)
    
          (addr, 1)
        })
    
        //聚合
        val result: RDD[((String, String), Int)] = addrAndOne.reduceByKey(_ + _)
    
        println(result.collect().toBuffer)
        //ArrayBuffer(((云南,昆明),126), ((北京,北京),1535), ((河北,石家庄),383), ((未知省份,未知城市),1), ((陕西,西安),1824), ((重庆,重庆),868))
    
      }
    
    
    
    
      /**
       * 封装二进制IP地址转换为十进制的IP地址的方法
       * 输入:二进制的IP地址 --- 字符串
       * 输出:十进制的IP地址 --- Long类型
       */
      def binary2Decimal(s: String): Long = {
        val arr: Array[String] = s.split("\\.")
        //判断
        if (arr.length != 4) return 0L
    
        var decimalIp: Long = 0
        for ( i <- 0 to 3 ) decimalIp += (arr(3-i).toLong * Math.pow(256, i).toLong)
    
        decimalIp
      }
    
    
      /**
       * 封装通过二进制地址查找实际地址的方法
       * 使用二分查找法
       * 输入值为:规则数组,十进制IP地址
       * 返回值为:元组(省份,城市)
       */
      def ip2City(arr: Array[(Long, Long, String, String)], decimalIp: Long): Tuple2[String, String]={
    
        //定义一个中间索引
        var mid = arr.length / 2
        //中间元组
        val midValue = arr(mid)
    
        if (decimalIp>= midValue._1){
          if (decimalIp> arr.last._2) return ("未知省份","未知城市")
          else if (decimalIp> midValue._2) return ip2City(arr.takeRight(arr.length - mid -1), decimalIp)
          else return (midValue._3, midValue._4)
    
        } else if (decimalIp< arr(0)._1) return ("未知省份","未知城市")
    
        else return ip2City(arr.take(mid) , decimalIp)
      }
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值