Spark

Spark

一个用于大规模数据处理的统一计算引擎,相当于Hadoop中的MapReduce。但除最终计算结果外其他数据都保存在内存,且任务切分比单一的Map+Reduce更加灵活。

基于内存计算,它的速度可以达到MapReduce的上百倍

Spark vs Hadoop

综合能力:Spark是一个综合性质的计算引擎,Hadoop既包含MapReduce(计算)还包含HDFS(存储)和YARN(资源管理)

计算模型:Spark任务可以包含多个计算操作,轻松实现复杂迭代计算,Hadoop中MapReduce任务值包含Map和Reduce两个阶段,不够灵活

处理速度:Spark任务的数据存在内存中,而Hadoop中MapReduce任务基于磁盘

应用场景

  1. 低延时的海量数据计算需求 (Spark Core)
  2. 低延时SQL交互查询需求(Spark SQL)
  3. 准实时(秒级)海量数据计算需求i(Spark Streaming)

安装

Spark Standalone 集群安装部署

要提前配置好网络、jdk 等配置

  1. 下载安装包到主节点并解压,进入conf目录
  2. 重命名 spark-env.sh.template 为 spark-env.sh 并添加 JAVA_HOME SPARK_MASTER_HOST
  3. 重命名 slaves 文件,添加从节点网络地址

启动:sbin/start-all.sh 使用命令 jps 可看见表示主节点的 Master 或表示从节点的 Worker。通过 8080 端口可以看见 web 控制界面

# 提交任务
./bin/spark-submit \
  --class <main-class> \
  --master <master-url> \
  --deploy-mode <deploy-mode> \
  --conf <key>=<value> \
  ... # other options
  <application-jar> \
  [application-arguments]

./bin/spark-submit \
  --class org.apache.spark.examples.SparkPi \  # 任务类
  --master spark://192.168.56.152:7077 \
  examples/jars/spark-examples_2.12-3.1.3.jar \
  3                                            # 计算多少次
  
./bin/spark-submit  --class org.apache.spark.examples.SparkPi --master spark://192.168.56.152:7077  examples/jars/spark-examples_2.12-3.1.3.jar 3

Spark ON YARN 集群安装部署(推荐)

依赖Hadoop集群,spark节点需要也是Hadoop节点。不需要启动spark进程

  1. 下载解压
  2. 将 spark-env.sh.template 改名为 spark-env.sh,添加
    export JAVA_HOME=xxx
    export HADOOP_CONF_DIR=/usr/local/hadoop/etc/hadoop

提交任务

./bin/spark-submit \
  --class org.apache.spark.examples.SparkPi \
  --master yarn \
  --deploy-mode cluster \
  
./bin/spark-submit --class org.apache.spark.examples.SparkPi --master yarn --deploy-mode cluster examples/jars/spark-examples_2.12-3.1.3.jar

# 结果见于 http://hadoop-master:8088/cluster/apps

原理

提交任务后,Spark 从HDFS中读取数据进行计算,加载到RDD中,RDD是一种弹性计算框架具有分区能力并将数据放置在内存中。RDD中数据在经过计算后会一一对应地传递到新的RDD,没有shuffle过程。这个过程中可以多次调用高阶函数,如 map flatMap。

什么是RDD?

  • RDD通常通过Hadoop上文件创建,也可以通过程序中地集合来创建
  • RDD是Spark提供地核心抽象,全称为 Rdsillient Distributed Dataset,即弹性分布式数据集
  • 三个特点:
    • 弹性:RDD数据默认情况下存放在内存中,但是在内存资源不足时,Spark也会自动将RDD数据写入磁盘
    • 分布式:RDD 在抽象上来说是一种元素数据地集合,它是被分区的,每个分区分布在集群的不同节点上,从而让RDD中数据可以被并行操作
    • 容错性:RDD 最重要的特性就是提供了容错性,可以自动从节点失败中恢复过来,从数据源中重新加载数据

 

Spark 架构相关进程

  • Driver:编写出的Spark程序由Driver进程负责执行
  • Master:集群的主节点中启动的进程,集群资源管理与分配、集群的监控
  • Worker:集群的从节点中启动的进程,负责启动其他进程来启动任务
  • Executor:Worker负责启动,用于数据处理和数据计算
  • Task:由Executor负责启动的线程,它是真正干活的,处理、计算数据

 

开发Spark计算任务

Q:读取文件中所有内容,计算每个单词出现的次数?

创建环境:新建一个java的maven项目,添加scala模块依赖,在maven中添加spark-core依赖。注意spark、scala与maven依赖的版本要适配

<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-core -->
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-core_2.12</artifactId>
    <version>3.1.3</version>
    <scope>provided</scope>
</dependency>

scala 实现词频统计

package org.example

import org.apache.spark.{SparkConf, SparkContext}

object WordCountScala {

  def main(args: Array[String]): Unit = {
    // 1. 创建一个 SparkContext
    val conf = new SparkConf()
    conf.setAppName("WordCount") // 任务起名
      .setMaster("local")        // 表示在本地执行,方便调试。真正执行任务还需提交到spark集群
    val sparkContext = new SparkContext(conf)

    // 2. 加载数据
    /*
     * linesRDD 数据如下:
     * hello world
     * hello scala
     * hello world
     */
    val linesRDD = sparkContext.textFile("C:\\data\\tmp.txt")

    // 3. 切割字符串,将数据切成单词
    /*
     * wordsRDD 数据如下:
     * hello
     * world
     * hello
     * scala
     * hello
     * world
     */
    val wordsRDD = linesRDD.flatMap(_.split(" "))

    // 4. 迭代words,将每个word转换为 (word, 1) 这种形式
    /*
     * pairRDD 数据如下:
     * (hello, 1)
     * (world, 1)
     * (hello, 1)
     * (scala, 1)
     * (hello, 1)
     * (world, 1)
     */
    val pairRDD = wordsRDD.map((_, 1))

    // 5. 根据key进行分组聚合统计
    /*
     * (hello, 3)
     * (world, 2)
     * (scala, 1)
     */
    val wordCountRDD = pairRDD.reduceByKey(_ + _) // 相同的key分为一组,一组中数字进行累加

    // 6. 打印结果
    /*
        scala : 1
        hello : 3
        world : 2
     */
    wordCountRDD.foreach(wordsRDD => println(wordsRDD._1 + " : " + wordsRDD._2))

    // 7. 停止SparkContext
    sparkContext.stop()
  }
}

java 实现词频统计

package org.example;

import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.PairFunction;
import scala.Tuple2;

import java.util.Iterator;
import java.util.List;

public class WordCount {
    public static void main(String[] args) {
        SparkConf conf = new SparkConf();
        conf.setAppName("WordCount").setMaster("local");
        JavaSparkContext sparkContext = new JavaSparkContext(conf);
        JavaRDD<String> linesRDD = sparkContext.textFile("c:\\data\\tmp.txt");

        // 以下可以使用 lambda 表达式简化
        // 参数中泛型,第一个是输入数据类型(待分割的字符串),第二个是输出数据类型(被分割后的单词)
        linesRDD.flatMap(new FlatMapFunction<String, String>() {
            @Override
            public Iterator<String> call(String s) throws Exception {
                return List.of(s.split(" ")).iterator();
            }
            // PairFunction 的参数分别为: 1. 输入数据类型 2. 输出tuple的第一个参数类型 3. 输出tuple的第二个参数类型
            // 注意:如果后面耀荣 xxxByKey 操作,前面都需要用 mapToPair 处理
        }).mapToPair(new PairFunction<String, String, Integer>() {
            @Override
            public Tuple2<String, Integer> call(String s) throws Exception {
                return new Tuple2<>(s, 1);
            }
        }).reduceByKey(new Function2<Integer, Integer, Integer>() {
            @Override
            public Integer call(Integer integer, Integer integer2) throws Exception {
                return integer + integer2;
            }
        }).foreach(tuple2 -> System.out.println(tuple2._1 + " : " + tuple2._2));

        sparkContext.stop();
    }
}

开启 Spark history server 服务

任意一台 Spark 节点上启动 historyServer 服务

  1. 将 spark-defaults.conf.template 重命名为 spark-defaults.conf
  2. 在 spark-defaults.conf 中添加
    spark.eventLog.enabled=true
    spark.eventLog.compress=true
    spark.eventLog.dir=hdfs://ubuntu:9000/logs/spark            # hdfs 中日志位置
    spark.history.fs.logDirectory=hdfs://ubuntu:9000/logs/spark
    spark.yarn.historyServer.address=http://ubuntu-02:18080     # 在启动 spark history server 的节点上指定ui端口
  3. 在 spark-env.sh
    export SPARK_HISTORY_OPTS="-Dspark.history.ui.port=18080 -Dspark.history.fs.logDirectory=hdfs://ubuntu:9000/logs/spark"
  4. 启动 ${SPARK_HOME}/sbin/start-history-server.sh

访问 ubuntu-02:18080 可看见 spark 日志管理界面

在执行spark任务时可以在hadoop的yarn的管理界面看到任务详情,其中有spark的log管理界面链接

 

任务提交

  1. 直接idea执行,方便本地环境调试
  2. 使用spark-submit提交到集群中执行(实际工作中使用)
    1. 修改代码,注释掉 .setMaster("local"),将读取文件的路径改为动态指定,要从hdfs中读取文件
    2. 修改pom.xml
      pom.xml
              <plugins>
                  <!-- java 编译插件 -->
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-compiler-plugin</artifactId>
                      <version>3.7.0</version>
                      <configuration>
                          <encoding>UTF-8</encoding>
                      </configuration>
                  </plugin>
                  <!-- scala 编译插件 -->
                  <plugin>
                      <groupId>net.alchim31.maven</groupId>
                      <artifactId>scala-maven-plugin</artifactId>
                      <version>4.5.3</version>
                      <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>add-source</goal>
                                  <goal>testCompile</goal>
                              </goals>
                          </execution>
                      </executions>
                      <configuration>
                          <args>
                              <!-- 编译时使用 libs 目录下的 jar 包,通过 mvn scala:help 查看说明 -->
                              <arg>-extdirs</arg>
                              <arg>${project.basedir}/libs</arg>
                          </args>
                          <scalaCompatVersion>2.12</scalaCompatVersion>
                          <scalaVersion>2.12.16</scalaVersion>
                      </configuration>
                  </plugin>
      
                  <!-- 打包插件 -->
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-assembly-plugin</artifactId>
                      <version>3.2.0</version>
                      <configuration>
                          <descriptorRefs>
                              <descriptorRef>jar-with-dependencies</descriptorRef>
                          </descriptorRefs>
                          <archive>
                              <manifest>
                                  <mainClass></mainClass>
                              </manifest>
                          </archive>
                      </configuration>
                      <executions>
                          <execution>
                              <id>make-assembly</id>
                              <phase>package</phase>
                              <goals>
                                  <goal>single</goal>
                              </goals>
                          </execution>
                      </executions>
                  </plugin>
              </plugins>
    3. 打包成jar文件,并提交到spark节点
    4. 提交任务
      https://spark.apache.org/docs/latest/submitting-applications.html
      ./spark-submit \
        --class org.example.WordCountScala \  # 指定任务启动类
        --master yarn \
        --deploy-mode client \
        --executor-memory 1G \
        --num-executors 1 \
        ~/bigdata/learnScala-1.0-SNAPSHOT-jar-with-dependencies.jar \  # jar包位置
        hdfs://ubuntu:9000/get-docker.sh                               # 使用命令行参数指定文件
  3. 使用 spark-shell,方便在集群环境调试代码
    bin/spark-shell启动shell环境,会在本地启动一个 spark standalone 集群。 spark-shell --master yarn --deploy-mode client启动 on yarn 模式的 spark
    然后在scala的交互命令下执行程序
    交互环境提供了 SparkContext 变量可以直接使用,默认提供:
    Spark context Web UI available at http://ubuntu-02:4040
    Spark context available as 'sc' (master = local[*], app id = local-1654939862172).
    Spark session available as 'spark'.

    如果使用 on yarn 模式则无法在控制台看见结果,结果输出到 yarn 中,需要到 yarn history 中查看

 

创建 RDD

RDD 是 Spark 编程的核心, 在进行 Spark 编程时, 首要任务是创建一个初始的 RDD

三种创建方式:

  1. 集合:用于测试
  2. 本地文件:临时处理
  3. HDFS 文件:生产环境

使用集合创建 RDD

如果要通过集合来创建 RDD , 需要针对程序中的集合, 调用 SparkContext 的 parallelize() 方法。 Spark 会 将集合中的数据拷贝到集群上, 形成一个分布式的数据集合, 也就是一个 RDD 。 相当于, 集合中的部分数据会到一个节点上, 而另一部分数据会到其它节点上。 然后就可以用并行的方式 来操作这个分布式数据集合了。

调用 parallelize() 时, 有一个重要的参数可以指定, 就是将集合切分成多少个 partition 。 Spark 会为每一个 partition 运行一个 task 来进行处理。 Spark 默认会根据集群的配置来设置 partition 的数量。 我们也可以在调用 parallelize() 方法时, 传入第二个 参数, 来设置 RDD 的 partition 数量, 例如: parallelize(arr, 5)

package org.example

import org.apache.spark.{SparkConf, SparkContext}

/**
 * 使用集合创建 RDD
 */
object CreateRDDByArrayScala {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    conf.setAppName("CreateRDDByArray").setMaster("local")
    val sc = new SparkContext(conf)

    val arr = Array(1, 2, 3, 4, 5)

    val rdd = sc.parallelize(arr)

    // 只有这个调用 spark 的方法才会真正到spark集群上执行,其余都是在drive上执行
    val sum = rdd.reduce(_ + _)

    println(sum)
  }
}
package org.example;

import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;

import java.util.Arrays;

public class CreateRDDByArrayJava {
    public static void main(String[] args) {
        SparkConf conf = new SparkConf();
        conf.setAppName("CreateRDDByArrayJava").setMaster("local");
        JavaSparkContext sc = new JavaSparkContext(conf);

        JavaRDD<Integer> rdd = sc.parallelize(Arrays.asList(1, 2, 3, 4, 5));

        Integer sum = rdd.reduce((i, i2) -> i + i2);

        System.out.println("sum = " + sum);
    }
}

使用本地文件或HDFS文件创建RDD

通过 SparkContext 的 textFile() 方法, 可以针对本地文件或 HDFS 文件创建 RDD , RDD 中的每个元素就是文 件中的一行文本内容 textFile() 方法支持针对目录、 压缩文件以及通配符创建

RDD Spark 默认会为 HDFS 文件的每一个 Block 创建一个 partition , 也可以通过 textFile() 的第二个参数手动设置 分区数量, 只能比 Block 数量多, 不能比 Block 数量少, 比 Block 数量少的话你的设置是不生效的

使用 val rdd = sc.textFile(path)获取rdd,具体代码见前文词频统计案例。

 

Transformation 和 Action

spark 对于rdd的操作总体可以分为两类:

  1. Transformation:这里的 Transformation 可以翻译为转换,表示是针对 RDD 中数据的转换操作,主要会针对已有的 RDD 创建一个新的 RDD:常见的有 map 、 flatMap 、 filter 等等
  2. Action:Action 可以翻译为执行,表示是触发任务执行的操作,主要对 RDD 进行最后的操作,比如遍历、 reduce 、 保存到文件等,并且还可以把结果返回给 Driver 程序

无论是哪种操作都会被称为算子,如:map算子、reduce算子。
Transformation算子有一个特性:lazy,即懒加载。
Action 的特性: 执行 Action 操作才会触发一个 Spark 任务的运行, 从而触发这个 Action 之前所有的 Transformation 的执行

sc.textFile("/hello.txt")
    // 前几步都是 Transformation 操作,只是进行逻辑记录,没有真正执行
    .flatMap(_.split("\\s+"))
    .map((_, 1))
    .reduceByKey(_ + _)
    // 这一步是 Action 操作,触发之前所有 Transformation 算子的执行,这些算子被拆分成多个task到多个节点上执行
    // 只有执行到这一行代码时,任务才计算,如果没有这个Action,前面算子不进行执行
    .foreach(wordsRDD => println(wordsRDD._1 + " : " + wordsRDD._2))

Transformation

https://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations

常见算子

Transformation 操作
package org.example

import org.apache.spark.{SparkConf, SparkContext}

object TransformationOpScala {

  def mapOp(sc: SparkContext): Unit = {
    val rdd = sc.parallelize(Array(1, 2, 3, 4))
    rdd.map(_ * 2).foreach(println(_))
  }

  def filterOp(sc: SparkContext): Unit = {
    val rdd = sc.parallelize(Array(1, 2, 3, 4))
    rdd.filter(_ % 2 == 0).foreach(println(_))
  }

  def flatMapOp(sc: SparkContext): Unit = {
    val rdd = sc.parallelize(Array("hello world", "hello scala"))
    rdd.flatMap(_.split("\\s+")).foreach(println(_))
  }

  def groupByKeyOp(sc: SparkContext): Unit = {
    // 数据为主播id与所处的大区号
    val rdd = sc.parallelize(Array((150001, "US"), (150002, "CN"), (150003, "CN")))
    // 因为要按照大区号进行分组,所以tuple中的元素要调换位置
    // 注意:在使用类似 groupByKey 这种基于key的算子时,要提前包rdd中数据组装成tuple2这种形式
    rdd.map(tup => (tup._2, tup._1)).groupByKey().foreach(tup => {
      print(tup._1 + " : ")
      for (uid <- tup._2) {
        print(uid + " ")
      }
      println()
    })
  }

  def groupByKeyOp2(sc: SparkContext): Unit = {
    // 数据为主播id与所处的大区号
    val rdd = sc.parallelize(Array((150001, "US", "male"), (150002, "CN", "female"), (150003, "CN", "male")))
    // 如果tuple中数据超过2列怎么办?
    // 要把key作为tuple2的第一列,剩下的可以再使用一个tuple2
    // 此时map生产新数据为 ("US", (150001, "male"))
    rdd.map(tup => (tup._2, (tup._1, tup._3))).groupByKey().foreach(tup => {
      print(tup._1 + " : ")
      for ((uid, gender) <- tup._2) {
        print("< " + uid + ", " + gender + " >")
      }
      println()
    })
  }

  def reduceByKeyOp(sc: SparkContext): Unit = {
    val rdd = sc.parallelize(Array((150001, "US"), (150002, "CN"), (150003, "CN")))
    rdd.map(tup => (tup._2, 1)).reduceByKey(_ + _).foreach(println(_))
  }

  def sortByKeyOp(sc: SparkContext): Unit = {
    val rdd = sc.parallelize(Array((150001, 400), (150002, 200), (150003, 300)))
    rdd.map(tup => (tup._2, tup._1))
      .sortByKey(false)  // 默认升序,使用false指定降序
      .foreach(println(_))
    rdd.sortBy(_._2, false).foreach(println(_)) // 当元素数量为3时,返回值为 (key, (val1, val2, val3))
  }

  def joinOp(sc: SparkContext): Unit = {
    val rdd1 = sc.parallelize(Array((150001, "US"), (150002, "CN"), (150003, "CN")))
    val rdd2 = sc.parallelize(Array((150001, 400), (150002, 200), (150003, 300)))

    val rdd = rdd1.join(rdd2)
    rdd.foreach(tup => {
      println(tup._1 + "\t" + tup._2._1 + "\t" + tup._2._2)
    })
  }

  def distincOp(sc: SparkContext): Unit = {
    val rdd = sc.parallelize(Array((150001, "US"), (150002, "CN"), (150003, "CN")))
    rdd.map(_._2).distinct().foreach(println(_))
  }

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    conf.setAppName("TransformationOpScala").setMaster("local")
    val sc = new SparkContext(conf)

    // map : 对集合中每个元素乘以 2
    mapOp(sc)
    // filter : 过滤出集合中的偶数
    filterOp(sc)
    // flatMap : 将行拆分为单词
    flatMapOp(sc)
    // groupByKey : 对每个大区的主播进行分组
    groupByKeyOp(sc)
    groupByKeyOp2(sc)
    // reduceByKey : 统计每个大区的主播数量
    reduceByKeyOp(sc)
    // sortByKey : 对主播的音浪收入排序
    sortByKeyOp(sc)
    // join : 打印每个主播的大区信息和音浪收入
    joinOp(sc)
    // distinct : 统计当天开播的大区信息
    distincOp(sc)
  }
}

Action

https://spark.apache.org/docs/latest/rdd-programming-guide.html#actions

Action 操作
package org.example

import org.apache.spark.{SparkConf, SparkContext}

object ActionOpScala {

  def reduceOp(sc: SparkContext) = {
    val rdd = sc.parallelize(Array(1, 2, 3, 4))
    println(rdd.reduce(_ + _))
  }

  def collectOp(sc: SparkContext) = {
    val rdd = sc.parallelize(Array(1, 2, 3, 4))
    // collect 返回的是一个Array数组
    // 注意:如果RDD中数据过大,不建议使用collect,因为数据最终会返回给Driver进程所在节点
    val res = rdd.collect()
    for (item <- res) {
      println(item)
    }
  }

  def takeOp(sc: SparkContext): Unit = {
    val rdd = sc.parallelize(Array(1, 2, 3, 4))
    println(rdd.take(2))
  }

  def countOp(sc: SparkContext) = {
    val rdd = sc.parallelize(Array(1, 2, 3, 4))
    println(rdd.count())
  }

  def saveAsTextFileOp(sc: SparkContext) = {
    val rdd = sc.parallelize(Array(1, 2, 3, 4))
    rdd.saveAsTextFile("hdfs://ubuntu:9000/deleteMe")
  }

  def countByKeyOp(sc: SparkContext) = {
    val rdd = sc.parallelize(Array(("A", 1), ("B", 2), ("A", 3), ("C", 4)))
    val res = rdd.countByKey()
    for ((k, v) <- res) {
      println(k + " : " + v)
    }
  }

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    conf.setAppName("TransformationOpScala").setMaster("local")
    val sc = new SparkContext(conf)

    // reduce : 聚合计算
    reduceOp(sc)
    // collect : 获取元素集合
    collectOp(sc)
    // take(n) : 获取前 n 个元素
    takeOp(sc)
    // count : 获取元素总数
    countOp(sc)
    // saveAsTextFile : 保存文件
    saveAsTextFileOp(sc)
    // countByKey : 统计相同的 key 出现多少次
  }
}

共享变量

默认情况下,一个算子函数中使用到了某个外部变量,那么这个变量会被拷贝到每个task中,此时每个task只能操作自己的那份数据。Spark 提供了两种变量:

  1. BroadcastVariable 广播变量
  2. Accumulator 累加器

BroadcastVariable

广播变量将为每个节点拷贝一份而非为每个task,好处是优化性能,减少网络传输及内存消耗。
通过调用 SparkContext 的 broadcast() 方法,针对某个变量创建只读的广播变量,可以通过广播变量的 value() 方法获取值

当使用的变量为一个庞大的集合等需要占用庞大的空间,广播变量就非常有用。

package org.example

import org.apache.spark.{SparkConf, SparkContext}

object BroadcastVariableScala {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    conf.setAppName("BroadcastVariableScala").setMaster("local")
    val sc = new SparkContext(conf)
    val rdd = sc.parallelize(Array(1, 2, 3, 4, 5))

    val varable = 2
    rdd.map(_ * varable) // 此时每个task都会复制一个变量

    val varableBroadcast = sc.broadcast(varable)
    rdd.map(_ * varableBroadcast.value) // 此时只在每个节点上复制一份

    sc.stop()
  }
}

Accumulator

累加器,主要用于多个节点对一个变量进行共享性的操作。正常情况下,一个算子内部可能产生多个task并行,所以task内部的聚合操作都是局部的;要实现多个task全局聚合计算时就要使用 Accumulator 这个共享累加变量。

注意:Accumulator 只提供了累计功能,再task中只能对 Accumulator 进行累计操作,不能读取它的值。只有Driver 进程才能读取。

package org.example

import org.apache.spark.{SparkConf, SparkContext}

object AccumulatorOpScala {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    conf.setAppName("AccumulatorOpScala").setMaster("local")
    val sc = new SparkContext(conf)

    val rdd = sc.parallelize(Array(1, 2, 3, 4, 5))

    var total = 0                // 在 Driver 进程上定义此变量
    rdd.foreach(i => total += i) // 局部累加,每个task内部各自累加分到自己节点的数字
    println(total)               // total仍然0,因为前一个foreach是在各个RDD节点上累加的,而此处打印的total是在Driver上的

    // 使用 Accumulator 实现累加
    val sumAccumulator = sc.longAccumulator
    rdd.foreach(i => sumAccumulator.add(i)) // 此处只能累加,不能获取值
    println(sumAccumulator.value)           // 获取值

    sc.stop()
  }
}
package org.example;

import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.util.LongAccumulator;

import java.util.Arrays;

public class AccumulatorOpJava {
    public static void main(String[] args) {
        SparkConf conf = new SparkConf();
        conf.setAppName("AccumulatorOpJava").setMaster("local");
        JavaSparkContext sc = new JavaSparkContext(conf);
        JavaRDD<Integer> rdd = sc.parallelize(Arrays.asList(1, 2, 3, 4, 5));

        // 注意:此处sc为JavaSparkContext,sc.sc()获得SparkContext
        LongAccumulator accumulator = sc.sc().longAccumulator();
        rdd.foreach(i -> accumulator.add(i));

        System.out.println("accumulator = " + accumulator);
        sc.stop();
    }
}

RDD 持久化

当使用持久化后第二次计算的耗时将远小于第一次,文件越大效果越明显

package org.example

import org.apache.spark.{SparkConf, SparkContext}

object PersistRddScala {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    conf.setAppName("PersistRddScala").setMaster("local")
    val sc = new SparkContext(conf)

    // 注意 cache 的用法和位置
    // cache 默认是基于内存的持久化
    // cache() = persist(StorageLevel.MEMORY_ONLY)
    val rdd = sc.textFile("C:\\data\\tmp.txt").cache()

    var startTime = System.currentTimeMillis()
    var count = rdd.count()
    var endTime = System.currentTimeMillis()
    println(count)
    println("第一次耗时: " + endTime - startTime)

    startTime = System.currentTimeMillis()
    count = rdd.count()
    endTime = System.currentTimeMillis()
    println(count)
    println("第二次耗时: " + endTime - startTime)

    sc.stop()
  }
}

 

综合案例:TopN主播统计

Q: 计算一款直播app的每个大区当天金币收入 TopN 的主播,不同国家的主播划分到不同大区。

日志数据:

gift_record.log

gift_record.log
{"uid": 720123141001, "vid": 14943445328940001, "good_id": "223", "gold": 9, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141002, "vid": 14943445328940001, "good_id": "223", "gold": 11, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141003, "vid": 14943445328940002, "good_id": "223", "gold": 47, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141004, "vid": 14943445328940002, "good_id": "223", "gold": 36, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141005, "vid": 14943445328940003, "good_id": "223", "gold": 30, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141006, "vid": 14943445328940003, "good_id": "223", "gold": 21, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141007, "vid": 14943445328940004, "good_id": "223", "gold": 4, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141008, "vid": 14943445328940004, "good_id": "223", "gold": 28, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141009, "vid": 14943445328940005, "good_id": "223", "gold": 14, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141010, "vid": 14943445328940005, "good_id": "223", "gold": 40, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141011, "vid": 14943445328940006, "good_id": "223", "gold": 10, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141012, "vid": 14943445328940006, "good_id": "223", "gold": 39, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141013, "vid": 14943445328940007, "good_id": "223", "gold": 46, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141014, "vid": 14943445328940007, "good_id": "223", "gold": 31, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141015, "vid": 14943445328940008, "good_id": "223", "gold": 3, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141016, "vid": 14943445328940008, "good_id": "223", "gold": 47, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141017, "vid": 14943445328940009, "good_id": "223", "gold": 49, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141018, "vid": 14943445328940009, "good_id": "223", "gold": 7, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141019, "vid": 14943445328940010, "good_id": "223", "gold": 20, "timestamp": 1494344574, "type": "gift_record"}
{"uid": 720123141020, "vid": 14943445328940010, "good_id": "223", "gold": 28, "timestamp": 1494344574, "type": "gift_record"}

video_info.log

video_info.log
{"uid": 840717325101, "vid": 14943445328940001, "area": "US", "status": "1", "watch_num": 257, "share_num": 729, "type": "video_info"}
{"uid": 840717325102, "vid": 14943445328940002, "area": "CN", "status": "1", "watch_num": 58, "share_num": 271, "type": "video_info"}
{"uid": 840717325103, "vid": 14943445328940003, "area": "CN", "status": "1", "watch_num": 533, "share_num": 399, "type": "video_info"}
{"uid": 840717325104, "vid": 14943445328940004, "area": "CN", "status": "1", "watch_num": 312, "share_num": 293, "type": "video_info"}
{"uid": 840717325105, "vid": 14943445328940005, "area": "ID", "status": "1", "watch_num": 416, "share_num": 88, "type": "video_info"}
{"uid": 840717325106, "vid": 14943445328940006, "area": "ID", "status": "1", "watch_num": 355, "share_num": 654, "type": "video_info"}
{"uid": 840717325107, "vid": 14943445328940007, "area": "CN", "status": "1", "watch_num": 397, "share_num": 383, "type": "video_info"}
{"uid": 840717325108, "vid": 14943445328940008, "area": "CN", "status": "1", "watch_num": 425, "share_num": 873, "type": "video_info"}
{"uid": 840717325109, "vid": 14943445328940009, "area": "ID", "status": "1", "watch_num": 80, "share_num": 124, "type": "video_info"}
{"uid": 840717325110, "vid": 14943445328940010, "area": "CN", "status": "1", "watch_num": 96, "share_num": 740, "type": "video_info"}

A:

  1. 首先获取两份数据中的核心字段,使用fastjson包解析数据
    主播开播记录(video_info.log):主播ID uid,直播间ID vid,大区 area
    (vid, (uid, area))
    用户送礼记录(gift_info.log):直播间ID vid,金币数量 gold
    (vid, gold)
    这样可以把这两份数据关联到一起
  2. 对送礼记录进行聚合,对相同vid的数据求和 (vid, gold_sum)
  3. 将两份数据join 到一起, vid 作为 join 的 key
    (vid, ((uid, area), gold_sum))
  4. 使用 map 迭代 join 后的数据,最后获取到 uid area gold_sum 字段
    由于一个主播一天可能开播多次,后面要基于 uid 和 area 再做一次聚合,uid 和 area 是一一对应的
    ((uid, area), gold_sum)
  5. 使用 reduceByKey 算子对数据进行聚合
    ((uid, area), gold_sum_all)
  6. 接下来针对大区进行分组,先用map 转换形式,再使用 groupByKey 对数据分组
    (area, (uid, gold_sum_all))
    area, [(uid, gold_sum_all),(uid, gold_sum_all),(uid, gold_sum_all)]
  7. 使用map迭代每个分组内数据,按照金币数量倒序排序,取前N个,最后输出 (area, TopN)。TopN 就是主播id与金币数量拼成的字符串
  8. 使用 foreach 进行打印
package org.example

import com.alibaba.fastjson.JSON
import org.apache.spark.{SparkConf, SparkContext}

object TopNScala {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
    conf.setAppName("TopNScala").setMaster("local")
    val sc = new SparkContext(conf)

    // 1. 首先获取两份数据中的核心字段,使用fastjson包解析数据
    val videoInfoRDD = sc.textFile("C:\\code\\video_info.log")
    val giftRecordRDD = sc.textFile("C:\\code\\gift_record.log")

    val videoInfoFieldRDD = videoInfoRDD.map(line => {
      val jsonObj = JSON.parseObject(line)
      val vid = jsonObj.getString("vid")
      val uid = jsonObj.getString("uid")
      val area = jsonObj.getString("area")
      (vid, (uid, area))
    })

    val giftRecordFieldRDD = giftRecordRDD.map(line => {
      val jsonObj = JSON.parseObject(line)
      val vid = jsonObj.getString("vid")
      val gold = jsonObj.getInteger("gold")
      (vid, gold)
    })

    // 2. 对送礼记录进行聚合,对相同vid的数据求和 (vid, gold_sum)
    val giftRecordFieldAggRDD = giftRecordFieldRDD.reduceByKey(_ + _)

    // 3. 将两份数据join 到一起, vid 作为 join 的 key
    // (vid, ((uid, area), gold_sum))
    val rdd = videoInfoFieldRDD.join(giftRecordFieldAggRDD)

    // 4. 使用 map 迭代 join 后的数据,最后获取到 uid area gold_sum 字段
    // ((uid, area), gold_sum)
    val joinMapRDD = rdd.map(tup => {
      // tup: (vid, ((uid, area), gold_sum))
      val uid = tup._2._1._1
      val area = tup._2._1._2
      val gold_sum = tup._2._2
      ((uid, area), gold_sum)
    })

    // 5. 使用 reduceByKey 算子对数据进行聚合
    val reduceRDD = joinMapRDD.reduceByKey(_ + _)

    // 6. 接下来针对大区进行分组,先用map 转换形式,再使用 groupByKey 对数据分组
    // map: (area, (uid, gold_sum_all))
    // groupByKey: area, [(uid, gold_sum_all),(uid, gold_sum_all),(uid, gold_sum_all)]
    val groupRDD = reduceRDD.map(tup => (tup._1._2, (tup._1._1, tup._2))).groupByKey()

    // 7. 使用map迭代每个分组内数据,按照金币数量倒序排序,取前N个,最后输出 (area, TopN)。TopN 就是主播id与金币数量拼成的字符串
    val top3RDD = groupRDD.map(tup => {
      val area = tup._1
      // uid:gold_sum_all,uid:gold_sum_all,uid:gold_sum_all
      val top3 = tup._2
        .toList         // 把 iterable 转为list
        .sortBy(_._2)
        .reverse        // 默认升序,这里反转为降序
        .take(3)
        .map(tup => tup._1 + ":" + tup._2).mkString(",")
      (area, top3)
    })

    // 8. 打印
    top3RDD.foreach(tup => println(tup._1 + "\t" + tup._2))

    sc.stop()
  }
}

 

sortByKey 如何实现全局排序?

如何使用32G内存的服务器排序1T数据?

将数据按照大小分为32G,每份在内存内排序然后按序输出到文件中,排序后的文件就是有序数据。

posted @ 2022-06-12 21:40  某某人8265  阅读(709)  评论(0编辑  收藏  举报